Skip to content

Commit f291f3f

Browse files
authored
editor: show session homes in breadcrumbs (#333404)
editor: use session homes in breadcrumbs Add URI home formatting so internal agent session paths render with stable provider labels instead of exposing session IDs. Register homes for Agent Host and Copilot CLI sessions, including resumed SDK artifact paths and pending Quick Chats.\n\nFixes #330410\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent ee9c82e commit f291f3f

21 files changed

Lines changed: 566 additions & 66 deletions

File tree

src/vs/editor/standalone/browser/standaloneServices.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -982,6 +982,10 @@ class StandaloneUriLabelService implements ILabelService {
982982
throw new Error('Not implemented');
983983
}
984984

985+
public getUriHome(): undefined {
986+
return undefined;
987+
}
988+
985989
public registerCachedFormatter(formatter: ResourceLabelFormatter): IDisposable {
986990
return this.registerFormatter(formatter);
987991
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { joinPath } from '../../../base/common/resources.js';
7+
import { URI } from '../../../base/common/uri.js';
8+
9+
export function workspacelessScratchDir(userHome: URI, sessionId: string): URI {
10+
return joinPath(userHome, '.copilot', 'chats', sessionId);
11+
}

src/vs/platform/agentHost/node/copilot/copilotAgent.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import { IInstantiationService } from '../../../instantiation/common/instantiati
3131
import { ILogService, LogLevel } from '../../../log/common/log.js';
3232
import { ITelemetryService } from '../../../telemetry/common/telemetry.js';
3333
import { INativeEnvironmentService } from '../../../../platform/environment/common/environment.js';
34-
import { workspacelessScratchDir } from '../workspacelessScratchDir.js';
34+
import { workspacelessScratchDir } from '../../common/workspacelessScratchDir.js';
3535
import { IAgentHostCheckpointService } from '../../common/agentHostCheckpointService.js';
3636
import type { IAgentHostClientTelemetryContext } from '../../common/agentHostTelemetry.js';
3737
import { IAgentHostReviewService } from '../../common/agentHostReviewService.js';

src/vs/platform/agentHost/node/workspacelessScratchDir.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,15 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import * as fs from 'fs/promises';
7-
import { joinPath } from '../../../base/common/resources.js';
87
import { URI } from '../../../base/common/uri.js';
8+
import { workspacelessScratchDir } from '../common/workspacelessScratchDir.js';
99

1010
/**
1111
* Stable, deterministic per-session scratch directory for a workspace-less
1212
* (workspace-less chat) session: `<userHome>/.copilot/chats/<sessionId>`. Shared by the
1313
* Copilot and Claude agents so both resolve the same cwd for a session that was
1414
* created with no `workingDirectory`.
1515
*/
16-
export function workspacelessScratchDir(userHome: URI, sessionId: string): URI {
17-
return joinPath(userHome, '.copilot', 'chats', sessionId);
18-
}
19-
2016
/** Ensures the workspace-less scratch dir exists (mkdir -p), returning it. */
2117
export async function ensureWorkspacelessScratchDir(userHome: URI, sessionId: string): Promise<URI> {
2218
const dir = workspacelessScratchDir(userHome, sessionId);

src/vs/platform/label/common/label.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ export interface ILabelService {
2828
getHostLabel(scheme: string, authority?: string): string;
2929
getHostTooltip(scheme: string, authority?: string): string | undefined;
3030
getSeparator(scheme: string, authority?: string): '/' | '\\';
31+
/** Returns the display home containing `resource`. */
32+
getUriHome(resource: URI): URI | undefined;
3133

3234
registerFormatter(formatter: ResourceLabelFormatter): IDisposable;
3335
readonly onDidChangeFormatters: Event<IFormatterChangeEvent>;
@@ -53,6 +55,8 @@ export interface IFormatterChangeEvent {
5355
export interface ResourceLabelFormatter {
5456
scheme: string;
5557
authority?: string;
58+
/** URI path used as a display home. Runtime registrations only. */
59+
home?: string;
5660
priority?: boolean;
5761
formatting: ResourceLabelFormatting;
5862
}

src/vs/sessions/LAYOUT.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ The durable state and transition catalog lives in [SINGLE_PANE_SCENARIOS.md](SIN
7676

7777
Editors must be opened through `IEditorService`. Sessions-specific presentation must not bypass editor service behavior by opening directly on an editor group.
7878

79+
Session providers register internal per-session directories as resource label homes. URI labels render as `<home label>/<relative path>`, and breadcrumbs render the same home label as their root segment. Without a matching home formatter, existing URI-label and breadcrumb behavior is unchanged.
80+
7981
## Custom views
8082

8183
`ICustomViewService` owns the active contributed full-surface view.

src/vs/sessions/contrib/providers/agentHost/browser/baseAgentHostSessionsProvider.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2536,8 +2536,9 @@ export abstract class BaseAgentHostSessionsProvider extends Disposable implement
25362536

25372537
protected readonly _onDidChangeSessions = this._register(new Emitter<ISessionChangeEvent>());
25382538
private readonly _onDidChangeSessionsFromNotifications = this._register(new Emitter<ISessionChangeEvent>());
2539-
private readonly _onDidChangeSessionsImmediately = Event.any(this._onDidChangeSessions.event, this._onDidChangeSessionsFromNotifications.event);
2539+
protected readonly _onDidChangeSessionsImmediately = Event.any(this._onDidChangeSessions.event, this._onDidChangeSessionsFromNotifications.event);
25402540
readonly onDidChangeSessions = debounceSessionChangeEvents(this._onDidChangeSessionsFromNotifications.event, this._onDidChangeSessions.event, this._store);
2541+
protected readonly _onDidChangeDraftSessions = this._register(new Emitter<void>());
25412542

25422543
protected readonly _onDidReplaceSession = this._register(new Emitter<{ readonly from: ISession; readonly to: ISession }>());
25432544
readonly onDidReplaceSession: Event<{ readonly from: ISession; readonly to: ISession }> = this._onDidReplaceSession.event;
@@ -2668,11 +2669,13 @@ export abstract class BaseAgentHostSessionsProvider extends Disposable implement
26682669
*/
26692670
protected _disposeAllNewSessions(): void {
26702671
this._newSessions.clearAndDisposeAll();
2672+
this._onDidChangeDraftSessions.fire();
26712673
}
26722674

26732675
deleteNewSession(sessionId: string): void {
26742676
if (this._newSessions.has(sessionId)) {
26752677
this._newSessions.deleteAndDispose(sessionId);
2678+
this._onDidChangeDraftSessions.fire();
26762679
}
26772680
}
26782681

@@ -3135,6 +3138,37 @@ export abstract class BaseAgentHostSessionsProvider extends Disposable implement
31353138
return sessions;
31363139
}
31373140

3141+
getResourceLabelHomes(): { readonly uri: URI; readonly label: string }[] {
3142+
const homes: { readonly uri: URI; readonly label: string }[] = [];
3143+
for (const session of this.getKnownSessions()) {
3144+
if (session.isQuickChat?.get()) {
3145+
const adapter = session instanceof AgentHostSessionAdapter ? session : undefined;
3146+
const label = this.getResourceLabelHomeLabel(session);
3147+
homes.push(...(adapter?.workingDirectories ?? []).map(uri => ({ uri, label })));
3148+
}
3149+
}
3150+
return homes;
3151+
}
3152+
3153+
protected getResourceLabelHomeLabel(session: ISession): string {
3154+
const providerLabel = this.sessionTypes.find(type => type.id === session.sessionType)?.label ?? session.sessionType;
3155+
return `${providerLabel}/${localize('sessionHome', "Session")}`;
3156+
}
3157+
3158+
protected getKnownSessions(): ISession[] {
3159+
const sessions = new Map<string, ISession>();
3160+
for (const session of this._sessionCache.values()) {
3161+
sessions.set(session.resource.toString(), session);
3162+
}
3163+
for (const newSession of this._newSessions.values()) {
3164+
sessions.set(newSession.session.resource.toString(), newSession.session);
3165+
}
3166+
if (this._pendingSession) {
3167+
sessions.set(this._pendingSession.resource.toString(), this._pendingSession);
3168+
}
3169+
return [...sessions.values()];
3170+
}
3171+
31383172
getSessionByResource(resource: URI): ISession | undefined {
31393173
for (const newSession of this._newSessions.values()) {
31403174
if (newSession.session.resource.toString() === resource.toString()) {
@@ -3258,6 +3292,7 @@ export abstract class BaseAgentHostSessionsProvider extends Disposable implement
32583292
throw err;
32593293
}
32603294
this._newSessions.set(newSession.sessionId, newSession);
3295+
this._onDidChangeDraftSessions.fire();
32613296
newSession.observeClientCustomAgents(activeClientScope.customAgents, () => {
32623297
this._onDidChangeCustomAgents.fire();
32633298
this._onDidChangeCustomizations.fire();
@@ -3787,6 +3822,7 @@ export abstract class BaseAgentHostSessionsProvider extends Disposable implement
37873822
clearSessionConfig(sessionId: string): void {
37883823
if (this._newSessions.has(sessionId)) {
37893824
this._newSessions.deleteAndDispose(sessionId);
3825+
this._onDidChangeDraftSessions.fire();
37903826
}
37913827
}
37923828

@@ -4723,6 +4759,7 @@ export abstract class BaseAgentHostSessionsProvider extends Disposable implement
47234759
newSession.graduate();
47244760
if (this._newSessions.get(newSession.sessionId) === newSession) {
47254761
this._newSessions.deleteAndDispose(newSession.sessionId);
4762+
this._onDidChangeDraftSessions.fire();
47264763
}
47274764
// Clear the pending session before firing the replace event so
47284765
// that any synchronous listener calling getSessions() sees only
@@ -4753,6 +4790,7 @@ export abstract class BaseAgentHostSessionsProvider extends Disposable implement
47534790
newSession.graduate();
47544791
if (this._newSessions.get(newSession.sessionId) === newSession) {
47554792
this._newSessions.deleteAndDispose(newSession.sessionId);
4793+
this._onDidChangeDraftSessions.fire();
47564794
}
47574795
this._onDidChangeSessions.fire({ added: [], removed: [skeleton], changed: [] });
47584796
throw new Error(localize('sessionNotCommitted', "Agent host session was not committed."));

src/vs/sessions/contrib/providers/agentHost/browser/localAgentHostSessionsProvider.ts

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@ import { DisposableStore, IDisposable } from '../../../../../base/common/lifecyc
1212
import { ResourceSet } from '../../../../../base/common/map.js';
1313
import { Schemas } from '../../../../../base/common/network.js';
1414
import { autorun, constObservable, IObservable } from '../../../../../base/common/observable.js';
15-
import { basename, dirname, isEqualOrParent, relativePath } from '../../../../../base/common/resources.js';
15+
import { basename, dirname, isEqualOrParent, joinPath, relativePath } from '../../../../../base/common/resources.js';
1616
import { ThemeIcon } from '../../../../../base/common/themables.js';
1717
import { URI } from '../../../../../base/common/uri.js';
1818
import { localize } from '../../../../../nls.js';
1919
import { type AgentHostUriMapper, LOCAL_AGENT_HOST_AUTHORITY, toAgentHostContentUri, toAgentHostUri } from '../../../../../platform/agentHost/common/agentHostUri.js';
20-
import { type IAgentSessionMetadata } from '../../../../../platform/agentHost/common/agent.js';
20+
import { AgentSession, type IAgentSessionMetadata } from '../../../../../platform/agentHost/common/agent.js';
2121
import { affectsAgentHostProviderPreference, IAgentConnection, IAgentHostService, shouldSurfaceLocalAgentHostProvider } from '../../../../../platform/agentHost/common/agentService.js';
22+
import { workspacelessScratchDir } from '../../../../../platform/agentHost/common/workspacelessScratchDir.js';
2223
import type { AgentCustomization, ISessionGitState } from '../../../../../platform/agentHost/common/state/sessionState.js';
2324
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
2425
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
@@ -34,13 +35,15 @@ import { IPreparedNewSession, ISessionsProviderAutomations, type ISessionsProvid
3435
import { WorkspaceNotTrustedError } from '../../../../services/sessions/common/sessionsManagement.js';
3536
import { IAgentHostActiveClientService } from '../../../../../workbench/contrib/chat/browser/agentSessions/agentHost/agentHostActiveClientService.js';
3637
import { IChatWidgetService } from '../../../../../workbench/contrib/chat/browser/chat.js';
37-
import { getCopilotCliSessionRawId, migratedCopilotCliResource } from '../../../../../workbench/contrib/chat/browser/copilotCliEventsUri.js';
38+
import { buildLocalSessionStateUri, getCopilotCliSessionRawId, migratedCopilotCliResource } from '../../../../../workbench/contrib/chat/browser/copilotCliEventsUri.js';
3839
import { adoptLegacyCopilotCliResource, isLegacyMigrationEnabledAtStartup, LEGACY_MIGRATION_RESTORE_TIMEOUT_MS, LEGACY_MIGRATION_TIMEOUT_MS } from '../../../../../workbench/contrib/chat/browser/agentSessions/agentHost/agentHostLegacyMigration.js';
3940
import { IChatService } from '../../../../../workbench/contrib/chat/common/chatService/chatService.js';
4041
import { IChatSessionsService } from '../../../../../workbench/contrib/chat/common/chatSessionsService.js';
4142
import { ILanguageModelsService, type ILanguageModelChatMetadata } from '../../../../../workbench/contrib/chat/common/languageModels.js';
4243
import { IWorkbenchEnvironmentService } from '../../../../../workbench/services/environment/common/environmentService.js';
4344
import { isAgentHostProvider, LOCAL_AGENT_HOST_PROVIDER_ID, type IAgentHostSessionsProvider } from '../../../../common/agentHostSessionsProvider.js';
45+
import { IPathService } from '../../../../../workbench/services/path/common/pathService.js';
46+
import { ResourceLabelHomeStore } from '../../../../../workbench/services/label/common/resourceLabelHomeStore.js';
4447
import { buildAgentHostSessionWorkspace, readBranchProtectionPatterns } from '../../../../common/agentHostSessionWorkspace.js';
4548
import { IDevContainerAgentHostService } from '../../../../common/devContainerAgentHostService.js';
4649
import { ChatModelSource, IGitHubInfo, ISession, ISessionWorkspace, ISessionWorkspaceBrowseAction, SESSION_WORKSPACE_GROUP_LOCAL } from '../../../../services/sessions/common/session.js';
@@ -118,6 +121,7 @@ export class LocalAgentHostSessionsProvider extends BaseAgentHostSessionsProvide
118121
private _automationSessionResources = new ResourceSet();
119122
private readonly _devContainerAvailableDrafts = new Set<string>();
120123
private readonly _devContainerDrafts = new Set<string>();
124+
private readonly _resourceLabelHomes: ResourceLabelHomeStore;
121125

122126
override get order(): number {
123127
return -1;
@@ -172,8 +176,10 @@ export class LocalAgentHostSessionsProvider extends BaseAgentHostSessionsProvide
172176
@IWorkspaceTrustRequestService private readonly _workspaceTrustRequestService: IWorkspaceTrustRequestService,
173177
@IDevContainerAgentHostService private readonly _devContainerAgentHostService: IDevContainerAgentHostService,
174178
@ISessionsProvidersService private readonly _sessionsProvidersService: ISessionsProvidersService,
179+
@IPathService pathService: IPathService,
175180
) {
176181
super(chatSessionsService, chatService, chatWidgetService, languageModelsService, _configurationService, logService, gitHubService, instantiationService, sessionsService, activeClientService, storageService, dialogService, workspaceTrustManagementService);
182+
this._resourceLabelHomes = this._register(instantiationService.createInstance(ResourceLabelHomeStore));
177183
const legacyAutomations = this._register(instantiationService.createInstance(AutomationStore, providerAutomationStorageKey(this.id)));
178184
const automations = this._register(instantiationService.createInstance(ReconnectableAgentHostAutomationStore, this.id, legacyAutomations, {
179185
toHost: resource => resource,
@@ -194,6 +200,35 @@ export class LocalAgentHostSessionsProvider extends BaseAgentHostSessionsProvide
194200
// started and the first `listSessions()` round-trip (gated on
195201
// authentication settling below) reconciles them.
196202
this._enableSessionCachePersistence(LOCAL_AGENT_HOST_CACHED_SESSIONS_STORAGE_KEY, LOCAL_AGENT_HOST_CACHED_SESSIONS_STORAGE_KEY_LEGACY);
203+
204+
const updateResourceLabelHomes = () => {
205+
const homes = this.getResourceLabelHomes();
206+
const userHome = pathService.userHome({ preferLocal: true });
207+
const sessionStateRoot = buildLocalSessionStateUri(userHome);
208+
for (const session of this.getKnownSessions()) {
209+
const rawId = AgentSession.id(session.resource);
210+
const label = this.getResourceLabelHomeLabel(session);
211+
if (session.isQuickChat?.get() && (session.sessionType === 'copilotcli' || session.sessionType === 'claude')) {
212+
homes.push({ uri: workspacelessScratchDir(userHome, rawId), label });
213+
}
214+
if (session.sessionType === 'copilotcli') {
215+
homes.push({ uri: joinPath(sessionStateRoot, rawId), label });
216+
for (const artifact of session.artifacts?.get() ?? []) {
217+
if (!artifact.uri || !isEqualOrParent(artifact.uri, sessionStateRoot)) {
218+
continue;
219+
}
220+
const artifactSessionId = relativePath(sessionStateRoot, artifact.uri)?.split('/')[0];
221+
if (artifactSessionId) {
222+
homes.push({ uri: joinPath(sessionStateRoot, artifactSessionId), label });
223+
}
224+
}
225+
}
226+
}
227+
this._resourceLabelHomes.set(homes);
228+
};
229+
this._register(this._onDidChangeSessionsImmediately(updateResourceLabelHomes));
230+
this._register(this._onDidChangeDraftSessions.event(updateResourceLabelHomes));
231+
updateResourceLabelHomes();
197232
this._register(autorun(reader => {
198233
this._automationSessionResources = new ResourceSet(this.automations.runs.read(reader).flatMap(run => run.sessionResource ? [run.sessionResource] : []));
199234
const changed = this.syncAutomationSessionMarkers(this._sessionCache.values());

0 commit comments

Comments
 (0)