From 199093dc4b4bb9e3a42aeaa9d55a788eaab8bcef Mon Sep 17 00:00:00 2001 From: Vijay Upadya <41652029+vijayupadya@users.noreply.github.com> Date: Fri, 14 Aug 2026 17:59:08 -0700 Subject: [PATCH] agentHost: EH migration - show worktree sessions in a window opened on their repository folder (#330973) agentHost: show worktree sessions in a window opened on their repository folder --- .../agentHost/agentHostSessionListStore.ts | 23 +++++++++++- .../agentHostChatContribution.test.ts | 35 +++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostSessionListStore.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostSessionListStore.ts index 6fdd6b06d82..d9f62c4052f 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostSessionListStore.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostSessionListStore.ts @@ -348,6 +348,10 @@ export class AgentHostSessionListStore extends Disposable { modifiedAt: new Date(session.modifiedTime).toISOString(), changes: session.changes, workingDirectories: session.workingDirectories?.map(d => d.toString()), + // The repository root a worktree-isolated session belongs to; the + // workspace filter matches on it because the worktree itself lives + // outside the repository folder. + ...(session.project ? { project: { uri: session.project.uri.toString(), displayName: session.project.displayName } } : {}), // Carry `_meta` so the adoptable-legacy marker survives into the list // item; consumers use it to avoid passively restoring (and thereby // migrating) an un-adopted legacy Copilot CLI session. @@ -371,7 +375,7 @@ export class AgentHostSessionListStore extends Disposable { /** Uses workspace-file provenance for multi-root workspaces and path containment otherwise. */ private _isSessionInWorkspace(entry: IAgentHostSessionListEntry): boolean { - const workingDirectories = entry.summary.workingDirectories?.map(directory => URI.parse(directory)) ?? []; + const workingDirectories = this._containmentCandidates(entry.summary); const workspace = this._workspaceContextService.getWorkspace(); const folders = workspace.folders; const configuration = workspace.configuration; @@ -398,6 +402,23 @@ export class AgentHostSessionListStore extends Disposable { ); } + /** + * The directories a session may be matched against a workspace folder by: its + * working directories plus its server-owned project (repository) root. A + * worktree-isolated session runs out of a directory outside the repository + * (`.worktrees/` for agent-host worktrees, `copilot-worktrees/` + * for legacy extension-host ones), so working directories alone would hide it + * from a window opened on that repository; its project root is the primary + * repository root and restores the match. + */ + private _containmentCandidates(summary: SessionSummary): readonly URI[] { + const candidates = summary.workingDirectories?.map(directory => URI.parse(directory)) ?? []; + if (summary.project?.uri) { + candidates.push(URI.parse(summary.project.uri)); + } + return candidates; + } + private _toRemoval(entry: IAgentHostSessionListEntry): IAgentHostSessionListRemoval { return { provider: entry.provider, diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostChatContribution.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostChatContribution.test.ts index b5293cacee5..919d87d69e0 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostChatContribution.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostChatContribution.test.ts @@ -3316,6 +3316,41 @@ suite('AgentHostChatContribution', () => { assert.deepStrictEqual(listController.items.map(item => item.label), ['Contains folder']); }); + test('worktree session is shown in a window opened on its repository folder', async () => { + const { instantiationService, agentHostService } = createTestServices(disposables); + + const folder = URI.file('/src/repo'); + instantiationService.stub(IWorkspaceContextService, { + getWorkbenchState: () => WorkbenchState.FOLDER, + getWorkspace: () => ({ id: 'folder', folders: [{ uri: folder, name: 'repo', index: 0, toResource: () => folder }] }), + getWorkspaceFolder: () => null, + onDidChangeWorkspaceFolders: Event.None, + }); + + agentHostService.addSession({ + session: AgentSession.uri('copilot', 'worktree'), + startTime: 1000, + modifiedTime: 2000, + summary: 'Worktree session', + // A worktree lives in the `.worktrees` sibling, never under the folder. + workingDirectories: [URI.file('/src/repo.worktrees/feature')], + project: { uri: folder, displayName: 'repo' }, + }); + agentHostService.addSession({ + session: AgentSession.uri('copilot', 'other-repo-worktree'), + startTime: 1000, + modifiedTime: 2000, + summary: 'Other repo worktree session', + workingDirectories: [URI.file('/src/other.worktrees/feature')], + project: { uri: URI.file('/src/other'), displayName: 'other' }, + }); + + const listController = createSessionListController(disposables, instantiationService, agentHostService); + await listController.refresh(CancellationToken.None); + + assert.deepStrictEqual(listController.items.map(item => item.label), ['Worktree session']); + }); + test('sessionAdded notification filters out sessions outside the workspace', async () => { const { instantiationService, agentHostService } = createTestServices(disposables);