33 * Licensed under the MIT License. See License.txt in the project root for license information.
44 *--------------------------------------------------------------------------------------------*/
55
6- import { PermissionMode } from '@anthropic-ai/claude-agent-sdk' ;
76import * as vscode from 'vscode' ;
87import { ChatExtendedRequestHandler } from 'vscode' ;
98import { ConfigKey , IConfigurationService } from '../../../platform/configuration/common/configurationService' ;
@@ -12,7 +11,7 @@ import { IGitService } from '../../../platform/git/common/gitService';
1211import { ILogService } from '../../../platform/log/common/logService' ;
1312import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService' ;
1413import { CancellationToken } from '../../../util/vs/base/common/cancellation' ;
15- import { Disposable , IDisposable } from '../../../util/vs/base/common/lifecycle' ;
14+ import { Disposable } from '../../../util/vs/base/common/lifecycle' ;
1615import { URI } from '../../../util/vs/base/common/uri' ;
1716import { generateUuid } from '../../../util/vs/base/common/uuid' ;
1817import { ClaudeFolderInfo } from '../claude/common/claudeFolderInfo' ;
@@ -27,7 +26,6 @@ import { IClaudeSlashCommandService } from '../claude/vscode-node/claudeSlashCom
2726import { IChatFolderMruService } from '../common/folderRepositoryManager' ;
2827import { buildChatHistory } from './chatHistoryBuilder' ;
2928import { ClaudeSessionOptionBuilder , FOLDER_OPTION_ID , isPermissionMode , PERMISSION_MODE_OPTION_ID } from './claudeSessionOptionBuilder' ;
30- import { getSelectedOption } from './sessionOptionGroupBuilder' ;
3129
3230// Import the tool permission handlers
3331import '../claude/vscode-node/toolPermissionHandlers/index' ;
@@ -103,8 +101,14 @@ export class ClaudeChatSessionContentProvider extends Disposable implements vsco
103101 }
104102
105103 const modelId = parseClaudeModelId ( request . model . id ) ;
106- const permissionMode = this . _controller . getPermissionModeForSession ( effectiveSessionId ) ;
107- const folderInfo = await this . _controller . getFolderInfoForSession ( effectiveSessionId ) ;
104+ const selectedPermissionId = chatSessionContext . inputState . groups . find ( group => group . id === PERMISSION_MODE_OPTION_ID ) ?. selected ?. id ;
105+ if ( ! selectedPermissionId || ! isPermissionMode ( selectedPermissionId ) ) {
106+ throw new Error ( `Permission mode not set for session ${ effectiveSessionId } ` ) ;
107+ }
108+ const permissionMode = selectedPermissionId ;
109+ const selectedFolderId = chatSessionContext . inputState . groups . find ( group => group . id === FOLDER_OPTION_ID ) ?. selected ?. id ;
110+ const selectedFolderUri = selectedFolderId ? URI . file ( selectedFolderId ) : undefined ;
111+ const folderInfo = await this . _controller . getFolderInfoForSession ( effectiveSessionId , selectedFolderUri ) ;
108112
109113 // Commit UI state to session state service before invoking agent manager
110114 this . sessionStateService . setModelIdForSession ( effectiveSessionId , modelId ) ;
@@ -163,9 +167,8 @@ export class ClaudeChatSessionContentProvider extends Disposable implements vsco
163167 * Reads sessions from ~/.claude/projects/<folder-slug>/, where each file name is a session id (GUID).
164168 *
165169 * Owns the input state (getChatSessionInputState) lifecycle: wiring external
166- * state listeners, persisting selections to metadata, and resolving permission
167- * mode / folder info for sessions. Group construction is delegated to
168- * {@link ClaudeSessionOptionBuilder}.
170+ * state listeners and resolving folder info for sessions. Group construction
171+ * is delegated to {@link ClaudeSessionOptionBuilder}.
169172 */
170173export class ClaudeChatSessionItemController extends Disposable {
171174 private readonly _controller : vscode . ChatSessionItemController ;
@@ -200,16 +203,6 @@ export class ClaudeChatSessionItemController extends Disposable {
200203 ) ;
201204 item . iconPath = new vscode . ThemeIcon ( 'claude' ) ;
202205 item . timing = { created : Date . now ( ) } ;
203-
204- const permissionModeSelection = getSelectedOption ( context . inputState . groups , PERMISSION_MODE_OPTION_ID ) ;
205- const permissionMode = permissionModeSelection ?. id ;
206- const folderSelection = getSelectedOption ( context . inputState . groups , FOLDER_OPTION_ID ) ;
207- const folder = folderSelection ?. id ? URI . file ( folderSelection . id ) : undefined ;
208-
209- item . metadata = {
210- permissionMode,
211- cwd : folder ,
212- } ;
213206 this . _inProgressItems . set ( newSessionId , item ) ;
214207 return item ;
215208 } ;
@@ -248,7 +241,19 @@ export class ClaudeChatSessionItemController extends Disposable {
248241 const newItem = this . _controller . createChatSessionItem ( ClaudeSessionUri . forSessionId ( result . sessionId ) , title ) ;
249242 newItem . iconPath = new vscode . ThemeIcon ( 'claude' ) ;
250243 newItem . timing = { created : Date . now ( ) } ;
251- newItem . metadata = item ?. metadata ? { ...item . metadata } : undefined ;
244+
245+ // Copy parent session state to the forked session
246+ const parentSessionId = ClaudeSessionUri . getSessionId ( sessionResource ) ;
247+ const parentPermission = this . _sessionStateService . getPermissionModeForSession ( parentSessionId ) ;
248+ const parentFolder = this . _sessionStateService . getFolderInfoForSession ( parentSessionId ) ;
249+ this . _sessionStateService . setPermissionModeForSession ( result . sessionId , parentPermission ) ;
250+ if ( parentFolder ) {
251+ this . _sessionStateService . setFolderInfoForSession ( result . sessionId , {
252+ ...parentFolder ,
253+ additionalDirectories : [ ...( parentFolder . additionalDirectories ?? [ ] ) ] ,
254+ } ) ;
255+ }
256+
252257 this . _controller . items . add ( newItem ) ;
253258 return newItem ;
254259 } ;
@@ -273,44 +278,30 @@ export class ClaudeChatSessionItemController extends Disposable {
273278 // #region Input State
274279
275280 private _setupInputState ( ) : void {
276- const trackedStates : { ref : WeakRef < vscode . ChatSessionInputState > ; subscription : IDisposable } [ ] = [ ] ;
281+ const trackedStates : { ref : WeakRef < vscode . ChatSessionInputState > } [ ] = [ ] ;
277282
278283 const sweepStaleEntries = ( ) => {
279284 for ( let i = trackedStates . length - 1 ; i >= 0 ; i -- ) {
280285 if ( ! trackedStates [ i ] . ref . deref ( ) ) {
281- trackedStates [ i ] . subscription . dispose ( ) ;
282286 trackedStates . splice ( i , 1 ) ;
283287 }
284288 }
285289 } ;
286290
287- // Dispose all subscriptions when the content provider is disposed
288- this . _register ( {
289- dispose : ( ) => {
290- for ( const entry of trackedStates ) {
291- entry . subscription . dispose ( ) ;
292- }
293- trackedStates . length = 0 ;
291+ this . _controller . getChatSessionInputState = async ( sessionResource , context , token ) => {
292+ if ( context . previousInputState ) {
293+ const state = this . _controller . createChatSessionInputState ( [ ...context . previousInputState . groups ] ) ;
294+ trackedStates . push ( { ref : new WeakRef ( state ) } ) ;
295+ return state ;
294296 }
295- } ) ;
296297
297- this . _controller . getChatSessionInputState = async ( sessionResource , context , token ) => {
298298 const isExistingSession = sessionResource && await this . _claudeCodeSessionService . getSession ( sessionResource , token ) !== undefined ;
299299
300300 const groups = isExistingSession
301301 ? await this . _buildExistingSessionGroups ( sessionResource )
302- : await this . _optionBuilder . buildNewSessionGroups ( context . previousInputState ) ;
302+ : await this . _optionBuilder . buildNewSessionGroups ( ) ;
303303 const state = this . _controller . createChatSessionInputState ( groups ) ;
304-
305- const ref = new WeakRef ( state ) ;
306- const subscription = state . onDidChange ( ( ) => {
307- const s = ref . deref ( ) ;
308- if ( s ) {
309- this . _handleInputStateChange ( s ) ;
310- }
311- } ) ;
312- trackedStates . push ( { ref, subscription } ) ;
313-
304+ trackedStates . push ( { ref : new WeakRef ( state ) } ) ;
314305 return state ;
315306 } ;
316307
@@ -342,11 +333,6 @@ export class ClaudeChatSessionItemController extends Disposable {
342333 if ( e . permissionMode === undefined ) {
343334 return ;
344335 }
345- const existingMode = this . getMetadata ( e . sessionId ) ?. permissionMode ;
346- if ( e . permissionMode === existingMode ) {
347- return ;
348- }
349- this . setMetadata ( e . sessionId , { permissionMode : e . permissionMode } ) ;
350336 for ( const entry of trackedStates ) {
351337 const state = entry . ref . deref ( ) ;
352338 if ( state ?. sessionResource ) {
@@ -368,23 +354,24 @@ export class ClaudeChatSessionItemController extends Disposable {
368354
369355 private async _buildExistingSessionGroups ( sessionResource : vscode . Uri ) : Promise < vscode . ChatSessionProviderOptionGroup [ ] > {
370356 const sessionId = ClaudeSessionUri . getSessionId ( sessionResource ) ;
371- const permissionMode = this . getPermissionModeForSession ( sessionId ) ;
372- const workspaceFolders = this . _workspaceService . getWorkspaceFolders ( ) ;
373- const folderUri = workspaceFolders . length !== 1 ? await this . _getDefaultFolderForSession ( sessionId ) : undefined ;
374- return this . _optionBuilder . buildExistingSessionGroups ( permissionMode , folderUri ) ;
375- }
357+ const permissionMode = this . _sessionStateService . getPermissionModeForSession ( sessionId ) ;
376358
377- private _handleInputStateChange ( state : vscode . ChatSessionInputState ) : void {
378- const { permissionMode, folderUri } = this . _optionBuilder . getSelections ( state . groups ) ;
379- const sessionId = state . sessionResource ? ClaudeSessionUri . getSessionId ( state . sessionResource ) : undefined ;
380- if ( sessionId ) {
381- if ( permissionMode ) {
382- this . setMetadata ( sessionId , { permissionMode } ) ;
383- }
384- if ( folderUri ) {
385- this . setMetadata ( sessionId , { cwd : folderUri } ) ;
359+ const workspaceFolders = this . _workspaceService . getWorkspaceFolders ( ) ;
360+ let folderUri : URI | undefined ;
361+ if ( workspaceFolders . length !== 1 ) {
362+ const stateFolder = this . _sessionStateService . getFolderInfoForSession ( sessionId ) ;
363+ if ( stateFolder ) {
364+ folderUri = URI . file ( stateFolder . cwd ) ;
365+ } else {
366+ const session = await this . _claudeCodeSessionService . getSession ( sessionResource , CancellationToken . None ) ;
367+ if ( session ?. cwd ) {
368+ folderUri = URI . file ( session . cwd ) ;
369+ } else {
370+ folderUri = await this . _optionBuilder . getDefaultFolder ( ) ;
371+ }
386372 }
387373 }
374+ return this . _optionBuilder . buildExistingSessionGroups ( permissionMode , folderUri ) ;
388375 }
389376
390377 private async _rebuildInputState ( state : vscode . ChatSessionInputState ) : Promise < void > {
@@ -397,26 +384,9 @@ export class ClaudeChatSessionItemController extends Disposable {
397384
398385 // #endregion
399386
400- // #region Permission Mode & Folder Resolution
387+ // #region Folder Resolution
401388
402- private async _getDefaultFolderForSession ( sessionId : string ) : Promise < URI | undefined > {
403- const selected = this . getMetadata ( sessionId ) ?. cwd ;
404- if ( selected ) {
405- return selected ;
406- }
407-
408- const defaultFolder = await this . _optionBuilder . getDefaultFolder ( ) ;
409- if ( defaultFolder ) {
410- this . setMetadata ( sessionId , { cwd : defaultFolder } ) ;
411- }
412- return defaultFolder ;
413- }
414-
415- getPermissionModeForSession ( sessionId : string ) : PermissionMode {
416- return this . getMetadata ( sessionId ) ?. permissionMode ?? this . _sessionStateService . getPermissionModeForSession ( sessionId ) ;
417- }
418-
419- async getFolderInfoForSession ( sessionId : string ) : Promise < ClaudeFolderInfo > {
389+ async getFolderInfoForSession ( sessionId : string , selectedFolderUri ?: URI ) : Promise < ClaudeFolderInfo > {
420390 const workspaceFolders = this . _workspaceService . getWorkspaceFolders ( ) ;
421391
422392 if ( workspaceFolders . length === 1 ) {
@@ -426,21 +396,21 @@ export class ClaudeChatSessionItemController extends Disposable {
426396 } ;
427397 }
428398
429- // Multi-root or empty workspace: use the selected folder
430- const selectedFolder = this . getMetadata ( sessionId ) ?. cwd ;
399+ // Multi-root or empty workspace: resolve selected folder from inputState, sessionStateService, or session file
400+ const folderUri = selectedFolderUri ?? await this . _resolveSessionFolder ( sessionId ) ;
431401
432402 if ( workspaceFolders . length > 1 ) {
433- const cwd = selectedFolder ?. fsPath ?? workspaceFolders [ 0 ] . fsPath ;
403+ const cwd = folderUri ?. fsPath ?? workspaceFolders [ 0 ] . fsPath ;
434404 const additionalDirectories = workspaceFolders
435405 . map ( f => f . fsPath )
436406 . filter ( p => p !== cwd ) ;
437407 return { cwd, additionalDirectories } ;
438408 }
439409
440410 // Empty workspace
441- if ( selectedFolder ) {
411+ if ( folderUri ) {
442412 return {
443- cwd : selectedFolder . fsPath ,
413+ cwd : folderUri . fsPath ,
444414 additionalDirectories : [ ] ,
445415 } ;
446416 }
@@ -461,45 +431,23 @@ export class ClaudeChatSessionItemController extends Disposable {
461431 } ;
462432 }
463433
464- // #endregion
465-
466- // #region Metadata
467-
468- setMetadata ( sessionId : string , metadata : Partial < { permissionMode : PermissionMode ; cwd ?: URI } > ) : void {
469- const item = this . _controller . items . get ( ClaudeSessionUri . forSessionId ( sessionId ) ) ;
470- if ( item ) {
471- item . metadata = {
472- ...item . metadata ,
473- permissionMode : metadata . permissionMode ?? item . metadata ?. permissionMode ,
474- cwd : metadata . cwd ?? item . metadata ?. cwd ,
475- } ;
434+ private async _resolveSessionFolder ( sessionId : string ) : Promise < URI | undefined > {
435+ const stateFolder = this . _sessionStateService . getFolderInfoForSession ( sessionId ) ;
436+ if ( stateFolder ) {
437+ return URI . file ( stateFolder . cwd ) ;
476438 }
477- }
478439
479- getMetadata ( sessionId : string ) : { permissionMode ?: PermissionMode ; cwd ?: URI } | undefined {
480- const candidate = this . _controller . items . get ( ClaudeSessionUri . forSessionId ( sessionId ) ) ;
481- if ( candidate ) {
482- if ( candidate . metadata ?. permissionMode !== undefined && ! isPermissionMode ( candidate . metadata . permissionMode ) ) {
483- this . _logService . warn ( `Invalid permission mode "${ candidate . metadata ?. permissionMode } " found in metadata for session ${ sessionId } . Falling back to default.` ) ;
484- candidate . metadata = {
485- permissionMode : 'acceptEdits' ,
486- cwd : candidate . metadata ?. cwd ,
487- } ;
488- }
489- if ( candidate . metadata ?. cwd && ! ( URI . isUri ( candidate . metadata . cwd ) ) ) {
490- this . _logService . warn ( `Invalid cwd "${ candidate . metadata . cwd } " found in metadata for session ${ sessionId } . Ignoring.` ) ;
491- candidate . metadata = {
492- permissionMode : candidate . metadata . permissionMode ,
493- cwd : undefined ,
494- } ;
495- }
496- return {
497- permissionMode : candidate . metadata ?. permissionMode ,
498- cwd : candidate . metadata ?. cwd ,
499- } ;
440+ const sessionResource = ClaudeSessionUri . forSessionId ( sessionId ) ;
441+ const session = await this . _claudeCodeSessionService . getSession ( sessionResource , CancellationToken . None ) ;
442+ if ( session ?. cwd ) {
443+ return URI . file ( session . cwd ) ;
500444 }
445+
446+ return this . _optionBuilder . getDefaultFolder ( ) ;
501447 }
502448
449+ // #endregion
450+
503451 updateItemLabel ( sessionId : string , label : string ) : void {
504452 const resource = ClaudeSessionUri . forSessionId ( sessionId ) ;
505453 const item = this . _controller . items . get ( resource ) ;
@@ -575,11 +523,6 @@ export class ClaudeChatSessionItemController extends Disposable {
575523 lastRequestEnded : session . lastRequestEnded ,
576524 } ;
577525 item . iconPath = new vscode . ThemeIcon ( 'claude' ) ;
578- item . metadata = {
579- // Allow it to be set when opened
580- permissionMode : undefined ,
581- cwd : session . cwd ? URI . file ( session . cwd ) : undefined
582- } ;
583526 return item ;
584527 }
585528
0 commit comments