Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
fb5883e
Stream output for SDK built-in shell tool
anthonykim1 Jul 17, 2026
27ce7bf
better todo comment
anthonykim1 Jul 17, 2026
c9aed0a
Bump Copilot SDK to 1.0.8 preview
anthonykim1 Jul 22, 2026
b3225ed
Exclude unused Koffi native modules
anthonykim1 Jul 22, 2026
86a2c4b
Merge remote-tracking branch 'upstream/main' into anthonykim1/shellOu…
anthonykim1 Jul 22, 2026
9aaa7bf
Merge branch 'anthonykim1/copilotJul22' into anthonykim1/shellOutputS…
anthonykim1 Jul 22, 2026
49b20d3
Adopt AHP terminal completion changes and stream built-in shell outpu…
anthonykim1 Jul 22, 2026
57b320b
Track last emitted snapshot for non-pty shell streaming
anthonykim1 Jul 22, 2026
974718c
Normalize output terminal data to CRLF in the terminal manager
anthonykim1 Jul 22, 2026
8a821b4
no need to complicate too much with lf crlf
anthonykim1 Jul 22, 2026
c30c63c
Merge branch 'main' into anthonykim1/shellOutputStreaming
anthonykim1 Jul 22, 2026
5cc326a
Fix output-only shell command lifecycle rendering
anthonykim1 Jul 23, 2026
bbc83bd
keep non pty output handling scoped to agent hsot
anthonykim1 Jul 23, 2026
05bb38c
fix output only terminal completion state
anthonykim1 Jul 23, 2026
0fc5da8
simplify agent host output terminal lifecycle
anthonykim1 Jul 23, 2026
717e049
Merge branch 'main' into anthonykim1/shellOutputStreaming
anthonykim1 Jul 23, 2026
6b2e153
leave unnncessary background stuff out
anthonykim1 Jul 23, 2026
2c26669
scope non pty shell terminals by session
anthonykim1 Jul 23, 2026
7771a1a
change ahp version to be proper
anthonykim1 Jul 23, 2026
acd86b2
agentHost: regenerate AHP completion types
anthonykim1 Jul 23, 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
@@ -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;

/**

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DonJayamanne Copilot is telling me #326807 hand-wrote this file and its now getting replaced with the new .ahp-version bc they dont seem to be in upstream ahp.

Dont think there would be regression since label was on attachment.label though. Please can you double check?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Files impacted (generated by copilot)

• The completion cleanup affects four files:

  - src/vs/platform/agentHost/common/state/protocol/channels-session/commands.ts:284 —
    regenerated; removes handwritten CompletionItem.label.

  - src/vs/platform/agentHost/node/copilot/copilotSlashCommandCompletionProvider.ts:200 — puts
    and sorts the display label using attachment.label.

  - src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/
    agentHostSessionHandler.ts:888 — maps attachment.label into the workbench completion item.

  - src/vs/workbench/contrib/chat/test/browser/agentSessions/
    agentHostChatContribution.test.ts:9658 — verifies a visible label is preserved when
    insertText is empty

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, thats weird, I see, the model must have made this change :(
I'll take a look

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to merge your PR, i'll have a look at this once its merged.
There's no point blocking your PR

* 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
Loading
Loading