src/vs/platform/agentHost/node/agentHostTerminalManager.ts

961 LOC · 780 covered · 181 uncovered · 140 ranges · 2232 concepts · 28 introducers · 1038 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 > /*--------------------------------------------------------------------------------------------- agentHostTerminalManager.ts ×41
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 * as fs from 'fs';
7 > import { DeferredPromise, raceCancellablePromises, timeout } from '../../../base/common/async.js';
8 > import { Emitter } from '../../../base/common/event.js';
9 > import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../base/common/lifecycle.js';
10 > import { dirname, parse as pathParse } from '../../../base/common/path.js';
11 > import * as platform from '../../../base/common/platform.js';
12 > import { getSystemShell } from '../../../base/node/shell.js';
13 > import { URI } from '../../../base/common/uri.js';
14 > import { generateUuid } from '../../../base/common/uuid.js';
15 > import { AiAgentEnvValue, AiAgentEnvVar } from '../../chat/common/aiAgentEnv.js';
16 > import { createDecorator } from '../../instantiation/common/instantiation.js';
17 > import { ILogService } from '../../log/common/log.js';
18 > import { IProductService } from '../../product/common/productService.js';
19 > import { getShellIntegrationInjection } from '../../terminal/node/terminalEnvironment.js';
20 > import { AgentHostConfigKey, agentHostCustomizationConfigSchema } from '../common/agentHostCustomizationConfig.js';
21 > import { ActionType } from '../common/state/protocol/actions.js';
22 > import type { CreateTerminalParams } from '../common/state/protocol/commands.js';
23 > import { TerminalClaim, TerminalContentPart, TerminalInfo, TerminalState, TerminalClaimKind } from '../common/state/protocol/state.js';
24 > import { isTerminalAction } from '../common/state/sessionActions.js';
25 > import { ROOT_STATE_URI } from '../common/state/sessionState.js';
26 > import { IAgentConfigurationService } from './agentConfigurationService.js';
27 > import { AgentHostHeadlessTerminal } from './agentHostHeadlessTerminal.js';
28 > import { isZsh } from './agentHostShellUtils.js';
29 > import { AgentHostStateManager, IAgentHostStateManager } from './agentHostStateManager.js';
30 > import { Osc633Event, Osc633EventType, Osc633ParseSegment, Osc633Parser } from './osc633Parser.js';
31 >
32 > const WAIT_FOR_PROMPT_TIMEOUT = 10_000;
33 > const HEADLESS_TERMINAL_SCROLLBACK = 0;
34 > const DSR_CURSOR_POSITION_QUERY = '\x1b[6n';
35 > const DEC_DSR_CURSOR_POSITION_QUERY = '\x1b[?6n';
36 > const SERVER_HANDLED_QUERY_PREFIXES = ['\x1b[?6', '\x1b[?', '\x1b[6', '\x1b[', '\x1b'];
37 >
38 > export const IAgentHostTerminalManager = createDecorator<IAgentHostTerminalManager>('agentHostTerminalManager');
39 >
40 > export interface ICommandFinishedEvent {
41 > commandId: string;
42 > exitCode: number | undefined;
43 > command: string;
44 > output: string;
45 > }
46 >
47 > export interface ITerminalQueryFilterState {
48 > pendingData: string;
49 > }
50 >
51 > export interface ISendTextOptions {
52 > shouldExecute: boolean;
53 > /**
54 > * Match workbench terminal sendText: wrap in bracketed paste markers only
55 > * when requested by the caller and enabled by the terminal.
56 > */
57 > bracketedPasteMode?: boolean;
58 > }
59 >
60 > export interface IFormatTerminalTextOptions {
61 > shouldExecute: boolean;
62 > forceBracketedPasteMode?: boolean;
63 > }
64 >
65 > export function removeServerHandledTerminalQueries(data: string, state: ITerminalQueryFilterState): string {
67 > !state.pendingData
68 > && !data.includes(DSR_CURSOR_POSITION_QUERY)
69 > && !data.includes(DEC_DSR_CURSOR_POSITION_QUERY) agentHostTerminalManager.ts ×1
70 > && !getServerHandledTerminalQueryPrefix(data)
73 > }
75 > const combinedData = state.pendingData + data;
76 > const pendingData = getServerHandledTerminalQueryPrefix(combinedData);
77 > const dataToFilter = pendingData ? combinedData.substring(0, combinedData.length - pendingData.length) : combinedData; agentHostTerminalManager.ts ×5
78 > state.pendingData = pendingData;
79 > if (!dataToFilter.includes(DSR_CURSOR_POSITION_QUERY) && !dataToFilter.includes(DEC_DSR_CURSOR_POSITION_QUERY)) {
80 > return dataToFilter; agentHostTerminalManager.ts ×2
81 > }
82 > return dataToFilter agentHostTerminalManager.ts ×2
83 > .replaceAll(DEC_DSR_CURSOR_POSITION_QUERY, '')
84 > .replaceAll(DSR_CURSOR_POSITION_QUERY, '');
85 > }
87 > function getServerHandledTerminalQueryPrefix(data: string): string { agentHostTerminalManager.ts ×5
88 > for (const prefix of SERVER_HANDLED_QUERY_PREFIXES) {
89 > if (data.endsWith(prefix)) {
90 > return prefix; agentHostTerminalManager.ts ×2
91 > }
93 > return '';
94 > }
96 > export function formatTerminalText(data: string, options: IFormatTerminalTextOptions): string {
97 > if (options.forceBracketedPasteMode) { agentHostTerminalManager.ts ×3
98 > data = `\x1b[200~${data}\x1b[201~`; agentHostTerminalManager.ts ×1
99 > }
100 > data = data.replace(/\r?\n/g, '\r'); agentHostTerminalManager.ts ×3
101 > if (options.shouldExecute && !data.endsWith('\r')) {
102 > data += '\r'; agentHostTerminalManager.ts ×1
103 > }
104 > return data; agentHostTerminalManager.ts ×3
105 > }
107 > /**
108 > * Service interface for terminal management in the agent host.
109 > */
110 > export interface IAgentHostTerminalManager {
111 > readonly _serviceBrand: undefined;
112 > createTerminal(params: CreateTerminalParams, options?: { shell?: string; preventShellHistory?: boolean; nonInteractive?: boolean }): Promise<void>;
113 > writeInput(uri: string, data: string): void;
114 > sendText(uri: string, data: string, options: ISendTextOptions): Promise<void>;
115 > onData(uri: string, cb: (data: string) => void): IDisposable;
116 > onExit(uri: string, cb: (exitCode: number) => void): IDisposable;
117 > onClaimChanged(uri: string, cb: (claim: TerminalClaim) => void): IDisposable;
118 > onCommandFinished(uri: string, cb: (event: ICommandFinishedEvent) => void): IDisposable;
119 > createAltBufferPromise(uri: string, store: DisposableStore): Promise<void>;
120 > getContent(uri: string): string | undefined;
121 > getClaim(uri: string): TerminalClaim | undefined;
122 > hasTerminal(uri: string): boolean;
123 > getExitCode(uri: string): number | undefined;
124 > supportsCommandDetection(uri: string): boolean;
125 > disposeTerminal(uri: string): void;
126 > getTerminalInfos(): TerminalInfo[];
127 > getTerminalState(uri: string): TerminalState | undefined;
128 > getDefaultShell(): Promise<string>;
129 > createOutputTerminal(uri: string, options: { title: string; claim: TerminalClaim }): void;
130 > appendOutputTerminalData(uri: string, data: string): void;
131 > resetOutputTerminal(uri: string): void;
132 > finalizeOutputTerminal(uri: string, exitCode: number | undefined): void;
133 > }
134 >
135 > // node-pty is loaded dynamically to avoid bundling issues in non-node environments
136 > let nodePtyModule: typeof import('node-pty') | undefined;
137 async function getNodePty(): Promise<typeof import('node-pty')> {
138 if (!nodePtyModule) {
139 nodePtyModule = await import('node-pty');
140 }
141 return nodePtyModule;
142 }
144 > /** Per-terminal command detection tracking state. */
145 > interface ICommandTracker {
146 > readonly parser: Osc633Parser;
147 > readonly nonce: string;
148 > commandCounter: number;
149 > detectionAvailableEmitted: boolean;
150 > pendingCommandLine?: string;
151 > activeCommandId?: string;
152 > activeCommandTimestamp?: number;
153 > }
154 >
155 > /** Represents a single managed terminal with its PTY process. */
156 > interface IManagedTerminal {
157 > readonly uri: string;
158 > readonly store: DisposableStore;
159 > readonly pty: import('node-pty').IPty;
160 > readonly onDataEmitter: Emitter<string>;
161 > readonly onExitEmitter: Emitter<number>;
162 > readonly onClaimChangedEmitter: Emitter<TerminalClaim>;
163 > readonly onCommandFinishedEmitter: Emitter<ICommandFinishedEvent>;
164 > title: string;
165 > cwd: string;
166 > cols: number;
167 > rows: number;
168 > content: TerminalContentPart[];
169 > contentSize: number;
170 > claim: TerminalClaim;
171 > exitCode?: number;
172 > commandTracker?: ICommandTracker;
173 > headlessTerminal?: AgentHostHeadlessTerminal;
174 > terminalQueryFilterState: ITerminalQueryFilterState;
175 > }
176 >
177 > /**
178 > * A lightweight output-only terminal channel: no PTY behind it, plain-text
179 > * content appended by its owner (e.g. runtime-executed shell tools). Served
180 > * to subscribers with `isPty: false` so clients skip VT parsing.
181 > */
182 > interface IOutputTerminal {
183 > title: string;
184 > content: TerminalContentPart[];
185 > contentSize: number;
186 > claim: TerminalClaim;
187 > exitCode?: number;
188 > }
189 >
190 > /**
191 > * Manages terminal processes for the agent host. Each terminal is backed by
192 > * a node-pty instance and identified by a protocol URI.
193 > *
194 > * Listens to the {@link AgentHostStateManager} for client-dispatched terminal
195 > * actions (input, resize, claim changes) and dispatches server-originated
196 > * PTY output back through the state manager.
197 > */
198 > export class AgentHostTerminalManager extends Disposable implements IAgentHostTerminalManager {
199 > declare readonly _serviceBrand: undefined;
200 >
201 > private readonly _terminals = new Map<string, IManagedTerminal>();
202 > private readonly _outputTerminals = new Map<string, IOutputTerminal>();
203 >
204 > constructor(
205 > @IAgentHostStateManager private readonly _stateManager: AgentHostStateManager, agentHostTerminalManager.ts ×4
206 > @ILogService private readonly _logService: ILogService,
207 > @IProductService private readonly _productService: IProductService,
208 > @IAgentConfigurationService private readonly _configurationService: IAgentConfigurationService,
209 > ) {
210 > super();
211 >
212 > // React to client-dispatched terminal actions flowing through the state manager
213 > this._register(this._stateManager.onDidEmitEnvelope(envelope => {
214 > const action = envelope.action; agentHostTerminalManager.ts ×6
215 > if (!isTerminalAction(action)) {
217 > }
218 > const channel = envelope.channel; agentHostTerminalManager.ts ×8
219 > switch (action.type) {
220 > case ActionType.TerminalInput:
221 this._writeInput(channel, action.data);
222 break;
223 > case ActionType.TerminalResized: agentHostTerminalManager.ts ×6
224 this._resize(channel, action.cols, action.rows);
225 break;
226 > case ActionType.TerminalClaimed: agentHostTerminalManager.ts ×6
227 this._setClaim(channel, action.claim);
228 break;
229 > case ActionType.TerminalTitleChanged: agentHostTerminalManager.ts ×6
230 this._setTitle(channel, action.title);
231 break;
232 > case ActionType.TerminalCleared: agentHostTerminalManager.ts ×6
233 > this._clearContent(channel); agentHostTerminalManager.ts ×7
234 > break;
237 > }
239 > /** Get metadata for all active terminals (for root state). */
240 > getTerminalInfos(): TerminalInfo[] {
241 > return [...this._terminals.values()].map(t => ({ agentHostTerminalManager.ts ×2
242 > resource: t.uri, agentHostTerminalManager.ts ×21
243 > title: t.title,
244 > claim: t.claim,
245 > exitCode: t.exitCode,
247 > }
249 > /** Get the full state for a terminal (for subscribe snapshots). */
250 > getTerminalState(uri: string): TerminalState | undefined {
251 > const outputTerminal = this._outputTerminals.get(uri); agentHostTerminalManager.ts ×2
252 > if (outputTerminal) {
254 > title: outputTerminal.title,
255 > content: outputTerminal.content,
256 > exitCode: outputTerminal.exitCode,
257 > claim: outputTerminal.claim,
258 > isPty: false,
259 > };
260 > }
261 > const terminal = this._terminals.get(uri); agentHostTerminalManager.ts ×1
262 > if (!terminal) {
263 > return undefined;
264 > }
265 return {
266 title: terminal.title,
267 cwd: terminal.cwd,
268 cols: terminal.cols,
269 rows: terminal.rows,
270 content: terminal.content,
271 exitCode: terminal.exitCode,
272 claim: terminal.claim,
273 supportsCommandDetection: terminal.commandTracker?.detectionAvailableEmitted,
274 > isPty: true, agentHostTerminalManager.ts ×2
275 > };
276 > }
278 > /**
279 > * Create a new terminal backed by node-pty.
280 > * Spawns the user's default shell.
281 > */
282 > async createTerminal(params: CreateTerminalParams, options?: { shell?: string; preventShellHistory?: boolean; nonInteractive?: boolean }): Promise<void> {
283 > const uri = params.channel; agentHostTerminalManager.ts ×21
284 > if (this._terminals.has(uri)) {
285 throw new Error(`Terminal already exists: ${uri}`);
286 }
288 > const cwd = await this._resolveCwd(params.cwd, uri);
289 > const cols = params.cols ?? 80;
290 > const rows = params.rows ?? 24;
291 >
292 > const shell = options?.shell ?? await this.getDefaultShell();
293 > const name = platform.isWindows ? 'cmd' : 'xterm-256color';
294 >
295 > this._logService.info(`[TerminalManager] Creating terminal ${uri}: shell=${shell}, cwd=${cwd}, cols=${cols}, rows=${rows}`);
296 >
297 > // Shell integration — inject scripts so the shell emits OSC 633 sequences
298 > const nonce = generateUuid();
299 > const env: Record<string, string> = { ...process.env as Record<string, string> };
300 > // Attribute these commands to VS Code. Already inherited from the agent
301 > // host process; set here as defense in depth.
302 > env[AiAgentEnvVar] = AiAgentEnvValue;
303 > if (options?.preventShellHistory) {
304 > // Picked up by the shell integration scripts to set HISTCONTROL=ignorespace agentHostTerminalManager.ts ×5
305 > // (bash) / HIST_IGNORE_SPACE (zsh), or suppress PSReadLine history (pwsh).
306 > // Combined with the leading-space prefix applied at command-write time, this
307 > // prevents agent-executed commands from polluting the user's shell history.
308 > env['VSCODE_PREVENT_SHELL_HISTORY'] = '1';
309 > }
310 > // Zsh-specific fixups for agent tool terminals: disable bang history agentHostTerminalManager.ts ×21
311 > // expansion and enable inline # comments.
312 > if (params.claim?.kind === TerminalClaimKind.Session && isZsh(shell)) {
313 > env['VSCODE_AGENT_ZSH_FIXUPS'] = '1'; agentHostTerminalManager.ts ×5
314 > }
315 > if (options?.nonInteractive) { agentHostTerminalManager.ts ×21
316 > // Suppress paging and interactive prompts so that tool-spawned agentHostTerminalManager.ts ×5
317 > // terminals produce clean, machine-friendly output. An empty
318 > // string disables paging in git, less, and most CLI tools and
319 > // is safe on all platforms (unlike 'cat' which isn't on Windows PATH).
320 > env['LC_ALL'] = 'C.UTF-8';
321 > env['PAGER'] = '';
322 > env['GIT_PAGER'] = '';
323 > env['GH_PAGER'] = '';
324 > env['GIT_TERMINAL_PROMPT'] = '0';
325 > env['DEBIAN_FRONTEND'] = 'noninteractive';
326 > }
327 > let shellArgs: string[] = []; agentHostTerminalManager.ts ×21
328 > if (platform.isMacintosh) {
329 const shellName = pathParse(shell).name;
330 if (shellName.match(/(zsh|bash)/)) {
331 shellArgs = ['--login'];
332 }
333 }
335 > const injection = await getShellIntegrationInjection(
336 > { executable: shell, args: shellArgs, forceShellIntegration: true },
337 > {
338 > shellIntegration: { enabled: true, suggestEnabled: false, nonce },
339 > windowsUseConptyDll: false,
340 > environmentVariableCollections: undefined,
341 > workspaceFolder: undefined,
342 > isScreenReaderOptimized: false,
343 > },
344 > undefined,
345 > this._logService,
346 > this._productService,
347 > );
348 >
349 > let commandTracker: ICommandTracker | undefined;
350 >
351 > if (injection.type === 'injection') {
352 > this._logService.info(`[TerminalManager] Shell integration injected for ${uri}`);
353 > if (injection.envMixin) {
354 > for (const [key, value] of Object.entries(injection.envMixin)) {
355 > if (value !== undefined) {
356 > env[key] = value;
357 > }
358 > }
359 > }
360 > if (injection.newArgs) {
361 > shellArgs = injection.newArgs;
362 > }
363 > if (injection.filesToCopy) {
364 > for (const f of injection.filesToCopy) { agentHostTerminalManager.ts ×5
365 > try {
366 > await fs.promises.mkdir(dirname(f.dest), { recursive: true });
367 > await fs.promises.copyFile(f.source, f.dest);
368 > } catch {
369 // Swallow — another process may be using the same temp dir
370 }
372 > }
373 > commandTracker = { agentHostTerminalManager.ts ×21
374 > parser: new Osc633Parser(),
375 > nonce,
376 > commandCounter: 0,
377 > detectionAvailableEmitted: false,
378 > };
379 > } else {
380 this._logService.info(`[TerminalManager] Shell integration not available for ${uri}: ${injection.reason}`);
381 }
383 > const ptyProcess = await this._spawnPty(shell, shellArgs, {
384 > name,
385 > cwd,
386 > env,
387 > cols,
388 > rows,
389 > });
390 >
391 > const store = new DisposableStore();
392 > const claim: TerminalClaim = params.claim ?? { kind: TerminalClaimKind.Client, clientId: '' };
393 >
394 > const onDataEmitter = store.add(new Emitter<string>());
395 > const onExitEmitter = store.add(new Emitter<number>());
396 > const onClaimChangedEmitter = store.add(new Emitter<TerminalClaim>());
397 > const onCommandFinishedEmitter = store.add(new Emitter<ICommandFinishedEvent>());
398 > const headlessTerminal = store.add(new AgentHostHeadlessTerminal({
399 > cols,
400 > rows,
401 > scrollback: HEADLESS_TERMINAL_SCROLLBACK,
402 > logService: this._logService,
403 > }));
404 >
405 > const managed: IManagedTerminal = {
406 > uri,
407 > store,
408 > pty: ptyProcess,
409 > onDataEmitter,
410 > onExitEmitter,
411 > onClaimChangedEmitter,
412 > onCommandFinishedEmitter,
413 > title: params.name ?? shell,
414 > cwd,
415 > cols,
416 > rows,
417 > content: [],
418 > contentSize: 0,
419 > claim,
420 > commandTracker,
421 > headlessTerminal,
422 > terminalQueryFilterState: { pendingData: '' },
423 > };
424 >
425 > this._terminals.set(uri, managed);
426 > store.add(headlessTerminal.onResponseData(data => {
427 > this._logService.debug(`[TerminalManager] Writing headless terminal response for ${uri}: ${JSON.stringify(data)}`); agentHostTerminalManager.ts ×1
428 > try {
429 > ptyProcess.write(data);
430 > } catch (err) {
431 this._logService.debug(`[TerminalManager] Failed to write headless terminal response for ${uri}: ${err instanceof Error ? err.message : String(err)}`);
432 }
434 >
435 > // Wire PTY events → protocol events
436 > store.add(toDisposable(() => {
437 > try { ptyProcess.kill(); } catch { /* already dead */ }
438 > }));
439 >
440 > const onFirstData = new DeferredPromise<void>();
441 > const dataListener = ptyProcess.onData(rawData => {
442 > void managed.headlessTerminal?.writePtyData(rawData);
443 > this._handlePtyData(managed, rawData);
444 > onFirstData.complete();
445 > });
446 > store.add(toDisposable(() => dataListener.dispose()));
447 >
448 > const exitListener = ptyProcess.onExit(e => {
449 managed.exitCode = e.exitCode;
450 managed.onExitEmitter.fire(e.exitCode);
451 onFirstData.complete();
452 this._stateManager.dispatchServerAction(uri, {
453 type: ActionType.TerminalExited,
454 exitCode: e.exitCode,
455 });
456 this._broadcastTerminalList();
458 > store.add(toDisposable(() => exitListener.dispose()));
459 >
460 > // Poll for title changes (non-Windows)
461 > if (!platform.isWindows) {
462 > const titleInterval = setInterval(() => {
463 const newTitle = ptyProcess.process;
464 if (newTitle && newTitle !== managed.title) {
465 managed.title = newTitle;
466 this._stateManager.dispatchServerAction(uri, {
467 type: ActionType.TerminalTitleChanged,
468 title: newTitle,
469 });
470 this._broadcastTerminalList();
471 }
473 > store.add(toDisposable(() => clearInterval(titleInterval)));
474 > }
475 >
476 > await raceCancellablePromises([onFirstData.p, timeout(WAIT_FOR_PROMPT_TIMEOUT)]);
477 >
478 > this._broadcastTerminalList();
479 > }
481 > protected async _spawnPty(file: string, args: string[], options: import('node-pty').IPtyForkOptions | import('node-pty').IWindowsPtyForkOptions): Promise<import('node-pty').IPty> {
482 const nodePty = await getNodePty();
483 return nodePty.spawn(file, args, options);
484 }
486 > /** Send input data to a terminal's PTY process (from client-dispatched actions). */
487 > private _writeInput(uri: string, data: string): void {
488 this.writeInput(uri, data);
489 }
491 > /** Send input data to a terminal's PTY process. */
492 > writeInput(uri: string, data: string): void {
493 > const terminal = this._terminals.get(uri); agentHostTerminalManager.ts ×3
494 > if (terminal && terminal.exitCode === undefined) {
495 > terminal.pty.write(data);
496 > }
497 > }
499 > /** Send formatted text to a terminal's PTY process. */
500 > async sendText(uri: string, data: string, options: ISendTextOptions): Promise<void> {
501 > const terminal = this._terminals.get(uri); agentHostTerminalManager.ts ×3
502 > let forceBracketedPasteMode = false;
503 > if (options.bracketedPasteMode) {
504 > await terminal?.headlessTerminal?.whenPtyDataFlushed(); agentHostHeadlessTerminal.ts ×1
505 > forceBracketedPasteMode = !!terminal?.headlessTerminal?.isBracketedPasteMode();
506 > }
507 > this.writeInput(uri, formatTerminalText(data, { shouldExecute: options.shouldExecute, forceBracketedPasteMode })); agentHostTerminalManager.ts ×3
508 > }
510 > /** Register a callback for PTY data events on a terminal. */
511 > onData(uri: string, cb: (data: string) => void): IDisposable {
512 const terminal = this._terminals.get(uri);
513 if (!terminal) {
514 return toDisposable(() => { });
515 }
516 return terminal.onDataEmitter.event(cb);
517 }
519 > /** Register a callback for PTY exit events on a terminal. */
520 > onExit(uri: string, cb: (exitCode: number) => void): IDisposable {
521 const terminal = this._terminals.get(uri);
522 if (!terminal) {
523 return toDisposable(() => { });
524 }
525 return terminal.onExitEmitter.event(cb);
526 }
528 > /** Register a callback for terminal claim changes. */
529 > onClaimChanged(uri: string, cb: (claim: TerminalClaim) => void): IDisposable {
530 const terminal = this._terminals.get(uri);
531 if (!terminal) {
532 return toDisposable(() => { });
533 }
534 return terminal.onClaimChangedEmitter.event(cb);
535 }
537 > /** Register a callback for command completion events (requires shell integration). */
538 > onCommandFinished(uri: string, cb: (event: ICommandFinishedEvent) => void): IDisposable {
539 > const terminal = this._terminals.get(uri); agentHostTerminalManager.ts ×10
540 > if (!terminal) {
541 return toDisposable(() => { });
542 }
543 > return terminal.onCommandFinishedEmitter.event(cb); agentHostTerminalManager.ts ×10
544 > }
546 > createAltBufferPromise(uri: string, store: DisposableStore): Promise<void> {
547 > const terminal = this._terminals.get(uri); agentHostTerminalManager.ts ×2
548 > if (!terminal?.headlessTerminal) {
549 return new Promise(() => { });
550 }
551 > return terminal.headlessTerminal.createAltBufferPromise(store); agentHostTerminalManager.ts ×2
552 > }
554 > /** Get accumulated scrollback content for a terminal as raw text. */
555 > getContent(uri: string): string | undefined {
556 const terminal = this._terminals.get(uri);
557 if (!terminal) {
558 return undefined;
559 }
560 return terminal.content.map(p => p.type === 'command' ? p.output : p.value).join('');
561 }
563 > /** Get the current claim for a terminal. */
564 > getClaim(uri: string): TerminalClaim | undefined {
565 return this._terminals.get(uri)?.claim;
566 }
568 > /** Check whether a terminal exists. */
569 > hasTerminal(uri: string): boolean {
570 > return this._terminals.has(uri); agentHostTerminalManager.ts ×6
571 > }
573 > /** Whether the terminal has shell integration active for command detection. */
574 > supportsCommandDetection(uri: string): boolean {
575 const terminal = this._terminals.get(uri);
576 return terminal?.commandTracker?.detectionAvailableEmitted ?? false;
577 }
579 > /** Get the exit code for a terminal, or undefined if still running. */
580 > getExitCode(uri: string): number | undefined {
581 return this._terminals.get(uri)?.exitCode;
582 }
584 > /** Resize a terminal. */
585 > private _resize(uri: string, cols: number, rows: number): void {
586 const terminal = this._terminals.get(uri);
587 if (terminal && terminal.exitCode === undefined) {
588 terminal.cols = cols;
589 terminal.rows = rows;
590 terminal.pty.resize(cols, rows);
591 terminal.headlessTerminal?.resize(cols, rows);
592 }
593 }
595 > /** Update a terminal's claim. */
596 > private _setClaim(uri: string, claim: TerminalClaim): void {
597 const terminal = this._terminals.get(uri);
598 if (terminal) {
599 terminal.claim = claim;
600 terminal.onClaimChangedEmitter.fire(claim);
601 this._broadcastTerminalList();
602 }
603 }
605 > /** Update a terminal's title. */
606 > private _setTitle(uri: string, title: string): void {
607 const terminal = this._terminals.get(uri);
608 if (terminal) {
609 terminal.title = title;
610 this._broadcastTerminalList();
611 }
612 }
614 > /** Clear a terminal's scrollback buffer. */
615 > private _clearContent(uri: string): void {
616 > const terminal = this._terminals.get(uri); agentHostTerminalManager.ts ×7
617 > if (terminal) {
618 terminal.content = [];
619 terminal.contentSize = 0;
620 terminal.headlessTerminal?.clear();
621 }
624 > /** Process raw PTY output: parse OSC 633 sequences, dispatch actions, track content. */
625 > private _handlePtyData(managed: IManagedTerminal, rawData: string): void {
626 > const tracker = managed.commandTracker; agentHostTerminalManager.ts ×21
627 >
628 > // Without command detection there are no OSC 633 sequences to
629 > // interleave — the whole chunk is command output. With a tracker,
630 > // process cleaned-data and events in stream order so that output which
631 > // arrives before a CommandFinished marker (commonly in the same PTY
632 > // read for fast commands) is appended to the command's output BEFORE the
633 > // finished event snapshots it. Handling all events first would emit
634 > // CommandFinished with the not-yet-appended output missing.
635 > const segments: Osc633ParseSegment[] = tracker
636 > ? tracker.parser.parseSegments(rawData)
637 : (rawData.length > 0 ? [{ kind: 'data', data: rawData }] : []);
639 > // Preserve OSC 633 stream order when emitting AHP actions: command data must remain between
640 > // TerminalCommandExecuted and TerminalCommandFinished, matching the AHP contract and xterm.
641 > let pendingClientData = '';
642 > const flushClientData = (): void => {
643 > if (pendingClientData.length === 0) {
645 > }
646 > managed.onDataEmitter.fire(pendingClientData); agentHostTerminalManager.ts ×21
647 > this._stateManager.dispatchServerAction(managed.uri, {
648 > type: ActionType.TerminalData,
649 > data: pendingClientData,
650 > });
651 > pendingClientData = '';
652 > };
653 >
654 > for (const segment of segments) {
655 > if (segment.kind === 'event') {
656 > flushClientData(); agentHostTerminalManager.ts ×10
657 > this._handleOsc633Event(managed, tracker!, segment.event);
658 > continue;
659 > }
661 > // Agent Host's server-side headless terminal answers CPR so terminals
662 > // work without an attached client. Hide those queries from client xterms
663 > // to avoid a second CPR response flowing back through AgentHostPty.input.
664 > const cleanedData = removeServerHandledTerminalQueries(segment.data, managed.terminalQueryFilterState);
665 > if (cleanedData.length > 0) {
666 > this._appendToContent(managed, cleanedData);
667 > pendingClientData += cleanedData;
668 > }
669 > }
670 >
671 > flushClientData();
672 >
673 > // Trim content if too large
674 > this._trimContent(managed);
675 > }
677 > /** Handle a parsed OSC 633 event by dispatching the appropriate protocol actions. */
678 > private _handleOsc633Event(managed: IManagedTerminal, tracker: ICommandTracker, event: Osc633Event): void {
679 > // Emit TerminalCommandDetectionAvailable on first sequence agentHostTerminalManager.ts ×10
680 > if (!tracker.detectionAvailableEmitted) {
681 > tracker.detectionAvailableEmitted = true;
682 > this._stateManager.dispatchServerAction(managed.uri, {
683 > type: ActionType.TerminalCommandDetectionAvailable,
684 > });
685 > }
686 >
687 > switch (event.type) {
688 > case Osc633EventType.CommandLine: {
689 // Only trust command lines with a valid nonce
690 if (event.nonce === tracker.nonce) {
691 tracker.pendingCommandLine = event.commandLine;
692 }
693 break;
694 }
696 > case Osc633EventType.CommandExecuted: {
697 > const commandId = `cmd-${++tracker.commandCounter}`;
698 > const commandLine = tracker.pendingCommandLine ?? '';
699 > const timestamp = Date.now();
700 > tracker.pendingCommandLine = undefined;
701 > tracker.activeCommandId = commandId;
702 > tracker.activeCommandTimestamp = timestamp;
703 >
704 > // Push a new command content part
705 > managed.content.push({
706 > type: 'command',
707 > commandId,
708 > commandLine,
709 > output: '',
710 > timestamp,
711 > isComplete: false,
712 > });
713 >
714 > this._stateManager.dispatchServerAction(managed.uri, {
715 > type: ActionType.TerminalCommandExecuted,
716 > commandId,
717 > commandLine,
718 > timestamp,
719 > });
720 > break;
721 > }
722 >
723 > case Osc633EventType.CommandFinished: {
724 > const finishedCommandId = tracker.activeCommandId;
725 > if (!finishedCommandId) {
726 break;
727 }
728 > const durationMs = tracker.activeCommandTimestamp !== undefined agentHostTerminalManager.ts ×10
729 > ? Date.now() - tracker.activeCommandTimestamp
730 : undefined;
732 > // Mark the command content part as complete and collect output
733 > let commandLine = '';
734 > let commandOutput = '';
735 > for (const part of managed.content) {
736 > if (part.type === 'command' && part.commandId === finishedCommandId) {
737 > part.isComplete = true;
738 > part.exitCode = event.exitCode;
739 > part.durationMs = durationMs;
740 > commandLine = part.commandLine;
741 > commandOutput = part.output;
742 > break;
743 > }
744 > }
745 >
746 > tracker.activeCommandId = undefined;
747 > tracker.activeCommandTimestamp = undefined;
748 >
749 > managed.onCommandFinishedEmitter.fire({
750 > commandId: finishedCommandId,
751 > exitCode: event.exitCode,
752 > command: commandLine,
753 > output: commandOutput,
754 > });
755 >
756 > this._stateManager.dispatchServerAction(managed.uri, {
757 > type: ActionType.TerminalCommandFinished,
758 > commandId: finishedCommandId,
759 > exitCode: event.exitCode,
760 > durationMs,
761 > });
762 > break;
763 > }
764 >
765 > case Osc633EventType.Property: {
766 if (event.key === 'Cwd') {
767 managed.cwd = event.value;
768 this._stateManager.dispatchServerAction(managed.uri, {
769 type: ActionType.TerminalCwdChanged,
770 cwd: event.value,
771 });
772 }
773 break;
774 }
776 > }
778 > /** Append cleaned data to the terminal's structured content array. */
779 > private _appendToContent(managed: { content: TerminalContentPart[]; contentSize: number }, data: string): void {
780 > const tail = managed.content.length > 0 ? managed.content[managed.content.length - 1] : undefined; agentHostTerminalManager.ts ×8
781 >
782 > if (tail?.type === 'command' && !tail.isComplete) {
783 > // Active command — append to its output agentHostTerminalManager.ts ×10
784 > tail.output += data;
785 > managed.contentSize += data.length;
786 > } else if (tail?.type === 'unclassified') { agentHostTerminalManager.ts ×8
787 > // Extend the existing unclassified part agentHostTerminalManager.ts ×1
788 > tail.value += data;
789 > managed.contentSize += data.length;
791 > // Start a new unclassified part
792 > managed.content.push({ type: 'unclassified', value: data });
793 > managed.contentSize += data.length;
794 > }
797 > private _getContentPartSize(part: TerminalContentPart): number {
798 return part.type === 'command' ? part.output.length : part.value.length;
799 }
801 > /** Trim content parts to stay within the rolling buffer limit. */
802 > private _trimContent(managed: { content: TerminalContentPart[]; contentSize: number }): void {
803 > const maxSize = 100_000; agentHostTerminalManager.ts ×8
804 > const targetSize = 80_000;
805 > if (managed.contentSize <= maxSize) {
806 > return;
807 > }
808 // Drop whole parts from the front while possible
809 > while (managed.contentSize > targetSize && managed.content.length > 1) { agentHostTerminalManager.ts ×8
810 const removed = managed.content.shift()!;
811 managed.contentSize -= this._getContentPartSize(removed);
812 }
813 // If the single remaining (or first) part is still over budget, trim its text
814 > if (managed.contentSize > targetSize && managed.content.length > 0) { agentHostTerminalManager.ts ×8
815 const head = managed.content[0];
816 const excess = managed.contentSize - targetSize;
817 if (head.type === 'command') {
818 head.output = head.output.slice(excess);
819 } else {
820 head.value = head.value.slice(excess);
821 }
822 managed.contentSize -= excess;
823 }
826 > /**
827 > * Create an output-only terminal channel. Unlike {@link createTerminal}
828 > * there is no PTY behind it: the owner appends plain-text output via
829 > * {@link appendOutputTerminalData}. The channel is not announced on the
830 > * root terminal list — clients discover it through the tool result's
831 > * terminal content block and subscribe to its URI.
832 > */
833 > createOutputTerminal(uri: string, options: { title: string; claim: TerminalClaim }): void {
834 > if (this._terminals.has(uri) || this._outputTerminals.has(uri)) { agentHostTerminalManager.ts ×6
835 throw new Error(`Terminal already exists: ${uri}`);
836 }
837 > this._outputTerminals.set(uri, { agentHostTerminalManager.ts ×6
838 > title: options.title,
839 > content: [],
840 > contentSize: 0,
841 > claim: options.claim,
842 > });
843 > }
845 > /** Append plain-text data to an output-only terminal and stream it to subscribers. */
846 > appendOutputTerminalData(uri: string, data: string): void {
847 > const terminal = this._outputTerminals.get(uri); agentHostTerminalManager.ts ×6
848 > if (!terminal || data.length === 0) {
849 return;
850 }
851 > this._appendToContent(terminal, data); agentHostTerminalManager.ts ×6
852 > this._trimContent(terminal);
853 > this._stateManager.dispatchServerAction(uri, {
854 > type: ActionType.TerminalData,
855 > data,
856 > });
857 > }
859 > /** Clear an output-only terminal's content (e.g. when cumulative source output was rewritten). */
860 > resetOutputTerminal(uri: string): void {
861 > const terminal = this._outputTerminals.get(uri); agentHostTerminalManager.ts ×7
862 > if (!terminal) {
863 return;
864 }
865 > terminal.content = []; agentHostTerminalManager.ts ×7
866 > terminal.contentSize = 0;
867 > this._stateManager.dispatchServerAction(uri, {
868 > type: ActionType.TerminalCleared,
869 > });
870 > }
872 > /** Record the command's exit on an output-only terminal and notify subscribers. */
873 > finalizeOutputTerminal(uri: string, exitCode: number | undefined): void {
874 > const terminal = this._outputTerminals.get(uri); agentHostTerminalManager.ts ×1
875 > if (!terminal || terminal.exitCode !== undefined) {
876 > return;
877 > }
878 > if (exitCode !== undefined) {
879 > terminal.exitCode = exitCode;
880 > this._stateManager.dispatchServerAction(uri, {
881 > type: ActionType.TerminalExited,
882 > exitCode,
883 > });
884 > }
885 > }
887 > /** Dispose a terminal: kill the process and remove it. */
888 > disposeTerminal(uri: string): void {
889 > if (this._outputTerminals.delete(uri)) { agentHostTerminalManager.ts ×7
890 > return;
891 > }
892 const terminal = this._terminals.get(uri);
893 if (terminal) {
894 this._terminals.delete(uri);
895 terminal.store.dispose();
896 this._broadcastTerminalList();
897 }
900 > async getDefaultShell(): Promise<string> {
901 const configured = this._configurationService.getRootValue(agentHostCustomizationConfigSchema, AgentHostConfigKey.DefaultShell);
902 if (configured) {
903 try {
904 await fs.promises.access(configured, fs.constants.X_OK);
905 return configured;
906 } catch (err) {
907 this._logService.warn(`[TerminalManager] Configured defaultShell '${configured}' is not accessible, falling back to system shell: ${err instanceof Error ? err.message : String(err)}`);
908 }
909 }
910 return getSystemShell(platform.OS, process.env);
911 }
913 > /**
914 > * Resolves the cwd string from {@link CreateTerminalParams} to an
915 > * accessible filesystem path, falling back to $HOME if the requested
916 > * directory is missing (otherwise node-pty exits silently with code 1).
917 > * Accepts either a `file://` URI string or a raw absolute filesystem path.
918 > */
919 > private async _resolveCwd(cwd: string | undefined, terminalURI: string): Promise<string> {
920 > let resolved = cwd; agentHostTerminalManager.ts ×21
921 > if (cwd) {
922 > const parsed = URI.parse(cwd);
923 > if (parsed.scheme === 'file' && parsed.fsPath && parsed.fsPath !== '/') {
924 > resolved = parsed.fsPath;
925 > } else {
926 this._logService.warn(`[TerminalManager] Ignoring non-file cwd for ${terminalURI}: ${cwd}`);
927 }
929 >
930 > try {
931 > if (resolved) {
932 > const stat = await fs.promises.stat(resolved);
933 > if (stat.isDirectory()) {
934 > return resolved;
935 > }
936 > }
937 > } catch {
938 // fall through to fallback
939 }
940
941 > const fallback = process.env['HOME'] || process.env['USERPROFILE'] || process.cwd(); agentHostTerminalManager.ts ×21
942 > this._logService.warn(`[TerminalManager] cwd '${resolved}' is not accessible, falling back to ${fallback}`);
943 > return fallback;
944 > }
946 > /** Dispatch root/terminalsChanged with the current terminal list. */
947 > private _broadcastTerminalList(): void {
948 > this._stateManager.dispatchServerAction(ROOT_STATE_URI, { agentHostTerminalManager.ts ×21
949 > type: ActionType.RootTerminalsChanged,
950 > terminals: this.getTerminalInfos(),
951 > });
952 > }
954 > override dispose(): void {
955 > for (const terminal of this._terminals.values()) { agentHostTerminalManager.ts ×4
956 > terminal.store.dispose(); agentHostTerminalManager.ts ×21
957 > }
958 > this._terminals.clear(); agentHostTerminalManager.ts ×4
959 > super.dispose();
960 > }