@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.
- package/package.json +13 -13
- package/src/commands/mcp/add.ts +8 -4
- package/src/config/cliArgParser.ts +2 -0
- package/src/config/config.ts +2 -0
- package/src/config/configBuilder.ts +3 -0
- package/src/config/configError.ts +23 -0
- package/src/config/extensions/extensionEnablement.ts +62 -1
- package/src/config/interactiveContext.ts +2 -1
- package/src/config/intermediateConfig.ts +4 -0
- package/src/config/keyBindings.ts +29 -3
- package/src/config/policy.ts +7 -1
- package/src/config/postConfigRuntime.ts +20 -14
- package/src/config/settings-schema/schema-ui.ts +9 -0
- package/src/config/settingsLoader.ts +21 -17
- package/src/config/settingsSchema.ts +2 -2
- package/src/config/trustedFolders.ts +275 -58
- package/src/config/yargsOptions.ts +7 -0
- package/src/generated/git-commit.json +1 -1
- package/src/nonInteractiveCli.ts +1 -0
- package/src/nonInteractiveCliSupport.ts +171 -51
- package/src/runtime/interactiveToolScheduler.ts +6 -6
- package/src/services/BuiltinCommandLoader.ts +33 -25
- package/src/services/FileCommandLoader.ts +34 -7
- package/src/test-utils/async.ts +20 -0
- package/src/test-utils/render.tsx +10 -1
- package/src/ui/App.tsx +17 -10
- package/src/ui/AppContainerRuntime.tsx +7 -9
- package/src/ui/cliUiRuntime.ts +28 -0
- package/src/ui/commands/authCommand.ts +240 -3
- package/src/ui/commands/compressCommand.ts +9 -1
- package/src/ui/commands/directoryCommand.tsx +1 -1
- package/src/ui/commands/logoutCommand.ts +2 -2
- package/src/ui/commands/permissionsCommand.ts +77 -20
- package/src/ui/commands/policiesCommand.ts +6 -1
- package/src/ui/commands/quotaCommand.ts +501 -0
- package/src/ui/commands/statsCommand.ts +2 -2
- package/src/ui/commands/statsQuota.ts +1 -1
- package/src/ui/commands/subagentCommand.ts +59 -27
- package/src/ui/components/AuthDialog.tsx +3 -3
- package/src/ui/components/Composer.tsx +1 -0
- package/src/ui/components/DialogManager.tsx +9 -2
- package/src/ui/components/FolderTrustDialog.tsx +71 -60
- package/src/ui/components/PermissionsModifyTrustDialog.tsx +211 -146
- package/src/ui/components/ProfileCreateWizard/constants.ts +13 -1
- package/src/ui/components/QueuedMessagesPanel.tsx +388 -0
- package/src/ui/components/inputPromptHooks.ts +2 -0
- package/src/ui/components/inputPromptKeyHandlers.ts +14 -0
- package/src/ui/components/inputPromptTypes.ts +6 -0
- package/src/ui/components/messages/AiMessage.tsx +24 -12
- package/src/ui/components/messages/ToolMessage.tsx +26 -40
- package/src/ui/containers/AppContainer/builders/buildUIActions.ts +3 -1
- package/src/ui/containers/AppContainer/builders/buildUIState.ts +5 -4
- package/src/ui/containers/AppContainer/hooks/useAppBootstrap.ts +3 -33
- package/src/ui/containers/AppContainer/hooks/useAppDialogs.ts +2 -14
- package/src/ui/containers/AppContainer/hooks/useAppInput.ts +31 -25
- package/src/ui/containers/AppContainer/hooks/useAppLayout.ts +90 -60
- package/src/ui/containers/AppContainer/hooks/useDisplayPreferences.ts +10 -0
- package/src/ui/containers/AppContainer/hooks/useInputHandling.ts +3 -17
- package/src/ui/containers/AppContainer/hooks/useKeybindings.ts +51 -38
- package/src/ui/containers/AppContainer/hooks/useSteer.ts +51 -0
- package/src/ui/contexts/KeypressContext.tsx +13 -2
- package/src/ui/contexts/ShellCommandDisplayContext.tsx +103 -0
- package/src/ui/contexts/TodoContext.tsx +0 -13
- package/src/ui/contexts/TodoProvider.tsx +1 -34
- package/src/ui/contexts/UIActionsContext.tsx +2 -1
- package/src/ui/contexts/UIStateContext.tsx +3 -2
- package/src/ui/hooks/agentStream/streamUtils.ts +0 -24
- package/src/ui/hooks/agentStream/thoughtState.ts +110 -30
- package/src/ui/hooks/agentStream/types.ts +1 -0
- package/src/ui/hooks/agentStream/useAgentEventStream.ts +8 -17
- package/src/ui/hooks/agentStream/useAgentStream.ts +2 -2
- package/src/ui/hooks/agentStream/useAgentStreamLifecycle.ts +4 -5
- package/src/ui/hooks/agentStream/useAgentStreamOrchestration.ts +12 -5
- package/src/ui/hooks/agentStream/useQueuedSubmissions.ts +99 -0
- package/src/ui/hooks/agentStream/useStreamEventHandlers.ts +4 -13
- package/src/ui/hooks/agentStream/useStreamState.ts +60 -41
- package/src/ui/hooks/agentStream/useSubmitQuery.ts +284 -59
- package/src/ui/hooks/slashCommandProcessorSupport.ts +4 -0
- package/src/ui/hooks/useAgentStream-test-helpers.ts +1 -0
- package/src/ui/hooks/useAutoAcceptIndicator.ts +13 -0
- package/src/ui/hooks/useFolderTrust.ts +118 -81
- package/src/ui/hooks/useGitBranchName.ts +79 -33
- package/src/ui/hooks/useIdeTrustListener.ts +5 -19
- package/src/ui/hooks/usePermissionsModifyTrust.ts +223 -85
- package/src/ui/hooks/useReactToolScheduler.ts +13 -17
- package/src/ui/hooks/useTodoPausePreserver.ts +0 -55
- package/src/ui/layouts/DefaultAppLayout.tsx +2 -0
- package/src/ui/layouts/DefaultAppLayoutHelpers.tsx +37 -162
- package/src/ui/layouts/InlineContent.tsx +198 -0
- package/src/ui/trustDialogHelpers.ts +190 -0
- package/src/utils/cleanup-state.ts +115 -0
- package/src/utils/cleanup.ts +32 -34
- package/src/utils/sandbox-containers.ts +183 -53
- package/src/utils/sandbox-exec.ts +9 -3
- package/src/zed-integration/acp-terminal-shell-host.ts +112 -0
- package/src/zed-integration/acp-types.ts +35 -0
- package/src/zed-integration/fileSystemService.ts +1 -1
- package/src/zed-integration/runZedIntegration.ts +142 -0
- package/src/zed-integration/zed-agent-event-handler.ts +179 -0
- package/src/zed-integration/zed-agent-setup.ts +43 -0
- package/src/zed-integration/zed-command-registry.ts +167 -0
- package/src/zed-integration/zed-config-options.ts +280 -0
- package/src/zed-integration/zed-content-utils.ts +2 -2
- package/src/zed-integration/zed-helpers.ts +132 -0
- package/src/zed-integration/zed-initialize.ts +64 -0
- package/src/zed-integration/zed-plan-update.ts +20 -0
- package/src/zed-integration/zed-prompt-command.ts +77 -0
- package/src/zed-integration/zed-session-config.ts +119 -0
- package/src/zed-integration/zed-session-errors.ts +337 -0
- package/src/zed-integration/zed-session-events.ts +216 -0
- package/src/zed-integration/zed-session-info.ts +255 -0
- package/src/zed-integration/zed-session-lifecycle.ts +186 -0
- package/src/zed-integration/zed-session-listing.ts +153 -0
- package/src/zed-integration/zed-session-loader.ts +193 -0
- package/src/zed-integration/zed-session-pagination.ts +240 -0
- package/src/zed-integration/zed-session-replay.ts +564 -0
- package/src/zed-integration/zed-stream-batcher.ts +236 -0
- package/src/zed-integration/zed-terminal-manager.ts +416 -0
- package/src/zed-integration/zed-terminal-setup.ts +72 -0
- package/src/zed-integration/zed-test-helpers.ts +349 -4
- package/src/zed-integration/zed-tool-handler.ts +47 -5
- package/src/zed-integration/zedIntegration.ts +538 -587
- package/src/services/todo-continuation/todoContinuationService.ts +0 -610
- package/src/ui/containers/AppContainer/hooks/useIdeRestartHotkey.ts +0 -33
- package/src/ui/containers/AppContainer/hooks/useTodoContinuationFlow.ts +0 -160
- 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(
|
|
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
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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
|
}
|