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 @@ -51,6 +51,7 @@ export interface InstructionsCollectionEvent {
claudeAgentsCount: number;
}


export interface IAutomaticInstructionsCollectorContext {
readonly tools: Map<vscode.LanguageModelToolInformation, boolean>;
readonly modeInstructions2?: vscode.ChatRequestModeInstructions;
Expand Down Expand Up @@ -550,7 +551,7 @@ export class AutomaticInstructionsCollector implements IAutomaticInstructionsCol
lines.push('<agents>');
lines.push('Here is a list of agents that can be used when running a subagent.');
lines.push('Each agent has optionally a description with the agent\'s purpose and expertise. When asked to run a subagent, choose the most appropriate agent from this list.');
lines.push(`Use the ${getToolReferencePromptContent(runSubagentTool)} tool with an agent name from this list to run that agent, or omit agentName to use the current agent.`);
lines.push(`Use the ${getToolReferencePromptContent(runSubagentTool)} tool with the agent name to run the subagent.`);

for (const agent of customAgents) {
if (!canInvokeAgent(agent)) {
Expand Down Expand Up @@ -970,4 +971,3 @@ namespace ICustomInstructionsDebugInfo {
return result.join('\n');
}
}

6 changes: 4 additions & 2 deletions extensions/git/src/test/smoke.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,8 @@ suite('git smoke test', function () {
assert.strictEqual(repository.state.indexChanges.length, 0);
});

test('opens notebook diff and file from active notebook editor', async function () {
// diabled because of https://github.com/microsoft/vscode/issues/327142
test.skip('opens notebook diff and file from active notebook editor', async function () {
const committed = JSON.stringify({ cells: [{ cell_type: 'code', source: ['x = 1'], metadata: {}, outputs: [], execution_count: null }], metadata: {}, nbformat: 4, nbformat_minor: 5 });
fs.writeFileSync(file('notebook.ipynb'), committed);
await repository.add([file('notebook.ipynb')]);
Expand All @@ -174,7 +175,8 @@ suite('git smoke test', function () {
}
});

test('rename/delete conflict', async function () {
// diabled because of https://github.com/microsoft/vscode/issues/327142
test.skip('rename/delete conflict', async function () {
await commands.executeCommand('workbench.view.scm');

const appjs = file('app.js');
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
ea279d99
1ed58e31
Original file line number Diff line number Diff line change
Expand Up @@ -1344,7 +1344,6 @@ export const enum ToolResultContentType {
Resource = 'resource',
FileEdit = 'fileEdit',
Terminal = 'terminal',
TerminalComplete = 'terminalComplete',
Subagent = 'subagent',
}

Expand Down Expand Up @@ -1402,6 +1401,11 @@ export interface ToolResultFileEditContent extends FileEdit {
* Clients can subscribe to the terminal's URI to stream its output in real
* time, providing live feedback while a tool is executing.
*
* When the command exits, {@link result} is filled in on the completed
* result, retaining the outcome for clients that did not subscribe. This
* records the command's exit, not the terminal's — the terminal may keep
* running afterwards.
*
* @category Tool Result Content
*/
export interface ToolResultTerminalContent {
Expand All @@ -1410,34 +1414,31 @@ export interface ToolResultTerminalContent {
resource: URI;
/** Display title for the terminal content */
title: string;
/**
* Whether this terminal-style resource is backed by a pseudoterminal.
* When `false`, output is plain text and clients do not need to parse
* VT sequences.
*/
isPty?: boolean;
/** Outcome of the command, present once it has exited. */
result?: TerminalCommandResult;
}

/**
* Record of a command executed by a terminal-style tool (e.g. a shell tool),
* appended to the tool result when the command exits.
*
* This records the command's exit, not the terminal's — the terminal may
* keep running afterwards.
*
* When live output was exposed through a terminal channel (a
* {@link ToolResultTerminalContent} block in the same tool result),
* {@link resource} identifies that channel; otherwise this block stands alone
* as the retained command result.
* Outcome of a command run in a terminal-style tool, filled in on
* {@link ToolResultTerminalContent.result} once the command exits.
*
* @category Tool Result Content
*/
export interface ToolResultTerminalCompleteContent {
type: ToolResultContentType.TerminalComplete;
/**
* URI of the `ahp-terminal:` channel that carried live output for this
* command, if one was exposed.
*/
resource?: URI;
export interface TerminalCommandResult {
/** Exit code from the completed command, if reported by the runtime */
exitCode?: number;
/** Working directory where the command was executed */
cwd?: URI;
/** Preview of the command's output, if available */
/**
* Preview of the command's output, for clients that are not subscribed
* to the terminal or that arrive after it is disposed. When `isPty` is
* `true` the preview may contain VT sequences; when `false` it is plain
* text.
*/
preview?: string;
/** Whether `preview` is known to be incomplete or truncated */
truncated?: boolean;
Expand Down Expand Up @@ -1471,8 +1472,8 @@ export interface ToolResultSubagentContent {
* Mirrors the content blocks in MCP `CallToolResult.content`, plus
* `ToolResultResourceContent` for lazy-loading large results,
* `ToolResultFileEditContent` for file edit diffs,
* `ToolResultTerminalContent` for live terminal output,
* `ToolResultTerminalCompleteContent` for terminal-style completion metadata, and
* `ToolResultTerminalContent` for live terminal output and
* command completion metadata, and
* `ToolResultSubagentContent` for tool-spawned worker chats (AHP extensions).
*
* @category Tool Result Content
Expand All @@ -1483,5 +1484,4 @@ export type ToolResultContent =
| ToolResultResourceContent
| ToolResultFileEditContent
| ToolResultTerminalContent
| ToolResultTerminalCompleteContent
| ToolResultSubagentContent;
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,21 @@ export interface CreateSessionParams extends BaseParams {
*/
workingDirectories?: URI[];
/**
* The primary working directory for the session's **default chat** — the
* distinguished root that chat is centered on (see
* {@link ChatState.primaryWorkingDirectory}). A session has no primary of its
* own; this seeds the default chat's primary. When set, it MUST be one of
* {@link workingDirectories}. A client SHOULD supply this when the agent
* advertises {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; a
* host MAY reject creation that omits it, or fall back to the first entry of
* The primary working directory for the session's **default chat**.
*
* A session has no primary of its own — primary is a per-chat notion (see
* {@link ChatState.primaryWorkingDirectory}). But `createSession` implicitly
* creates the session's default chat, and there is no separate `createChat`
* call to carry that chat's create-time fields. This field is therefore the
* only place a client can designate the **default chat's** primary at birth;
* it is copied into that chat's read-only `primaryWorkingDirectory`. For any
* non-default chat, pass {@link CreateChatParams.primaryWorkingDirectory}
* instead.
*
* When set, it MUST be one of {@link workingDirectories}. A client SHOULD
* supply this when the agent advertises
* {@link MultipleWorkingDirectoriesCapability.requiresPrimary}; a host MAY
* reject creation that omits it, or fall back to the first entry of
* `workingDirectories`. Ignored for forked sessions (a fork inherits the
* source session's chats and their primaries).
*/
Expand Down Expand Up @@ -279,15 +287,6 @@ export interface CompletionItem {
*/
insertText: string;

/**
* Optional display label for the completion item. When omitted, the client
* SHOULD display {@link insertText}. Provide an explicit label when the
* inserted text differs from the label shown in the picker — e.g. a pure
* action item that inserts nothing (`insertText: ''`) but should still be
* shown to the user.
*/
label?: string;

/**
* If defined, the start of the range in the input's `text` that is replaced
* by `insertText`. The range is the half-open interval
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,12 @@ export interface TerminalState {
* are absent in the normal idle state.
*/
supportsCommandDetection?: boolean;
/**
* Whether this terminal-style resource is backed by a pseudoterminal.
* When `false`, output is plain text and clients do not need to parse
* VT sequences.
*/
isPty?: boolean;
}

// ─── Terminal Content Parts ──────────────────────────────────────────────────
Expand Down
2 changes: 1 addition & 1 deletion src/vs/platform/agentHost/common/state/sessionState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export {
type ToolCallContributor,
type ToolDefinition, type ToolResultContent,
type ToolResultFileEditContent,
type ToolResultTerminalCompleteContent,
type TerminalCommandResult,
type ToolResultSubagentContent,
type ToolResultTerminalContent,
type ToolResultTextContent,
Expand Down
97 changes: 95 additions & 2 deletions src/vs/platform/agentHost/node/agentHostTerminalManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ export interface IAgentHostTerminalManager {
getTerminalInfos(): TerminalInfo[];
getTerminalState(uri: string): TerminalState | undefined;
getDefaultShell(): Promise<string>;
createOutputTerminal(uri: string, options: { title: string; claim: TerminalClaim }): void;
appendOutputTerminalData(uri: string, data: string): void;
resetOutputTerminal(uri: string): void;
finalizeOutputTerminal(uri: string, exitCode: number | undefined): void;
}

// node-pty is loaded dynamically to avoid bundling issues in non-node environments
Expand Down Expand Up @@ -169,6 +173,19 @@ interface IManagedTerminal {
terminalQueryFilterState: ITerminalQueryFilterState;
}

/**
* A lightweight output-only terminal channel: no PTY behind it, plain-text
* content appended by its owner (e.g. runtime-executed shell tools). Served
* to subscribers with `isPty: false` so clients skip VT parsing.
*/
interface IOutputTerminal {
title: string;
content: TerminalContentPart[];
contentSize: number;
claim: TerminalClaim;
exitCode?: number;
}

/**
* Manages terminal processes for the agent host. Each terminal is backed by
* a node-pty instance and identified by a protocol URI.
Expand All @@ -181,6 +198,7 @@ export class AgentHostTerminalManager extends Disposable implements IAgentHostTe
declare readonly _serviceBrand: undefined;

private readonly _terminals = new Map<string, IManagedTerminal>();
private readonly _outputTerminals = new Map<string, IOutputTerminal>();

constructor(
@IAgentHostStateManager private readonly _stateManager: AgentHostStateManager,
Expand Down Expand Up @@ -229,6 +247,16 @@ export class AgentHostTerminalManager extends Disposable implements IAgentHostTe

/** Get the full state for a terminal (for subscribe snapshots). */
getTerminalState(uri: string): TerminalState | undefined {
const outputTerminal = this._outputTerminals.get(uri);
if (outputTerminal) {
return {
title: outputTerminal.title,
content: outputTerminal.content,
exitCode: outputTerminal.exitCode,
claim: outputTerminal.claim,
isPty: false,
};
}
const terminal = this._terminals.get(uri);
if (!terminal) {
return undefined;
Expand All @@ -242,6 +270,7 @@ export class AgentHostTerminalManager extends Disposable implements IAgentHostTe
exitCode: terminal.exitCode,
claim: terminal.claim,
supportsCommandDetection: terminal.commandTracker?.detectionAvailableEmitted,
isPty: true,
};
}

Expand Down Expand Up @@ -743,7 +772,7 @@ export class AgentHostTerminalManager extends Disposable implements IAgentHostTe
}

/** Append cleaned data to the terminal's structured content array. */
private _appendToContent(managed: IManagedTerminal, data: string): void {
private _appendToContent(managed: { content: TerminalContentPart[]; contentSize: number }, data: string): void {
const tail = managed.content.length > 0 ? managed.content[managed.content.length - 1] : undefined;

if (tail?.type === 'command' && !tail.isComplete) {
Expand All @@ -766,7 +795,7 @@ export class AgentHostTerminalManager extends Disposable implements IAgentHostTe
}

/** Trim content parts to stay within the rolling buffer limit. */
private _trimContent(managed: IManagedTerminal): void {
private _trimContent(managed: { content: TerminalContentPart[]; contentSize: number }): void {
const maxSize = 100_000;
const targetSize = 80_000;
if (managed.contentSize <= maxSize) {
Expand All @@ -790,8 +819,72 @@ export class AgentHostTerminalManager extends Disposable implements IAgentHostTe
}
}

/**
* Create an output-only terminal channel. Unlike {@link createTerminal}
* there is no PTY behind it: the owner appends plain-text output via
* {@link appendOutputTerminalData}. The channel is not announced on the
* root terminal list — clients discover it through the tool result's
* terminal content block and subscribe to its URI.
*/
createOutputTerminal(uri: string, options: { title: string; claim: TerminalClaim }): void {
if (this._terminals.has(uri) || this._outputTerminals.has(uri)) {
throw new Error(`Terminal already exists: ${uri}`);
}
this._outputTerminals.set(uri, {
title: options.title,
content: [],
contentSize: 0,
claim: options.claim,
});
}

/** Append plain-text data to an output-only terminal and stream it to subscribers. */
appendOutputTerminalData(uri: string, data: string): void {
const terminal = this._outputTerminals.get(uri);
if (!terminal || data.length === 0) {
return;
}
this._appendToContent(terminal, data);
this._trimContent(terminal);
this._stateManager.dispatchServerAction(uri, {
type: ActionType.TerminalData,
data,
});
}

/** Clear an output-only terminal's content (e.g. when cumulative source output was rewritten). */
resetOutputTerminal(uri: string): void {
const terminal = this._outputTerminals.get(uri);
if (!terminal) {
return;
}
terminal.content = [];
terminal.contentSize = 0;
this._stateManager.dispatchServerAction(uri, {
type: ActionType.TerminalCleared,
});
}

/** Record the command's exit on an output-only terminal and notify subscribers. */
finalizeOutputTerminal(uri: string, exitCode: number | undefined): void {
const terminal = this._outputTerminals.get(uri);
if (!terminal || terminal.exitCode !== undefined) {
return;
}
if (exitCode !== undefined) {
terminal.exitCode = exitCode;
this._stateManager.dispatchServerAction(uri, {
type: ActionType.TerminalExited,
exitCode,
});
}
}

/** Dispose a terminal: kill the process and remove it. */
disposeTerminal(uri: string): void {
if (this._outputTerminals.delete(uri)) {
return;
}
const terminal = this._terminals.get(uri);
if (terminal) {
this._terminals.delete(uri);
Expand Down
4 changes: 2 additions & 2 deletions src/vs/platform/agentHost/node/copilot/copilotAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1944,8 +1944,8 @@ export class CopilotAgent extends Disposable implements IAgent {
this._logService.info(`[Copilot:${context.sessionId}] sendMessage: cachedEntry=${hadCachedEntry}, hasActiveClient=${!!activeClient}, activeClientId=${activeClient ? '(set)' : '(none)'}`);
if (entry && activeClient && await activeClient.requiresRestart(entry.appliedSnapshot)) {
this._logService.info(`[Copilot:${context.sessionId}] Session config changed (requiresRestart=true), refreshing session. clients=[${[...activeClient.toolSet.clientIds()].join(', ') || '(none)'}]`);
// Dispose only the default chat so it resumes with the updated
// config; peer chats on the same entry are left intact.
// Finish disconnecting before resuming the same SDK session id.
await entry.destroySession();
this._sessions.get(context.sessionId)?.clearDefaultChat();
entry = undefined;
}
Expand Down
Loading
Loading