agentHostTerminalManager.ts ×21

Frontier kind: Code frontier

unlabeled · c_423d80934a2c

8 tests · 25781 LOC · 110 files · introduces 0 tests · 212 LOC · 2 files

Introduces — evidence that enters the hierarchy at this concept

Code
22 ranges212 lines · 2 files
Tests
0 tests

Contains — complete concept membership

All code (extent)
2323 ranges25781 lines · 110 files · Browse complete extent
All tests (intent)
8 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: 212 introduced LOC across 22 ranges. Expand a file to inspect source; the > gutter marks introduced lines.

src/vs/platform/agentHost/node/agentHostTerminalManager.ts 211 introduced LOC · 21 ranges

Open complete file

240 getTerminalInfos(): TerminalInfo[] {
241 return [...this._terminals.values()].map(t => ({
242 > resource: t.uri, agentHostTerminalManager.ts
243 > title: t.title,
244 > claim: t.claim,
245 > exitCode: t.exitCode,
246 }));
247 }
281 */
282 async createTerminal(params: CreateTerminalParams, options?: { shell?: string; preventShellHistory?: boolean; nonInteractive?: boolean }): Promise<void> {
283 > const uri = params.channel; agentHostTerminalManager.ts
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
305 // (bash) / HIST_IGNORE_SPACE (zsh), or suppress PSReadLine history (pwsh).
308 env['VSCODE_PREVENT_SHELL_HISTORY'] = '1';
309 }
310 > // Zsh-specific fixups for agent tool terminals: disable bang history agentHostTerminalManager.ts
311 > // expansion and enable inline # comments.
312 > if (params.claim?.kind === TerminalClaimKind.Session && isZsh(shell)) {
313 env['VSCODE_AGENT_ZSH_FIXUPS'] = '1';
314 }
315 > if (options?.nonInteractive) { agentHostTerminalManager.ts
316 // Suppress paging and interactive prompts so that tool-spawned
317 // terminals produce clean, machine-friendly output. An empty
325 env['DEBIAN_FRONTEND'] = 'noninteractive';
326 }
327 > let shellArgs: string[] = []; agentHostTerminalManager.ts
328 > if (platform.isMacintosh) {
329 const shellName = pathParse(shell).name;
330 if (shellName.match(/(zsh|bash)/)) {
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) {
365 try {
371 }
372 }
373 > commandTracker = { agentHostTerminalManager.ts
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)}`);
428 try {
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);
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) {
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 > }
480
481 protected async _spawnPty(file: string, args: string[], options: import('node-pty').IPtyForkOptions | import('node-pty').IWindowsPtyForkOptions): Promise<import('node-pty').IPty> {
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
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) {
644 return;
645 }
646 > managed.onDataEmitter.fire(pendingClientData); agentHostTerminalManager.ts
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();
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 > }
676
677 /** Handle a parsed OSC 633 event by dispatching the appropriate protocol actions. */
918 */
919 private async _resolveCwd(cwd: string | undefined, terminalURI: string): Promise<string> {
920 > let resolved = cwd; agentHostTerminalManager.ts
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
942 > this._logService.warn(`[TerminalManager] cwd '${resolved}' is not accessible, falling back to ${fallback}`);
943 > return fallback;
944 > }
945
946 /** Dispatch root/terminalsChanged with the current terminal list. */
947 private _broadcastTerminalList(): void {
948 > this._stateManager.dispatchServerAction(ROOT_STATE_URI, { agentHostTerminalManager.ts
949 > type: ActionType.RootTerminalsChanged,
950 > terminals: this.getTerminalInfos(),
951 > });
952 > }
953
954 override dispose(): void {
955 for (const terminal of this._terminals.values()) {
956 > terminal.store.dispose(); agentHostTerminalManager.ts
957 > }
958 this._terminals.clear();
959 super.dispose();
src/vs/platform/agentHost/common/state/protocol/channels-root/reducer.ts 1 introduced LOC · 1 range

Open complete file

24
25 case ActionType.RootTerminalsChanged:
26 > return { ...state, terminals: action.terminals }; reducer.ts
27
28 case ActionType.RootConfigChanged: