Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean>;
/** Builds the session workspace from session metadata; provider-specific (icon, providerLabel, requiresWorkspaceTrust). */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -145,6 +146,14 @@ export class RemoteAgentHostSessionsProvider extends BaseAgentHostSessionsProvid
private readonly _onDidDisconnect = this._register(new Emitter<void>());
protected override get onConnectionLost(): Event<void> { 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());
Expand Down Expand Up @@ -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 });
},
};
}
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ChatSendResult>; 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<ChatSendResult>; openSession?: boolean; storageService?: IStorageService; noConnection?: boolean; isWebPlatform?: boolean }): RemoteAgentHostSessionsProvider {
const instantiationService = disposables.add(new TestInstantiationService());

instantiationService.stub(IFileDialogService, {});
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -400,7 +405,7 @@ suite('RemoteAgentHostSessionsProvider', () => {
workingDirectory,
}));

const provider = createProvider(disposables, connection);
const provider = createProvider(disposables, connection, { isWebPlatform: true });
provider.getSessions();
await timeout(0);

Expand All @@ -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,
Expand Down Expand Up @@ -521,21 +526,21 @@ 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: {} });
});

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);
Expand All @@ -547,7 +552,7 @@ suite('RemoteAgentHostSessionsProvider', () => {
}, {
listedSessions: 0,
resolvedResource: session.resource.toString(),
resolvedWorkspaceLabel: 'project [Test Host]',
resolvedWorkspaceLabel: 'project',
});
});

Expand Down Expand Up @@ -855,7 +860,7 @@ suite('RemoteAgentHostSessionsProvider', () => {
test('session adapter has correct workspace from working directory', () => runWithFakedTimers<void>({ 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);

Expand All @@ -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);
}));

Expand Down Expand Up @@ -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
// `[<hostname>]` 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<void>({ 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<void>({ 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<void>({ 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 &nbsp; — 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<void>({ 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);
}));

});
Loading