Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
1810e63
Forward W3C traceparent to MCP servers via _meta (SEP-414)
zhichli Apr 27, 2026
a106aa6
Address PR feedback: preserve trace flags/state, plumb inbound $invok…
zhichli Apr 27, 2026
559812e
Refactor titlebar action handling and improve CSS legibility for "Ope…
mrleemurray Apr 28, 2026
5d68d6a
Remove opacity from "Open in Agents" icon filter for improved legibility
mrleemurray Apr 28, 2026
3b7bb5c
Refactor OpenInAgentsAction to support macOS and Windows for sibling …
mrleemurray Apr 28, 2026
389fcc7
inlineChat: make status placeholder registration a singleton (#312975)
jrieken Apr 28, 2026
d3b2cc9
Merge pull request #312984 from microsoft/mrleemurray/open-in-agents-…
mrleemurray Apr 28, 2026
8d97209
Add strongForeground color for enhanced text legibility (#312993)
mrleemurray Apr 28, 2026
4f2f275
Agents: Enhance account menu UI and chat status tooltip (#313001)
mrleemurray Apr 28, 2026
f9c47b1
Agents: Enable tab shrinking in narrow viewports for better accessibi…
mrleemurray Apr 28, 2026
13e84c2
Agents: Fix padding for editor action items (#313006)
mrleemurray Apr 28, 2026
c11c069
fix: restrict entitlements used for helper.app (#312734)
deepak1556 Apr 28, 2026
a7bcda7
feat: enhance inline chat functionality with additional services and …
jrieken Apr 28, 2026
757e7d3
Merge pull request #312929 from zhichli/zhichli/mcp-traceparent-sep414
zhichli Apr 28, 2026
573fb62
[cherry-pick] Multifile Diff Editor - fix error on initial reveal (#3…
vs-code-engineering[bot] Apr 28, 2026
6b2fbaf
Fixes component explorer lockfile
hediet Apr 28, 2026
e78dfbd
agentHost: fix other cases of client-provided tools getting stuck (#3…
connor4312 Apr 28, 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
8 changes: 8 additions & 0 deletions build/azure-pipelines/darwin/helper-entitlements.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
</dict>
</plist>
2 changes: 2 additions & 0 deletions build/darwin/sign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ function getEntitlementsForFile(filePath: string): string {
return path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-renderer-entitlements.plist');
} else if (filePath.includes(' Helper (Plugin).app')) {
return path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-plugin-entitlements.plist');
} else if (filePath.includes(' Helper.app')) {
return path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-entitlements.plist');
}
return path.join(baseDir, 'azure-pipelines', 'darwin', 'app-entitlements.plist');
}
Expand Down
1 change: 1 addition & 0 deletions build/lib/stylelint/vscode-known-variables.json
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,7 @@
"--vscode-symbolIcon-typeParameterForeground",
"--vscode-symbolIcon-unitForeground",
"--vscode-symbolIcon-variableForeground",
"--vscode-strongForeground",
"--vscode-tab-activeBackground",
"--vscode-tab-activeBorder",
"--vscode-tab-activeBorderTop",
Expand Down
19 changes: 19 additions & 0 deletions extensions/copilot/src/extension/tools/vscode-node/toolsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,25 @@ export class ToolsService extends BaseToolsService {

const startTime = Date.now();

// Propagate W3C trace context to tool invocations so downstream spans can be
// correlated with this `execute_tool` span. MCP tools forward this onto
// `_meta.traceparent`/`_meta.tracestate` of the JSON-RPC `tools/call` payload
// (MCP SEP-414, see #302301). Only set if not already supplied by the caller.
const optionsWithTrace = options as vscode.LanguageModelToolInvocationOptions<Object> & { traceparent?: string; tracestate?: string };
const ctx = span.getSpanContext();
if (ctx) {
if (!optionsWithTrace.traceparent) {
// Preserve the upstream W3C trace flags when available. Fall back to `01`
// (sampled) so downstream MCP servers continue to participate in the trace
// when the abstraction does not surface flags (e.g. tests, in-memory impl).
const flags = (ctx.traceFlags ?? 0x01).toString(16).padStart(2, '0');
optionsWithTrace.traceparent = `00-${ctx.traceId}-${ctx.spanId}-${flags}`;
}
if (!optionsWithTrace.tracestate && ctx.traceState) {
optionsWithTrace.tracestate = ctx.traceState;
}
}

return vscode.lm.invokeTool(getContributedToolName(name), options, token).then(
result => {
span.setStatus(SpanStatusCode.OK);
Expand Down
8 changes: 8 additions & 0 deletions extensions/copilot/src/platform/otel/common/otelService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ export const IOTelService = createServiceIdentifier<IOTelService>('IOTelService'
export interface TraceContext {
readonly traceId: string;
readonly spanId: string;
/**
* W3C trace flags from the source span context (e.g. `0x01` for sampled). Optional
* because not all impls preserve it; consumers that build a W3C `traceparent` should
* fall back to a sampled value when unset.
*/
readonly traceFlags?: number;
/** W3C tracestate serialized as a comma-separated key=value list, when present. */
readonly traceState?: string;
}

/**
Expand Down
6 changes: 4 additions & 2 deletions extensions/copilot/src/platform/otel/node/otelServiceImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ export class NodeOTelService implements IOTelService {
if (!ctx.traceId || !ctx.spanId) {
return undefined;
}
return { traceId: ctx.traceId, spanId: ctx.spanId };
return { traceId: ctx.traceId, spanId: ctx.spanId, traceFlags: ctx.traceFlags, traceState: ctx.traceState?.serialize() };
}

// ── Trace Context Store ── (for cross-boundary propagation)
Expand Down Expand Up @@ -620,7 +620,9 @@ class RealSpanHandle implements ISpanHandle {

getSpanContext(): TraceContext | undefined {
const ctx = this._span.spanContext();
return ctx.traceId && ctx.spanId ? { traceId: ctx.traceId, spanId: ctx.spanId } : undefined;
return ctx.traceId && ctx.spanId
? { traceId: ctx.traceId, spanId: ctx.spanId, traceFlags: ctx.traceFlags, traceState: ctx.traceState?.serialize() }
: undefined;
}

end(): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js';
import { IObservable, ITransaction, ObservablePromise, ObservableResolvedPromise, constObservable, derived, derivedObservableWithWritableCache, mapObservableArrayCached, observableFromValueWithChangeEvent, observableValue, transaction } from '../../../../base/common/observable.js';
import { IObservable, ITransaction, ObservablePromise, ObservableResolvedPromise, constObservable, derived, derivedObservableWithWritableCache, mapObservableArrayCached, observableFromValueWithChangeEvent, observableValue, transaction, waitForState } from '../../../../base/common/observable.js';
import { timeout } from '../../../../base/common/async.js';
import { URI } from '../../../../base/common/uri.js';
import { ContextKeyValue } from '../../../../platform/contextkey/common/contextkey.js';
Expand All @@ -28,6 +28,7 @@ export class MultiDiffEditorViewModel extends Disposable {
});

public readonly isLoading;
private readonly _waitForNewDiffs: IObservable<ObservablePromise<readonly RefCounted<DocumentDiffItemViewModel>[]>>;

public readonly items: IObservable<readonly DocumentDiffItemViewModel[]>;

Expand All @@ -36,10 +37,12 @@ export class MultiDiffEditorViewModel extends Disposable {
(reader, lastValue) => this.focusedDiffItem.read(reader) ?? (lastValue && this.items.read(reader).indexOf(lastValue) !== -1) ? lastValue : undefined
);

public async waitForDiffs(): Promise<void> {
for (const d of this.items.get()) {
await d.diffEditorViewModel.waitForDiff();
public async waitForDiffOr1s(): Promise<void> {
if (this._documents.get() === 'loading') {
await waitForState(this._documents, documents => documents !== 'loading');
}

await this._waitForNewDiffs.get().promise;
}

public collapseAll(): void {
Expand Down Expand Up @@ -75,7 +78,7 @@ export class MultiDiffEditorViewModel extends Disposable {
(d, store) => store.add(RefCounted.create(this._instantiationService.createInstance(DocumentDiffItemViewModel, d, this)))
).recomputeInitiallyAndOnChange(this._store);

const waitForNewDiffs: IObservable<ObservablePromise<readonly RefCounted<DocumentDiffItemViewModel>[]>> = derived(this, reader => {
this._waitForNewDiffs = derived(this, reader => {
const next = allItems.read(reader);
const unresolved = next.filter(i => !i.object.waitForInitialDiffOr1s.promiseResult.read(undefined));
if (unresolved.length === 0) {
Expand All @@ -86,7 +89,7 @@ export class MultiDiffEditorViewModel extends Disposable {
);
});

const resolved = new ObservableResolvedPromise(waitForNewDiffs, [] as readonly RefCounted<DocumentDiffItemViewModel>[], this._store);
const resolved = new ObservableResolvedPromise(this._waitForNewDiffs, [] as readonly RefCounted<DocumentDiffItemViewModel>[], this._store);

this.items = derived(this, reader => {
const resolvedItems = resolved.lastResolved.read(reader);
Expand Down
14 changes: 13 additions & 1 deletion src/vs/platform/agentHost/node/agentSideEffects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,18 @@ export class AgentSideEffects extends Disposable {
const agents = this._options.agents.read(reader);
this._publishAgentInfos(agents, reader);
}));

// Server-dispatched SessionToolCallComplete actions (e.g. from
// the disconnect timeout in ProtocolServerHandler) bypass
// handleAction, so the agent's SDK deferred never resolves.
// Listen for these envelopes and notify the agent directly.
this._register(this._stateManager.onDidEmitEnvelope(envelope => {
if (!envelope.origin && envelope.action.type === ActionType.SessionToolCallComplete) {
const action = envelope.action;
const agent = this._options.getAgent(action.session);
agent?.onClientToolCallComplete(URI.parse(action.session), action.toolCallId, action.result);
}
}));
}

/**
Expand Down Expand Up @@ -622,7 +634,7 @@ export class AgentSideEffects extends Disposable {
if (autoApproval !== undefined) {
this._toolCallAgents.delete(`${sessionKey}:${e.toolCallId}`);
agent.respondToPermissionRequest(e.toolCallId, true);
return;
e = { ...e, confirmationTitle: undefined }; // don't trigger confirmation
}
this._stateManager.dispatchServerAction(
this._permissionManager.createToolReadyAction(e, sessionKey, turnId)
Expand Down
109 changes: 108 additions & 1 deletion src/vs/platform/agentHost/node/protocolServerHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,15 @@ import {
type ReconnectParams,
type IStateSnapshot,
} from '../common/state/sessionProtocol.js';
import { ROOT_STATE_URI, SessionStatus } from '../common/state/sessionState.js';
import { ResponsePartKind, ROOT_STATE_URI, SessionStatus, ToolCallConfirmationReason, ToolCallStatus, ToolResultContentType, type SessionState } from '../common/state/sessionState.js';
import type { IProtocolServer, IProtocolTransport } from '../common/state/sessionTransport.js';
import { AgentHostStateManager } from './agentHostStateManager.js';

/** Default capacity of the server-side action replay buffer. */
const REPLAY_BUFFER_CAPACITY = 1000;

const CLIENT_TOOL_CALL_DISCONNECT_TIMEOUT = 30_000;

/** Build a JSON-RPC success response suitable for transport.send(). */
function jsonRpcSuccess(id: number, result: unknown): JsonRpcResponse {
return { jsonrpc: '2.0', id, result };
Expand Down Expand Up @@ -100,6 +102,7 @@ export class ProtocolServerHandler extends Disposable {

private readonly _clients = new Map<string, IConnectedClient>();
private readonly _replayBuffer: ActionEnvelope[] = [];
private readonly _clientToolCallDisconnectTimeouts = new Map<string, ReturnType<typeof setTimeout>>();

private readonly _onDidChangeConnectionCount = this._register(new Emitter<number>());

Expand Down Expand Up @@ -206,6 +209,7 @@ export class ProtocolServerHandler extends Disposable {
this._logService.info(`[ProtocolServer] Client disconnected: ${client.clientId}, subscriptions=${client.subscriptions.size}`);
this._clients.delete(client.clientId);
this._rejectPendingReverseRequests(client.clientId);
this._handleClientDisconnected(client.clientId);
this._onDidChangeConnectionCount.fire(this._clients.size);
}
disposables.dispose();
Expand Down Expand Up @@ -256,6 +260,7 @@ export class ProtocolServerHandler extends Disposable {
if (snapshot) {
snapshots.push(snapshot);
client.subscriptions.add(uri.toString());
this._clearClientToolCallDisconnectTimeout(params.clientId, uri.toString());
}
}
}
Expand Down Expand Up @@ -295,6 +300,7 @@ export class ProtocolServerHandler extends Disposable {
const actions: ActionEnvelope[] = [];
for (const sub of params.subscriptions) {
client.subscriptions.add(sub.toString());
this._clearClientToolCallDisconnectTimeout(params.clientId, sub.toString());
}
for (const envelope of this._replayBuffer) {
if (envelope.serverSeq > params.lastSeenServerSeq) {
Expand All @@ -311,12 +317,108 @@ export class ProtocolServerHandler extends Disposable {
if (snapshot) {
snapshots.push(snapshot);
client.subscriptions.add(sub);
this._clearClientToolCallDisconnectTimeout(params.clientId, sub);
}
}
return { client, response: { type: 'snapshot', snapshots } };
}
}

private _handleClientDisconnected(clientId: string): void {
for (const session of this._stateManager.getSessionUris()) {
const state = this._stateManager.getSessionState(session);
const ownsPendingToolCall = state ? this._hasPendingClientToolCall(state, clientId) : false;
if (state?.activeClient?.clientId === clientId) {
this._stateManager.dispatchServerAction({
type: ActionType.SessionActiveClientChanged,
session,
activeClient: null,
});
}
if (state?.activeClient?.clientId === clientId || ownsPendingToolCall) {
this._startClientToolCallDisconnectTimeout(clientId, session);
}
}
}

private _hasPendingClientToolCall(state: ReturnType<AgentHostStateManager['getSessionState']>, clientId: string): boolean {
const activeTurn = state?.activeTurn;
if (!activeTurn) {
return false;
}
return activeTurn.responseParts.some(part => part.kind === ResponsePartKind.ToolCall
&& part.toolCall.toolClientId === clientId
&& (part.toolCall.status === ToolCallStatus.Streaming || part.toolCall.status === ToolCallStatus.Running || part.toolCall.status === ToolCallStatus.PendingConfirmation));
}

private _hasReplacementActiveClientTool(state: SessionState, clientId: string, toolName: string): boolean {
const activeClient = state.activeClient;
return activeClient !== undefined
&& activeClient.clientId !== clientId
&& activeClient.tools.some(tool => tool.name === toolName);
}

private _startClientToolCallDisconnectTimeout(clientId: string, session: string): void {
this._clearClientToolCallDisconnectTimeout(clientId, session);
const key = this._clientToolCallDisconnectTimeoutKey(clientId, session);
this._clientToolCallDisconnectTimeouts.set(key, setTimeout(() => {
this._clientToolCallDisconnectTimeouts.delete(key);
this._completeDisconnectedClientToolCalls(clientId, session);
}, CLIENT_TOOL_CALL_DISCONNECT_TIMEOUT));
}

private _clearClientToolCallDisconnectTimeout(clientId: string, session: string): void {
const key = this._clientToolCallDisconnectTimeoutKey(clientId, session);
const timeout = this._clientToolCallDisconnectTimeouts.get(key);
if (timeout) {
clearTimeout(timeout);
this._clientToolCallDisconnectTimeouts.delete(key);
}
}

private _clientToolCallDisconnectTimeoutKey(clientId: string, session: string): string {
return `${clientId}\n${session}`;
}

private _completeDisconnectedClientToolCalls(clientId: string, session: string): void {
const state = this._stateManager.getSessionState(session);
const activeTurn = state?.activeTurn;
if (!activeTurn) {
return;
}
for (const part of activeTurn.responseParts) {
if (part.kind !== ResponsePartKind.ToolCall) {
continue;
}
const toolCall = part.toolCall;
if (toolCall.toolClientId === clientId && (toolCall.status === ToolCallStatus.Streaming || toolCall.status === ToolCallStatus.Running || toolCall.status === ToolCallStatus.PendingConfirmation)) {
const mayRetryWithReplacementClient = this._hasReplacementActiveClientTool(state, clientId, toolCall.toolName);
if (toolCall.status === ToolCallStatus.Streaming) {
this._stateManager.dispatchServerAction({
type: ActionType.SessionToolCallReady,
session,
turnId: activeTurn.id,
toolCallId: toolCall.toolCallId,
invocationMessage: toolCall.invocationMessage ?? toolCall.displayName,
confirmed: ToolCallConfirmationReason.NotNeeded,
});
}
this._stateManager.dispatchServerAction({
type: ActionType.SessionToolCallComplete,
session,
turnId: activeTurn.id,
toolCallId: toolCall.toolCallId,
result: {
success: false,
pastTenseMessage: `${toolCall.displayName} failed`,
...(mayRetryWithReplacementClient ? { content: [{ type: ToolResultContentType.Text, text: `The client that was running ${toolCall.displayName} disconnected, but another active client now provides ${toolCall.displayName}. You may try calling the tool again.` }] } : {}),
error: { message: `Client ${clientId} disconnected before completing ${toolCall.displayName}` },
},
});
}
}
}

// ---- Requests (expect a response) ---------------------------------------

/**
Expand All @@ -328,6 +430,7 @@ export class ProtocolServerHandler extends Disposable {
try {
const snapshot = await this._agentService.subscribe(URI.parse(params.resource));
client.subscriptions.add(params.resource);
this._clearClientToolCallDisconnectTimeout(client.clientId, params.resource);
return { snapshot };
} catch (err) {
if (err instanceof ProtocolError) {
Expand Down Expand Up @@ -606,6 +709,10 @@ export class ProtocolServerHandler extends Disposable {
pending.reject(new Error('ProtocolServerHandler disposed'));
}
this._pendingReverseRequests.clear();
for (const timeout of this._clientToolCallDisconnectTimeouts.values()) {
clearTimeout(timeout);
}
this._clientToolCallDisconnectTimeouts.clear();
this._replayBuffer.length = 0;
super.dispose();
}
Expand Down
Loading
Loading