Skip to content

Commit baa9c85

Browse files
authored
Merge pull request #308866 from microsoft/connor4312/ah-terminals-2
agentHost: integrate terminals with tool call
2 parents e13eb9b + 93a9f97 commit baa9c85

25 files changed

Lines changed: 1141 additions & 350 deletions

.vscode/launch.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@
5858
"request": "attach",
5959
"name": "Attach to Agent Host Process",
6060
"port": 5878,
61+
"restart": true,
62+
"timeout": 0,
6163
"outFiles": [
6264
"${workspaceFolder}/out/**/*.js"
6365
]
@@ -819,5 +821,15 @@
819821
],
820822
"preLaunchTask": "Ensure Prelaunch Dependencies"
821823
},
824+
{
825+
"name": "Renderer and Agent Host processes",
826+
"stopAll": true,
827+
"configurations": [
828+
"Launch VS Code Internal",
829+
"Attach to Main Process",
830+
"Attach to Agent Host Process"
831+
],
832+
"preLaunchTask": "Ensure Prelaunch Dependencies"
833+
},
822834
]
823835
}

src/vs/platform/agentHost/common/state/sessionReducers.ts

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,5 @@
66
// Re-exports the protocol reducers and adds VS Code-specific helpers.
77
// The actual reducer logic lives in the auto-generated protocol layer.
88

9-
import type { IToolCallState, ICompletedToolCall } from './sessionState.js';
10-
119
// Re-export reducers from the protocol layer
1210
export { rootReducer, sessionReducer, softAssertNever, isClientDispatchable } from './protocol/reducers.js';
13-
14-
// ---- Tool call metadata helpers (VS Code extensions via _meta) --------------
15-
16-
/**
17-
* Extracts the VS Code-specific `toolKind` rendering hint from a tool call's `_meta`.
18-
*/
19-
export function getToolKind(tc: IToolCallState | ICompletedToolCall): 'terminal' | undefined {
20-
return tc._meta?.toolKind as 'terminal' | undefined;
21-
}
22-
23-
/**
24-
* Extracts the VS Code-specific `language` hint from a tool call's `_meta`.
25-
*/
26-
export function getToolLanguage(tc: IToolCallState | ICompletedToolCall): string | undefined {
27-
return tc._meta?.language as string | undefined;
28-
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { URI } from '../../../base/common/uri.js';
1313
import * as os from 'os';
1414
import { AgentHostIpcChannels } from '../common/agentService.js';
1515
import { AgentService } from './agentService.js';
16+
import { IAgentHostTerminalManager } from './agentHostTerminalManager.js';
1617
import { CopilotAgent } from './copilot/copilotAgent.js';
1718
import { ProtocolServerHandler } from './protocolServerHandler.js';
1819
import { WebSocketProtocolServer } from './webSocketTransport.js';
@@ -89,6 +90,7 @@ function startAgentHost(): void {
8990
const diffComputeService = disposables.add(new NodeWorkerDiffComputeService(logService));
9091
diServices.set(IDiffComputeService, diffComputeService);
9192

93+
diServices.set(IAgentHostTerminalManager, agentService.terminalManager);
9294
const instantiationService = new InstantiationService(diServices);
9395
agentService.registerProvider(instantiationService.createInstance(CopilotAgent));
9496
} catch (err) {

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { InstantiationService } from '../../instantiation/common/instantiationSe
3131
import { ServiceCollection } from '../../instantiation/common/serviceCollection.js';
3232
import { CopilotAgent } from './copilot/copilotAgent.js';
3333
import { AgentService } from './agentService.js';
34+
import { IAgentHostTerminalManager } from './agentHostTerminalManager.js';
3435
import { WebSocketProtocolServer } from './webSocketTransport.js';
3536
import { ProtocolServerHandler } from './protocolServerHandler.js';
3637
import { FileService } from '../../files/common/fileService.js';
@@ -171,6 +172,7 @@ async function main(): Promise<void> {
171172
diServices.set(ISessionDataService, sessionDataService);
172173
diServices.set(IAgentPluginManager, pluginManager);
173174
diServices.set(IDiffComputeService, disposables.add(new NodeWorkerDiffComputeService(logService)));
175+
diServices.set(IAgentHostTerminalManager, agentService.terminalManager);
174176
const instantiationService = new InstantiationService(diServices);
175177
const copilotAgent = disposables.add(instantiationService.createInstance(CopilotAgent));
176178
agentService.registerProvider(copilotAgent);

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

Lines changed: 94 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,38 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { Disposable, DisposableStore, toDisposable } from '../../../base/common/lifecycle.js';
6+
import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../base/common/lifecycle.js';
7+
import { Emitter } from '../../../base/common/event.js';
78
import * as platform from '../../../base/common/platform.js';
89
import { ILogService } from '../../log/common/log.js';
10+
import { createDecorator } from '../../instantiation/common/instantiation.js';
911
import { ActionType } from '../common/state/protocol/actions.js';
1012
import type { ICreateTerminalParams } from '../common/state/protocol/commands.js';
1113
import { ITerminalClaim, ITerminalInfo, ITerminalState, TerminalClaimKind } from '../common/state/protocol/state.js';
1214
import { isTerminalAction } from '../common/state/sessionActions.js';
1315
import type { AgentHostStateManager } from './agentHostStateManager.js';
1416

17+
export const IAgentHostTerminalManager = createDecorator<IAgentHostTerminalManager>('agentHostTerminalManager');
18+
19+
/**
20+
* Service interface for terminal management in the agent host.
21+
*/
22+
export interface IAgentHostTerminalManager {
23+
readonly _serviceBrand: undefined;
24+
createTerminal(params: ICreateTerminalParams, options?: { shell?: string }): Promise<void>;
25+
writeInput(uri: string, data: string): void;
26+
onData(uri: string, cb: (data: string) => void): IDisposable;
27+
onExit(uri: string, cb: (exitCode: number) => void): IDisposable;
28+
onClaimChanged(uri: string, cb: (claim: ITerminalClaim) => void): IDisposable;
29+
getContent(uri: string): string | undefined;
30+
getClaim(uri: string): ITerminalClaim | undefined;
31+
hasTerminal(uri: string): boolean;
32+
getExitCode(uri: string): number | undefined;
33+
disposeTerminal(uri: string): void;
34+
getTerminalInfos(): ITerminalInfo[];
35+
getTerminalState(uri: string): ITerminalState | undefined;
36+
}
37+
1538
// node-pty is loaded dynamically to avoid bundling issues in non-node environments
1639
let nodePtyModule: typeof import('node-pty') | undefined;
1740
async function getNodePty(): Promise<typeof import('node-pty')> {
@@ -26,6 +49,9 @@ interface IManagedTerminal {
2649
readonly uri: string;
2750
readonly store: DisposableStore;
2851
readonly pty: import('node-pty').IPty;
52+
readonly onDataEmitter: Emitter<string>;
53+
readonly onExitEmitter: Emitter<number>;
54+
readonly onClaimChangedEmitter: Emitter<ITerminalClaim>;
2955
title: string;
3056
cwd: string;
3157
cols: number;
@@ -43,7 +69,8 @@ interface IManagedTerminal {
4369
* actions (input, resize, claim changes) and dispatches server-originated
4470
* PTY output back through the state manager.
4571
*/
46-
export class AgentHostTerminalManager extends Disposable {
72+
export class AgentHostTerminalManager extends Disposable implements IAgentHostTerminalManager {
73+
declare readonly _serviceBrand: undefined;
4774

4875
private readonly _terminals = new Map<string, IManagedTerminal>();
4976

@@ -110,7 +137,7 @@ export class AgentHostTerminalManager extends Disposable {
110137
* Create a new terminal backed by node-pty.
111138
* Spawns the user's default shell.
112139
*/
113-
async createTerminal(params: ICreateTerminalParams): Promise<void> {
140+
async createTerminal(params: ICreateTerminalParams, options?: { shell?: string }): Promise<void> {
114141
const uri = params.terminal;
115142
if (this._terminals.has(uri)) {
116143
throw new Error(`Terminal already exists: ${uri}`);
@@ -122,7 +149,7 @@ export class AgentHostTerminalManager extends Disposable {
122149
const cols = params.cols ?? 80;
123150
const rows = params.rows ?? 24;
124151

125-
const shell = this._getDefaultShell();
152+
const shell = options?.shell ?? this._getDefaultShell();
126153
const name = platform.isWindows ? 'cmd' : 'xterm-256color';
127154

128155
this._logService.info(`[TerminalManager] Creating terminal ${uri}: shell=${shell}, cwd=${cwd}, cols=${cols}, rows=${rows}`);
@@ -138,10 +165,17 @@ export class AgentHostTerminalManager extends Disposable {
138165
const store = new DisposableStore();
139166
const claim: ITerminalClaim = params.claim ?? { kind: TerminalClaimKind.Client, clientId: '' };
140167

168+
const onDataEmitter = store.add(new Emitter<string>());
169+
const onExitEmitter = store.add(new Emitter<number>());
170+
const onClaimChangedEmitter = store.add(new Emitter<ITerminalClaim>());
171+
141172
const managed: IManagedTerminal = {
142173
uri,
143174
store,
144175
pty: ptyProcess,
176+
onDataEmitter,
177+
onExitEmitter,
178+
onClaimChangedEmitter,
145179
title: params.name ?? shell,
146180
cwd,
147181
cols,
@@ -162,6 +196,7 @@ export class AgentHostTerminalManager extends Disposable {
162196
if (managed.content.length > 100_000) {
163197
managed.content = managed.content.slice(-80_000);
164198
}
199+
managed.onDataEmitter.fire(data);
165200
this._stateManager.dispatchServerAction({
166201
type: ActionType.TerminalData,
167202
terminal: uri,
@@ -172,6 +207,7 @@ export class AgentHostTerminalManager extends Disposable {
172207

173208
const exitListener = ptyProcess.onExit(e => {
174209
managed.exitCode = e.exitCode;
210+
managed.onExitEmitter.fire(e.exitCode);
175211
this._stateManager.dispatchServerAction({
176212
type: ActionType.TerminalExited,
177213
terminal: uri,
@@ -201,14 +237,66 @@ export class AgentHostTerminalManager extends Disposable {
201237
this._broadcastTerminalList();
202238
}
203239

204-
/** Send input data to a terminal's PTY process. */
240+
/** Send input data to a terminal's PTY process (from client-dispatched actions). */
205241
private _writeInput(uri: string, data: string): void {
242+
this.writeInput(uri, data);
243+
}
244+
245+
/** Send input data to a terminal's PTY process. */
246+
writeInput(uri: string, data: string): void {
206247
const terminal = this._terminals.get(uri);
207248
if (terminal && terminal.exitCode === undefined) {
208249
terminal.pty.write(data);
209250
}
210251
}
211252

253+
/** Register a callback for PTY data events on a terminal. */
254+
onData(uri: string, cb: (data: string) => void): IDisposable {
255+
const terminal = this._terminals.get(uri);
256+
if (!terminal) {
257+
return toDisposable(() => { });
258+
}
259+
return terminal.onDataEmitter.event(cb);
260+
}
261+
262+
/** Register a callback for PTY exit events on a terminal. */
263+
onExit(uri: string, cb: (exitCode: number) => void): IDisposable {
264+
const terminal = this._terminals.get(uri);
265+
if (!terminal) {
266+
return toDisposable(() => { });
267+
}
268+
return terminal.onExitEmitter.event(cb);
269+
}
270+
271+
/** Register a callback for terminal claim changes. */
272+
onClaimChanged(uri: string, cb: (claim: ITerminalClaim) => void): IDisposable {
273+
const terminal = this._terminals.get(uri);
274+
if (!terminal) {
275+
return toDisposable(() => { });
276+
}
277+
return terminal.onClaimChangedEmitter.event(cb);
278+
}
279+
280+
/** Get accumulated scrollback content for a terminal. */
281+
getContent(uri: string): string | undefined {
282+
return this._terminals.get(uri)?.content;
283+
}
284+
285+
/** Get the current claim for a terminal. */
286+
getClaim(uri: string): ITerminalClaim | undefined {
287+
return this._terminals.get(uri)?.claim;
288+
}
289+
290+
/** Check whether a terminal exists. */
291+
hasTerminal(uri: string): boolean {
292+
return this._terminals.has(uri);
293+
}
294+
295+
/** Get the exit code for a terminal, or undefined if still running. */
296+
getExitCode(uri: string): number | undefined {
297+
return this._terminals.get(uri)?.exitCode;
298+
}
299+
212300
/** Resize a terminal. */
213301
private _resize(uri: string, cols: number, rows: number): void {
214302
const terminal = this._terminals.get(uri);
@@ -224,6 +312,7 @@ export class AgentHostTerminalManager extends Disposable {
224312
const terminal = this._terminals.get(uri);
225313
if (terminal) {
226314
terminal.claim = claim;
315+
terminal.onClaimChangedEmitter.fire(claim);
227316
this._broadcastTerminalList();
228317
}
229318
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import type { ICreateTerminalParams } from '../common/state/protocol/commands.js
1818
import { AhpErrorCodes, AHP_SESSION_NOT_FOUND, ContentEncoding, JSON_RPC_INTERNAL_ERROR, ProtocolError, type IDirectoryEntry, type IResourceCopyParams, type IResourceCopyResult, type IResourceDeleteParams, type IResourceDeleteResult, type IResourceListResult, type IResourceMoveParams, type IResourceMoveResult, type IResourceReadResult, type IResourceWriteParams, type IResourceWriteResult, type IStateSnapshot } from '../common/state/sessionProtocol.js';
1919
import { ResponsePartKind, SessionStatus, ToolCallConfirmationReason, ToolCallStatus, TurnState, type IResponsePart, type ISessionFileDiff, type ISessionSummary, type IToolCallCompletedState, type ITurn } from '../common/state/sessionState.js';
2020
import { AgentSideEffects } from './agentSideEffects.js';
21-
import { AgentHostTerminalManager } from './agentHostTerminalManager.js';
21+
import { AgentHostTerminalManager, type IAgentHostTerminalManager } from './agentHostTerminalManager.js';
2222
import { ISessionDbUriFields, parseSessionDbUri } from './copilot/fileEditTracker.js';
2323
import { AgentHostStateManager } from './agentHostStateManager.js';
2424

@@ -59,6 +59,9 @@ export class AgentService extends Disposable implements IAgentService {
5959
/** Manages PTY-backed terminals for the agent host protocol. */
6060
private readonly _terminalManager: AgentHostTerminalManager;
6161

62+
/** Exposes the terminal manager for use by agent providers. */
63+
get terminalManager(): IAgentHostTerminalManager { return this._terminalManager; }
64+
6265
constructor(
6366
private readonly _logService: ILogService,
6467
private readonly _fileService: IFileService,

0 commit comments

Comments
 (0)