@vybestack/llxprt-code 0.10.0 → 0.11.0-nightly.260728.ffaa8d5d7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/package.json +13 -13
  2. package/src/commands/mcp/add.ts +8 -4
  3. package/src/config/cliArgParser.ts +2 -0
  4. package/src/config/config.ts +2 -0
  5. package/src/config/configBuilder.ts +3 -0
  6. package/src/config/configError.ts +23 -0
  7. package/src/config/extensions/extensionEnablement.ts +62 -1
  8. package/src/config/interactiveContext.ts +2 -1
  9. package/src/config/intermediateConfig.ts +4 -0
  10. package/src/config/keyBindings.ts +29 -3
  11. package/src/config/policy.ts +7 -1
  12. package/src/config/postConfigRuntime.ts +20 -14
  13. package/src/config/settings-schema/schema-ui.ts +9 -0
  14. package/src/config/settingsLoader.ts +21 -17
  15. package/src/config/settingsSchema.ts +2 -2
  16. package/src/config/trustedFolders.ts +275 -58
  17. package/src/config/yargsOptions.ts +7 -0
  18. package/src/generated/git-commit.json +1 -1
  19. package/src/nonInteractiveCli.ts +1 -0
  20. package/src/nonInteractiveCliSupport.ts +171 -51
  21. package/src/runtime/interactiveToolScheduler.ts +6 -6
  22. package/src/services/BuiltinCommandLoader.ts +33 -25
  23. package/src/services/FileCommandLoader.ts +34 -7
  24. package/src/test-utils/async.ts +20 -0
  25. package/src/test-utils/render.tsx +10 -1
  26. package/src/ui/App.tsx +17 -10
  27. package/src/ui/AppContainerRuntime.tsx +7 -9
  28. package/src/ui/cliUiRuntime.ts +28 -0
  29. package/src/ui/commands/authCommand.ts +240 -3
  30. package/src/ui/commands/compressCommand.ts +9 -1
  31. package/src/ui/commands/directoryCommand.tsx +1 -1
  32. package/src/ui/commands/logoutCommand.ts +2 -2
  33. package/src/ui/commands/permissionsCommand.ts +77 -20
  34. package/src/ui/commands/policiesCommand.ts +6 -1
  35. package/src/ui/commands/quotaCommand.ts +501 -0
  36. package/src/ui/commands/statsCommand.ts +2 -2
  37. package/src/ui/commands/statsQuota.ts +1 -1
  38. package/src/ui/commands/subagentCommand.ts +59 -27
  39. package/src/ui/components/AuthDialog.tsx +3 -3
  40. package/src/ui/components/Composer.tsx +1 -0
  41. package/src/ui/components/DialogManager.tsx +9 -2
  42. package/src/ui/components/FolderTrustDialog.tsx +71 -60
  43. package/src/ui/components/PermissionsModifyTrustDialog.tsx +211 -146
  44. package/src/ui/components/ProfileCreateWizard/constants.ts +13 -1
  45. package/src/ui/components/QueuedMessagesPanel.tsx +388 -0
  46. package/src/ui/components/inputPromptHooks.ts +2 -0
  47. package/src/ui/components/inputPromptKeyHandlers.ts +14 -0
  48. package/src/ui/components/inputPromptTypes.ts +6 -0
  49. package/src/ui/components/messages/AiMessage.tsx +24 -12
  50. package/src/ui/components/messages/ToolMessage.tsx +26 -40
  51. package/src/ui/containers/AppContainer/builders/buildUIActions.ts +3 -1
  52. package/src/ui/containers/AppContainer/builders/buildUIState.ts +5 -4
  53. package/src/ui/containers/AppContainer/hooks/useAppBootstrap.ts +3 -33
  54. package/src/ui/containers/AppContainer/hooks/useAppDialogs.ts +2 -14
  55. package/src/ui/containers/AppContainer/hooks/useAppInput.ts +31 -25
  56. package/src/ui/containers/AppContainer/hooks/useAppLayout.ts +90 -60
  57. package/src/ui/containers/AppContainer/hooks/useDisplayPreferences.ts +10 -0
  58. package/src/ui/containers/AppContainer/hooks/useInputHandling.ts +3 -17
  59. package/src/ui/containers/AppContainer/hooks/useKeybindings.ts +51 -38
  60. package/src/ui/containers/AppContainer/hooks/useSteer.ts +51 -0
  61. package/src/ui/contexts/KeypressContext.tsx +13 -2
  62. package/src/ui/contexts/ShellCommandDisplayContext.tsx +103 -0
  63. package/src/ui/contexts/TodoContext.tsx +0 -13
  64. package/src/ui/contexts/TodoProvider.tsx +1 -34
  65. package/src/ui/contexts/UIActionsContext.tsx +2 -1
  66. package/src/ui/contexts/UIStateContext.tsx +3 -2
  67. package/src/ui/hooks/agentStream/streamUtils.ts +0 -24
  68. package/src/ui/hooks/agentStream/thoughtState.ts +110 -30
  69. package/src/ui/hooks/agentStream/types.ts +1 -0
  70. package/src/ui/hooks/agentStream/useAgentEventStream.ts +8 -17
  71. package/src/ui/hooks/agentStream/useAgentStream.ts +2 -2
  72. package/src/ui/hooks/agentStream/useAgentStreamLifecycle.ts +4 -5
  73. package/src/ui/hooks/agentStream/useAgentStreamOrchestration.ts +12 -5
  74. package/src/ui/hooks/agentStream/useQueuedSubmissions.ts +99 -0
  75. package/src/ui/hooks/agentStream/useStreamEventHandlers.ts +4 -13
  76. package/src/ui/hooks/agentStream/useStreamState.ts +60 -41
  77. package/src/ui/hooks/agentStream/useSubmitQuery.ts +284 -59
  78. package/src/ui/hooks/slashCommandProcessorSupport.ts +4 -0
  79. package/src/ui/hooks/useAgentStream-test-helpers.ts +1 -0
  80. package/src/ui/hooks/useAutoAcceptIndicator.ts +13 -0
  81. package/src/ui/hooks/useFolderTrust.ts +118 -81
  82. package/src/ui/hooks/useGitBranchName.ts +79 -33
  83. package/src/ui/hooks/useIdeTrustListener.ts +5 -19
  84. package/src/ui/hooks/usePermissionsModifyTrust.ts +223 -85
  85. package/src/ui/hooks/useReactToolScheduler.ts +13 -17
  86. package/src/ui/hooks/useTodoPausePreserver.ts +0 -55
  87. package/src/ui/layouts/DefaultAppLayout.tsx +2 -0
  88. package/src/ui/layouts/DefaultAppLayoutHelpers.tsx +37 -162
  89. package/src/ui/layouts/InlineContent.tsx +198 -0
  90. package/src/ui/trustDialogHelpers.ts +190 -0
  91. package/src/utils/cleanup-state.ts +115 -0
  92. package/src/utils/cleanup.ts +32 -34
  93. package/src/utils/sandbox-containers.ts +183 -53
  94. package/src/utils/sandbox-exec.ts +9 -3
  95. package/src/zed-integration/acp-terminal-shell-host.ts +112 -0
  96. package/src/zed-integration/acp-types.ts +35 -0
  97. package/src/zed-integration/fileSystemService.ts +1 -1
  98. package/src/zed-integration/runZedIntegration.ts +142 -0
  99. package/src/zed-integration/zed-agent-event-handler.ts +179 -0
  100. package/src/zed-integration/zed-agent-setup.ts +43 -0
  101. package/src/zed-integration/zed-command-registry.ts +167 -0
  102. package/src/zed-integration/zed-config-options.ts +280 -0
  103. package/src/zed-integration/zed-content-utils.ts +2 -2
  104. package/src/zed-integration/zed-helpers.ts +132 -0
  105. package/src/zed-integration/zed-initialize.ts +64 -0
  106. package/src/zed-integration/zed-plan-update.ts +20 -0
  107. package/src/zed-integration/zed-prompt-command.ts +77 -0
  108. package/src/zed-integration/zed-session-config.ts +119 -0
  109. package/src/zed-integration/zed-session-errors.ts +337 -0
  110. package/src/zed-integration/zed-session-events.ts +216 -0
  111. package/src/zed-integration/zed-session-info.ts +255 -0
  112. package/src/zed-integration/zed-session-lifecycle.ts +186 -0
  113. package/src/zed-integration/zed-session-listing.ts +153 -0
  114. package/src/zed-integration/zed-session-loader.ts +193 -0
  115. package/src/zed-integration/zed-session-pagination.ts +240 -0
  116. package/src/zed-integration/zed-session-replay.ts +564 -0
  117. package/src/zed-integration/zed-stream-batcher.ts +236 -0
  118. package/src/zed-integration/zed-terminal-manager.ts +416 -0
  119. package/src/zed-integration/zed-terminal-setup.ts +72 -0
  120. package/src/zed-integration/zed-test-helpers.ts +349 -4
  121. package/src/zed-integration/zed-tool-handler.ts +47 -5
  122. package/src/zed-integration/zedIntegration.ts +538 -587
  123. package/src/services/todo-continuation/todoContinuationService.ts +0 -610
  124. package/src/ui/containers/AppContainer/hooks/useIdeRestartHotkey.ts +0 -33
  125. package/src/ui/containers/AppContainer/hooks/useTodoContinuationFlow.ts +0 -160
  126. package/src/ui/hooks/useTodoContinuation.ts +0 -299
@@ -0,0 +1,72 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Vybestack LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import type * as acp from '@agentclientprotocol/sdk';
8
+ import {
9
+ type Config,
10
+ type DebugLogger,
11
+ type MessageBus,
12
+ CoreMessageBusAdapter,
13
+ CoreShellToolHostAdapter,
14
+ CoreToolRegistryHostAdapter,
15
+ } from '@vybestack/llxprt-code-core';
16
+ import { ShellTool, ToolRegistry } from '@vybestack/llxprt-code-tools';
17
+ import { AcpTerminalShellHost } from './acp-terminal-shell-host.js';
18
+ import { TerminalManager } from './zed-terminal-manager.js';
19
+
20
+ export interface ZedTerminalSetup {
21
+ readonly registry: ToolRegistry;
22
+ readonly terminals: TerminalManager;
23
+ }
24
+
25
+ export function buildZedTerminalSetup(
26
+ sessionId: string,
27
+ config: Config,
28
+ baseRegistry: ToolRegistry,
29
+ connection: acp.AgentSideConnection,
30
+ logger: DebugLogger,
31
+ messageBus: MessageBus,
32
+ ): ZedTerminalSetup {
33
+ const outputLimit = config.getEphemeralSetting('tool-output-max-tokens');
34
+ const terminals = new TerminalManager(
35
+ sessionId,
36
+ connection,
37
+ config.getTargetDir(),
38
+ (update) => connection.sessionUpdate({ sessionId, update }),
39
+ logger,
40
+ typeof outputLimit === 'number' &&
41
+ Number.isFinite(outputLimit) &&
42
+ outputLimit > 0
43
+ ? outputLimit
44
+ : undefined,
45
+ );
46
+ const messageBusAdapter = new CoreMessageBusAdapter(messageBus);
47
+ const registry = new ToolRegistry(
48
+ new CoreToolRegistryHostAdapter(config),
49
+ messageBusAdapter,
50
+ );
51
+ const baseTools = baseRegistry.getAllTools();
52
+ let hasShellTool = false;
53
+ for (const tool of baseTools) {
54
+ if (tool.name === ShellTool.Name) {
55
+ hasShellTool = true;
56
+ continue;
57
+ }
58
+ registry.registerTool(tool);
59
+ }
60
+ if (hasShellTool) {
61
+ registry.registerTool(
62
+ new ShellTool(
63
+ new AcpTerminalShellHost(
64
+ new CoreShellToolHostAdapter(config),
65
+ terminals,
66
+ ),
67
+ messageBusAdapter,
68
+ ),
69
+ );
70
+ }
71
+ return { registry, terminals };
72
+ }
@@ -22,7 +22,10 @@ export type ConfirmationCapture = {
22
22
  requiresUserConfirmation?: boolean;
23
23
  };
24
24
 
25
- export function buildScriptedAgent(nextEvents: () => readonly AgentEvent[]): {
25
+ export function buildScriptedAgent(
26
+ nextEvents: () => readonly AgentEvent[],
27
+ toolKinds: Readonly<Record<string, string>> = {},
28
+ ): {
26
29
  agent: Agent;
27
30
  confirmations: ConfirmationCapture[];
28
31
  } {
@@ -37,6 +40,8 @@ export function buildScriptedAgent(nextEvents: () => readonly AgentEvent[]): {
37
40
  setApprovalMode: vi.fn(),
38
41
  dispose: vi.fn().mockResolvedValue(undefined),
39
42
  tools: {
43
+ get: (name: string) =>
44
+ Object.hasOwn(toolKinds, name) ? { kind: toolKinds[name] } : undefined,
40
45
  respondToConfirmation: (
41
46
  confirmationId: string,
42
47
  decision: ToolConfirmationOutcome,
@@ -63,13 +68,93 @@ export function buildScriptedAgent(nextEvents: () => readonly AgentEvent[]): {
63
68
  return { agent, confirmations };
64
69
  }
65
70
 
66
- export function buildFakeAgent(events: readonly AgentEvent[]): {
71
+ /**
72
+ * Like {@link buildScriptedAgent} but the stream blocks after yielding all
73
+ * events until the abort signal fires. Used to test cancel/dispose while a
74
+ * prompt turn is still in-flight.
75
+ */
76
+ export function buildBlockingScriptedAgent(
77
+ nextEvents: () => readonly AgentEvent[],
78
+ toolKinds: Readonly<Record<string, string>> = {},
79
+ ): {
80
+ agent: Agent;
81
+ confirmations: ConfirmationCapture[];
82
+ } {
83
+ const confirmations: ConfirmationCapture[] = [];
84
+ const agent = {
85
+ async *stream(_input: unknown, opts?: unknown): AsyncIterable<AgentEvent> {
86
+ for (const e of nextEvents()) {
87
+ yield e;
88
+ }
89
+ const signal = (opts as { signal?: AbortSignal } | undefined)?.signal;
90
+ if (signal === undefined) {
91
+ throw new Error(
92
+ 'buildBlockingScriptedAgent requires an AbortSignal in stream opts',
93
+ );
94
+ }
95
+ if (!signal.aborted) {
96
+ await new Promise<void>((resolve) => {
97
+ signal.addEventListener('abort', () => resolve(), { once: true });
98
+ });
99
+ }
100
+ },
101
+ getApprovalMode: (): ApprovalMode => 'default' as ApprovalMode,
102
+ setApprovalMode: vi.fn(),
103
+ dispose: vi.fn().mockResolvedValue(undefined),
104
+ tools: {
105
+ get: (name: string) =>
106
+ Object.hasOwn(toolKinds, name) ? { kind: toolKinds[name] } : undefined,
107
+ respondToConfirmation: (
108
+ confirmationId: string,
109
+ decision: ToolConfirmationOutcome,
110
+ payload?: ToolConfirmationPayload,
111
+ requiresUserConfirmation?: boolean,
112
+ ) => {
113
+ confirmations.push({
114
+ confirmationId,
115
+ decision,
116
+ ...(payload === undefined ? {} : { payload }),
117
+ ...(requiresUserConfirmation === undefined
118
+ ? {}
119
+ : { requiresUserConfirmation }),
120
+ });
121
+ },
122
+ onConfirmationRequest: () => () => {},
123
+ onToolUpdate: () => () => {},
124
+ setEditorCallbacks: () => {},
125
+ setEnabled: vi.fn().mockResolvedValue(undefined),
126
+ list: () => [],
127
+ keys: {},
128
+ },
129
+ } as unknown as Agent;
130
+ return { agent, confirmations };
131
+ }
132
+
133
+ export function buildFakeAgent(
134
+ events: readonly AgentEvent[],
135
+ toolKinds: Readonly<Record<string, string>> = {},
136
+ ): {
137
+ agent: Agent;
138
+ confirmations: ConfirmationCapture[];
139
+ } {
140
+ return buildScriptedAgent(() => events, toolKinds);
141
+ }
142
+
143
+ export function buildBlockingFakeAgent(
144
+ events: readonly AgentEvent[],
145
+ toolKinds: Readonly<Record<string, string>> = {},
146
+ ): {
67
147
  agent: Agent;
68
148
  confirmations: ConfirmationCapture[];
69
149
  } {
70
- return buildScriptedAgent(() => events);
150
+ return buildBlockingScriptedAgent(() => events, toolKinds);
71
151
  }
72
152
 
153
+ export type FakeTerminalHandle = Pick<
154
+ acp.TerminalHandle,
155
+ 'id' | 'currentOutput' | 'waitForExit' | 'kill' | 'release'
156
+ >;
157
+
73
158
  export class RecordingConnection {
74
159
  readonly messages: Array<
75
160
  | { kind: 'sessionUpdate'; update: acp.SessionUpdate }
@@ -84,6 +169,142 @@ export class RecordingConnection {
84
169
  optionId: ToolConfirmationOutcome.ProceedOnce,
85
170
  };
86
171
  private permissionRejection: Error | null = null;
172
+ private sessionUpdateFailAfter: number | null = null;
173
+ private sessionUpdateError: Error | null = null;
174
+ private sessionUpdateCalls = 0;
175
+
176
+ // --- Terminal test-double state ---
177
+
178
+ readonly createTerminalCalls: Array<{
179
+ command: string;
180
+ cwd?: string | null;
181
+ sessionId: string;
182
+ args?: string[];
183
+ env?: Array<{ name: string; value: string }>;
184
+ outputByteLimit?: number | null;
185
+ }> = [];
186
+
187
+ killCalls = 0;
188
+ releaseCalls = 0;
189
+
190
+ private terminalOutput = '';
191
+ private terminalExitDelayed = false;
192
+ private terminalExitResolvers = new Map<string, Array<() => void>>();
193
+ private terminalCreationWaiters = new Set<() => void>();
194
+ private terminalContentWaiters = new Set<() => void>();
195
+
196
+ setTerminalOutput(output: string): void {
197
+ this.terminalOutput = output;
198
+ }
199
+
200
+ delayTerminalExit(): void {
201
+ this.terminalExitDelayed = true;
202
+ }
203
+
204
+ resolveDelayedTerminalExit(terminalId?: string): void {
205
+ if (terminalId !== undefined) {
206
+ const resolvers = this.terminalExitResolvers.get(terminalId);
207
+ if (resolvers !== undefined) {
208
+ this.terminalExitResolvers.delete(terminalId);
209
+ for (const fn of resolvers) {
210
+ fn();
211
+ }
212
+ }
213
+ if (this.terminalExitResolvers.size === 0) {
214
+ this.terminalExitDelayed = false;
215
+ }
216
+ return;
217
+ }
218
+ for (const resolvers of this.terminalExitResolvers.values()) {
219
+ for (const fn of resolvers) {
220
+ fn();
221
+ }
222
+ }
223
+ this.terminalExitResolvers.clear();
224
+ this.terminalExitDelayed = false;
225
+ }
226
+
227
+ waitForTerminalProcessCreated(): Promise<void> {
228
+ if (this.createTerminalCalls.length > 0) return Promise.resolve();
229
+ return new Promise<void>((resolve) => {
230
+ const waiter = (): void => {
231
+ this.terminalCreationWaiters.delete(waiter);
232
+ resolve();
233
+ };
234
+ this.terminalCreationWaiters.add(waiter);
235
+ });
236
+ }
237
+
238
+ /**
239
+ * Resolves once at least one terminal content update (tool_call_update with
240
+ * a `terminal` content block) has been recorded — i.e., the Session has
241
+ * created and registered a terminal.
242
+ */
243
+ waitForTerminalCreated(timeoutMs = 5000): Promise<void> {
244
+ if (this.hasTerminalContentUpdate()) return Promise.resolve();
245
+ return new Promise<void>((resolve, reject) => {
246
+ const waiter = (): void => {
247
+ cleanup();
248
+ resolve();
249
+ };
250
+ const timer = setTimeout(() => {
251
+ cleanup();
252
+ reject(new Error('waitForTerminalCreated timed out'));
253
+ }, timeoutMs);
254
+ const cleanup = (): void => {
255
+ clearTimeout(timer);
256
+ this.terminalContentWaiters.delete(waiter);
257
+ };
258
+ this.terminalContentWaiters.add(waiter);
259
+ if (this.hasTerminalContentUpdate()) {
260
+ waiter();
261
+ }
262
+ });
263
+ }
264
+
265
+ private hasTerminalContentUpdate(): boolean {
266
+ return this.messages.some(
267
+ (m) =>
268
+ m.kind === 'sessionUpdate' &&
269
+ m.update.sessionUpdate === 'tool_call_update' &&
270
+ (m.update.content ?? []).some((c) => c.type === 'terminal'),
271
+ );
272
+ }
273
+
274
+ /**
275
+ * Arms sessionUpdate to REJECT starting with the (0-based) `afterCount`-th
276
+ * call: the first `afterCount` notifications are still recorded/delivered
277
+ * normally, then every subsequent call throws `error`. Used to simulate a
278
+ * dead/failing transport mid history-replay (issue #1604 FINDING A) so strict
279
+ * streamHistory delivery and loadSession cleanup can be asserted. `afterCount:
280
+ * 0` fails the very first update.
281
+ */
282
+ failSessionUpdateAfter(afterCount: number, error: Error): void {
283
+ this.sessionUpdateFailAfter = afterCount;
284
+ this.sessionUpdateError = error;
285
+ }
286
+
287
+ /**
288
+ * Disarms {@link failSessionUpdateAfter} and resets the call counter so a
289
+ * SUBSEQUENT operation on the same connection delivers normally — used to
290
+ * simulate a transport that has recovered before a retry load (issue #1604
291
+ * FINDING A partial-failure test).
292
+ */
293
+ clearSessionUpdateFailure(): void {
294
+ this.sessionUpdateFailAfter = null;
295
+ this.sessionUpdateError = null;
296
+ this.sessionUpdateCalls = 0;
297
+ }
298
+
299
+ clearSessionInfoUpdates(): void {
300
+ const retained = this.messages.filter(
301
+ (message) =>
302
+ message.kind !== 'sessionUpdate' ||
303
+ message.update.sessionUpdate !== 'session_info_update',
304
+ );
305
+ this.messages.splice(0, this.messages.length, ...retained);
306
+ }
307
+
87
308
  private gatedDeferred: {
88
309
  resolve: (o: acp.RequestPermissionOutcome) => void;
89
310
  promise: Promise<acp.RequestPermissionOutcome>;
@@ -125,7 +346,24 @@ export class RecordingConnection {
125
346
 
126
347
  sessionUpdate: Mock = vi.fn(
127
348
  async (params: acp.SessionNotification): Promise<void> => {
349
+ const shouldFail =
350
+ this.sessionUpdateFailAfter !== null &&
351
+ this.sessionUpdateCalls >= this.sessionUpdateFailAfter;
352
+ this.sessionUpdateCalls++;
353
+ if (shouldFail) {
354
+ throw (
355
+ this.sessionUpdateError ??
356
+ new Error('sessionUpdate transport failure')
357
+ );
358
+ }
128
359
  this.messages.push({ kind: 'sessionUpdate', update: params.update });
360
+ if (this.hasTerminalContentUpdate()) {
361
+ const waiters = [...this.terminalContentWaiters];
362
+ this.terminalContentWaiters.clear();
363
+ for (const resolve of waiters) {
364
+ resolve();
365
+ }
366
+ }
129
367
  },
130
368
  );
131
369
 
@@ -161,10 +399,111 @@ export class RecordingConnection {
161
399
  },
162
400
  );
163
401
 
402
+ createTerminal: Mock = vi.fn(
403
+ async (params: acp.CreateTerminalRequest): Promise<FakeTerminalHandle> => {
404
+ const call = {
405
+ command: params.command,
406
+ cwd: params.cwd,
407
+ sessionId: params.sessionId,
408
+ ...(params.args !== undefined ? { args: params.args } : {}),
409
+ ...(params.env !== undefined ? { env: params.env } : {}),
410
+ ...(params.outputByteLimit !== undefined
411
+ ? { outputByteLimit: params.outputByteLimit }
412
+ : {}),
413
+ };
414
+ this.createTerminalCalls.push(call);
415
+ const waiters = [...this.terminalCreationWaiters];
416
+ this.terminalCreationWaiters.clear();
417
+ for (const resolve of waiters) {
418
+ resolve();
419
+ }
420
+ const terminalId = `terminal-${this.createTerminalCalls.length}`;
421
+ return this.buildFakeTerminalHandle(terminalId);
422
+ },
423
+ );
424
+
425
+ private buildFakeTerminalHandle(terminalId: string): FakeTerminalHandle {
426
+ const handle = {
427
+ id: terminalId,
428
+ currentOutput: vi.fn(async () => ({
429
+ output: this.terminalOutput,
430
+ exitStatus: null,
431
+ truncated: false,
432
+ })),
433
+ waitForExit: vi.fn(async () => {
434
+ if (this.terminalExitDelayed) {
435
+ await new Promise<void>((resolve) => {
436
+ const existing = this.terminalExitResolvers.get(terminalId);
437
+ if (existing !== undefined) {
438
+ existing.push(resolve);
439
+ } else {
440
+ this.terminalExitResolvers.set(terminalId, [resolve]);
441
+ }
442
+ });
443
+ }
444
+ return { exitCode: 0, signal: null };
445
+ }),
446
+ kill: vi.fn(async () => {
447
+ this.killCalls += 1;
448
+ this.resolveDelayedTerminalExit(terminalId);
449
+ return {};
450
+ }),
451
+ release: vi.fn(async () => {
452
+ this.releaseCalls += 1;
453
+ this.resolveDelayedTerminalExit(terminalId);
454
+ return {};
455
+ }),
456
+ };
457
+ return handle;
458
+ }
459
+
460
+ /**
461
+ * Returns content-focused session updates, excluding infrastructure
462
+ * notifications (`available_commands_update` and `session_info_update`).
463
+ * Use {@link sessionInfoUpdates} for title/updatedAt assertions and
464
+ * {@link availableCommandUpdates} for command-registry assertions.
465
+ */
164
466
  onlySessionUpdates(): acp.SessionUpdate[] {
165
467
  return this.messages
166
468
  .filter((m) => m.kind === 'sessionUpdate')
167
- .map((m) => (m as { update: acp.SessionUpdate }).update);
469
+ .map((m) => (m as { update: acp.SessionUpdate }).update)
470
+ .filter(
471
+ (update) =>
472
+ update.sessionUpdate !== 'available_commands_update' &&
473
+ update.sessionUpdate !== 'session_info_update',
474
+ );
475
+ }
476
+
477
+ sessionInfoUpdates(): Array<
478
+ Extract<acp.SessionUpdate, { sessionUpdate: 'session_info_update' }>
479
+ > {
480
+ return this.messages
481
+ .filter((m) => m.kind === 'sessionUpdate')
482
+ .map((m) => (m as { update: acp.SessionUpdate }).update)
483
+ .filter(
484
+ (
485
+ update,
486
+ ): update is Extract<
487
+ acp.SessionUpdate,
488
+ { sessionUpdate: 'session_info_update' }
489
+ > => update.sessionUpdate === 'session_info_update',
490
+ );
491
+ }
492
+
493
+ availableCommandUpdates(): Array<
494
+ Extract<acp.SessionUpdate, { sessionUpdate: 'available_commands_update' }>
495
+ > {
496
+ return this.messages
497
+ .filter((m) => m.kind === 'sessionUpdate')
498
+ .map((m) => (m as { update: acp.SessionUpdate }).update)
499
+ .filter(
500
+ (
501
+ update,
502
+ ): update is Extract<
503
+ acp.SessionUpdate,
504
+ { sessionUpdate: 'available_commands_update' }
505
+ > => update.sessionUpdate === 'available_commands_update',
506
+ );
168
507
  }
169
508
 
170
509
  sessionUpdateKinds(): string[] {
@@ -179,6 +518,7 @@ export function buildMinimalConfig(): Config {
179
518
  getApprovalMode: () => 'default' as ApprovalMode,
180
519
  setApprovalMode: () => {},
181
520
  getTargetDir: () => '/project',
521
+ getProjectRoot: () => '/project',
182
522
  getFileService: () => ({ shouldIgnoreFile: () => false }),
183
523
  getFileFilteringOptions: () => ({
184
524
  respectGitIgnore: true,
@@ -187,6 +527,11 @@ export function buildMinimalConfig(): Config {
187
527
  getEnableRecursiveFileSearch: () => false,
188
528
  getFileSystemService: () => ({ readTextFile: async () => '' }),
189
529
  getMaxSessionTurns: () => 50,
530
+ // usage_update path (issue #1607): sendUsageUpdate resolves the context
531
+ // window via getTokenLimitForConfiguredContext(model, config).
532
+ getModel: () => 'test-model',
533
+ getContentGeneratorConfig: () => undefined,
534
+ getSessionRecordingService: () => undefined,
190
535
  } as unknown as Config;
191
536
  }
192
537
 
@@ -24,7 +24,11 @@ type Dict = Readonly<Record<string, unknown>>;
24
24
  export async function emitToolCallStart(
25
25
  call: AgentToolCall,
26
26
  sendUpdate: SendUpdateFn,
27
+ registeredKind?: string,
27
28
  ): Promise<void> {
29
+ // rawInput carries the tool arguments for parity with the replay start shape
30
+ // (zed-session-replay.ts buildToolCallStart) and because ACP recommends it on
31
+ // tool_call for client-side debugging (conformance tooling flags its absence).
28
32
  await sendUpdate({
29
33
  sessionUpdate: 'tool_call',
30
34
  toolCallId: call.id,
@@ -32,13 +36,15 @@ export async function emitToolCallStart(
32
36
  title: call.name,
33
37
  content: [],
34
38
  locations: buildToolLocations(call.args),
35
- kind: inferToolKind(call.name),
39
+ kind: resolveToolKind(call.name, registeredKind),
40
+ rawInput: call.args,
36
41
  });
37
42
  }
38
43
 
39
44
  export async function emitToolStatus(
40
45
  update: ToolUpdate,
41
46
  sendUpdate: SendUpdateFn,
47
+ registeredKind?: string,
42
48
  ): Promise<void> {
43
49
  const status = mapToolUpdateStatus(update.status);
44
50
  if (status === null) {
@@ -50,12 +56,14 @@ export async function emitToolStatus(
50
56
  toolCallId: update.id,
51
57
  status,
52
58
  content: text ? textContent(text) : [],
59
+ kind: resolveToolKind(update.name, registeredKind),
53
60
  });
54
61
  }
55
62
 
56
63
  export async function emitToolResult(
57
64
  result: AgentToolResult,
58
65
  sendUpdate: SendUpdateFn,
66
+ registeredKind?: string,
59
67
  ): Promise<void> {
60
68
  const isError = result.isError === true;
61
69
  const content = isError
@@ -66,6 +74,7 @@ export async function emitToolResult(
66
74
  toolCallId: result.id,
67
75
  status: isError ? 'failed' : 'completed',
68
76
  content,
77
+ kind: resolveToolKind(result.name, registeredKind),
69
78
  });
70
79
  }
71
80
 
@@ -85,6 +94,7 @@ export async function requestToolConfirmation(
85
94
  name: string,
86
95
  details: unknown,
87
96
  connection: acp.AgentSideConnection,
97
+ registeredKind?: string,
88
98
  ): Promise<PermissionRoundTripResult> {
89
99
  const confirmationDetails = coerceConfirmationDetails(details);
90
100
  const params: acp.RequestPermissionRequest = {
@@ -99,7 +109,7 @@ export async function requestToolConfirmation(
99
109
  title: confirmationDetails?.title ?? name,
100
110
  content: buildConfirmationContent(confirmationDetails),
101
111
  locations: buildConfirmationLocations(confirmationDetails),
102
- kind: inferToolKind(name),
112
+ kind: resolveToolKind(name, registeredKind),
103
113
  },
104
114
  };
105
115
  return parsePermissionOutcome(await connection.requestPermission(params));
@@ -231,7 +241,7 @@ function buildConfirmationLocations(
231
241
  return path === undefined ? [] : [buildLocation(path)];
232
242
  }
233
243
 
234
- function buildToolLocations(args: Dict): acp.ToolCallLocation[] {
244
+ export function buildToolLocations(args: Dict): acp.ToolCallLocation[] {
235
245
  const paths = [
236
246
  firstString(args, [
237
247
  'absolute_path',
@@ -319,7 +329,7 @@ function mapToolUpdateStatus(
319
329
  }
320
330
  }
321
331
 
322
- const TOOL_KIND_BY_NAME = new Map<string, acp.ToolKind>([
332
+ export const TOOL_KIND_BY_NAME: ReadonlyMap<string, acp.ToolKind> = new Map([
323
333
  ...[
324
334
  'read_file',
325
335
  'read_line_range',
@@ -348,12 +358,44 @@ const TOOL_KIND_BY_NAME = new Map<string, acp.ToolKind>([
348
358
  ...['run_shell_command', 'execute_command', 'exec'].map(
349
359
  (name) => [name, 'execute'] as const,
350
360
  ),
361
+ ...['direct_web_fetch', 'exa_web_search', 'web_fetch', 'web_search'].map(
362
+ (name) => [name, 'fetch'] as const,
363
+ ),
364
+ ...['todo_write', 'todo_read', 'todo_pause', 'save_memory'].map(
365
+ (name) => [name, 'think'] as const,
366
+ ),
351
367
  ]);
352
368
 
353
- function inferToolKind(name: string): acp.ToolKind | undefined {
369
+ export function inferToolKind(name: string): acp.ToolKind | undefined {
354
370
  return TOOL_KIND_BY_NAME.get(name);
355
371
  }
356
372
 
373
+ export const ACP_TOOL_KINDS: ReadonlySet<string> = new Set<acp.ToolKind>([
374
+ 'read',
375
+ 'edit',
376
+ 'delete',
377
+ 'move',
378
+ 'search',
379
+ 'execute',
380
+ 'think',
381
+ 'fetch',
382
+ 'switch_mode',
383
+ 'other',
384
+ ]);
385
+
386
+ export function toAcpToolKind(kind: string | undefined): acp.ToolKind {
387
+ return kind !== undefined && ACP_TOOL_KINDS.has(kind)
388
+ ? (kind as acp.ToolKind)
389
+ : 'other';
390
+ }
391
+
392
+ function resolveToolKind(
393
+ name: string,
394
+ registeredKind: string | undefined,
395
+ ): acp.ToolKind {
396
+ return toAcpToolKind(registeredKind ?? inferToolKind(name));
397
+ }
398
+
357
399
  function asRecord(value: unknown): Dict | null {
358
400
  return value !== null && typeof value === 'object' ? (value as Dict) : null;
359
401
  }