agentHostTerminalManager.ts ×41

Frontier kind: Code frontier

unlabeled · c_8f17917667a1

1038 tests · 24585 LOC · 110 files · introduces 0 tests · 285 LOC · 2 files

Introduces — evidence that enters the hierarchy at this concept

Code
42 ranges285 lines · 2 files
Tests
0 tests

Contains — complete concept membership

All code (extent)
1954 ranges24585 lines · 110 files · Browse complete extent
All tests (intent)
1038 testsBrowse complete intent

Neighbourhood graph

The orange circle is the focus. Violet and green circles are every ancestor and descendant, broader and narrower, at any distance; blue squares and pink diamonds are the introduced files and exact introduced tests of every visible concept, not only the focus's. Arrows point from broader to narrower concepts and bridge only concepts omitted from this view. Undirected links show source or test introduction. Concept and file size follows LOC; exact test nodes use test-count units.

Introduced files, introduced tests, and structurally relevant concept specialization

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 native relationship evidence on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the native relationship evidence on this page while the interactive map is unavailable.

Native relationship evidence

Every exact file and test below is linked only from the concept that introduces it.

Introduced tests

Every collected test enters the hierarchy at exactly one concept.

No tests are introduced at this concept. Its intent tests are introduced by other concepts.

Introduced code

Every collected source range enters the hierarchy at exactly one concept.

2 files ranked by introduced lines: 285 introduced LOC across 42 ranges. Expand a file to inspect source; the > gutter marks introduced lines.

src/vs/platform/agentHost/node/agentHostTerminalManager.ts 276 introduced LOC · 41 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostTerminalManager.ts
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 {
66 if (
67 !state.pendingData
84 .replaceAll(DSR_CURSOR_POSITION_QUERY, '');
85 }
87 function getServerHandledTerminalQueryPrefix(data: string): string {
88 for (const prefix of SERVER_HANDLED_QUERY_PREFIXES) {
93 return '';
94 }
96 > export function formatTerminalText(data: string, options: IFormatTerminalTextOptions): string {
97 if (options.forceBracketedPasteMode) {
98 data = `\x1b[200~${data}\x1b[201~`;
104 return data;
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) {
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,
206 @ILogService private readonly _logService: ILogService,
236 }));
237 }
239 > /** Get metadata for all active terminals (for root state). */
240 > getTerminalInfos(): TerminalInfo[] {
241 return [...this._terminals.values()].map(t => ({
242 resource: t.uri,
246 }));
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);
252 if (outputTerminal) {
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;
284 if (this._terminals.has(uri)) {
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);
494 if (terminal && terminal.exitCode === undefined) {
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);
502 let forceBracketedPasteMode = false;
507 this.writeInput(uri, formatTerminalText(data, { shouldExecute: options.shouldExecute, forceBracketedPasteMode }));
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) {
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) {
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) {
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);
540 if (!terminal) {
543 return terminal.onCommandFinishedEmitter.event(cb);
544 }
546 > createAltBufferPromise(uri: string, store: DisposableStore): Promise<void> {
547 const terminal = this._terminals.get(uri);
548 if (!terminal?.headlessTerminal) {
551 return terminal.headlessTerminal.createAltBufferPromise(store);
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) {
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);
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) {
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) {
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) {
611 }
612 }
614 > /** Clear a terminal's scrollback buffer. */
615 > private _clearContent(uri: string): void {
616 const terminal = this._terminals.get(uri);
617 if (terminal) {
621 }
622 }
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;
627
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
680 if (!tracker.detectionAvailableEmitted) {
775 }
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;
781
794 }
795 }
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;
804 const targetSize = 80_000;
823 }
824 }
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)) {
835 throw new Error(`Terminal already exists: ${uri}`);
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);
848 if (!terminal || data.length === 0) {
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);
862 if (!terminal) {
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);
875 if (!terminal || terminal.exitCode !== undefined) {
884 }
885 }
887 > /** Dispose a terminal: kill the process and remove it. */
888 > disposeTerminal(uri: string): void {
889 if (this._outputTerminals.delete(uri)) {
890 return;
897 }
898 }
900 > async getDefaultShell(): Promise<string> {
901 const configured = this._configurationService.getRootValue(agentHostCustomizationConfigSchema, AgentHostConfigKey.DefaultShell);
902 if (configured) {
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;
921 if (cwd) {
943 return fallback;
944 }
946 > /** Dispatch root/terminalsChanged with the current terminal list. */
947 > private _broadcastTerminalList(): void {
948 this._stateManager.dispatchServerAction(ROOT_STATE_URI, {
949 type: ActionType.RootTerminalsChanged,
951 });
952 }
954 > override dispose(): void {
955 for (const terminal of this._terminals.values()) {
956 terminal.store.dispose();
src/vs/platform/agentHost/node/agentHostShellUtils.ts 9 introduced LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostShellUtils.ts
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 { posix as pathPosix, win32 as pathWin32 } from '../../../base/common/path.js';
7 > import * as platform from '../../../base/common/platform.js';
8 >
9 > export function isZsh(shell: string): boolean {
10 if (platform.OS === platform.OperatingSystem.Windows) {
11 return /^zsh(?:\.exe)?$/i.test(pathWin32.basename(shell));