55
66import { CopilotClient } from '@github/copilot-sdk' ;
77import { rgPath } from '@vscode/ripgrep' ;
8- import { SequencerByKey } from '../../../../base/common/async.js' ;
8+ import { Limiter , SequencerByKey } from '../../../../base/common/async.js' ;
99import { Emitter } from '../../../../base/common/event.js' ;
1010import { Disposable , DisposableMap } from '../../../../base/common/lifecycle.js' ;
1111import { FileAccess } from '../../../../base/common/network.js' ;
@@ -17,7 +17,7 @@ import { IFileService } from '../../../files/common/files.js';
1717import { IInstantiationService } from '../../../instantiation/common/instantiation.js' ;
1818import { ILogService } from '../../../log/common/log.js' ;
1919import { IAgentPluginManager , ISyncedCustomization } from '../../common/agentPluginManager.js' ;
20- import { AgentSession , IAgent , IAgentAttachment , IAgentCreateSessionConfig , IAgentDescriptor , IAgentMessageEvent , IAgentModelInfo , IAgentProgressEvent , IAgentSessionMetadata , IAgentSubagentStartedEvent , IAgentToolCompleteEvent , IAgentToolStartEvent } from '../../common/agentService.js' ;
20+ import { AgentSession , IAgent , IAgentAttachment , IAgentCreateSessionConfig , IAgentCreateSessionResult , IAgentDescriptor , IAgentMessageEvent , IAgentModelInfo , IAgentProgressEvent , IAgentSessionMetadata , IAgentSessionProjectInfo , IAgentSubagentStartedEvent , IAgentToolCompleteEvent , IAgentToolStartEvent } from '../../common/agentService.js' ;
2121import { ISessionDataService } from '../../common/sessionDataService.js' ;
2222import { CustomizationStatus , ICustomizationRef , SessionInputResponseKind , type ISessionInputAnswer , type IPendingMessage , type PolicyState } from '../../common/state/sessionState.js' ;
2323import { CopilotAgentSession , SessionWrapperFactory } from './copilotAgentSession.js' ;
@@ -26,6 +26,7 @@ import { CopilotSessionWrapper } from './copilotSessionWrapper.js';
2626import { forkCopilotSessionOnDisk , getCopilotDataDir , truncateCopilotSessionOnDisk } from './copilotAgentForking.js' ;
2727import { IProtectedResourceMetadata } from '../../common/state/protocol/state.js' ;
2828import { IAgentHostTerminalManager } from '../agentHostTerminalManager.js' ;
29+ import { ICopilotSessionContext , projectFromCopilotContext } from './copilotGitProject.js' ;
2930import { createShellTools , ShellManager } from './copilotShellTools.js' ;
3031
3132/**
@@ -161,12 +162,23 @@ export class CopilotAgent extends Disposable implements IAgent {
161162 this . _logService . info ( '[Copilot] Listing sessions...' ) ;
162163 const client = await this . _ensureClient ( ) ;
163164 const sessions = await client . listSessions ( ) ;
164- const result : IAgentSessionMetadata [ ] = sessions . map ( s => ( {
165- session : AgentSession . uri ( this . id , s . sessionId ) ,
166- startTime : s . startTime . getTime ( ) ,
167- modifiedTime : s . modifiedTime . getTime ( ) ,
168- summary : s . summary ,
169- workingDirectory : typeof s . context ?. cwd === 'string' ? URI . file ( s . context . cwd ) : undefined ,
165+ const projectLimiter = new Limiter < IAgentSessionProjectInfo | undefined > ( 4 ) ;
166+ const projectByContext = new Map < string , Promise < IAgentSessionProjectInfo | undefined > > ( ) ;
167+ const result : IAgentSessionMetadata [ ] = await Promise . all ( sessions . map ( async s => {
168+ const session = AgentSession . uri ( this . id , s . sessionId ) ;
169+ let { project, resolved } = await this . _readSessionProject ( session ) ;
170+ if ( ! resolved ) {
171+ project = await this . _resolveSessionProject ( s . context , projectLimiter , projectByContext ) ;
172+ this . _storeSessionProjectResolution ( session , project ) ;
173+ }
174+ return {
175+ session,
176+ startTime : s . startTime . getTime ( ) ,
177+ modifiedTime : s . modifiedTime . getTime ( ) ,
178+ ...( project ? { project } : { } ) ,
179+ summary : s . summary ,
180+ workingDirectory : typeof s . context ?. cwd === 'string' ? URI . file ( s . context . cwd ) : undefined ,
181+ } ;
170182 } ) ) ;
171183 this . _logService . info ( `[Copilot] Found ${ result . length } sessions` ) ;
172184 return result ;
@@ -192,7 +204,7 @@ export class CopilotAgent extends Disposable implements IAgent {
192204 return result ;
193205 }
194206
195- async createSession ( config ?: IAgentCreateSessionConfig ) : Promise < URI > {
207+ async createSession ( config ?: IAgentCreateSessionConfig ) : Promise < IAgentCreateSessionResult > {
196208 this . _logService . info ( `[Copilot] Creating session... ${ config ?. model ? `model=${ config . model } ` : '' } ` ) ;
197209 const client = await this . _ensureClient ( ) ;
198210 const parsedPlugins = await this . _plugins . getAppliedPlugins ( ) ;
@@ -220,7 +232,9 @@ export class CopilotAgent extends Disposable implements IAgent {
220232 const agentSession = await this . _resumeSession ( newSessionId ) ;
221233 const session = agentSession . sessionUri ;
222234 this . _logService . info ( `[Copilot] Forked session created: ${ session . toString ( ) } ` ) ;
223- return session ;
235+ const project = await projectFromCopilotContext ( { cwd : config . workingDirectory ?. fsPath } ) ;
236+ this . _storeSessionMetadata ( session , undefined , config . workingDirectory , project , true ) ;
237+ return { session, ...( project ? { project } : { } ) } ;
224238 } ) ;
225239 }
226240
@@ -244,13 +258,13 @@ export class CopilotAgent extends Disposable implements IAgent {
244258 this . _plugins . setAppliedPlugins ( agentSession , parsedPlugins ) ;
245259 await agentSession . initializeSession ( ) ;
246260
247- // Persist model & working directory so we can recreate the session
248- // if the SDK loses it (e.g. sessions without messages).
249- this . _storeSessionMetadata ( agentSession . sessionUri , config ?. model , config ?. workingDirectory ) ;
250-
251261 const session = agentSession . sessionUri ;
252262 this . _logService . info ( `[Copilot] Session created: ${ session . toString ( ) } ` ) ;
253- return session ;
263+ const project = await projectFromCopilotContext ( { cwd : config ?. workingDirectory ?. fsPath } ) ;
264+ // Persist model, working directory, and project so we can recreate the
265+ // session if the SDK loses it and avoid rediscovering git metadata.
266+ this . _storeSessionMetadata ( agentSession . sessionUri , config ?. model , config ?. workingDirectory , project , true ) ;
267+ return { session, ...( project ? { project } : { } ) } ;
254268 }
255269
256270 async setClientCustomizations ( clientId : string , customizations : ICustomizationRef [ ] , progress ?: ( results : ISyncedCustomization [ ] ) => void ) : Promise < ISyncedCustomization [ ] > {
@@ -365,7 +379,7 @@ export class CopilotAgent extends Disposable implements IAgent {
365379 if ( entry ) {
366380 await entry . setModel ( model ) ;
367381 }
368- this . _storeSessionMetadata ( session , model , undefined ) ;
382+ this . _storeSessionMetadata ( session , model , undefined , undefined ) ;
369383 }
370384
371385 async shutdown ( ) : Promise < void > {
@@ -496,8 +510,11 @@ export class CopilotAgent extends Disposable implements IAgent {
496510
497511 private static readonly _META_MODEL = 'copilot.model' ;
498512 private static readonly _META_CWD = 'copilot.workingDirectory' ;
513+ private static readonly _META_PROJECT_RESOLVED = 'copilot.project.resolved' ;
514+ private static readonly _META_PROJECT_URI = 'copilot.project.uri' ;
515+ private static readonly _META_PROJECT_DISPLAY_NAME = 'copilot.project.displayName' ;
499516
500- private _storeSessionMetadata ( session : URI , model : string | undefined , workingDirectory : URI | undefined ) : void {
517+ private _storeSessionMetadata ( session : URI , model : string | undefined , workingDirectory : URI | undefined , project : IAgentSessionProjectInfo | undefined , projectResolved = project !== undefined ) : void {
501518 const dbRef = this . _sessionDataService . tryOpenDatabase ( session ) ;
502519 dbRef ?. then ( ref => {
503520 if ( ! ref ) {
@@ -511,6 +528,13 @@ export class CopilotAgent extends Disposable implements IAgent {
511528 if ( workingDirectory ) {
512529 work . push ( db . setMetadata ( CopilotAgent . _META_CWD , workingDirectory . toString ( ) ) ) ;
513530 }
531+ if ( projectResolved ) {
532+ work . push ( db . setMetadata ( CopilotAgent . _META_PROJECT_RESOLVED , 'true' ) ) ;
533+ }
534+ if ( project ) {
535+ work . push ( db . setMetadata ( CopilotAgent . _META_PROJECT_URI , project . uri . toString ( ) ) ) ;
536+ work . push ( db . setMetadata ( CopilotAgent . _META_PROJECT_DISPLAY_NAME , project . displayName ) ) ;
537+ }
514538 Promise . all ( work ) . finally ( ( ) => ref . dispose ( ) ) ;
515539 } ) ;
516540 }
@@ -534,6 +558,55 @@ export class CopilotAgent extends Disposable implements IAgent {
534558 }
535559 }
536560
561+ private async _readSessionProject ( session : URI ) : Promise < { project ?: IAgentSessionProjectInfo ; resolved : boolean } > {
562+ const ref = await this . _sessionDataService . tryOpenDatabase ( session ) ;
563+ if ( ! ref ) {
564+ return { resolved : false } ;
565+ }
566+ try {
567+ const [ resolved , uri , displayName ] = await Promise . all ( [
568+ ref . object . getMetadata ( CopilotAgent . _META_PROJECT_RESOLVED ) ,
569+ ref . object . getMetadata ( CopilotAgent . _META_PROJECT_URI ) ,
570+ ref . object . getMetadata ( CopilotAgent . _META_PROJECT_DISPLAY_NAME ) ,
571+ ] ) ;
572+ const project = uri && displayName ? { uri : URI . parse ( uri ) , displayName } : undefined ;
573+ return { project, resolved : resolved === 'true' || project !== undefined } ;
574+ } finally {
575+ ref . dispose ( ) ;
576+ }
577+ }
578+
579+ private _storeSessionProjectResolution ( session : URI , project : IAgentSessionProjectInfo | undefined ) : void {
580+ this . _storeSessionMetadata ( session , undefined , undefined , project , true ) ;
581+ }
582+
583+ private _resolveSessionProject ( context : ICopilotSessionContext | undefined , limiter : Limiter < IAgentSessionProjectInfo | undefined > , projectByContext : Map < string , Promise < IAgentSessionProjectInfo | undefined > > ) : Promise < IAgentSessionProjectInfo | undefined > {
584+ const key = this . _projectContextKey ( context ) ;
585+ if ( ! key ) {
586+ return Promise . resolve ( undefined ) ;
587+ }
588+
589+ let project = projectByContext . get ( key ) ;
590+ if ( ! project ) {
591+ project = limiter . queue ( ( ) => projectFromCopilotContext ( context ) ) ;
592+ projectByContext . set ( key , project ) ;
593+ }
594+ return project ;
595+ }
596+
597+ private _projectContextKey ( context : ICopilotSessionContext | undefined ) : string | undefined {
598+ if ( context ?. cwd ) {
599+ return `cwd:${ context . cwd } ` ;
600+ }
601+ if ( context ?. gitRoot ) {
602+ return `gitRoot:${ context . gitRoot } ` ;
603+ }
604+ if ( context ?. repository ) {
605+ return `repository:${ context . repository } ` ;
606+ }
607+ return undefined ;
608+ }
609+
537610 override dispose ( ) : void {
538611 this . _client ?. stop ( ) . catch ( ( ) => { /* best-effort */ } ) ;
539612 super . dispose ( ) ;
0 commit comments