Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
e5391a0
sessions: enable application badge in non-stable builds (#333878)
benibenj Sep 1, 2026
cf72409
[Perf] Chat: limit file completion search results (#333874)
roblourens Sep 1, 2026
42b530c
Sync complete skill directories to remote agent hosts (#333827)
pwang347 Sep 1, 2026
8a6a845
sessions: refine single-pane details behavior (#333881)
sandy081 Sep 1, 2026
ab4e1af
Show remaining turns to execution subagents (#332704)
piyushmadan Sep 1, 2026
9107861
sessions: Preserve background layouts per color scheme (#333873)
TylerLeonhardt Sep 1, 2026
32e32a7
Skip redundant customization location picker (#333888)
houghj16 Sep 1, 2026
d5a3b39
agentHost: disable flaky Codex plugin discovery E2E (#333893)
roblourens Sep 1, 2026
b5a065f
agentHost: preserve complete multi-root changes summary (#333703)
DonJayamanne Sep 2, 2026
93c1915
fix focus on the agents window new chat state (#333904)
justschen Sep 2, 2026
aa56e4e
remote tunnels: unify agent window access (#333886)
connor4312 Sep 2, 2026
98807d2
Render sandbox command approvals as terminal confirmations (#333883)
osortega Sep 2, 2026
7cc261e
Browser: support tracking the host session id separately from agentic…
kycutler Sep 2, 2026
48465bf
agentHost: do not revoke shared credentials when a client has no toke…
connor4312 Sep 2, 2026
5582533
Improve model selection and context handling in VS Code sandbox (#333…
osortega Sep 2, 2026
63acf08
Highlight generic tool output by content type (#333923)
roblourens Sep 2, 2026
d953e72
Perf: Improve perf of legacy Copilot CLI session listing & migration …
vijayupadya Sep 2, 2026
f87494c
Add Agent Host prompt overrides (#333920)
bhavyaus Sep 2, 2026
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 @@ -33,6 +33,7 @@ export class ExecutionSubagentPrompt extends PromptElement<ExecutionSubagentProm
// Check if we're at the last turn (to align with training where we coax final answer)
const currentTurn = toolCallRounds?.length ?? 0;
const isLastTurn = currentTurn >= this.props.maxExecutionTurns - 1;
const remainingTurns = Math.max(this.props.maxExecutionTurns - currentTurn, 1);

return (
<>
Expand Down Expand Up @@ -82,11 +83,9 @@ export class ExecutionSubagentPrompt extends PromptElement<ExecutionSubagentProm
toolCallResults={toolCallResults}
toolCallMode={CopilotToolMode.FullContext}
/>
{isLastTurn && (
<UserMessage priority={900}>
OK, your allotted iterations are finished. Show the &lt;final_answer&gt;.
</UserMessage>
)}
<UserMessage priority={900}>
You have {remainingTurns} of {this.props.maxExecutionTurns} allotted iterations remaining. When one iteration remains, do not call tools; return only the &lt;final_answer&gt;.
</UserMessage>
{!isLastTurn && this.props.hasBackgroundCommand && (
<UserMessage priority={900}>
One or more commands are running in the background. You do not have the ability to monitor them. Show the &lt;final_answer&gt;.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,8 @@ import { DEV_CONTAINER_AGENT_HOST_CHANNEL, IDevContainerAgentHostMainService } f
import { DevContainerAgentHostMainService } from '../../../platform/agentHost/node/devContainerAgentHostService.js';
import { IWSLRemoteAgentHostMainService, WSL_REMOTE_AGENT_HOST_CHANNEL } from '../../../platform/agentHost/common/wslRemoteAgentHost.js';
import { WSLRemoteAgentHostMainService } from '../../../platform/agentHost/node/wslRemoteAgentHostService.js';
import { ITunnelAgentHostMainService, ITunnelAgentHostHostingService, TUNNEL_AGENT_HOST_CHANNEL, TUNNEL_HOST_CHANNEL } from '../../../platform/agentHost/common/tunnelAgentHost.js';
import { ITunnelAgentHostMainService, TUNNEL_AGENT_HOST_CHANNEL } from '../../../platform/agentHost/common/tunnelAgentHost.js';
import { TunnelAgentHostMainService } from '../../../platform/agentHost/node/tunnelAgentHostService.js';
import { TunnelHostMainService } from '../../../platform/agentHost/node/tunnelHostMainService.js';
import { IUserDataProfilesService } from '../../../platform/userDataProfile/common/userDataProfile.js';
import { IExtensionsProfileScannerService } from '../../../platform/extensionManagement/common/extensionsProfileScannerService.js';
import { PolicyChannelClient } from '../../../platform/policy/common/policyIpc.js';
Expand Down Expand Up @@ -434,9 +433,6 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter {
// Tunnel Agent Host
services.set(ITunnelAgentHostMainService, new SyncDescriptor(TunnelAgentHostMainService, undefined, true));

// Tunnel Host (hosting local agent host for remote connections)
services.set(ITunnelAgentHostHostingService, new SyncDescriptor(TunnelHostMainService, undefined, true));

return new InstantiationService(services);
}

Expand Down Expand Up @@ -529,9 +525,6 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter {
const tunnelAgentHostChannel = ProxyChannel.fromService(accessor.get(ITunnelAgentHostMainService), this._store);
this.server.registerChannel(TUNNEL_AGENT_HOST_CHANNEL, tunnelAgentHostChannel);

// Tunnel Host
const tunnelHostChannel = ProxyChannel.fromService(accessor.get(ITunnelAgentHostHostingService), this._store);
this.server.registerChannel(TUNNEL_HOST_CHANNEL, tunnelHostChannel);
}

private registerErrorHandler(logService: ILogService): void {
Expand Down
10 changes: 10 additions & 0 deletions src/vs/platform/agentHost/common/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1228,6 +1228,16 @@ export interface IAgent {

// ---- Metadata -----------------------------------------------------------

/**
* Warms a short-lived, in-memory cache of per-session metadata from a single
* bulk provider call, so a subsequent burst of {@link getChatMetadata} calls
* (e.g. a `listSessions` pass over a large catalogue) can be served without
* one provider round-trip per session. Returns a disposable that clears the
* cache; callers dispose it once the burst is complete. Optional: providers
* without a cheap bulk read simply omit it and pay per session.
*/
prewarmSessionMetadata?(): Promise<IDisposable>;

/** Retrieve metadata for an exact registered chat. Ambient catalogue reads never set {@link IAgentChatMetadataOptions.activation}. */
getChatMetadata(chat: URI, context: URI | IAgentChatContext, providerData?: string, options?: IAgentChatMetadataOptions): Promise<IAgentChatMetadata | undefined>;

Expand Down
15 changes: 15 additions & 0 deletions src/vs/platform/agentHost/common/agentModelPricing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,19 @@ const NUMBER_KEYS = [
'discountPercent',
] as const satisfies readonly (keyof IAgentModelPricingMeta)[];

/**
* Flat-dotted `_meta` key the Copilot agent host publishes a model's capability category under.
*
* A host that derives its model list from the Copilot SDK namespaces its metadata by producer
* rather than using the flat {@link IAgentModelPricingMeta} key names, so the category arrives
* under this key instead of `category`. Read as a fallback so sandbox models still show a
* capability category in the picker hover; the flat key wins when both are present.
*
* Only the category is mapped: such a host surfaces no billing information at all, so there is no
* multiplier or cost to recover.
*/
const COPILOT_MODEL_PICKER_CATEGORY_META_KEY = 'copilot.modelPickerCategory';

/**
* Reads the well-known {@link IAgentModelPricingMeta} keys from a model's open `_meta` bag, ignoring any unrelated
* provider-specific keys and values of the wrong type. Returns an object containing only the keys that were present
Expand All @@ -85,6 +98,8 @@ export function readAgentModelPricingMeta(model: IAgentModelInfo | SessionModelI
}
if (typeof meta.category === 'string') {
result.category = meta.category;
} else if (typeof meta[COPILOT_MODEL_PICKER_CATEGORY_META_KEY] === 'string') {
result.category = meta[COPILOT_MODEL_PICKER_CATEGORY_META_KEY];
}
const rawPromo = meta.promo;
if (rawPromo && typeof rawPromo === 'object' && !Array.isArray(rawPromo)) {
Expand Down
16 changes: 15 additions & 1 deletion src/vs/platform/agentHost/common/copilotCliConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ export interface ICopilotCliModelCapabilityOverride {
readonly excludedTools?: readonly string[];
/** Deep-merged over the runtime's resolved defaults (e.g. `supports.vision`). */
readonly modelCapabilities?: Record<string, unknown>;
/** Inline YAML with system-prompt and tool-description overrides. */
readonly promptOverrideString?: string;
/** Path to a YAML file with system-prompt and tool-description overrides. */
readonly promptOverrideFile?: string;
}

/** Map of model id → capability override. */
Expand Down Expand Up @@ -206,7 +210,7 @@ export const copilotCliConfigSchema = createSchema({
[CopilotCliConfigKey.ModelCapabilityOverrides]: schemaProperty<CopilotCliModelCapabilityOverrides>({
type: 'object',
title: localize('agentHost.config.modelCapabilityOverrides.title', "Model Capability Overrides"),
description: localize('agentHost.config.modelCapabilityOverrides.description', "Per-model capability overrides for Copilot SDK sessions, keyed by model id (`*` matches every model; a specific entry wins field-by-field). Aliasing a model id to a known `family` routes it to that family's tuned system prompt and tool profile without changing the model id sent to the runtime; the remaining fields override reasoning effort, tool enablement, and model capability limits per model. Only affects Copilot SDK sessions; intended for experimentation."),
description: localize('agentHost.config.modelCapabilityOverrides.description', "Per-model overrides for Copilot SDK sessions. Use `*` to match every model. Intended for experimentation."),
additionalProperties: {
type: 'object',
title: localize('agentHost.config.modelCapabilityOverrides.entry.title', "Capability Override"),
Expand Down Expand Up @@ -240,6 +244,16 @@ export const copilotCliConfigSchema = createSchema({
title: localize('agentHost.config.modelCapabilityOverrides.modelCapabilities.title', "Model Capabilities"),
description: localize('agentHost.config.modelCapabilityOverrides.modelCapabilities.description', "Per-property model capability overrides passed through to the Copilot SDK's `modelCapabilities` session field (e.g. `{ \"supports\": { \"vision\": false }, \"limits\": { \"max_context_window_tokens\": 64000 } }`), deep-merged over the runtime's resolved defaults for this model. Applied when the session launches or resumes."),
},
promptOverrideString: {
type: 'string',
title: localize('agentHost.config.modelCapabilityOverrides.promptOverrideString.title', "Prompt Override String"),
description: localize('agentHost.config.modelCapabilityOverrides.promptOverrideString.description', "Inline YAML that overrides the system prompt and/or SDK tool descriptions for sessions on this model. Takes precedence over `promptOverrideFile`."),
},
promptOverrideFile: {
type: 'string',
title: localize('agentHost.config.modelCapabilityOverrides.promptOverrideFile.title', "Prompt Override File"),
description: localize('agentHost.config.modelCapabilityOverrides.promptOverrideFile.description', "Path to a YAML file that overrides the system prompt and/or SDK tool descriptions for sessions on this model. Ignored when `promptOverrideString` is also set."),
},
},
},
default: {},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

/**
* Reader for the permission metadata a remote agent host echoes onto a tool
* call that is waiting for approval.
*
* A remote host describes the pending decision (run a command, read a file, …)
* but does not stamp the `_meta.toolKind` rendering hint local agent adapters
* provide, so the kind is recovered from here instead.
*/

interface IHasPermissionRequestMeta {
readonly _meta?: Record<string, unknown>;
}

/**
* The permission kinds that carry a rendering consequence. A remote host
* reports more kinds than these; the rest are left unrecognized so they fall
* through to the generic tool presentation.
*/
export const enum AgentPermissionRequestKind {
/** Execute a shell command. */
Commands = 'commands',
/** Read a single file. */
Read = 'read',
}

export interface IAgentPermissionRequestMeta {
readonly kind?: AgentPermissionRequestKind;
}

/**
* Normalizes a wire `kind`. A shell request arrives as `"commands"` on the
* projected payload and `"shell"` on the raw one.
*
* A path-batched request (`"path"`, whose own `accessKind` may be `"shell"`) is
* not a command: its subject is a list of paths, not a command line.
*/
function normalizeKind(value: unknown): AgentPermissionRequestKind | undefined {
switch (value) {
case 'commands':
case 'shell':
return AgentPermissionRequestKind.Commands;
case 'read':
return AgentPermissionRequestKind.Read;
default:
return undefined;
}
}

function readKind(value: unknown): AgentPermissionRequestKind | undefined {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return undefined;
}
return normalizeKind((value as Record<string, unknown>)['kind']);
}

/**
* Reads the recognized permission metadata from a tool call's `_meta` bag.
*
* Hosts echo the same request as `promptRequest` (the prompt-shaped
* projection) and `permissionRequest` (the raw form); older hosts send only
* the raw one.
*/
export function readAgentPermissionRequestMeta(source: IHasPermissionRequestMeta): IAgentPermissionRequestMeta {
const meta = source._meta;
if (!meta) {
return {};
}
const kind = readKind(meta['promptRequest']) ?? readKind(meta['permissionRequest']);
return kind ? { kind } : {};
}
23 changes: 19 additions & 4 deletions src/vs/platform/agentHost/common/state/sessionReducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,29 @@
// Re-export reducers from the protocol layer
export { rootReducer, sessionReducer, chatReducer, changesetReducer, annotationsReducer, automationReducer, automationRunReducer, softAssertNever, isClientDispatchable } from './protocol/reducers.js';

import { AgentPermissionRequestKind, readAgentPermissionRequestMeta } from '../meta/agentPermissionRequestMeta.js';
import { readToolCallMeta, type ToolKind } from '../meta/agentToolCallMeta.js';
import type { ICompletedToolCall, ToolCallState } from './sessionState.js';

/** Rendering kinds implied by a remote host's permission request. */
const PERMISSION_REQUEST_TOOL_KINDS: Readonly<Partial<Record<AgentPermissionRequestKind, ToolKind>>> = {
[AgentPermissionRequestKind.Commands]: 'terminal',
[AgentPermissionRequestKind.Read]: 'read',
};

/**
* Extracts the VS Code-specific `toolKind` hint from a tool call's `_meta`
* bag. This is not part of the protocol and is injected by the agent adapter
* (e.g. `copilotEventMapper`).
* Extracts the VS Code-specific `toolKind` rendering hint for a tool call.
*
* Normally the `_meta.toolKind` flag an agent adapter injects (e.g.
* `copilotEventMapper`); it is not part of the protocol. A remote agent host
* does not stamp that key, so for a call awaiting approval the kind comes from
* the permission request it echoes instead.
*/
export function getToolKind(tc: ToolCallState | ICompletedToolCall): ToolKind | undefined {
return readToolCallMeta(tc).toolKind;
const kind = readToolCallMeta(tc).toolKind;
if (kind) {
return kind;
}
const permissionKind = readAgentPermissionRequestMeta(tc).kind;
return permissionKind ? PERMISSION_REQUEST_TOOL_KINDS[permissionKind] : undefined;
}
39 changes: 0 additions & 39 deletions src/vs/platform/agentHost/common/tunnelAgentHost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -534,14 +534,6 @@ export interface ITunnelAgentHostService {
getAuthProvider(options?: { silent?: boolean }): Promise<'github' | 'microsoft' | undefined>;
}

// ---- Tunnel hosting (exposing the local agent host to remote clients) --------

/** IPC channel name for the tunnel host service. */
export const TUNNEL_HOST_CHANNEL = 'tunnelHost';

/** Output channel ID for the tunnel host logs. */
export const TUNNEL_HOST_LOG_ID = 'tunnelHostService';

/** Information about an actively hosted tunnel. */
export interface ITunnelHostInfo {
readonly tunnelName: string;
Expand All @@ -560,34 +552,3 @@ export function isTunnelHosted(sharingInfo: ITunnelHostInfo | undefined, tunnel:
? sharingInfo.tunnelId === tunnel.tunnelId
: sharingInfo.tunnelName === tunnel.name;
}

/** Status of the tunnel host. */
export type TunnelHostStatus =
| { readonly active: false }
| { readonly active: true; readonly info: ITunnelHostInfo };

/**
* Shared-process service that hosts a dev tunnel using the code CLI.
*/
export const ITunnelAgentHostHostingService = createDecorator<ITunnelAgentHostHostingService>('tunnelAgentHostHostingService');

export interface ITunnelAgentHostHostingService {
readonly _serviceBrand: undefined;

/** Fires when the hosting status changes. */
readonly onDidChangeStatus: Event<TunnelHostStatus>;

/**
* Start hosting a dev tunnel that exposes the local agent host.
*
* @param token The user's access token.
* @param authProvider The auth provider that issued the token.
*/
startHosting(token: string, authProvider: 'github' | 'microsoft'): Promise<ITunnelHostInfo>;

/** Stop hosting and clean up the tunnel. */
stopHosting(): Promise<void>;

/** Get the current hosting status. */
getStatus(): Promise<TunnelHostStatus>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -988,7 +988,7 @@ export class AgentHostChangesetService extends Disposable implements IAgentHostC
orderedSources.push(nonGitDiffs);
}
const evaluation = evaluateMultiRootDiffSources(orderedSources);
if (evaluation.outcome === 'failed') {
if (evaluation.outcome !== 'complete') {
// No source produced diffs (total failure or no sources at all).
// Preserve the previously cached summary instead of clobbering it
// with a spurious zero aggregate.
Expand Down
31 changes: 31 additions & 0 deletions src/vs/platform/agentHost/node/agentHostDatabase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ export interface IAgentHostDatabaseExternalUpdate {
readonly external: boolean;
}

export interface IAgentHostDatabaseModifiedTimeUpdate {
readonly session: string;
readonly modifiedTime: number;
}

export interface IAgentHostDatabase extends IDisposable {
/**
* Records a session with source-aware provenance. When requested, the
Expand All @@ -56,6 +61,8 @@ export interface IAgentHostDatabase extends IDisposable {
updateSessionExternal(updates: readonly IAgentHostDatabaseExternalUpdate[]): Promise<void>;
/** Advances the durable last-observed modification time. */
updateSessionModifiedTime(session: string, modifiedTime: number): Promise<boolean>;
/** Advances the durable last-observed modification time for many sessions in one transaction. */
updateSessionModifiedTimes(updates: readonly IAgentHostDatabaseModifiedTimeUpdate[]): Promise<void>;
getSession(session: string): Promise<IAgentHostDatabaseSession | undefined>;
listSessions(): Promise<readonly IAgentHostDatabaseSession[]>;
isSessionRegistryEmpty(): Promise<boolean>;
Expand Down Expand Up @@ -302,6 +309,30 @@ export class AgentHostDatabase implements IAgentHostDatabase {
return changes > 0;
}

async updateSessionModifiedTimes(updates: readonly IAgentHostDatabaseModifiedTimeUpdate[]): Promise<void> {
// Advancing durable recency for a large catalogue one statement at a time
// dominates discovery, so the whole batch is flushed in a single
// transaction. The `modified_time < ?` guard keeps each advance monotonic
// even if a concurrent write moved a row forward since the snapshot.
const statements = updates
.filter(({ modifiedTime }) => Number.isFinite(modifiedTime))
.map(({ session, modifiedTime }) => `UPDATE sessions SET modified_time = ${modifiedTime} WHERE session_uri = ${quoteSqlString(session)} AND modified_time < ${modifiedTime}`);
if (statements.length === 0) {
return;
}
const database = await this._ensureDatabase();
try {
await exec(database, `BEGIN IMMEDIATE;\n${statements.join(';\n')};\nCOMMIT`);
} catch (error) {
try {
await exec(database, 'ROLLBACK');
} catch (rollbackError) {
throw new AggregateError([error, rollbackError], 'Failed to advance session modified times');
}
throw error;
}
}

async listSessions(): Promise<readonly IAgentHostDatabaseSession[]> {
const rows = await all(await this._ensureDatabase(), 'SELECT session_uri, provider, start_time, modified_time, external, registration_source FROM sessions', []);
return rows.map(row => ({
Expand Down
Loading
Loading