From c0afb27b1b89a8646bb4939b6b0f8e3ce475b348 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Thu, 23 Apr 2026 16:15:33 -0700 Subject: [PATCH] Drop [hostname] from remote agent host session labels on web On web the titlebar host filter dropdown already scopes sessions to a single host, making the '[]' suffix in the session workspace label and the host label in the session card description redundant. On web only: - Omit the providerLabel from remote agent host session workspace labels so group headers show just '' instead of ' []'. - Skip the host description on session cards (active sessions fall back to 'Working...' / 'Input needed' / 'Failed' status text). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../browser/baseAgentHostSessionsProvider.ts | 2 +- .../remoteAgentHostSessionsProvider.ts | 16 ++- .../remoteAgentHostSessionsProvider.test.ts | 107 ++++++++++++++++-- 3 files changed, 109 insertions(+), 16 deletions(-) diff --git a/src/vs/sessions/contrib/agentHost/browser/baseAgentHostSessionsProvider.ts b/src/vs/sessions/contrib/agentHost/browser/baseAgentHostSessionsProvider.ts index 2e6e07c72cdd1..06c33b79eec09 100644 --- a/src/vs/sessions/contrib/agentHost/browser/baseAgentHostSessionsProvider.ts +++ b/src/vs/sessions/contrib/agentHost/browser/baseAgentHostSessionsProvider.ts @@ -44,7 +44,7 @@ import { ISendRequestOptions, ISessionChangeEvent } from '../../../services/sess */ export interface IAgentHostAdapterOptions { readonly icon: ThemeIcon; - readonly description: IMarkdownString; + readonly description: IMarkdownString | undefined; /** Loading observable wired to the provider's authentication-pending state. */ readonly loading: IObservable; /** Builds the session workspace from session metadata; provider-specific (icon, providerLabel, requiresWorkspaceTrust). */ diff --git a/src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHostSessionsProvider.ts b/src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHostSessionsProvider.ts index df0ddee4e0ae2..c80293464bd73 100644 --- a/src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHostSessionsProvider.ts +++ b/src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHostSessionsProvider.ts @@ -10,6 +10,7 @@ import { DisposableStore } from '../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../base/common/network.js'; import { basename, dirname } from '../../../../base/common/resources.js'; import { IObservable, observableValue } from '../../../../base/common/observable.js'; +import { isWeb } from '../../../../base/common/platform.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { URI } from '../../../../base/common/uri.js'; import { localize } from '../../../../nls.js'; @@ -145,6 +146,14 @@ export class RemoteAgentHostSessionsProvider extends BaseAgentHostSessionsProvid private readonly _onDidDisconnect = this._register(new Emitter()); protected override get onConnectionLost(): Event { return this._onDidDisconnect.event; } + /** + * Overridable seam so tests can exercise both the web and non-web + * branches of the label/description gating without depending on the + * ambient {@link isWeb} constant (the browser test runner always + * reports `isWeb === true`). + */ + protected get isWebPlatform(): boolean { return isWeb; } + private _connection: IAgentConnection | undefined; private _defaultDirectory: string | undefined; private readonly _connectionListeners = this._register(new DisposableStore()); @@ -246,12 +255,13 @@ export class RemoteAgentHostSessionsProvider extends BaseAgentHostSessionsProvid } protected _adapterOptions() { + const web = this.isWebPlatform; return { - description: new MarkdownString().appendText(this.label), + description: web ? undefined : new MarkdownString().appendText(this.label), buildWorkspace: (project: IAgentSessionMetadata['project'], workingDirectory: URI | undefined) => { const uriForDescription = project?.uri ?? workingDirectory; const description = uriForDescription ? this._labelService.getUriLabel(dirname(uriForDescription), { relative: false }) : undefined; - return buildAgentHostSessionWorkspace(project, workingDirectory, { providerLabel: this.label, fallbackIcon: Codicon.remote, requiresWorkspaceTrust: false, description }); + return buildAgentHostSessionWorkspace(project, workingDirectory, { providerLabel: web ? undefined : this.label, fallbackIcon: Codicon.remote, requiresWorkspaceTrust: false, description }); }, }; } @@ -496,7 +506,7 @@ export class RemoteAgentHostSessionsProvider extends BaseAgentHostSessionsProvid private _buildWorkspaceFromUri(uri: URI): ISessionWorkspace { const folderName = basename(uri) || uri.path; return { - label: `${folderName} [${this.label}]`, + label: this.isWebPlatform ? folderName : `${folderName} [${this.label}]`, description: this._labelService.getUriLabel(dirname(uri), { relative: false }), group: this.label, icon: Codicon.remote, diff --git a/src/vs/sessions/contrib/remoteAgentHost/test/browser/remoteAgentHostSessionsProvider.test.ts b/src/vs/sessions/contrib/remoteAgentHost/test/browser/remoteAgentHostSessionsProvider.test.ts index 7799f9eabbe48..79cc6272e14bd 100644 --- a/src/vs/sessions/contrib/remoteAgentHost/test/browser/remoteAgentHostSessionsProvider.test.ts +++ b/src/vs/sessions/contrib/remoteAgentHost/test/browser/remoteAgentHostSessionsProvider.test.ts @@ -177,7 +177,7 @@ function createSession(id: string, opts?: { provider?: string; summary?: string; }; } -function createProvider(disposables: DisposableStore, connection: MockAgentConnection, overrides?: { address?: string; connectionName?: string | undefined; sendRequest?: (resource: URI, message: string, options?: IChatSendRequestOptions) => Promise; openSession?: boolean; storageService?: IStorageService; noConnection?: boolean }): RemoteAgentHostSessionsProvider { +function createProvider(disposables: DisposableStore, connection: MockAgentConnection, overrides?: { address?: string; connectionName?: string | undefined; sendRequest?: (resource: URI, message: string, options?: IChatSendRequestOptions) => Promise; openSession?: boolean; storageService?: IStorageService; noConnection?: boolean; isWebPlatform?: boolean }): RemoteAgentHostSessionsProvider { const instantiationService = disposables.add(new TestInstantiationService()); instantiationService.stub(IFileDialogService, {}); @@ -206,7 +206,12 @@ function createProvider(disposables: DisposableStore, connection: MockAgentConne name: overrides !== undefined && Object.prototype.hasOwnProperty.call(overrides, 'connectionName') ? overrides.connectionName ?? '' : 'Test Host', }; - const provider = disposables.add(instantiationService.createInstance(RemoteAgentHostSessionsProvider, config)); + const providerCtor = overrides?.isWebPlatform !== undefined + ? class extends RemoteAgentHostSessionsProvider { + protected override get isWebPlatform(): boolean { return overrides.isWebPlatform!; } + } + : RemoteAgentHostSessionsProvider; + const provider = disposables.add(instantiationService.createInstance(providerCtor, config)); if (!overrides?.noConnection) { provider.setConnection(connection); } @@ -312,12 +317,12 @@ suite('RemoteAgentHostSessionsProvider', () => { // ---- Workspace resolution ------- test('resolveWorkspace builds workspace from URI', () => { - const provider = createProvider(disposables, connection); + const provider = createProvider(disposables, connection, { isWebPlatform: true }); const uri = URI.parse('vscode-agent-host://auth/home/user/project'); const ws = provider.resolveWorkspace(uri); assert.ok(ws, 'resolveWorkspace should resolve vscode-agent-host:// URIs'); - assert.strictEqual(ws.label, 'project [Test Host]'); + assert.strictEqual(ws.label, 'project'); assert.strictEqual(ws.repositories.length, 1); assert.strictEqual(ws.repositories[0].uri.toString(), uri.toString()); assert.strictEqual(ws.repositories[0].detail, undefined); @@ -400,7 +405,7 @@ suite('RemoteAgentHostSessionsProvider', () => { workingDirectory, })); - const provider = createProvider(disposables, connection); + const provider = createProvider(disposables, connection, { isWebPlatform: true }); provider.getSessions(); await timeout(0); @@ -411,7 +416,7 @@ suite('RemoteAgentHostSessionsProvider', () => { workingDirectory: workspace?.repositories[0]?.workingDirectory?.toString(), detail: workspace?.repositories[0]?.detail, }, { - label: 'vscode [Test Host]', + label: 'vscode', repository: projectUri.toString(), workingDirectory: workingDirectory.toString(), detail: undefined, @@ -521,13 +526,13 @@ suite('RemoteAgentHostSessionsProvider', () => { // ---- Session lifecycle ------- test('createNewSession returns session with correct fields', () => { - const provider = createProvider(disposables, connection); + const provider = createProvider(disposables, connection, { isWebPlatform: true }); const session = provider.createNewSession(URI.parse('vscode-agent-host://auth/home/user/project'), provider.sessionTypes[0].id); assert.strictEqual(session.providerId, provider.id); assert.strictEqual(session.status.get(), SessionStatus.Untitled); assert.ok(session.workspace.get()); - assert.strictEqual(session.workspace.get()?.label, 'project [Test Host]'); + assert.strictEqual(session.workspace.get()?.label, 'project'); // sessionType should be the logical type, not the resource scheme assert.strictEqual(session.sessionType, provider.sessionTypes[0].id); assert.deepStrictEqual(provider.getSessionConfig(session.sessionId), { schema: { type: 'object', properties: {} }, values: {} }); @@ -535,7 +540,7 @@ suite('RemoteAgentHostSessionsProvider', () => { test('createNewSession clears session config when resolving config is unavailable', async () => { connection.failResolveSessionConfig = true; - const provider = createProvider(disposables, connection); + const provider = createProvider(disposables, connection, { isWebPlatform: true }); const workspaceUri = URI.parse('vscode-agent-host://auth/home/user/project'); const session = provider.createNewSession(workspaceUri, provider.sessionTypes[0].id); const resolved = provider.getSessionByResource(session.resource); @@ -547,7 +552,7 @@ suite('RemoteAgentHostSessionsProvider', () => { }, { listedSessions: 0, resolvedResource: session.resource.toString(), - resolvedWorkspaceLabel: 'project [Test Host]', + resolvedWorkspaceLabel: 'project', }); }); @@ -855,7 +860,7 @@ suite('RemoteAgentHostSessionsProvider', () => { test('session adapter has correct workspace from working directory', () => runWithFakedTimers({ useFakeTimers: true }, async () => { connection.addSession(createSession('ws-sess', { summary: 'WS Test', workingDirectory: URI.parse('vscode-agent-host://localhost__4321/file/-/home/user/myrepo') })); - const provider = createProvider(disposables, connection); + const provider = createProvider(disposables, connection, { isWebPlatform: true }); provider.getSessions(); await timeout(0); @@ -865,7 +870,7 @@ suite('RemoteAgentHostSessionsProvider', () => { const workspace = wsSession!.workspace.get(); assert.ok(workspace, 'Workspace should be populated'); - assert.strictEqual(workspace!.label, 'myrepo [Test Host]'); + assert.strictEqual(workspace!.label, 'myrepo'); assert.strictEqual(workspace!.repositories[0].detail, undefined); })); @@ -1008,4 +1013,82 @@ suite('RemoteAgentHostSessionsProvider', () => { assert.strictEqual(connection.sessionUnsubscribeCounts.get(sessionUriStr), 1); })); + // ---- Non-web label formatting (native desktop) ------- + // + // In the browser test runner `isWeb` is always `true`, so by default + // every test above exercises the web branch (which drops the + // `[]` suffix because the titlebar host filter renders it + // redundantly). These tests pin the non-web (desktop) behaviour where + // the host suffix / host description must still appear. + + test('non-web: resolveWorkspace includes [host] suffix in label', () => { + const provider = createProvider(disposables, connection, { isWebPlatform: false }); + const uri = URI.parse('vscode-agent-host://auth/home/user/project'); + const ws = provider.resolveWorkspace(uri); + + assert.ok(ws); + assert.strictEqual(ws.label, 'project [Test Host]'); + }); + + test('non-web: session workspace from project metadata includes [host] suffix', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + const projectUri = URI.parse('vscode-agent-host://localhost__4321/file/-/home/user/vscode'); + connection.addSession(createSession('project-1', { + summary: 'Project Session', + project: { uri: projectUri, displayName: 'vscode' }, + })); + + const provider = createProvider(disposables, connection, { isWebPlatform: false }); + provider.getSessions(); + await timeout(0); + + assert.strictEqual(provider.getSessions()[0].workspace.get()?.label, 'vscode [Test Host]'); + })); + + test('non-web: session workspace from working directory includes [host] suffix', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + connection.addSession(createSession('ws-sess', { + summary: 'WS Test', + workingDirectory: URI.parse('vscode-agent-host://localhost__4321/file/-/home/user/myrepo'), + })); + + const provider = createProvider(disposables, connection, { isWebPlatform: false }); + provider.getSessions(); + await timeout(0); + + const wsSession = provider.getSessions().find(s => s.title.get() === 'WS Test'); + assert.strictEqual(wsSession?.workspace.get()?.label, 'myrepo [Test Host]'); + })); + + test('non-web: createNewSession workspace label includes [host] suffix', () => { + const provider = createProvider(disposables, connection, { isWebPlatform: false }); + const session = provider.createNewSession(URI.parse('vscode-agent-host://auth/home/user/project'), provider.sessionTypes[0].id); + + assert.strictEqual(session.workspace.get()?.label, 'project [Test Host]'); + }); + + test('non-web: session description is the host label', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + connection.addSession(createSession('desc-sess', { summary: 'Desc Test' })); + + const provider = createProvider(disposables, connection, { isWebPlatform: false }); + provider.getSessions(); + await timeout(0); + + const session = provider.getSessions().find(s => s.title.get() === 'Desc Test'); + const description = session?.description.get(); + assert.ok(description, 'description should be defined on non-web'); + // MarkdownString.appendText escapes spaces as   — verify the + // host label is present rather than the exact serialized form. + assert.ok(description!.value.includes('Test') && description!.value.includes('Host')); + })); + + test('web: session description is undefined (host filter dropdown replaces it)', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + connection.addSession(createSession('desc-sess-web', { summary: 'Desc Web' })); + + const provider = createProvider(disposables, connection, { isWebPlatform: true }); + provider.getSessions(); + await timeout(0); + + const session = provider.getSessions().find(s => s.title.get() === 'Desc Web'); + assert.strictEqual(session?.description.get(), undefined); + })); + });