microsoft/vscode · #333404

editor: show session homes in breadcrumbs

sandy081 · merged Aug 30, 202621 files · 566 + / 66
src/vs/editor/standalone/browser/standaloneServices.ts4 + / 0
@@ -982,6 +982,10 @@ class StandaloneUriLabelService implements ILabelService { 		throw new Error('Not implemented'); 	} +	public getUriHome(): undefined {+		return undefined;+	}+ 	public registerCachedFormatter(formatter: ResourceLabelFormatter): IDisposable { 		return this.registerFormatter(formatter); 	}
src/vs/platform/agentHost/common/workspacelessScratchDir.tsadded11 + / 0
@@ -0,0 +1,11 @@+/*---------------------------------------------------------------------------------------------+ *  Copyright (c) Microsoft Corporation. All rights reserved.+ *  Licensed under the MIT License. See License.txt in the project root for license information.+ *--------------------------------------------------------------------------------------------*/++import { joinPath } from '../../../base/common/resources.js';+import { URI } from '../../../base/common/uri.js';++export function workspacelessScratchDir(userHome: URI, sessionId: string): URI {+	return joinPath(userHome, '.copilot', 'chats', sessionId);+}
src/vs/platform/agentHost/node/copilot/copilotAgent.ts1 + / 1
@@ -31,7 +31,7 @@ import { IInstantiationService } from '../../../instantiation/common/instantiati import { ILogService, LogLevel } from '../../../log/common/log.js'; import { ITelemetryService } from '../../../telemetry/common/telemetry.js'; import { INativeEnvironmentService } from '../../../../platform/environment/common/environment.js';-import { workspacelessScratchDir } from '../workspacelessScratchDir.js';+import { workspacelessScratchDir } from '../../common/workspacelessScratchDir.js'; import { IAgentHostCheckpointService } from '../../common/agentHostCheckpointService.js'; import type { IAgentHostClientTelemetryContext } from '../../common/agentHostTelemetry.js'; import { IAgentHostReviewService } from '../../common/agentHostReviewService.js';
src/vs/platform/agentHost/node/workspacelessScratchDir.ts1 + / 5
@@ -4,19 +4,15 @@  *--------------------------------------------------------------------------------------------*/  import * as fs from 'fs/promises';-import { joinPath } from '../../../base/common/resources.js'; import { URI } from '../../../base/common/uri.js';+import { workspacelessScratchDir } from '../common/workspacelessScratchDir.js';  /**  * Stable, deterministic per-session scratch directory for a workspace-less  * (workspace-less chat) session: `<userHome>/.copilot/chats/<sessionId>`. Shared by the  * Copilot and Claude agents so both resolve the same cwd for a session that was  * created with no `workingDirectory`.  */-export function workspacelessScratchDir(userHome: URI, sessionId: string): URI {-	return joinPath(userHome, '.copilot', 'chats', sessionId);-}- /** Ensures the workspace-less scratch dir exists (mkdir -p), returning it. */ export async function ensureWorkspacelessScratchDir(userHome: URI, sessionId: string): Promise<URI> { 	const dir = workspacelessScratchDir(userHome, sessionId);
src/vs/platform/label/common/label.ts4 + / 0
@@ -28,6 +28,8 @@ export interface ILabelService { 	getHostLabel(scheme: string, authority?: string): string; 	getHostTooltip(scheme: string, authority?: string): string | undefined; 	getSeparator(scheme: string, authority?: string): '/' | '\\';+	/** Returns the display home containing `resource`. */+	getUriHome(resource: URI): URI | undefined;  	registerFormatter(formatter: ResourceLabelFormatter): IDisposable; 	readonly onDidChangeFormatters: Event<IFormatterChangeEvent>;@@ -53,6 +55,8 @@ export interface IFormatterChangeEvent { export interface ResourceLabelFormatter { 	scheme: string; 	authority?: string;+	/** URI path used as a display home. Runtime registrations only. */+	home?: string; 	priority?: boolean; 	formatting: ResourceLabelFormatting; }
src/vs/sessions/LAYOUT.md2 + / 0
@@ -76,6 +76,8 @@ The durable state and transition catalog lives in [SINGLE_PANE_SCENARIOS.md](SIN  Editors must be opened through `IEditorService`. Sessions-specific presentation must not bypass editor service behavior by opening directly on an editor group. +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.+ ## Custom views  `ICustomViewService` owns the active contributed full-surface view.
src/vs/sessions/contrib/providers/agentHost/browser/baseAgentHostSessionsProvider.ts39 + / 1
@@ -2536,8 +2536,9 @@ export abstract class BaseAgentHostSessionsProvider extends Disposable implement  	protected readonly _onDidChangeSessions = this._register(new Emitter<ISessionChangeEvent>()); 	private readonly _onDidChangeSessionsFromNotifications = this._register(new Emitter<ISessionChangeEvent>());-	private readonly _onDidChangeSessionsImmediately = Event.any(this._onDidChangeSessions.event, this._onDidChangeSessionsFromNotifications.event);+	protected readonly _onDidChangeSessionsImmediately = Event.any(this._onDidChangeSessions.event, this._onDidChangeSessionsFromNotifications.event); 	readonly onDidChangeSessions = debounceSessionChangeEvents(this._onDidChangeSessionsFromNotifications.event, this._onDidChangeSessions.event, this._store);+	protected readonly _onDidChangeDraftSessions = this._register(new Emitter<void>());  	protected readonly _onDidReplaceSession = this._register(new Emitter<{ readonly from: ISession; readonly to: ISession }>()); 	readonly onDidReplaceSession: Event<{ readonly from: ISession; readonly to: ISession }> = this._onDidReplaceSession.event;@@ -2668,11 +2669,13 @@ export abstract class BaseAgentHostSessionsProvider extends Disposable implement 	 */ 	protected _disposeAllNewSessions(): void { 		this._newSessions.clearAndDisposeAll();+		this._onDidChangeDraftSessions.fire(); 	}  	deleteNewSession(sessionId: string): void { 		if (this._newSessions.has(sessionId)) { 			this._newSessions.deleteAndDispose(sessionId);+			this._onDidChangeDraftSessions.fire(); 		} 	} @@ -3135,6 +3138,37 @@ export abstract class BaseAgentHostSessionsProvider extends Disposable implement 		return sessions; 	} +	getResourceLabelHomes(): { readonly uri: URI; readonly label: string }[] {+		const homes: { readonly uri: URI; readonly label: string }[] = [];+		for (const session of this.getKnownSessions()) {+			if (session.isQuickChat?.get()) {+				const adapter = session instanceof AgentHostSessionAdapter ? session : undefined;+				const label = this.getResourceLabelHomeLabel(session);+				homes.push(...(adapter?.workingDirectories ?? []).map(uri => ({ uri, label })));+			}+		}+		return homes;+	}++	protected getResourceLabelHomeLabel(session: ISession): string {+		const providerLabel = this.sessionTypes.find(type => type.id === session.sessionType)?.label ?? session.sessionType;+		return `${providerLabel}/${localize('sessionHome', "Session")}`;+	}++	protected getKnownSessions(): ISession[] {+		const sessions = new Map<string, ISession>();+		for (const session of this._sessionCache.values()) {+			sessions.set(session.resource.toString(), session);+		}+		for (const newSession of this._newSessions.values()) {+			sessions.set(newSession.session.resource.toString(), newSession.session);+		}+		if (this._pendingSession) {+			sessions.set(this._pendingSession.resource.toString(), this._pendingSession);+		}+		return [...sessions.values()];+	}+ 	getSessionByResource(resource: URI): ISession | undefined { 		for (const newSession of this._newSessions.values()) { 			if (newSession.session.resource.toString() === resource.toString()) {@@ -3258,6 +3292,7 @@ export abstract class BaseAgentHostSessionsProvider extends Disposable implement 			throw err; 		} 		this._newSessions.set(newSession.sessionId, newSession);+		this._onDidChangeDraftSessions.fire(); 		newSession.observeClientCustomAgents(activeClientScope.customAgents, () => { 			this._onDidChangeCustomAgents.fire(); 			this._onDidChangeCustomizations.fire();@@ -3787,6 +3822,7 @@ export abstract class BaseAgentHostSessionsProvider extends Disposable implement 	clearSessionConfig(sessionId: string): void { 		if (this._newSessions.has(sessionId)) { 			this._newSessions.deleteAndDispose(sessionId);+			this._onDidChangeDraftSessions.fire(); 		} 	} @@ -4723,6 +4759,7 @@ export abstract class BaseAgentHostSessionsProvider extends Disposable implement 				newSession.graduate(); 				if (this._newSessions.get(newSession.sessionId) === newSession) { 					this._newSessions.deleteAndDispose(newSession.sessionId);+					this._onDidChangeDraftSessions.fire(); 				} 				// Clear the pending session before firing the replace event so 				// that any synchronous listener calling getSessions() sees only@@ -4753,6 +4790,7 @@ export abstract class BaseAgentHostSessionsProvider extends Disposable implement 		newSession.graduate(); 		if (this._newSessions.get(newSession.sessionId) === newSession) { 			this._newSessions.deleteAndDispose(newSession.sessionId);+			this._onDidChangeDraftSessions.fire(); 		} 		this._onDidChangeSessions.fire({ added: [], removed: [skeleton], changed: [] }); 		throw new Error(localize('sessionNotCommitted', "Agent host session was not committed."));
src/vs/sessions/contrib/providers/agentHost/browser/localAgentHostSessionsProvider.ts38 + / 3
@@ -12,13 +12,14 @@ import { DisposableStore, IDisposable } from '../../../../../base/common/lifecyc import { ResourceSet } from '../../../../../base/common/map.js'; import { Schemas } from '../../../../../base/common/network.js'; import { autorun, constObservable, IObservable } from '../../../../../base/common/observable.js';-import { basename, dirname, isEqualOrParent, relativePath } from '../../../../../base/common/resources.js';+import { basename, dirname, isEqualOrParent, joinPath, relativePath } from '../../../../../base/common/resources.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { URI } from '../../../../../base/common/uri.js'; import { localize } from '../../../../../nls.js'; import { type AgentHostUriMapper, LOCAL_AGENT_HOST_AUTHORITY, toAgentHostContentUri, toAgentHostUri } from '../../../../../platform/agentHost/common/agentHostUri.js';-import { type IAgentSessionMetadata } from '../../../../../platform/agentHost/common/agent.js';+import { AgentSession, type IAgentSessionMetadata } from '../../../../../platform/agentHost/common/agent.js'; import { affectsAgentHostProviderPreference, IAgentConnection, IAgentHostService, shouldSurfaceLocalAgentHostProvider } from '../../../../../platform/agentHost/common/agentService.js';+import { workspacelessScratchDir } from '../../../../../platform/agentHost/common/workspacelessScratchDir.js'; import type { AgentCustomization, ISessionGitState } from '../../../../../platform/agentHost/common/state/sessionState.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';@@ -34,13 +35,15 @@ import { IPreparedNewSession, ISessionsProviderAutomations, type ISessionsProvid import { WorkspaceNotTrustedError } from '../../../../services/sessions/common/sessionsManagement.js'; import { IAgentHostActiveClientService } from '../../../../../workbench/contrib/chat/browser/agentSessions/agentHost/agentHostActiveClientService.js'; import { IChatWidgetService } from '../../../../../workbench/contrib/chat/browser/chat.js';-import { getCopilotCliSessionRawId, migratedCopilotCliResource } from '../../../../../workbench/contrib/chat/browser/copilotCliEventsUri.js';+import { buildLocalSessionStateUri, getCopilotCliSessionRawId, migratedCopilotCliResource } from '../../../../../workbench/contrib/chat/browser/copilotCliEventsUri.js'; import { adoptLegacyCopilotCliResource, isLegacyMigrationEnabledAtStartup, LEGACY_MIGRATION_RESTORE_TIMEOUT_MS, LEGACY_MIGRATION_TIMEOUT_MS } from '../../../../../workbench/contrib/chat/browser/agentSessions/agentHost/agentHostLegacyMigration.js'; import { IChatService } from '../../../../../workbench/contrib/chat/common/chatService/chatService.js'; import { IChatSessionsService } from '../../../../../workbench/contrib/chat/common/chatSessionsService.js'; import { ILanguageModelsService, type ILanguageModelChatMetadata } from '../../../../../workbench/contrib/chat/common/languageModels.js'; import { IWorkbenchEnvironmentService } from '../../../../../workbench/services/environment/common/environmentService.js'; import { isAgentHostProvider, LOCAL_AGENT_HOST_PROVIDER_ID, type IAgentHostSessionsProvider } from '../../../../common/agentHostSessionsProvider.js';+import { IPathService } from '../../../../../workbench/services/path/common/pathService.js';+import { ResourceLabelHomeStore } from '../../../../../workbench/services/label/common/resourceLabelHomeStore.js'; import { buildAgentHostSessionWorkspace, readBranchProtectionPatterns } from '../../../../common/agentHostSessionWorkspace.js'; import { IDevContainerAgentHostService } from '../../../../common/devContainerAgentHostService.js'; 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 	private _automationSessionResources = new ResourceSet(); 	private readonly _devContainerAvailableDrafts = new Set<string>(); 	private readonly _devContainerDrafts = new Set<string>();+	private readonly _resourceLabelHomes: ResourceLabelHomeStore;  	override get order(): number { 		return -1;@@ -172,8 +176,10 @@ export class LocalAgentHostSessionsProvider extends BaseAgentHostSessionsProvide 		@IWorkspaceTrustRequestService private readonly _workspaceTrustRequestService: IWorkspaceTrustRequestService, 		@IDevContainerAgentHostService private readonly _devContainerAgentHostService: IDevContainerAgentHostService, 		@ISessionsProvidersService private readonly _sessionsProvidersService: ISessionsProvidersService,+		@IPathService pathService: IPathService, 	) { 		super(chatSessionsService, chatService, chatWidgetService, languageModelsService, _configurationService, logService, gitHubService, instantiationService, sessionsService, activeClientService, storageService, dialogService, workspaceTrustManagementService);+		this._resourceLabelHomes = this._register(instantiationService.createInstance(ResourceLabelHomeStore)); 		const legacyAutomations = this._register(instantiationService.createInstance(AutomationStore, providerAutomationStorageKey(this.id))); 		const automations = this._register(instantiationService.createInstance(ReconnectableAgentHostAutomationStore, this.id, legacyAutomations, { 			toHost: resource => resource,@@ -194,6 +200,35 @@ export class LocalAgentHostSessionsProvider extends BaseAgentHostSessionsProvide 		// started and the first `listSessions()` round-trip (gated on 		// authentication settling below) reconciles them. 		this._enableSessionCachePersistence(LOCAL_AGENT_HOST_CACHED_SESSIONS_STORAGE_KEY, LOCAL_AGENT_HOST_CACHED_SESSIONS_STORAGE_KEY_LEGACY);++		const updateResourceLabelHomes = () => {+			const homes = this.getResourceLabelHomes();+			const userHome = pathService.userHome({ preferLocal: true });+			const sessionStateRoot = buildLocalSessionStateUri(userHome);+			for (const session of this.getKnownSessions()) {+				const rawId = AgentSession.id(session.resource);+				const label = this.getResourceLabelHomeLabel(session);+				if (session.isQuickChat?.get() && (session.sessionType === 'copilotcli' || session.sessionType === 'claude')) {+					homes.push({ uri: workspacelessScratchDir(userHome, rawId), label });+				}+				if (session.sessionType === 'copilotcli') {+					homes.push({ uri: joinPath(sessionStateRoot, rawId), label });+					for (const artifact of session.artifacts?.get() ?? []) {+						if (!artifact.uri || !isEqualOrParent(artifact.uri, sessionStateRoot)) {+							continue;+						}+						const artifactSessionId = relativePath(sessionStateRoot, artifact.uri)?.split('/')[0];+						if (artifactSessionId) {+							homes.push({ uri: joinPath(sessionStateRoot, artifactSessionId), label });+						}+					}+				}+			}+			this._resourceLabelHomes.set(homes);+		};+		this._register(this._onDidChangeSessionsImmediately(updateResourceLabelHomes));+		this._register(this._onDidChangeDraftSessions.event(updateResourceLabelHomes));+		updateResourceLabelHomes(); 		this._register(autorun(reader => { 			this._automationSessionResources = new ResourceSet(this.automations.runs.read(reader).flatMap(run => run.sessionResource ? [run.sessionResource] : [])); 			const changed = this.syncAutomationSessionMarkers(this._sessionCache.values());
src/vs/sessions/contrib/providers/agentHost/test/browser/localAgentHostSessionsProvider.test.ts84 + / 4
@@ -60,6 +60,9 @@ import { IPullRequestIconCache, PullRequestIconCache } from '../../../../github/ import { computePullRequestIcon, GitHubPullRequestState, type IGitHubPullRequest } from '../../../../github/common/types.js'; import { IWorkbenchEnvironmentService } from '../../../../../../workbench/services/environment/common/environmentService.js'; import { IAgentHostSessionsProvider } from '../../../../../common/agentHostSessionsProvider.js';+import { IPathService } from '../../../../../../workbench/services/path/common/pathService.js';+import { MockLabelService } from '../../../../../../workbench/services/label/test/common/mockLabelService.js';+import { TestPathService } from '../../../../../../workbench/test/browser/workbenchTestServices.js';  // ---- Mock IAgentHostService ------------------------------------------------- @@ -438,7 +441,7 @@ function createSchemaDefaultConfigurationService(): TestConfigurationService {  function createProvider(disposables: DisposableStore, agentHostService: MockAgentHostService, contributions = [ 	{ type: 'agent-host-copilotcli', name: 'copilot', displayName: 'Copilot', description: 'test', icon: undefined },-], options?: { sendRequest?: (resource: URI, message: string, options?: IChatSendRequestOptions) => Promise<ChatSendResult>; acquireOrLoadSession?: (resource: URI) => Promise<IChatModelReference | undefined>; languageModelIds?: string[]; lookupLanguageModel?: (modelId: string) => ILanguageModelChatMetadata | undefined; hiddenLanguageModelIds?: ReadonlySet<string>; languageModelVisibilityChanges?: Event<void>; openSession?: boolean; configurationService?: IConfigurationService; activeSession?: IObservable<IActiveSession | undefined>; visibleSessions?: IObservable<readonly (IActiveSession | undefined)[]>; activeClient?: Omit<SessionActiveClient, 'clientId'>; activeClientAgents?: IObservable<readonly AgentCustomization[]>; activeClientScope?: (sessionType: string, roots: readonly URI[]) => IAgentCustomizationScope; storageService?: IStorageService; isSessionsWindow?: boolean; confirmDelete?: boolean; workspaceTrusted?: boolean; requestWorkspaceTrust?: (uri: URI) => Promise<boolean>; workspaceTrustBarrier?: DeferredPromise<void>; workspaceTrustError?: Error; setUrisTrust?: (uris: URI[], trusted: boolean) => Promise<void>; gitHubService?: IGitHubService; devContainerAgentHostService?: IDevContainerAgentHostService; sessionsProvidersService?: ISessionsProvidersService }): LocalAgentHostSessionsProvider {+], options?: { sendRequest?: (resource: URI, message: string, options?: IChatSendRequestOptions) => Promise<ChatSendResult>; acquireOrLoadSession?: (resource: URI) => Promise<IChatModelReference | undefined>; languageModelIds?: string[]; lookupLanguageModel?: (modelId: string) => ILanguageModelChatMetadata | undefined; hiddenLanguageModelIds?: ReadonlySet<string>; languageModelVisibilityChanges?: Event<void>; openSession?: boolean; configurationService?: IConfigurationService; activeSession?: IObservable<IActiveSession | undefined>; visibleSessions?: IObservable<readonly (IActiveSession | undefined)[]>; activeClient?: Omit<SessionActiveClient, 'clientId'>; activeClientAgents?: IObservable<readonly AgentCustomization[]>; activeClientScope?: (sessionType: string, roots: readonly URI[]) => IAgentCustomizationScope; storageService?: IStorageService; isSessionsWindow?: boolean; confirmDelete?: boolean; workspaceTrusted?: boolean; requestWorkspaceTrust?: (uri: URI) => Promise<boolean>; workspaceTrustBarrier?: DeferredPromise<void>; workspaceTrustError?: Error; setUrisTrust?: (uris: URI[], trusted: boolean) => Promise<void>; gitHubService?: IGitHubService; devContainerAgentHostService?: IDevContainerAgentHostService; sessionsProvidersService?: ISessionsProvidersService; pathService?: IPathService; labelService?: ILabelService }): LocalAgentHostSessionsProvider { 	const instantiationService = disposables.add(new TestInstantiationService());  	instantiationService.stub(IAgentHostService, agentHostService);@@ -485,9 +488,7 @@ function createProvider(disposables: DisposableStore, agentHostService: MockAgen 		onDidChangeLanguageModels: Event.None, 		onDidChangeModelVisibility: options?.languageModelVisibilityChanges ?? Event.None, 	});-	instantiationService.stub(ILabelService, {-		getUriLabel: (uri: URI) => uri.path,-	});+	instantiationService.stub(ILabelService, options?.labelService ?? new MockLabelService()); 	instantiationService.stub(ILogService, new NullLogService()); 	const storageService = options?.storageService ?? disposables.add(new InMemoryStorageService()); 	instantiationService.stub(IStorageService, storageService);@@ -498,6 +499,7 @@ function createProvider(disposables: DisposableStore, agentHostService: MockAgen 		override findPullRequestNumberByHeadBranch = async () => undefined; 	}()); 	instantiationService.stub(IPullRequestIconCache, instantiationService.createInstance(PullRequestIconCache));+	instantiationService.stub(IPathService, options?.pathService ?? new TestPathService(URI.file('/home/test'))); 	const activeSessionObs = options?.activeSession ?? constObservable<IActiveSession | undefined>(undefined); 	const visibleSessionsObs = options?.visibleSessions ?? constObservable<readonly (IActiveSession | undefined)[]>([]); 	instantiationService.stub(ISessionsService, new class extends mock<ISessionsService>() {@@ -5722,6 +5724,84 @@ suite('LocalAgentHostSessionsProvider', () => { 		}); 	})); +	test('registers provider-neutral resource label homes for quick chats and provider state', () => runWithFakedTimers<void>({ useFakeTimers: true }, async () => {+		const claudeHome = URI.file('/home/test/.agent/chats/claude-session');+		agentHost.addSession(createSession('claude-session', { provider: 'claude', summary: 'Claude Quick Chat', quickChat: true, workingDirectory: claudeHome }));+		agentHost.addSession(createSession('copilot-session', { summary: 'Copilot Session' }));+		const pathService = new TestPathService(URI.file('/home/test'));+		const labelService = new MockLabelService();++		const provider = createProvider(disposables, agentHost, undefined, { pathService, labelService });+		provider.getSessions();+		await timeout(0);+		const getHomeLabel = (resource: URI): string | undefined => {+			const home = labelService.getUriHome(resource);+			return home ? labelService.getUriLabel(home) : undefined;+		};++		assert.deepStrictEqual({+			quickChat: getHomeLabel(URI.joinPath(claudeHome, 'artifact.md')),+			copilotState: getHomeLabel(URI.file('/home/test/.copilot/session-state/copilot-session/artifact.md')),+		}, {+			quickChat: 'claude/Session',+			copilotState: 'Copilot/Session',+		});++		provider.dispose();+		assert.deepStrictEqual({+			quickChat: labelService.getUriHome(URI.joinPath(claudeHome, 'artifact.md')),+			copilotState: labelService.getUriHome(URI.file('/home/test/.copilot/session-state/copilot-session/artifact.md')),+		}, {+			quickChat: undefined,+			copilotState: undefined,+		});+	}));++	test('registers the session state home before a quick chat is materialized', () => {+		const pathService = new TestPathService(URI.file('/home/test'));+		const labelService = new MockLabelService();+		const provider = createProvider(disposables, agentHost, undefined, { pathService, labelService });++		const session = provider.createQuickChat(provider.sessionTypes[0].id);+		const rawId = AgentSession.id(session.resource);+		const resource = URI.file(`/home/test/.copilot/chats/${rawId}/files/plan.md`);++		const home = labelService.getUriHome(resource);+		assert.deepStrictEqual({+			home: home?.toString(),+			label: home ? labelService.getUriLabel(home) : undefined,+		}, {+			home: URI.file(`/home/test/.copilot/chats/${rawId}`).toString(),+			label: 'Copilot/Session',+		});+	});++	test('registers the SDK session state home from recorded artifacts', () => runWithFakedTimers<void>({ useFakeTimers: true }, async () => {+		const metadata = createSession('ahp-session', { summary: 'Artifact Session' });+		agentHost.addSession({+			...metadata,+			_meta: withSessionArtifacts(metadata._meta, [{+				id: 'artifact',+				type: SessionArtifactType.File,+				label: 'Plan',+				isArtifact: true,+				uri: 'file:///home/test/.copilot/session-state/sdk-session/files/plan.md',+			}])+		});+		const labelService = new MockLabelService();+		const provider = createProvider(disposables, agentHost, undefined, {+			pathService: new TestPathService(URI.file('/home/test')),+			labelService,+		});+		provider.getSessions();+		await timeout(0);++		assert.strictEqual(+			labelService.getUriHome(URI.file('/home/test/.copilot/session-state/sdk-session/files/plan.md'))?.toString(),+			URI.file('/home/test/.copilot/session-state/sdk-session').toString()+		);+	}));+ 	test('session adapter uses raw ID as fallback title', () => runWithFakedTimers<void>({ useFakeTimers: true }, async () => { 		agentHost.addSession(createSession('abcdef1234567890')); 
src/vs/sessions/contrib/providers/copilotChatSessions/browser/copilotChatSessionsProvider.ts21 + / 0
@@ -54,6 +54,9 @@ import { structuralEquals } from '../../../../../base/common/equals.js'; import { CopilotCLISessionType } from '../../agentHost/browser/baseAgentHostSessionsProvider.js'; import { createChangesets } from './copilotChatSessionsChangesets.js'; import { IUriIdentityService } from '../../../../../platform/uriIdentity/common/uriIdentity.js';+import { IPathService } from '../../../../../workbench/services/path/common/pathService.js';+import { ResourceLabelHomeStore } from '../../../../../workbench/services/label/common/resourceLabelHomeStore.js';+import { buildLocalSessionStateUri } from '../../../../../workbench/contrib/chat/browser/copilotCliEventsUri.js'; import { IAgentHostEnablementService } from '../../../../../platform/agentHost/common/agentHostEnablementService.js'; import { isCloudSandboxEnabled } from '../../../../../platform/agentHost/common/cloudSandboxAgentHost.js'; import { getWorkbenchContribution } from '../../../../../workbench/common/contributions.js';@@ -1422,6 +1425,7 @@ export class CopilotChatSessionsProvider extends Disposable implements ISessions  	/** Cache of adapted sessions, keyed by resource URI string. */ 	private readonly _sessionCache = new Map<string, AgentSessionAdapter | CopilotCLISession | RemoteNewSession>();+	private readonly _resourceLabelHomes: ResourceLabelHomeStore;  	/** 	 * Resources of committed sessions that are currently in-flight (i.e.@@ -1500,11 +1504,14 @@ export class CopilotChatSessionsProvider extends Disposable implements ISessions 		@ILabelService private readonly labelService: ILabelService, 		@IChatModeService private readonly chatModeService: IChatModeService, 		@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,+		@IPathService private readonly pathService: IPathService, 		@IGitService private readonly gitService: IGitService, 	) { 		super();+		this._resourceLabelHomes = this._register(this.instantiationService.createInstance(ResourceLabelHomeStore));  		this._multiChatEnabled = this.configurationService.getValue<boolean>(COPILOT_MULTI_CHAT_SETTING) ?? true;+		this._register(this._onDidChangeSessions.event(() => this._updateResourceLabelHomes()));  		this._register(runOnChange(this.agentHostEnablementService.enabled, () => { 			this._onDidChangeSessionTypes.fire();@@ -2963,6 +2970,20 @@ export class CopilotChatSessionsProvider extends Disposable implements ISessions 		} 	} +	private _updateResourceLabelHomes(): void {+		const sessionStateRoot = buildLocalSessionStateUri(this.pathService.userHome({ preferLocal: true }));+		const homes: { readonly uri: URI; readonly label: string }[] = [];+		for (const session of this._sessionCache.values()) {+			if (session.sessionType === SessionType.CopilotCLI) {+				const rawId = session.resource.path.replace(/^\//, '');+				if (rawId) {+					homes.push({ uri: URI.joinPath(sessionStateRoot, rawId), label: `${CopilotCLISessionType.label}/${localize('sessionHome', "Session")}` });+				}+			}+		}+		this._resourceLabelHomes.set(homes);+	}+ 	private _refreshSessionCacheMultiChat( 		addedData: ICopilotChatSession[], 		removedData: ICopilotChatSession[],
src/vs/sessions/contrib/providers/copilotChatSessions/test/browser/copilotChatSessionsProvider.test.ts22 + / 8
@@ -44,6 +44,9 @@ import { ChatConfiguration, ChatPermissionLevel } from '../../../../../../workbe import { CopilotChatSessionsProvider, COPILOT_PROVIDER_ID, CopilotCloudSessionType, ICopilotChatSession } from '../../browser/copilotChatSessionsProvider.js'; import { ILogService, NullLogService } from '../../../../../../platform/log/common/log.js'; import { ILabelService } from '../../../../../../platform/label/common/label.js';+import { IPathService } from '../../../../../../workbench/services/path/common/pathService.js';+import { MockLabelService } from '../../../../../../workbench/services/label/test/common/mockLabelService.js';+import { TestPathService } from '../../../../../../workbench/test/browser/workbenchTestServices.js'; import { IUriIdentityService } from '../../../../../../platform/uriIdentity/common/uriIdentity.js'; import { extUri } from '../../../../../../base/common/resources.js'; import { CopilotCLISessionType } from '../../../agentHost/browser/baseAgentHostSessionsProvider.js';@@ -240,7 +243,7 @@ function createProviderWithConfig( 	disposables: DisposableStore, 	model: MockAgentSessionsModel, 	opts?: ICreateProviderOptions,-): { provider: CopilotChatSessionsProvider; configService: TestConfigurationService; agentHostEnabled: ISettableObservable<boolean> } {+): { provider: CopilotChatSessionsProvider; configService: TestConfigurationService; agentHostEnabled: ISettableObservable<boolean>; labelService: MockLabelService } { 	const instantiationService = disposables.add(new TestInstantiationService());  	const configService = new TestConfigurationService();@@ -304,16 +307,16 @@ function createProviderWithConfig( 	}); 	// Stub IInstantiationService so provider can use createInstance for CopilotCLISession 	instantiationService.stub(IInstantiationService, instantiationService);-	instantiationService.stub(ILabelService, {-		getUriLabel: (uri: URI) => uri.path,-	});+	const labelService = new MockLabelService();+	instantiationService.stub(ILabelService, labelService);+	instantiationService.stub(IPathService, new TestPathService(URI.file('/home/test'))); 	instantiationService.stub(IUriIdentityService, { extUri }); 	instantiationService.stub(IGitService, opts?.gitService ?? { repositories: [], openRepository: async () => undefined }); 	instantiationService.stub(IGitHubService, opts?.gitHubService ?? new TestGitHubService()); 	instantiationService.stub(IPullRequestIconCache, opts?.pullRequestIconCache ?? new TestPullRequestIconCache());  	const provider = disposables.add(instantiationService.createInstance(CopilotChatSessionsProvider));-	return { provider, configService, agentHostEnabled };+	return { provider, configService, agentHostEnabled, labelService }; }  // ---- Provider factory for send/cancel tests ---------------------------------@@ -394,9 +397,8 @@ function createProviderForSendTests( 	instantiationService.stub(ILanguageModelToolsService, { toToolReferences: () => [] }); 	instantiationService.stub(IGitService, { openRepository: async () => undefined }); 	instantiationService.stub(IInstantiationService, instantiationService);-	instantiationService.stub(ILabelService, {-		getUriLabel: (uri: URI) => uri.path,-	});+	instantiationService.stub(ILabelService, new MockLabelService());+	instantiationService.stub(IPathService, new TestPathService(URI.file('/home/test'))); 	instantiationService.stub(IUriIdentityService, { extUri }); 	instantiationService.stub(IAgentHostEnablementService, { _serviceBrand: undefined, enabled: constObservable(opts?.agentHostEnabled ?? true), managedSandboxEnforced: constObservable(false) }); 	instantiationService.stub(IContextKeyService, new MockContextKeyService());@@ -494,6 +496,18 @@ suite('CopilotChatSessionsProvider', () => { 		assert.strictEqual(sessions.length, 2); 	}); +	test('registers Copilot CLI session state directories as resource label homes', () => {+		const resource = URI.from({ scheme: AgentSessionProviders.Background, path: '/session-1' });+		model.addSession(createMockAgentSession(resource));++		const { labelService } = createProviderWithConfig(disposables, model);++		assert.strictEqual(+			labelService.getUriHome(URI.file('/home/test/.copilot/session-state/session-1/artifact.md'))?.toString(),+			URI.file('/home/test/.copilot/session-state/session-1').toString()+		);+	});+ 	test('getSessions does not emit session changes while reading the initial cache', () => { 		const resource = URI.from({ scheme: AgentSessionProviders.Background, path: '/session' }); 		model.addSession(createMockAgentSession(resource));
src/vs/sessions/contrib/providers/remoteAgentHost/browser/remoteAgentHostSessionsProvider.ts11 + / 0
@@ -34,6 +34,7 @@ import { IChatWidgetService } from '../../../../../workbench/contrib/chat/browse import { IChatService } from '../../../../../workbench/contrib/chat/common/chatService/chatService.js'; import { IChatSessionsService } from '../../../../../workbench/contrib/chat/common/chatSessionsService.js'; import { ILanguageModelsService } from '../../../../../workbench/contrib/chat/common/languageModels.js';+import { ResourceLabelHomeStore } from '../../../../../workbench/services/label/common/resourceLabelHomeStore.js'; import { IAgentHostConnectProgress, IAgentHostGroup } from '../../../../common/agentHostSessionsProvider.js'; import { buildAgentHostSessionWorkspace, readBranchProtectionPatterns } from '../../../../common/agentHostSessionWorkspace.js'; import { IGitHubInfo, ISession, ISessionType, ISessionWorkspace, ISessionWorkspaceBrowseAction, SESSION_WORKSPACE_GROUP_REMOTE } from '../../../../services/sessions/common/session.js';@@ -170,6 +171,7 @@ export class RemoteAgentHostSessionsProvider extends BaseAgentHostSessionsProvid 	private _connection: IAgentConnection | undefined; 	private _defaultDirectory: string | undefined; 	private readonly _connectionListeners = this._register(new DisposableStore());+	private readonly _resourceLabelHomes: ResourceLabelHomeStore; 	private readonly _connectionAuthority: string; 	private readonly _connectOnDemand: (() => Promise<void>) | undefined; 	private readonly _disconnectOnDemand: (() => Promise<void>) | undefined;@@ -210,6 +212,7 @@ export class RemoteAgentHostSessionsProvider extends BaseAgentHostSessionsProvid 		@IWorkspaceTrustManagementService workspaceTrustManagementService: IWorkspaceTrustManagementService, 	) { 		super(chatSessionsService, chatService, chatWidgetService, languageModelsService, _configurationService, logService, gitHubService, instantiationService, sessionsService, activeClientService, storageService, dialogService, workspaceTrustManagementService);+		this._resourceLabelHomes = this._register(instantiationService.createInstance(ResourceLabelHomeStore));  		this._connectionAuthority = agentHostAuthority(config.address); 		this._connectOnDemand = config.connectOnDemand;@@ -220,6 +223,13 @@ export class RemoteAgentHostSessionsProvider extends BaseAgentHostSessionsProvid 		this._defaultChangesetKind = config.defaultChangesetKind; 		this.onDidReportConnectProgress = config.onDidReportConnectProgress; 		this.canConnectOnDemand = !!config.connectOnDemand;+		const updateResourceLabelHomes = () => {+			const homes = this.getResourceLabelHomes();+			this._resourceLabelHomes.set(homes);+		};+		this._register(this._onDidChangeSessionsImmediately(updateResourceLabelHomes));+		this._register(this._onDidChangeDraftSessions.event(updateResourceLabelHomes));+		updateResourceLabelHomes(); 		const displayName = config.name || config.address;  		this.id = `agenthost-${this._connectionAuthority}`;@@ -252,6 +262,7 @@ export class RemoteAgentHostSessionsProvider extends BaseAgentHostSessionsProvid 		}];  		this._enableSessionCachePersistence(this._storageKey, `${CACHED_SESSIONS_STORAGE_PREFIX_LEGACY}${this._connectionAuthority}`);+		updateResourceLabelHomes(); 		this._register(this._configurationService.onDidChangeConfiguration(e => { 			if (e.affectsConfiguration('git.branchProtection')) { 				this._refreshSessionWorkspaces();
src/vs/sessions/contrib/providers/remoteAgentHost/test/browser/remoteAgentHostSessionsProvider.test.ts2 + / 3
@@ -46,6 +46,7 @@ import { CopilotCLISessionType } from '../../../agentHost/browser/baseAgentHostS import { IObservable, constObservable } from '../../../../../../base/common/observable.js'; import { IActiveSession } from '../../../../../services/sessions/common/sessionsManagement.js'; import { ISessionsService } from '../../../../../services/sessions/browser/sessionsService.js';+import { MockLabelService } from '../../../../../../workbench/services/label/test/common/mockLabelService.js';  // ---- Mock connection -------------------------------------------------------- @@ -261,9 +262,7 @@ function createProvider(disposables: DisposableStore, connection: MockAgentConne 	}); 	instantiationService.stub(IStorageService, overrides?.storageService ?? disposables.add(new InMemoryStorageService())); 	instantiationService.stub(IProgressService, {});-	instantiationService.stub(ILabelService, {-		getUriLabel: (uri: URI) => uri.path,-	});+	instantiationService.stub(ILabelService, new MockLabelService()); 	instantiationService.stub(ILogService, new NullLogService()); 	instantiationService.stub(IGitHubService, new class extends mock<IGitHubService>() { 		override findPullRequestNumberByHeadBranch = async () => undefined;
src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts24 + / 4
@@ -12,6 +12,7 @@ import { dirname, isEqual } from '../../../../base/common/resources.js'; import { URI } from '../../../../base/common/uri.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { FileKind } from '../../../../platform/files/common/files.js';+import { ILabelService } from '../../../../platform/label/common/label.js'; import { IWorkspaceContextService, IWorkspaceFolder, WorkbenchState } from '../../../../platform/workspace/common/workspace.js'; import { BreadcrumbsConfig } from './breadcrumbs.js'; import { IEditorPane } from '../../../common/editor.js';@@ -30,7 +31,7 @@ export class FileElement { 	} } -type FileInfo = { path: FileElement[]; folder?: IWorkspaceFolder };+type FileInfo = { path: FileElement[]; folder?: IWorkspaceFolder; home?: URI };  export class OutlineElement2 { 	constructor(@@ -60,13 +61,19 @@ export class BreadcrumbsModel { 		@IWorkspaceContextService private readonly _workspaceService: IWorkspaceContextService, 		@IWorkspaceFolderLabelService private readonly _workspaceFolderLabelService: IWorkspaceFolderLabelService, 		@IOutlineService private readonly _outlineService: IOutlineService,+		@ILabelService private readonly _labelService: ILabelService, 	) { 		this._cfgFilePath = BreadcrumbsConfig.FilePath.bindTo(configurationService); 		this._cfgSymbolPath = BreadcrumbsConfig.SymbolPath.bindTo(configurationService);  		this._disposables.add(this._cfgFilePath.onDidChange(_ => this._onDidUpdate.fire(this))); 		this._disposables.add(this._cfgSymbolPath.onDidChange(_ => this._onDidUpdate.fire(this))); 		this._workspaceService.onDidChangeWorkspaceFolders(this._onDidChangeWorkspaceFolders, this, this._disposables);+		this._disposables.add(this._labelService.onDidChangeFormatters(e => {+			if (e.scheme === this.resource.scheme) {+				this._updateFileInfo();+			}+		})); 		this._fileInfo = this._initFilePathInfo(resource);  		if (editor) {@@ -87,7 +94,7 @@ export class BreadcrumbsModel { 	}  	isRelative(): boolean {-		return Boolean(this._fileInfo.folder);+		return Boolean(this._fileInfo.folder || this._fileInfo.home); 	}  	getElements(): ReadonlyArray<FileElement | OutlineElement2> {@@ -131,12 +138,13 @@ export class BreadcrumbsModel {  		const info: FileInfo = { 			folder: this._workspaceService.getWorkspaceFolder(uri) ?? undefined,-			path: []+			path: [],+			home: this._labelService.getUriHome(uri), 		};  		let uriPrefix: URI | null = uri; 		while (uriPrefix && uriPrefix.path !== '/') {-			if (info.folder && isEqual(info.folder.uri, uriPrefix)) {+			if ((info.folder && isEqual(info.folder.uri, uriPrefix)) || (info.home && isEqual(info.home, uriPrefix))) { 				break; 			} 			info.path.unshift(new FileElement(uriPrefix, info.path.length === 0 ? FileKind.FILE : FileKind.FOLDER));@@ -147,6 +155,14 @@ export class BreadcrumbsModel { 			} 		} +		if (info.home) {+			const separator = this._labelService.getSeparator(info.home.scheme, info.home.authority);+			const labels = this._labelService.getUriLabel(info.home).split(separator).filter(Boolean);+			for (let index = labels.length - 1; index >= 0; index--) {+				info.path.unshift(new FileElement(info.home, index === 0 ? FileKind.ROOT_FOLDER : FileKind.FOLDER, labels[index]));+			}+		}+ 		if (info.folder && this._workspaceService.getWorkbenchState() === WorkbenchState.WORKSPACE) { 			const folderCount = this._workspaceService.getWorkspace().folders.length; 			if (folderCount > 1 || isEqual(info.folder.uri, this.resource)) {@@ -161,6 +177,10 @@ export class BreadcrumbsModel { 	}  	private _onDidChangeWorkspaceFolders() {+		this._updateFileInfo();+	}++	private _updateFileInfo(): void { 		this._fileInfo = this._initFilePathInfo(this.resource); 		this._onDidUpdate.fire(this); 	}
src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts3 + / 0
@@ -809,6 +809,9 @@ class MockCommandService implements ICommandService {  class MockLabelService implements ILabelService { 	_serviceBrand: undefined;+	getUriHome(): undefined {+		return undefined;+	} 	getUriLabel(resource: URI, options?: { relative?: boolean | undefined; noPrefix?: boolean | undefined }): string { 		return normalize(resource.fsPath); 	}
src/vs/workbench/services/label/common/labelService.ts57 + / 23
@@ -12,7 +12,7 @@ import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry, IWo import { Registry } from '../../../../platform/registry/common/platform.js'; import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService.js'; import { IWorkspaceContextService, IWorkspace, isWorkspace, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceIdentifier, toWorkspaceIdentifier, WORKSPACE_EXTENSION, isUntitledWorkspace, isTemporaryWorkspace } from '../../../../platform/workspace/common/workspace.js';-import { basenameOrAuthority, basename, joinPath, dirname } from '../../../../base/common/resources.js';+import { basenameOrAuthority, basename, dirname, isEqualOrParent, joinPath, relativePath } from '../../../../base/common/resources.js'; import { tildify, getPathLabel } from '../../../../base/common/labels.js'; import { ILabelService, ResourceLabelFormatter, ResourceLabelFormatting, IFormatterChangeEvent, Verbosity } from '../../../../platform/label/common/label.js'; import { ExtensionsRegistry } from '../../extensions/common/extensionsRegistry.js';@@ -178,36 +178,70 @@ export class LabelService extends Disposable implements ILabelService { 		this.userHome = await this.pathService.userHome(); 	} -	findFormatting(resource: URI): ResourceLabelFormatting | undefined {-		let bestResult: ResourceLabelFormatter | undefined;+	getUriHome(resource: URI): URI | undefined {+		const formatter = this.findHomeFormatter(resource);+		return formatter?.home ? resource.with({ path: formatter.home, query: null, fragment: null }) : undefined;+	} +	private findHomeFormatter(resource: URI): ResourceLabelFormatter | undefined {+		let bestResult: ResourceLabelFormatter | undefined; 		for (const formatter of this.formatters) {-			if (formatter.scheme === resource.scheme) {-				if (!formatter.authority && (!bestResult || formatter.priority)) {-					bestResult = formatter;-					continue;-				}--				if (!formatter.authority) {-					continue;-				}--				if (match(formatter.authority, resource.authority, { ignoreCase: true }) &&-					(-						!bestResult?.authority ||-						formatter.authority.length > bestResult.authority.length ||-						((formatter.authority.length === bestResult.authority.length) && formatter.priority)-					)-				) {-					bestResult = formatter;-				}+			if (!formatter.home || formatter.scheme !== resource.scheme || (formatter.authority && !match(formatter.authority, resource.authority, { ignoreCase: true }))) {+				continue;+			}+			if (!isEqualOrParent(resource, resource.with({ path: formatter.home }))) {+				continue;+			}+			const authorityLength = formatter.authority?.length ?? 0;+			const bestAuthorityLength = bestResult?.authority?.length ?? 0;+			const bestHomeLength = bestResult?.home?.length ?? 0;+			if (!bestResult ||+				formatter.home.length > bestHomeLength ||+				(formatter.home.length === bestHomeLength && authorityLength > bestAuthorityLength) ||+				(formatter.home.length === bestHomeLength && authorityLength === bestAuthorityLength && formatter.priority)+			) {+				bestResult = formatter; 			} 		}+		return bestResult;+	} -		return bestResult ? bestResult.formatting : undefined;+	findFormatting(resource: URI): ResourceLabelFormatting | undefined {+		let bestResult: ResourceLabelFormatter | undefined;+		for (const formatter of this.formatters) {+			if (formatter.home || formatter.scheme !== resource.scheme) {+				continue;+			}+			if (!formatter.authority && (!bestResult || formatter.priority)) {+				bestResult = formatter;+				continue;+			}+			if (!formatter.authority) {+				continue;+			}+			if (match(formatter.authority, resource.authority, { ignoreCase: true }) &&+				(+					!bestResult?.authority ||+					formatter.authority.length > bestResult.authority.length ||+					((formatter.authority.length === bestResult.authority.length) && formatter.priority)+				)+			) {+				bestResult = formatter;+			}+		}+		return bestResult?.formatting; 	}  	getUriLabel(resource: URI, options: { relative?: boolean; noPrefix?: boolean; separator?: '/' | '\\'; appendWorkspaceSuffix?: boolean } = {}): string {+		const homeFormatter = this.findHomeFormatter(resource);+		if (homeFormatter?.home) {+			const home = resource.with({ path: homeFormatter.home, query: null, fragment: null });+			const separator = options.separator ?? homeFormatter.formatting.separator;+			const path = relativePath(home, resource);+			const label = this.formatUri(home, homeFormatter.formatting);+			return path ? `${label}${separator}${this.adjustPathSeparators(path, separator)}` : label;+		}+ 		let formatting = this.findFormatting(resource); 		if (formatting && options.separator) { 			// mixin separator if defined from the outside
src/vs/workbench/services/label/common/resourceLabelHomeStore.tsadded50 + / 0
@@ -0,0 +1,50 @@+/*---------------------------------------------------------------------------------------------+ *  Copyright (c) Microsoft Corporation. All rights reserved.+ *  Licensed under the MIT License. See License.txt in the project root for license information.+ *--------------------------------------------------------------------------------------------*/++import { Disposable, DisposableMap } from '../../../../base/common/lifecycle.js';+import { getComparisonKey } from '../../../../base/common/resources.js';+import { URI } from '../../../../base/common/uri.js';+import { ILabelService } from '../../../../platform/label/common/label.js';++export interface IResourceLabelHome {+	readonly uri: URI;+	readonly label: string;+}++export class ResourceLabelHomeStore extends Disposable {++	private readonly registrations = this._register(new DisposableMap<string>());++	constructor(+		@ILabelService private readonly labelService: ILabelService,+	) {+		super();+	}++	set(homes: readonly IResourceLabelHome[]): void {+		const keyFor = (home: IResourceLabelHome) => `${getComparisonKey(home.uri)}\0${home.label}`;+		const homeKeys = new Set(homes.map(keyFor));+		for (const home of homes) {+			const key = keyFor(home);+			if (!this.registrations.has(key)) {+				this.registrations.set(key, this.labelService.registerFormatter({+					scheme: home.uri.scheme,+					authority: home.uri.authority || undefined,+					home: home.uri.path,+					priority: true,+					formatting: {+						label: home.label,+						separator: this.labelService.getSeparator(home.uri.scheme, home.uri.authority),+					},+				}));+			}+		}+		for (const [key] of this.registrations) {+			if (!homeKeys.has(key)) {+				this.registrations.deleteAndDispose(key);+			}+		}+	}+}
src/vs/workbench/services/label/test/browser/label.test.ts76 + / 0
@@ -18,6 +18,7 @@ import { ResourceLabelFormatter } from '../../../../../platform/label/common/lab import { sep } from '../../../../../base/common/path.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { DisposableStore } from '../../../../../base/common/lifecycle.js';+import { ResourceLabelHomeStore } from '../../common/resourceLabelHomeStore.js';  suite('URI Label', () => { 	let labelService: LabelService;@@ -83,6 +84,81 @@ suite('URI Label', () => { 		assert.strictEqual(labelService.getUriBasenameLabel(uri1), 'END'); 	}); +	test('returns the most specific registered resource label home', () => {+		const outer = URI.file('/home/test/.agent');+		const inner = URI.file('/home/test/.agent/sessions/session-id');+		const outerRegistration = labelService.registerFormatter({ scheme: outer.scheme, home: outer.path, formatting: { label: 'Agent', separator: '/' } });+		const innerRegistration = labelService.registerFormatter({ scheme: inner.scheme, home: inner.path, formatting: { label: 'Session', separator: '/' } });++		const resource = URI.file('/home/test/.agent/sessions/session-id/file.md');+		assert.deepStrictEqual({+			home: labelService.getUriHome(resource)?.toString(),+			label: labelService.getUriLabel(resource),+		}, {+			home: inner.toString(),+			label: 'Session/file.md',+		});++		innerRegistration.dispose();+		assert.deepStrictEqual({+			home: labelService.getUriHome(resource)?.toString(),+			label: labelService.getUriLabel(resource),+		}, {+			home: outer.toString(),+			label: 'Agent/sessions/session-id/file.md',+		});+		outerRegistration.dispose();+	});++	test('ignores resource label homes for a different authority', () => {+		const mismatchedRegistration = labelService.registerFormatter({+			scheme: 'test',+			authority: 'other',+			home: '/sessions/session-id',+			formatting: { label: 'Session', separator: '/' },+		});++		assert.strictEqual(labelService.getUriHome(URI.parse('test://current/sessions/session-id/file.md')), undefined);++		mismatchedRegistration.dispose();+	});++	test('resource label homes take precedence over ordinary formatter changes', () => {+		const resource = URI.file('/home/test/.agent/sessions/session-id/file.md');+		const root = URI.file('/home/test/.agent/sessions/session-id');+		const first = labelService.registerFormatter({+			scheme: 'file',+			formatting: { label: 'FIRST${path}', separator: '/' },+		});+		const homes = new ResourceLabelHomeStore(labelService);+		homes.set([{ uri: root, label: 'Session' }]);++		assert.strictEqual(labelService.getUriLabel(resource), 'Session/file.md');++		first.dispose();+		const second = labelService.registerFormatter({+			scheme: 'file',+			formatting: { label: 'SECOND${path}', separator: '/' },+		});+		assert.strictEqual(labelService.getUriLabel(resource), 'Session/file.md');++		homes.dispose();+		second.dispose();+	});++	test('keeps equivalent resource label home registrations until all are disposed', () => {+		const root = URI.file('/home/test/.agent/sessions/session-id');+		const formatter = { scheme: root.scheme, home: root.path, formatting: { label: 'Session', separator: '/' as const } };+		const first = labelService.registerFormatter(formatter);+		const second = labelService.registerFormatter({ ...formatter });++		first.dispose();+		assert.strictEqual(labelService.getUriLabel(labelService.getUriHome(URI.file('/home/test/.agent/sessions/session-id/file.md'))!), 'Session');++		second.dispose();+		assert.strictEqual(labelService.getUriHome(URI.file('/home/test/.agent/sessions/session-id/file.md')), undefined);+	});+ 	test('custom authority', function () { 		labelService.registerFormatter({ 			scheme: 'vscode',
src/vs/workbench/services/label/test/common/mockLabelService.ts45 + / 5
@@ -3,20 +3,29 @@  *  Licensed under the MIT License. See License.txt in the project root for license information.  *--------------------------------------------------------------------------------------------*/ -import { Emitter, Event } from '../../../../../base/common/event.js';-import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js';+import { Emitter } from '../../../../../base/common/event.js';+import { IDisposable } from '../../../../../base/common/lifecycle.js'; import { basename, normalize } from '../../../../../base/common/path.js';+import { isEqualOrParent } from '../../../../../base/common/resources.js'; import { URI } from '../../../../../base/common/uri.js'; import { IFormatterChangeEvent, ILabelService, ResourceLabelFormatter, Verbosity } from '../../../../../platform/label/common/label.js'; import { IWorkspace, IWorkspaceIdentifier } from '../../../../../platform/workspace/common/workspace.js';  export class MockLabelService implements ILabelService { 	_serviceBrand: undefined;+	private formatters: ResourceLabelFormatter[] = [];+	private readonly _onDidChangeFormatters = new Emitter<IFormatterChangeEvent>();+	readonly onDidChangeFormatters = this._onDidChangeFormatters.event;  	registerCachedFormatter(formatter: ResourceLabelFormatter): IDisposable {-		throw new Error('Method not implemented.');+		return this.registerFormatter(formatter); 	} 	getUriLabel(resource: URI, options?: { relative?: boolean | undefined; noPrefix?: boolean | undefined }): string {+		const formatter = this.findHomeFormatter(resource);+		if (formatter?.home) {+			const relativePath = resource.path.slice(formatter.home.length).replace(/^\//, '');+			return relativePath ? `${formatter.formatting.label}/${relativePath}` : formatter.formatting.label;+		} 		return normalize(resource.fsPath); 	} 	getUriBasenameLabel(resource: URI): string {@@ -35,7 +44,38 @@ export class MockLabelService implements ILabelService { 		return '/'; 	} 	registerFormatter(formatter: ResourceLabelFormatter): IDisposable {-		return Disposable.None;+		this.formatters.push(formatter);+		this._onDidChangeFormatters.fire({ scheme: formatter.scheme });+		return {+			dispose: () => {+				this.formatters = this.formatters.filter(candidate => candidate !== formatter);+				this._onDidChangeFormatters.fire({ scheme: formatter.scheme });+			}+		}; 	}-	readonly onDidChangeFormatters: Event<IFormatterChangeEvent> = new Emitter<IFormatterChangeEvent>().event;++	getUriHome(resource: URI): URI | undefined {+		const formatter = this.findHomeFormatter(resource);+		return formatter?.home ? resource.with({ path: formatter.home, query: null, fragment: null }) : undefined;+	}++	private findHomeFormatter(resource: URI): ResourceLabelFormatter | undefined {+		let result: ResourceLabelFormatter | undefined;+		for (const formatter of this.formatters) {+			if (!formatter.home) {+				continue;+			}+			if (formatter.scheme !== resource.scheme || (formatter.authority && formatter.authority !== resource.authority)) {+				continue;+			}+			if (!isEqualOrParent(resource, resource.with({ path: formatter.home }))) {+				continue;+			}+			if (!result || formatter.home.length > (result.home?.length ?? 0)) {+				result = formatter;+			}+		}+		return result;+	}+ }
src/vs/workbench/test/browser/componentFixtures/fixtureUtils.ts1 + / 0
@@ -775,6 +775,7 @@ export function registerWorkbenchServices(registration: ServiceRegistration): vo 		getSeparator: () => '/', 		registerFormatter: () => ({ dispose: () => { } }), 		onDidChangeFormatters: () => ({ dispose: () => { } }),+		getUriHome: () => undefined, 		registerCachedFormatter: () => ({ dispose: () => { } }), 		_serviceBrand: undefined, 		getHostTooltip: () => '',
src/vs/workbench/test/browser/parts/editor/breadcrumbModel.test.ts70 + / 9
@@ -14,6 +14,7 @@ import { Workspace } from '../../../../../platform/workspace/test/common/testWor import { mock } from '../../../../../base/test/common/mock.js'; import { IOutlineService } from '../../../../services/outline/browser/outline.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';+import { MockLabelService } from '../../../../services/label/test/common/mockLabelService.js'; import { IWorkspaceFolderLabelService } from '../../../../services/workspaces/common/workspaceFolderLabelService.js'; import { Emitter } from '../../../../../base/common/event.js'; @@ -26,6 +27,8 @@ suite('Breadcrumb Model', function () { 			return folder.uri.path.slice(folder.uri.path.lastIndexOf('/') + 1); 		} 	};+	const outlineService = new class extends mock<IOutlineService>() { };+	const labelService = new MockLabelService(); 	const configService = new class extends TestConfigurationService { 		override getValue<T>(...args: Parameters<TestConfigurationService['getValue']>): T | undefined { 			if (args[0] === 'breadcrumbs.filePath') {@@ -41,14 +44,18 @@ suite('Breadcrumb Model', function () { 		} 	}; +	function createModel(resource: URI, workspace: TestContextService = workspaceService): BreadcrumbsModel {+		return new BreadcrumbsModel(resource, undefined, configService, workspace, workspaceFolderLabelService, outlineService, labelService);+	}+ 	teardown(function () { 		model.dispose(); 	});  	ensureNoDisposablesAreLeakedInTestSuite();  	test('file element equality includes the rendered label', function () {-		model = new BreadcrumbsModel(URI.parse('foo:/bar/baz/ws/file.ts'), undefined, configService, workspaceService, workspaceFolderLabelService, new class extends mock<IOutlineService>() { });+		model = createModel(URI.parse('foo:/bar/baz/ws/file.ts')); 		const uri = URI.parse('foo:/worktrees/project-feature');  		assert.deepStrictEqual({@@ -62,7 +69,7 @@ suite('Breadcrumb Model', function () {  	test('only uri, inside workspace', function () { -		model = new BreadcrumbsModel(URI.parse('foo:/bar/baz/ws/some/path/file.ts'), undefined, configService, workspaceService, workspaceFolderLabelService, new class extends mock<IOutlineService>() { });+		model = createModel(URI.parse('foo:/bar/baz/ws/some/path/file.ts')); 		const elements = model.getElements();  		assert.strictEqual(elements.length, 3);@@ -77,7 +84,7 @@ suite('Breadcrumb Model', function () {  	test('display uri matters for FileElement', function () { -		model = new BreadcrumbsModel(URI.parse('foo:/bar/baz/ws/some/PATH/file.ts'), undefined, configService, workspaceService, workspaceFolderLabelService, new class extends mock<IOutlineService>() { });+		model = createModel(URI.parse('foo:/bar/baz/ws/some/PATH/file.ts')); 		const elements = model.getElements();  		assert.strictEqual(elements.length, 3);@@ -92,7 +99,7 @@ suite('Breadcrumb Model', function () {  	test('only uri, outside workspace', function () { -		model = new BreadcrumbsModel(URI.parse('foo:/outside/file.ts'), undefined, configService, workspaceService, workspaceFolderLabelService, new class extends mock<IOutlineService>() { });+		model = createModel(URI.parse('foo:/outside/file.ts')); 		const elements = model.getElements();  		assert.strictEqual(elements.length, 2);@@ -103,13 +110,67 @@ suite('Breadcrumb Model', function () { 		assert.strictEqual(two.uri.toString(), 'foo:/outside/file.ts'); 	}); +	test('shows only the path relative to a contributed resource label home', function () {+		const root = URI.file('/Users/test/.copilot/session-state/5ec17bb7-5596-41c5-9d24-4787d8b0a698');+		const registration = labelService.registerFormatter({ scheme: root.scheme, home: root.path, formatting: { label: 'Copilot/Session', separator: '/' } });+		const resource = URI.file('/Users/test/.copilot/session-state/5ec17bb7-5596-41c5-9d24-4787d8b0a698/folder/file.md');+		model = createModel(resource);++		assert.deepStrictEqual({+			isRelative: model.isRelative(),+			elements: (model.getElements() as FileElement[]).map(element => ({+				name: element.label ?? element.uri.path.slice(element.uri.path.lastIndexOf('/') + 1),+				kind: element.kind,+			}))+		}, {+			isRelative: true,+			elements: [+				{ name: 'Copilot', kind: FileKind.ROOT_FOLDER },+				{ name: 'Session', kind: FileKind.FOLDER },+				{ name: 'folder', kind: FileKind.FOLDER },+				{ name: 'file.md', kind: FileKind.FILE },+			]+		});+		registration.dispose();+	});++	test('updates when a resource label home is contributed after model creation', function () {+		const resource = URI.file('/Users/test/.copilot/session-state/5ec17bb7-5596-41c5-9d24-4787d8b0a698/file.md');+		model = createModel(resource);+		const root = URI.file('/Users/test/.copilot/session-state/5ec17bb7-5596-41c5-9d24-4787d8b0a698');+		const registration = labelService.registerFormatter({ scheme: root.scheme, home: root.path, formatting: { label: 'Copilot/Session', separator: '/' } });++		assert.deepStrictEqual((model.getElements() as FileElement[]).map(element => ({+			name: element.label ?? element.uri.path.slice(element.uri.path.lastIndexOf('/') + 1),+			kind: element.kind,+		})), [+			{ name: 'Copilot', kind: FileKind.ROOT_FOLDER },+			{ name: 'Session', kind: FileKind.FOLDER },+			{ name: 'file.md', kind: FileKind.FILE },+		]);+		registration.dispose();+	});++	test('keeps the full path without a contributed resource label home', function () {+		const resource = URI.file('/Users/test/.copilot/session-state/5ec17bb7-5596-41c5-9d24-4787d8b0a698/file.md');+		model = createModel(resource);++		assert.deepStrictEqual({+			isRelative: model.isRelative(),+			names: (model.getElements() as FileElement[]).map(element => element.uri.path.slice(element.uri.path.lastIndexOf('/') + 1))+		}, {+			isRelative: false,+			names: ['Users', 'test', '.copilot', 'session-state', '5ec17bb7-5596-41c5-9d24-4787d8b0a698', 'file.md']+		});+	});+ 	test('omits workspace root in single-root VS Code workspace', function () { 		const workspace = new TestContextService(new Workspace( 			'ffff', 			[new WorkspaceFolder({ uri: URI.parse('foo:/bar/baz/ws'), name: 'ws', index: 0 })], 			URI.parse('foo:/workspace.code-workspace') 		));-		model = new BreadcrumbsModel(URI.parse('foo:/bar/baz/ws/file.ts'), undefined, configService, workspace, workspaceFolderLabelService, new class extends mock<IOutlineService>() { });+		model = createModel(URI.parse('foo:/bar/baz/ws/file.ts'), workspace);  		assert.deepStrictEqual((model.getElements() as FileElement[]).map(element => ({ 			uri: element.uri.toString(),@@ -125,7 +186,7 @@ suite('Breadcrumb Model', function () { 			[new WorkspaceFolder({ uri: URI.parse('foo:/bar/baz/ws'), name: 'ws (branch)', index: 0 })], 			URI.parse('foo:/workspace.code-workspace') 		));-		model = new BreadcrumbsModel(URI.parse('foo:/bar/baz/ws/some/file.ts'), undefined, configService, workspace, workspaceFolderLabelService, new class extends mock<IOutlineService>() { });+		model = createModel(URI.parse('foo:/bar/baz/ws/some/file.ts'), workspace);  		assert.deepStrictEqual((model.getElements() as FileElement[]).map(element => ({ 			uri: element.uri.toString(),@@ -142,7 +203,7 @@ suite('Breadcrumb Model', function () { 			[new WorkspaceFolder({ uri: URI.parse('foo:/bar/baz/ws'), name: 'ws (branch)', index: 0 })], 			URI.parse('foo:/workspace.code-workspace') 		));-		model = new BreadcrumbsModel(URI.parse('foo:/bar/baz/ws'), undefined, configService, workspace, workspaceFolderLabelService, new class extends mock<IOutlineService>() { });+		model = createModel(URI.parse('foo:/bar/baz/ws'), workspace);  		assert.deepStrictEqual((model.getElements() as FileElement[]).map(element => ({ 			uri: element.uri.toString(),@@ -162,7 +223,7 @@ suite('Breadcrumb Model', function () { 			], 			URI.parse('foo:/workspace.code-workspace') 		));-		model = new BreadcrumbsModel(URI.parse('foo:/worktrees/docs-feature/guide.md'), undefined, configService, workspace, workspaceFolderLabelService, new class extends mock<IOutlineService>() { });+		model = createModel(URI.parse('foo:/worktrees/docs-feature/guide.md'), workspace);  		assert.deepStrictEqual((model.getElements() as FileElement[]).map(element => ({ 			uri: element.uri.toString(),@@ -177,7 +238,7 @@ suite('Breadcrumb Model', function () { 	test('updates workspace root and label when folders change', function () { 		const firstFolder = new WorkspaceFolder({ uri: URI.parse('foo:/worktrees/project-feature'), name: 'project (feature)', index: 0 }); 		const workspace = new TestContextService(new Workspace('ffff', [firstFolder], URI.parse('foo:/workspace.code-workspace')));-		model = new BreadcrumbsModel(URI.parse('foo:/worktrees/project-feature/file.ts'), undefined, configService, workspace, workspaceFolderLabelService, new class extends mock<IOutlineService>() { });+		model = createModel(URI.parse('foo:/worktrees/project-feature/file.ts'), workspace);  		const secondFolder = new WorkspaceFolder({ uri: URI.parse('foo:/worktrees/docs-feature'), name: 'docs (feature)', index: 1 }); 		workspace.setWorkspace(new Workspace('ffff', [firstFolder, secondFolder], URI.parse('foo:/workspace.code-workspace')));