Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion src/vs/platform/agentHost/node/copilot/copilotAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export class CopilotAgent extends Disposable implements IAgent {
getDescriptor(): IAgentDescriptor {
return {
provider: 'copilot',
displayName: 'Agent Host - Copilot',
displayName: 'Copilot',
description: 'Copilot SDK agent running in a dedicated process',
};
}
Expand Down
16 changes: 10 additions & 6 deletions src/vs/sessions/SESSIONS_PROVIDER.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ A sessions provider encapsulates a compute environment. It owns workspace discov

| Property | Type | Description |
|----------|------|-------------|
| `id` | `string` | Unique provider instance ID (e.g., `'default-copilot'`, `'agenthost-hostA-copilot'`) |
| `id` | `string` | Unique provider instance ID (e.g., `'default-copilot'`, `'agenthost-hostA'`) |
| `label` | `string` | Display label |
| `icon` | `ThemeIcon` | Provider icon |
| `sessionTypes` | `readonly ISessionType[]` | Session types this provider supports |
Expand Down Expand Up @@ -325,18 +325,22 @@ src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHost.contribution.ts
```

- Monitors `IRemoteAgentHostService.onDidChangeConnections`
- Creates one `RemoteAgentHostSessionsProvider` per agent per connection
- Registers via `sessionsProvidersService.registerProvider(sessionsProvider)` into a per-agent `DisposableStore`
- Creates one `RemoteAgentHostSessionsProvider` per connection
- Registers via `sessionsProvidersService.registerProvider(sessionsProvider)`
- Disposes providers when connections are removed

#### Identity

| Property | Format |
|----------|--------|
| `id` | `'agenthost-${sanitizedAuthority}-${agentProvider}'` |
| `label` | Connection name or `'${agentProvider} (${address})'` |
| `id` | `'agenthost-${sanitizedAuthority}'` |
| `label` | Connection name or `address` |
| `icon` | `Codicon.remote` |
| `sessionTypes` | `[CopilotCLISessionType]` (reuses the platform type) |
| `sessionTypes` | Dynamically populated from `rootState.agents`; one entry per agent, each id from `remoteAgentHostSessionTypeId(sanitizedAuthority, agent.provider)` (format: `'remote-${sanitizedAuthority}-${agent.provider}'`), label is the agent's `displayName` |

The session type id is built by the pure helper in `common/remoteAgentHostSessionType.ts`. It is used as the `ISession.sessionType`, the resource URI scheme registered via `registerChatSessionContentProvider`, and the `targetChatSessionType` published by `AgentHostLanguageModelProvider` — keeping these unified so the model picker finds the host's own models.

Agents are discovered dynamically from each host's `rootState`; there is no hard-coded allowlist of supported agent providers. A single `RemoteAgentHostSessionsProvider` per host fans out into one `ISessionType` per advertised agent, and fires `onDidChangeSessionTypes` when the host's agent list changes. Each incoming session's type is derived from its backend URI scheme, so sessions for any agent the host exposes route through the same provider.

#### Browse Actions

Expand Down
22 changes: 18 additions & 4 deletions src/vs/sessions/contrib/chat/browser/sessionTypePicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { ActionListItemKind, IActionListDelegate, IActionListItem } from '../../
import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js';
import { ISessionsProvidersService } from '../../../services/sessions/browser/sessionsProvidersService.js';
import { autorun } from '../../../../base/common/observable.js';
import { ISessionType } from '../../../services/sessions/common/session.js';
import { ISession, ISessionType } from '../../../services/sessions/common/session.js';

export class SessionTypePicker extends Disposable {

Expand All @@ -31,19 +31,33 @@ export class SessionTypePicker extends Disposable {
) {
super();

this._register(autorun(reader => {
const session = this.sessionsManagementService.activeSession.read(reader);
const refresh = (session: ISession | undefined) => {
if (session) {
this._supportedSessionTypes = this.sessionsManagementService.getSessionTypes(session);
const provider = this.sessionsProvidersService.getProvider(session.providerId);
this._allProviderSessionTypes = provider ? [...provider.sessionTypes] : [];
const providerTypes = provider ? [...provider.sessionTypes] : [];
const providerTypeIds = new Set(providerTypes.map(t => t.id));
this._allProviderSessionTypes = [
...providerTypes,
...this._supportedSessionTypes.filter(t => !providerTypeIds.has(t.id)),
];
this._sessionType = session.sessionType;
} else {
this._supportedSessionTypes = [];
this._allProviderSessionTypes = [];
this._sessionType = undefined;
}
this._updateTriggerLabel();
};

this._register(autorun(reader => {
const session = this.sessionsManagementService.activeSession.read(reader);
refresh(session);
}));
// Re-read when a provider advertises/removes session types at runtime
// (e.g. a remote agent host discovers a new agent).
this._register(this.sessionsManagementService.onDidChangeSessionTypes(() => {
refresh(this.sessionsManagementService.activeSession.get());
}));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { URI } from '../../../../base/common/uri.js';
import { generateUuid } from '../../../../base/common/uuid.js';
import { localize } from '../../../../nls.js';
import { AgentSession, IAgentHostService, type IAgentSessionMetadata } from '../../../../platform/agentHost/common/agentService.js';
import type { IRootState } from '../../../../platform/agentHost/common/state/protocol/state.js';
import { ActionType, isSessionAction } from '../../../../platform/agentHost/common/state/sessionActions.js';
import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js';
import { ChatViewPaneTarget, IChatWidgetService } from '../../../../workbench/contrib/chat/browser/chat.js';
Expand All @@ -41,13 +42,6 @@ function sessionTypeForProvider(provider: string): string {
return `agent-host-${provider}`;
}

/** Session type for the local agent host. ID matches the targetChatSessionType on language models. */
const LocalAgentHostSessionType: ISessionType = {
id: sessionTypeForProvider(DEFAULT_AGENT_PROVIDER),
label: localize('localAgentHost', "Local Agent Host"),
icon: Codicon.vm,
};

/**
* Adapts agent host session metadata into an {@link ISession} for the
* local agent host. Also exposes settable observables so the cache
Expand Down Expand Up @@ -188,8 +182,17 @@ export class LocalAgentHostSessionsProvider extends Disposable implements ISessi
readonly id = LOCAL_PROVIDER_ID;
readonly label: string;
readonly icon: ThemeIcon = Codicon.vm;
readonly sessionTypes: readonly ISessionType[];
readonly capabilities = { multipleChatsPerSession: false };
private readonly _localLabel = localize('localAgentHostSessionTypeLocation', "Local");
private _hasRootStateSnapshot = false;
private _sessionTypes: ISessionType[] = [];
get sessionTypes(): readonly ISessionType[] {
const rootStateValue = this._agentHostService.rootState.value;
return this._hasRootStateSnapshot || rootStateValue !== undefined ? this._sessionTypes : this._getSessionTypesFromContributions();
}

private readonly _onDidChangeSessionTypes = this._register(new Emitter<void>());
readonly onDidChangeSessionTypes: Event<void> = this._onDidChangeSessionTypes.event;

readonly browseActions: readonly ISessionWorkspaceBrowseAction[];

Expand Down Expand Up @@ -221,8 +224,6 @@ export class LocalAgentHostSessionsProvider extends Disposable implements ISessi

this.label = localize('localAgentHostLabel', "Local Agent Host");

this.sessionTypes = [LocalAgentHostSessionType];

this.browseActions = [{
label: localize('folders', "Folders"),
icon: Codicon.folderOpened,
Expand Down Expand Up @@ -250,6 +251,61 @@ export class LocalAgentHostSessionsProvider extends Disposable implements ISessi
this._handleIsDoneChanged(e.action.session, e.action.isDone);
}
}));

const rootStateValue = this._agentHostService.rootState.value;
if (rootStateValue !== undefined) {
this._hasRootStateSnapshot = true;
}
if (rootStateValue && !(rootStateValue instanceof Error)) {
this._syncSessionTypesFromRootState(rootStateValue);
}
this._register(this._agentHostService.rootState.onDidChange(rootState => {
const didHydrate = !this._hasRootStateSnapshot;
this._hasRootStateSnapshot = true;
this._syncSessionTypesFromRootState(rootState, didHydrate);
}));
}

private _syncSessionTypesFromRootState(rootState: IRootState, forceFire = false): void {
const next = rootState.agents.map((agent): ISessionType => ({
id: sessionTypeForProvider(agent.provider),
label: this._formatSessionTypeLabel(agent.displayName || agent.provider),
icon: Codicon.vm,
}));

const prev = this._sessionTypes;
if (!forceFire && prev.length === next.length && prev.every((t, i) => t.id === next[i].id && t.label === next[i].label)) {
return;
}
this._sessionTypes = next;
this._onDidChangeSessionTypes.fire();
}

private _formatSessionTypeLabel(agentLabel: string): string {
return localize('localAgentHostSessionType', "{0} [{1}]", agentLabel, this._localLabel);
}

private _getSessionTypesFromContributions(): ISessionType[] {
return this._chatSessionsService.getAllChatSessionContributions()
.filter(contribution => contribution.type.startsWith('agent-host-'))
.map((contribution): ISessionType => ({
id: contribution.type,
label: this._formatSessionTypeLabel(contribution.displayName),
icon: Codicon.vm,
}));
}

private _sessionTypeById(id: string): ISessionType {
const advertised = this.sessionTypes.find(t => t.id === id);
if (advertised) {
return advertised;
}
const contribution = this._chatSessionsService.getChatSessionContribution(id);
if (contribution) {
return { id, label: this._formatSessionTypeLabel(contribution.displayName), icon: Codicon.vm };
}
const provider = id.startsWith('agent-host-') ? id.substring('agent-host-'.length) : id;
return { id, label: this._formatSessionTypeLabel(provider), icon: Codicon.vm };
}

// -- Workspaces --
Expand All @@ -270,7 +326,15 @@ export class LocalAgentHostSessionsProvider extends Disposable implements ISessi

// -- Sessions --

getSessionTypes(_sessionId: string): ISessionType[] {
getSessionTypes(sessionId: string): ISessionType[] {
if (this._currentNewSession?.sessionId === sessionId) {
return [...this.sessionTypes];
}
const rawId = this._rawIdFromChatId(sessionId);
const cached = rawId ? this._sessionCache.get(rawId) : undefined;
if (cached) {
return [this._sessionTypeById(cached.sessionType)];
}
return [...this.sessionTypes];
}

Expand All @@ -294,7 +358,16 @@ export class LocalAgentHostSessionsProvider extends Disposable implements ISessi
this._currentNewSession = undefined;
this._selectedModelId = undefined;

const resource = URI.from({ scheme: sessionTypeForProvider(DEFAULT_AGENT_PROVIDER), path: `/untitled-${generateUuid()}` });
const defaultType = this.sessionTypes[0];
if (!defaultType) {
throw new Error(localize('noAgents', "Local agent host has not advertised any agents yet."));
}

return this._createNewSessionForType(workspace, defaultType);
}

private _createNewSessionForType(workspace: ISessionWorkspace, sessionType: ISessionType): ISession {
const resource = URI.from({ scheme: sessionType.id, path: `/untitled-${generateUuid()}` });
const status = observableValue<SessionStatus>(this, SessionStatus.Untitled);
const title = observableValue(this, '');
const updatedAt = observableValue(this, new Date());
Expand All @@ -316,7 +389,7 @@ export class LocalAgentHostSessionsProvider extends Disposable implements ISessi
sessionId: `${this.id}:${resource.toString()}`,
resource,
providerId: this.id,
sessionType: this.sessionTypes[0].id,
sessionType: sessionType.id,
icon: Codicon.vm,
createdAt,
workspace: observableValue(this, workspace),
Expand All @@ -340,8 +413,26 @@ export class LocalAgentHostSessionsProvider extends Disposable implements ISessi
return session;
}

setSessionType(_sessionId: string, _type: ISessionType): ISession {
throw new Error('Local agent host sessions do not support changing session type');
setSessionType(sessionId: string, type: ISessionType): ISession {
const prev = this._currentNewSession;
if (!prev || prev.sessionId !== sessionId) {
throw new Error(localize('cannotChangeExistingSessionType', "Cannot change session type on an existing local agent host session."));
}
const newType = this.sessionTypes.find(t => t.id === type.id);
if (!newType) {
throw new Error(localize('unknownSessionType', "Session type '{0}' is not available on local agent host.", type.id));
}
if (newType.id === prev.sessionType) {
return prev;
}
const workspace = prev.workspace.get();
if (!workspace) {
throw new Error('Pending session has no workspace');
}

const rebuilt = this._createNewSessionForType(workspace, newType);
this._onDidReplaceSession.fire({ from: prev, to: rebuilt });
return rebuilt;
}

setModel(sessionId: string, modelId: string): void {
Expand Down Expand Up @@ -507,7 +598,6 @@ export class LocalAgentHostSessionsProvider extends Disposable implements ISessi

for (const meta of sessions) {
const rawId = AgentSession.id(meta.session);
const provider = AgentSession.provider(meta.session) ?? DEFAULT_AGENT_PROVIDER;
currentKeys.add(rawId);

const existing = this._sessionCache.get(rawId);
Expand All @@ -516,7 +606,8 @@ export class LocalAgentHostSessionsProvider extends Disposable implements ISessi
changed.push(existing);
}
} else {
const cached = new LocalSessionAdapter(meta, this.id, sessionTypeForProvider(provider), this.sessionTypes[0].id);
const sessionType = this._sessionTypeForMetadata(meta);
const cached = new LocalSessionAdapter(meta, this.id, sessionType, sessionType);
this._sessionCache.set(rawId, cached);
added.push(cached);
}
Expand Down Expand Up @@ -585,12 +676,17 @@ export class LocalAgentHostSessionsProvider extends Disposable implements ISessi
isRead: summary.isRead,
isDone: summary.isDone,
};
const provider = AgentSession.provider(sessionUri) ?? DEFAULT_AGENT_PROVIDER;
const cached = new LocalSessionAdapter(meta, this.id, sessionTypeForProvider(provider), this.sessionTypes[0].id);
const sessionType = this._sessionTypeForMetadata(meta);
const cached = new LocalSessionAdapter(meta, this.id, sessionType, sessionType);
this._sessionCache.set(rawId, cached);
this._onDidChangeSessions.fire({ added: [cached], removed: [], changed: [] });
}

private _sessionTypeForMetadata(meta: IAgentSessionMetadata): string {
const provider = AgentSession.provider(meta.session) ?? DEFAULT_AGENT_PROVIDER;
return sessionTypeForProvider(provider);
}

private _handleSessionRemoved(session: URI | string): void {
const rawId = AgentSession.id(session);
const cached = this._sessionCache.get(rawId);
Expand Down
Loading
Loading