diff --git a/extensions/copilot/src/extension/chatSessions/common/chatSessionMetadataStore.ts b/extensions/copilot/src/extension/chatSessions/common/chatSessionMetadataStore.ts index 78d876be13f91..97e2c19b43969 100644 --- a/extensions/copilot/src/extension/chatSessions/common/chatSessionMetadataStore.ts +++ b/extensions/copilot/src/extension/chatSessions/common/chatSessionMetadataStore.ts @@ -124,4 +124,6 @@ export interface IChatSessionMetadataStore { storeForkedSessionMetadata(sourceSessionId: string, targetSessionId: string, customTitle: string): Promise; setSessionOrigin(sessionId: string): Promise; getSessionOrigin(sessionId: string): Promise<'vscode' | 'other'>; + setSessionParentId(sessionId: string, parentSessionId: string): Promise; + getSessionParentId(sessionId: string): Promise; } diff --git a/extensions/copilot/src/extension/chatSessions/common/test/mockChatSessionMetadataStore.ts b/extensions/copilot/src/extension/chatSessions/common/test/mockChatSessionMetadataStore.ts index 9bc85d003c1e3..b4d76b55ec77b 100644 --- a/extensions/copilot/src/extension/chatSessions/common/test/mockChatSessionMetadataStore.ts +++ b/extensions/copilot/src/extension/chatSessions/common/test/mockChatSessionMetadataStore.ts @@ -143,4 +143,12 @@ export class MockChatSessionMetadataStore implements IChatSessionMetadataStore { async getSessionOrigin(sessionId: string): Promise<'vscode' | 'other'> { return this._sessionOrigins.get(sessionId) ?? 'vscode'; } + + setSessionParentId(_sessionId: string, _parentSessionId: string): Promise { + return Promise.resolve(); + } + + getSessionParentId(_sessionId: string): Promise { + return Promise.resolve(undefined); + } } diff --git a/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSessionService.ts b/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSessionService.ts index 0b9367e73d8b4..5b323ae81378c 100644 --- a/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSessionService.ts +++ b/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSessionService.ts @@ -69,6 +69,7 @@ export type ISessionOptions = { debugTargetSessionIds?: readonly string[]; mcpServerMappings?: McpServerMappings; additionalWorkspaces?: IWorkspaceInfo[]; + sessionParentId?: string; } export type IGetSessionOptions = ISessionOptions & { sessionId: string }; export type ICreateSessionOptions = ISessionOptions & { sessionId?: string }; @@ -567,7 +568,15 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS const session = this.createCopilotSession(sdkSession, options.workspace, options.agent?.name, sessionManager); session.object.add(mcpGateway); + + // Set origin void this._chatSessionMetadataStore.setSessionOrigin(session.object.sessionId); + + // Set session parent id + if (options.sessionParentId) { + void this._chatSessionMetadataStore.setSessionParentId(session.object.sessionId, options.sessionParentId); + } + return session; } catch (error) { diff --git a/extensions/copilot/src/extension/chatSessions/vscode-node/chatSessionMetadataStoreImpl.ts b/extensions/copilot/src/extension/chatSessions/vscode-node/chatSessionMetadataStoreImpl.ts index 6a28e040e6c06..2213eca09fa7a 100644 --- a/extensions/copilot/src/extension/chatSessions/vscode-node/chatSessionMetadataStoreImpl.ts +++ b/extensions/copilot/src/extension/chatSessions/vscode-node/chatSessionMetadataStoreImpl.ts @@ -371,6 +371,16 @@ export class ChatSessionMetadataStore extends Disposable implements IChatSession return 'other'; } + public async setSessionParentId(sessionId: string, parentSessionId: string): Promise { + await this._intialize.value; + await this.updateMetadataFields(sessionId, { parentSessionId }); + } + + public async getSessionParentId(sessionId: string): Promise { + const metadata = await this.getSessionMetadata(sessionId, false); + return metadata?.parentSessionId; + } + private async getSessionMetadata(sessionId: string, createMetadataFileIfNotFound = true): Promise { if (isUntitledSessionId(sessionId)) { return undefined; diff --git a/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLIChatSessions.ts b/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLIChatSessions.ts index 4e15302e7612f..33b1d3fc1f81e 100644 --- a/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLIChatSessions.ts +++ b/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLIChatSessions.ts @@ -405,7 +405,10 @@ export class CopilotCLIChatSessionContentProvider extends Disposable implements workingDirectory: vscode.Uri | undefined, ): Promise<{ readonly [key: string]: unknown }> { if (worktreeProperties) { + const sessionParentId = await this._metadataStore.getSessionParentId(sessionId); + return { + sessionParentId, autoCommit: worktreeProperties.autoCommit !== false, baseCommit: worktreeProperties?.baseCommit, baseBranchName: worktreeProperties.version === 2 @@ -451,7 +454,8 @@ export class CopilotCLIChatSessionContentProvider extends Disposable implements } satisfies { readonly [key: string]: unknown }; } - const [sessionRequestDetails, repositoryProperties] = await Promise.all([ + const [sessionParentId, sessionRequestDetails, repositoryProperties] = await Promise.all([ + this._metadataStore.getSessionParentId(sessionId), this._metadataStore.getRequestDetails(sessionId), this._metadataStore.getRepositoryProperties(sessionId) ]); @@ -470,6 +474,7 @@ export class CopilotCLIChatSessionContentProvider extends Disposable implements : undefined; return { + sessionParentId, isolationMode: IsolationMode.Workspace, repositoryPath: repositoryProperties?.repositoryPath, branchName: repositoryProperties?.branchName, diff --git a/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLIChatSessionsContribution.ts b/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLIChatSessionsContribution.ts index 0368336f844fd..8f15c18d065bb 100644 --- a/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLIChatSessionsContribution.ts +++ b/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLIChatSessionsContribution.ts @@ -61,6 +61,7 @@ const REPOSITORY_OPTION_ID = 'repository'; const _sessionWorktreeIsolationCache = new Map(); const BRANCH_OPTION_ID = 'branch'; const ISOLATION_OPTION_ID = 'isolation'; +const PARENT_SESSION_OPTION_ID = 'parentSessionId'; const LAST_USED_ISOLATION_OPTION_KEY = 'github.copilot.cli.lastUsedIsolationOption'; const OPEN_REPOSITORY_COMMAND_ID = 'github.copilot.cli.sessions.openRepository'; const OPEN_IN_COPILOT_CLI_COMMAND_ID = 'github.copilot.cli.openInCopilotCLI'; @@ -308,9 +309,12 @@ export class CopilotCLIChatSessionItemProvider extends Disposable implements vsc // repository state which we are passing along through the metadata worktreeProperties = await this.worktreeManager.getWorktreeProperties(session.id); + const sessionParentId = await this.chatSessionMetadataStore.getSessionParentId(session.id); + if (worktreeProperties) { // Worktree metadata = { + sessionParentId, autoCommit: worktreeProperties.autoCommit !== false, baseCommit: worktreeProperties?.baseCommit, baseBranchName: worktreeProperties.version === 2 @@ -373,6 +377,7 @@ export class CopilotCLIChatSessionItemProvider extends Disposable implements vsc : undefined; metadata = { + sessionParentId, isolationMode: IsolationMode.Workspace, repositoryPath: repositoryProperties?.repositoryPath, branchName: repositoryProperties?.branchName, @@ -1267,6 +1272,7 @@ export class CopilotCLIChatSessionParticipant extends Disposable { let { chatSessionContext } = context; const disposables = new DisposableStore(); let sessionId: string | undefined = undefined; + let sessionParentId: string | undefined = undefined; let sdkSessionId: string | undefined = undefined; try { @@ -1284,6 +1290,8 @@ export class CopilotCLIChatSessionParticipant extends Disposable { _sessionBranch.set(sessionId, value); } else if (opt.optionId === ISOLATION_OPTION_ID && value) { _sessionIsolation.set(sessionId, value as IsolationMode); + } else if (opt.optionId === PARENT_SESSION_OPTION_ID && value) { + sessionParentId = value; } } } @@ -1369,7 +1377,7 @@ export class CopilotCLIChatSessionParticipant extends Disposable { }; const newBranch = (isUntitled && request.prompt && this.branchNameGenerator) ? this.branchNameGenerator.generateBranchName(fakeContext, token) : undefined; - const sessionResult = await this.getOrCreateSession(request, chatSessionContext, stream, { model, agent, newBranch }, disposables, token); + const sessionResult = await this.getOrCreateSession(request, chatSessionContext, stream, { model, agent, newBranch, sessionParentId }, disposables, token); const session = sessionResult.session; if (session) { disposables.add(session); @@ -1729,7 +1737,7 @@ export class CopilotCLIChatSessionParticipant extends Disposable { } } - private async getOrCreateSession(request: vscode.ChatRequest, chatSessionContext: vscode.ChatSessionContext, stream: vscode.ChatResponseStream, options: { model: { model: string; reasoningEffort?: string } | undefined; agent: SweCustomAgent | undefined; newBranch?: Promise }, disposables: DisposableStore, token: vscode.CancellationToken): Promise<{ session: IReference | undefined; trusted: boolean }> { + private async getOrCreateSession(request: vscode.ChatRequest, chatSessionContext: vscode.ChatSessionContext, stream: vscode.ChatResponseStream, options: { model: { model: string; reasoningEffort?: string } | undefined; agent: SweCustomAgent | undefined; newBranch?: Promise; sessionParentId?: string }, disposables: DisposableStore, token: vscode.CancellationToken): Promise<{ session: IReference | undefined; trusted: boolean }> { const { resource } = chatSessionContext.chatSessionItem; const existingSessionId = this.sessionItemProvider.untitledSessionIdMapping.get(SessionIdForCLI.parse(resource)); const id = existingSessionId ?? SessionIdForCLI.parse(resource); @@ -1747,7 +1755,7 @@ export class CopilotCLIChatSessionParticipant extends Disposable { const debugTargetSessionIds = extractDebugTargetSessionIds(request.references); const mcpServerMappings = buildMcpServerMappings(request.tools); const session = isNewSession ? - await this.sessionService.createSession({ model: model?.model, reasoningEffort: model?.reasoningEffort, workspace: workspaceInfo, agent, debugTargetSessionIds, mcpServerMappings }, token) : + await this.sessionService.createSession({ model: model?.model, reasoningEffort: model?.reasoningEffort, workspace: workspaceInfo, agent, debugTargetSessionIds, mcpServerMappings, sessionParentId: options.sessionParentId }, token) : await this.sessionService.getSession({ sessionId: id, model: model?.model, reasoningEffort: model?.reasoningEffort, workspace: workspaceInfo, agent, debugTargetSessionIds, mcpServerMappings }, token); this.sessionItemProvider.notifySessionsChange(); // TODO @DonJayamanne We need to refresh to add this new session, but we need a label. diff --git a/extensions/copilot/src/extension/chatSessions/vscode-node/test/copilotCLIChatSessions.spec.ts b/extensions/copilot/src/extension/chatSessions/vscode-node/test/copilotCLIChatSessions.spec.ts index 507cdc2718f06..28e14e0e2f385 100644 --- a/extensions/copilot/src/extension/chatSessions/vscode-node/test/copilotCLIChatSessions.spec.ts +++ b/extensions/copilot/src/extension/chatSessions/vscode-node/test/copilotCLIChatSessions.spec.ts @@ -158,6 +158,7 @@ function createProvider() { const metadataStore = new class extends mock() { override getRequestDetails = vi.fn(async () => []); override getRepositoryProperties = vi.fn(async () => undefined); + override getSessionParentId = vi.fn(async () => undefined); }; const gitService = new TestGitService(); const folderRepositoryManager = new TestFolderRepositoryManager();