src/vs/platform/agentHost/node/copilot/copilotNonPtyShellTerminals.ts

196 LOC · 188 covered · 8 uncovered · 45 ranges · 980 concepts · 21 introducers · 462 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- mapSessionEvents.ts ×12
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import { Disposable, toDisposable } from '../../../../base/common/lifecycle.js';
7 > import { URI } from '../../../../base/common/uri.js';
8 > import { AgentSession } from '../../common/agentService.js';
9 > import { TerminalClaimKind, type TerminalCommandResult, type TerminalSessionClaim } from '../../common/state/protocol/state.js';
10 > import { IAgentHostTerminalManager } from '../agentHostTerminalManager.js';
11 >
12 > /**
13 > * Builds the terminal channel URI for a runtime-executed (non-pty) shell tool
14 > * call. The session owns the terminal namespace and each tool call addresses a
15 > * distinct child terminal, keeping the URI stable across live streaming and
16 > * history replay without colliding with other sessions or tool calls.
17 > */
18 > export function buildNonPtyShellTerminalUri(session: URI | string, toolCallId: string): string {
19 > return `agenthost-terminal://shell/${encodeURIComponent(AgentSession.id(session))}/${encodeURIComponent(toolCallId)}`; copilotNonPtyShellTerminals.ts ×1
20 > }
22 > interface INonPtyShellStream {
23 > readonly uri: string;
24 > readonly title: string;
25 > created: boolean;
26 > /** The last cumulative snapshot written to the channel. */
27 > lastEmitted: string;
28 > finalized: boolean;
29 > }
30 >
31 > /**
32 > * Extracts the command result from the runtime's stable text fallback. The
33 > * external SDK bridge currently removes the equivalent `shell_exit` content
34 > * block for compatibility with older SDK clients.
35 > */
36 > function parseCompletedShell(text: string | undefined): TerminalCommandResult | undefined { copilotNonPtyShellTerminals.ts ×1
37 > const match = text && /<shellId: ([^>\r\n]+) completed with exit code (-?\d+)>\s*$/.exec(text);
38 > if (!match) {
39 > return undefined; copilotNonPtyShellTerminals.ts ×2
40 > }
42 > exitCode: Number(match[2]),
43 > preview: text.slice(0, match.index),
44 > };
45 > }
47 > export interface INonPtyShellToolCompletion {
48 > readonly uri: string;
49 > readonly result?: TerminalCommandResult;
50 > readonly shouldRetire: boolean;
51 > }
52 >
53 > /**
54 > * Streams output of SDK-runtime-executed shell tool calls into output-only
55 > * AHP terminal channels. The runtime reports ANSI-stripped plain-text output
56 > * via `tool.execution_partial_result` as throttled cumulative snapshots that
57 > * may be rewritten once output is truncated (a trailing truncation marker
58 > * under the emit cap, a rolling tail past the large-output threshold); this
59 > * class emits only the unseen suffix as `terminal/data` while the snapshot
60 > * grows in place, and resets the channel when the snapshot was rewritten, so
61 > * subscribed clients receive live plain-text output (`isPty: false` — no VT
62 > * parsing needed).
63 > *
64 > * Created once per session and disposed with it, matching the pty-backed
65 > * `ShellManager` lifecycle.
66 > */
67 > export class NonPtyShellTerminalStreams extends Disposable {
68 >
69 > private readonly _streams = new Map<string, INonPtyShellStream>();
70 >
71 > constructor(
72 > private readonly _sessionUri: URI, copilotAgentSession.ts ×23
73 > @IAgentHostTerminalManager private readonly _terminalManager: IAgentHostTerminalManager,
74 > ) {
75 > super();
76 >
77 > this._register(toDisposable(() => {
78 > for (const stream of this._streams.values()) {
79 > if (stream.created) { copilotNonPtyShellTerminals.ts ×2
80 > this._terminalManager.disposeTerminal(stream.uri); copilotNonPtyShellTerminals.ts ×1
81 > }
83 > this._streams.clear(); copilotAgentSession.ts ×23
84 > }));
85 > }
87 > /**
88 > * Appends the unseen suffix of `cumulativeOutput` to the tool call's
89 > * output terminal, creating the channel on first call. Returns the channel
90 > * URI and whether this call created it (so the caller can attach the
91 > * terminal content block exactly once).
92 > */
93 > track(toolCallId: string, title: string): void {
94 > if (!this._streams.has(toolCallId)) { copilotAgentSession.ts ×1
95 > this._streams.set(toolCallId, {
96 > uri: buildNonPtyShellTerminalUri(this._sessionUri, toolCallId),
97 > title,
98 > lastEmitted: '',
99 > finalized: false,
100 > created: false,
101 > });
102 > }
103 > }
105 > append(toolCallId: string, cumulativeOutput: string): { uri: string; created: boolean } | undefined {
106 > const stream = this._streams.get(toolCallId); copilotNonPtyShellTerminals.ts ×4
107 > if (!stream) {
108 return undefined;
109 }
110 > const created = !stream.created; copilotNonPtyShellTerminals.ts ×4
111 > if (created) {
112 > this._createTerminal(toolCallId, stream); copilotAgentSession.ts ×2
113 > }
114 > if (stream.finalized || cumulativeOutput === stream.lastEmitted) { copilotNonPtyShellTerminals.ts ×4
115 > return { uri: stream.uri, created }; copilotNonPtyShellTerminals.ts ×1
116 > }
117 > if (cumulativeOutput.startsWith(stream.lastEmitted)) { copilotNonPtyShellTerminals.ts ×2
118 > this._terminalManager.appendOutputTerminalData(stream.uri, cumulativeOutput.slice(stream.lastEmitted.length));
119 > } else {
120 > // The snapshot no longer extends what we emitted — the runtime copilotNonPtyShellTerminals.ts ×1
121 > // rewrote it after truncation (marker under the emit cap, rolling
122 > // tail past the large-output threshold). Start the channel over.
123 > this._terminalManager.resetOutputTerminal(stream.uri);
124 > this._terminalManager.appendOutputTerminalData(stream.uri, cumulativeOutput);
125 > }
126 > stream.lastEmitted = cumulativeOutput; copilotNonPtyShellTerminals.ts ×2
127 > return { uri: stream.uri, created };
130 > /**
131 > * Records the process lifecycle information carried by tool completion.
132 > * A structured shell exit settles the channel.
133 > */
134 > completeToolCall(toolCallId: string, toolOutput: string | undefined, shellExit: { shellId: string; result: TerminalCommandResult } | undefined): INonPtyShellToolCompletion | undefined {
135 > const stream = this._streams.get(toolCallId); copilotNonPtyShellTerminals.ts ×3
136 > if (!stream) {
137 return undefined;
138 }
140 > const result = shellExit?.result ?? parseCompletedShell(toolOutput);
141 > if (!result) {
142 > if (!stream.created) { copilotNonPtyShellTerminals.ts ×2
143 > this._streams.delete(toolCallId); copilotNonPtyShellTerminals.ts ×1
144 > return undefined;
145 > }
146 > return { uri: stream.uri, shouldRetire: false }; copilotNonPtyShellTerminals.ts ×1
147 > }
148 > if (!stream.created) { copilotNonPtyShellTerminals.ts ×5
149 > this._createTerminal(toolCallId, stream); copilotNonPtyShellTerminals.ts ×1
150 > }
151 > if (result.preview !== undefined) { copilotNonPtyShellTerminals.ts ×5
152 > this.append(toolCallId, result.preview); copilotNonPtyShellTerminals.ts ×3
153 > }
154 > if (result.exitCode !== undefined) { copilotNonPtyShellTerminals.ts ×5
155 > this._finalize(stream, result.exitCode);
156 > }
157 > return {
158 > uri: stream.uri,
159 > result,
160 > shouldRetire: stream.finalized && result.preview !== undefined,
162 > }
164 > /**
165 > * Releases the live output resource after its static completion has been
166 > * published. Repeated calls are safe and do not dispose the resource twice.
167 > */
168 > retire(toolCallId: string): void {
169 > const stream = this._streams.get(toolCallId); copilotNonPtyShellTerminals.ts ×3
170 > if (!stream) {
171 return;
172 }
173 > this._streams.delete(toolCallId); copilotNonPtyShellTerminals.ts ×3
174 > if (stream.created) {
175 > this._terminalManager.disposeTerminal(stream.uri);
176 > }
177 > }
179 > private _finalize(stream: INonPtyShellStream, exitCode: number): void {
180 > if (stream.finalized) { copilotNonPtyShellTerminals.ts ×5
181 return;
182 }
183 > stream.finalized = true; copilotNonPtyShellTerminals.ts ×5
184 > this._terminalManager.finalizeOutputTerminal(stream.uri, exitCode);
185 > }
187 > private _createTerminal(toolCallId: string, stream: INonPtyShellStream): void {
188 > const claim: TerminalSessionClaim = { copilotNonPtyShellTerminals.ts ×1
189 > kind: TerminalClaimKind.Session,
190 > session: this._sessionUri.toString(),
191 > toolCallId,
192 > };
193 > this._terminalManager.createOutputTerminal(stream.uri, { title: stream.title, claim });
194 > stream.created = true;
195 > }