pi-extensions-tool-display 0.1.1 → 0.2.0
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/README.md +13 -3
- package/README.zh-CN.md +14 -4
- package/config.example.json +40 -0
- package/index.ts +6 -0
- package/package.json +16 -5
- package/src/agent-dir.ts +32 -0
- package/src/ansi-utils.ts +116 -0
- package/src/bash-display.ts +191 -0
- package/src/capabilities.ts +85 -0
- package/src/config-modal.ts +540 -0
- package/src/config-store.ts +303 -0
- package/src/debug-logger.ts +150 -0
- package/src/diff-presentation.ts +72 -0
- package/src/diff-renderer.ts +2517 -0
- package/src/disposable.ts +34 -0
- package/src/extension-lifecycle.ts +14 -0
- package/src/index.ts +126 -86
- package/src/line-width-safety.ts +93 -0
- package/src/modal-icons.ts +57 -0
- package/src/pending-diff-preview.ts +371 -0
- package/src/presets.ts +108 -0
- package/src/render-utils.ts +186 -0
- package/src/result-render-middleware.ts +98 -0
- package/src/settings-inspector-modal.ts +475 -0
- package/src/thinking-label.ts +343 -0
- package/src/tool-metadata.ts +139 -0
- package/src/tool-overrides.ts +2237 -0
- package/src/types.ts +105 -0
- package/src/user-message-box-markdown.ts +64 -0
- package/src/user-message-box-native.ts +66 -0
- package/src/user-message-box-patch.ts +72 -0
- package/src/user-message-box-renderer.ts +419 -0
- package/src/user-message-box-utils.ts +106 -0
- package/src/write-display-utils.ts +31 -0
- package/src/zellij-modal.ts +1027 -0
- package/tool-display-api-consumer.d.ts +68 -0
- package/tool-display-api-consumer.js +82 -0
- package/tsconfig.json +1 -0
|
@@ -0,0 +1,2237 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type {
|
|
4
|
+
BashToolDetails,
|
|
5
|
+
EditToolDetails,
|
|
6
|
+
ExtensionAPI,
|
|
7
|
+
ExtensionContext,
|
|
8
|
+
FindToolDetails,
|
|
9
|
+
GrepToolDetails,
|
|
10
|
+
LsToolDetails,
|
|
11
|
+
ReadToolDetails,
|
|
12
|
+
ToolDefinition,
|
|
13
|
+
ToolRenderResultOptions,
|
|
14
|
+
} from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import {
|
|
16
|
+
createBashTool,
|
|
17
|
+
createEditTool,
|
|
18
|
+
createFindTool,
|
|
19
|
+
createGrepTool,
|
|
20
|
+
createLsTool,
|
|
21
|
+
createReadTool,
|
|
22
|
+
createWriteTool,
|
|
23
|
+
formatSize,
|
|
24
|
+
} from "@earendil-works/pi-coding-agent";
|
|
25
|
+
import { Container, Spacer, Text, type Component } from "@earendil-works/pi-tui";
|
|
26
|
+
import { resolvePiAgentDir } from "./agent-dir.js";
|
|
27
|
+
import { renderBashCall } from "./bash-display.js";
|
|
28
|
+
import { logToolDisplayDebug } from "./debug-logger.js";
|
|
29
|
+
import { registerCleanup } from "./disposable.js";
|
|
30
|
+
import {
|
|
31
|
+
compactOutputLines,
|
|
32
|
+
countNonEmptyLines,
|
|
33
|
+
extractTextOutput,
|
|
34
|
+
isLikelyQuietCommand,
|
|
35
|
+
pluralize,
|
|
36
|
+
previewLines,
|
|
37
|
+
sanitizeAnsiForThemedOutput,
|
|
38
|
+
shortenPath,
|
|
39
|
+
splitLines,
|
|
40
|
+
} from "./render-utils.js";
|
|
41
|
+
import { renderEditDiffResult, renderWriteDiffResult } from "./diff-renderer.js";
|
|
42
|
+
import {
|
|
43
|
+
buildPendingEditPreviewData,
|
|
44
|
+
buildPendingWritePreviewData,
|
|
45
|
+
readWorkspaceUtf8File,
|
|
46
|
+
type PendingDiffPreviewData,
|
|
47
|
+
} from "./pending-diff-preview.js";
|
|
48
|
+
import {
|
|
49
|
+
buildPromptSnippetFromDescription,
|
|
50
|
+
extractPromptMetadata,
|
|
51
|
+
getTextField,
|
|
52
|
+
isMcpToolCandidate,
|
|
53
|
+
MCP_PROXY_PROMPT_GUIDELINES,
|
|
54
|
+
MCP_PROXY_PROMPT_SNIPPET,
|
|
55
|
+
toRecord,
|
|
56
|
+
} from "./tool-metadata.js";
|
|
57
|
+
import { BUILT_IN_TOOL_OVERRIDE_NAMES } from "./types.js";
|
|
58
|
+
import { toCustomToolOverrideKind, toCustomToolOutputMode, normalizeCustomToolOverrideEntry } from "./config-store.js";
|
|
59
|
+
import type {
|
|
60
|
+
BuiltInToolOverrideName,
|
|
61
|
+
CustomToolOverrideConfig,
|
|
62
|
+
ToolDisplayConfig,
|
|
63
|
+
} from "./types.js";
|
|
64
|
+
import {
|
|
65
|
+
countWriteContentLines,
|
|
66
|
+
getWriteContentSizeBytes,
|
|
67
|
+
shouldRenderWriteCallSummary,
|
|
68
|
+
} from "./write-display-utils.js";
|
|
69
|
+
|
|
70
|
+
interface BuiltInTools {
|
|
71
|
+
read: ReturnType<typeof createReadTool>;
|
|
72
|
+
grep: ReturnType<typeof createGrepTool>;
|
|
73
|
+
find: ReturnType<typeof createFindTool>;
|
|
74
|
+
ls: ReturnType<typeof createLsTool>;
|
|
75
|
+
bash: ReturnType<typeof createBashTool>;
|
|
76
|
+
edit: ReturnType<typeof createEditTool>;
|
|
77
|
+
write: ReturnType<typeof createWriteTool>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
type ConfigGetter = () => ToolDisplayConfig;
|
|
81
|
+
|
|
82
|
+
interface RuntimeToolDefinition {
|
|
83
|
+
name?: string;
|
|
84
|
+
label?: string;
|
|
85
|
+
description?: string;
|
|
86
|
+
parameters?: unknown;
|
|
87
|
+
prepareArguments?: unknown;
|
|
88
|
+
renderCall?: (args: Record<string, unknown>, theme: RenderTheme, context?: ToolRenderContextLike) => unknown;
|
|
89
|
+
renderResult?: (result: Record<string, unknown>, options: ToolRenderResultOptions, theme: RenderTheme, context?: ToolRenderContextLike) => unknown;
|
|
90
|
+
execute?: (toolCallId: string, params: Record<string, unknown>, signal: AbortSignal | undefined, onUpdate: unknown, ctx: { cwd: string }) => Promise<unknown>;
|
|
91
|
+
renderShell?: unknown;
|
|
92
|
+
[key: string]: unknown;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
interface RenderTheme {
|
|
96
|
+
fg(color: string, text: string): string;
|
|
97
|
+
bg?(color: string, text: string): string;
|
|
98
|
+
bold(text: string): string;
|
|
99
|
+
getBgAnsi?(color: string): string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
interface RtkCompactionInfo {
|
|
103
|
+
applied: boolean;
|
|
104
|
+
techniques: string[];
|
|
105
|
+
truncated: boolean;
|
|
106
|
+
originalLineCount?: number;
|
|
107
|
+
compactedLineCount?: number;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
interface ToolRenderContextLike {
|
|
111
|
+
args?: unknown;
|
|
112
|
+
toolCallId?: string;
|
|
113
|
+
state?: unknown;
|
|
114
|
+
cwd?: string;
|
|
115
|
+
argsComplete?: boolean;
|
|
116
|
+
isError?: boolean;
|
|
117
|
+
isPartial?: boolean;
|
|
118
|
+
expanded?: boolean;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface WriteExecutionMeta {
|
|
122
|
+
previousContent?: string;
|
|
123
|
+
fileExistedBeforeWrite: boolean;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
interface PendingDiffPreviewState {
|
|
127
|
+
key?: string;
|
|
128
|
+
data?: PendingDiffPreviewData;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
interface PiSettingsShellConfig {
|
|
132
|
+
shellPath?: unknown;
|
|
133
|
+
shellCommandPrefix?: unknown;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
interface BashToolOverrideOptions {
|
|
137
|
+
shellPath?: string;
|
|
138
|
+
commandPrefix?: string;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const builtInToolCache = new Map<string, BuiltInTools>();
|
|
142
|
+
const RTK_COMPACTION_LABEL = "compacted by RTK";
|
|
143
|
+
export const WRITE_EXECUTION_META_LIMIT = 100;
|
|
144
|
+
const WRITE_EXECUTION_META_STATE_KEY = "__piToolDisplayWriteExecutionMeta";
|
|
145
|
+
const EDIT_PENDING_PREVIEW_STATE_KEY = "__piToolDisplayEditPendingPreview";
|
|
146
|
+
const WRITE_PENDING_PREVIEW_STATE_KEY = "__piToolDisplayWritePendingPreview";
|
|
147
|
+
|
|
148
|
+
const TOOL_DISPLAY_API_KEY = Symbol.for("pi-tool-display.api.v1");
|
|
149
|
+
const TOOL_DISPLAY_PENDING_DECORATIONS_KEY = Symbol.for("pi-tool-display.pendingDecorations.v1");
|
|
150
|
+
const TOOL_DISPLAY_PENDING_RESULT_MIDDLEWARES_KEY = Symbol.for("pi-tool-display.pendingResultRenderMiddlewares.v1");
|
|
151
|
+
const TOOL_DISPLAY_REGISTER_TOOL_INTERCEPTOR_KEY = Symbol.for("pi-tool-display.registerToolInterceptor.v1");
|
|
152
|
+
const TOOL_DISPLAY_DECORATED_PROPERTIES = [
|
|
153
|
+
"renderCall",
|
|
154
|
+
"renderResult",
|
|
155
|
+
"renderShell",
|
|
156
|
+
"label",
|
|
157
|
+
"description",
|
|
158
|
+
"promptSnippet",
|
|
159
|
+
"promptGuidelines",
|
|
160
|
+
"parameters",
|
|
161
|
+
"prepareArguments",
|
|
162
|
+
] as const;
|
|
163
|
+
|
|
164
|
+
type ToolDisplayKind = "read" | "edit" | "mcp" | "generic";
|
|
165
|
+
|
|
166
|
+
export interface ToolDisplayAdapter {
|
|
167
|
+
id?: string;
|
|
168
|
+
toolName?: string;
|
|
169
|
+
kind?: ToolDisplayKind;
|
|
170
|
+
overrideExistingRenderers?: boolean;
|
|
171
|
+
pathFields?: string[];
|
|
172
|
+
getPath?: (args: unknown) => string | undefined;
|
|
173
|
+
getEditLineCount?: (args: unknown) => number;
|
|
174
|
+
renderCall?: (args: unknown, theme: RenderTheme, context: ToolRenderContextLike) => unknown;
|
|
175
|
+
renderResult?: (result: unknown, options: ToolRenderResultOptions, theme: RenderTheme, context?: ToolRenderContextLike) => unknown;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export interface ToolDisplayApi {
|
|
179
|
+
version: 1;
|
|
180
|
+
decorateTool<T extends RuntimeToolDefinition>(tool: T, adapter?: ToolDisplayAdapter): T;
|
|
181
|
+
registerAdapter(adapter: ToolDisplayAdapter): string;
|
|
182
|
+
unregisterAdapter(id: string): boolean;
|
|
183
|
+
registerResultRenderMiddleware(registration: ToolResultRenderMiddlewareRegistration): string;
|
|
184
|
+
unregisterResultRenderMiddleware(id: string): boolean;
|
|
185
|
+
hasResultRenderMiddleware(id: string): boolean;
|
|
186
|
+
activateResultRenderPipeline(toolName: string): void;
|
|
187
|
+
isResultRenderPipelineActive(toolName: string): boolean;
|
|
188
|
+
renderResultWithMiddleware(
|
|
189
|
+
context: ToolResultRenderMiddlewareContext,
|
|
190
|
+
renderBase: () => unknown,
|
|
191
|
+
): unknown;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export interface ToolResultRenderMiddlewareContext {
|
|
195
|
+
toolName: string;
|
|
196
|
+
result: unknown;
|
|
197
|
+
options: ToolRenderResultOptions;
|
|
198
|
+
theme: RenderTheme;
|
|
199
|
+
renderContext?: ToolRenderContextLike;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export type ToolResultRenderMiddleware = (
|
|
203
|
+
context: ToolResultRenderMiddlewareContext,
|
|
204
|
+
next: () => unknown,
|
|
205
|
+
) => unknown;
|
|
206
|
+
|
|
207
|
+
export interface ToolResultRenderMiddlewareRegistration {
|
|
208
|
+
id?: string;
|
|
209
|
+
toolName: string;
|
|
210
|
+
middleware: ToolResultRenderMiddleware;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
interface PendingToolDisplayDecoration {
|
|
214
|
+
tool: RuntimeToolDefinition;
|
|
215
|
+
adapter?: ToolDisplayAdapter;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
interface PendingResultRenderMiddleware extends ToolResultRenderMiddlewareRegistration {
|
|
219
|
+
id: string;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
type DecoratedPropertyName = typeof TOOL_DISPLAY_DECORATED_PROPERTIES[number];
|
|
223
|
+
type ToolPropertyDescriptorSnapshot = Partial<Record<DecoratedPropertyName, PropertyDescriptor>>;
|
|
224
|
+
|
|
225
|
+
type GlobalWithToolDisplayApi = typeof globalThis & {
|
|
226
|
+
[TOOL_DISPLAY_API_KEY]?: ToolDisplayApi;
|
|
227
|
+
[TOOL_DISPLAY_PENDING_DECORATIONS_KEY]?: PendingToolDisplayDecoration[];
|
|
228
|
+
[TOOL_DISPLAY_PENDING_RESULT_MIDDLEWARES_KEY]?: PendingResultRenderMiddleware[];
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
type PiWithRegisterToolInterception = ExtensionAPI & {
|
|
232
|
+
[TOOL_DISPLAY_REGISTER_TOOL_INTERCEPTOR_KEY]?: {
|
|
233
|
+
original: ExtensionAPI["registerTool"];
|
|
234
|
+
wrapped: ExtensionAPI["registerTool"];
|
|
235
|
+
};
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const decoratedToolDescriptors = new WeakMap<RuntimeToolDefinition, ToolPropertyDescriptorSnapshot>();
|
|
239
|
+
const decoratedTools = new Set<RuntimeToolDefinition>();
|
|
240
|
+
|
|
241
|
+
function registerRuntimeTool(pi: ExtensionAPI, tool: RuntimeToolDefinition): void {
|
|
242
|
+
pi.registerTool(tool as unknown as ToolDefinition);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function captureToolPropertyDescriptors(
|
|
246
|
+
tool: RuntimeToolDefinition,
|
|
247
|
+
descriptorSnapshots: WeakMap<RuntimeToolDefinition, ToolPropertyDescriptorSnapshot>,
|
|
248
|
+
decoratedTools: Set<RuntimeToolDefinition>,
|
|
249
|
+
): void {
|
|
250
|
+
if (descriptorSnapshots.has(tool)) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const snapshot: ToolPropertyDescriptorSnapshot = {};
|
|
255
|
+
for (const property of TOOL_DISPLAY_DECORATED_PROPERTIES) {
|
|
256
|
+
const descriptor = Object.getOwnPropertyDescriptor(tool, property);
|
|
257
|
+
if (descriptor) {
|
|
258
|
+
snapshot[property] = descriptor;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
descriptorSnapshots.set(tool, snapshot);
|
|
262
|
+
decoratedTools.add(tool);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function restoreToolPropertyDescriptors(
|
|
266
|
+
descriptorSnapshots: WeakMap<RuntimeToolDefinition, ToolPropertyDescriptorSnapshot>,
|
|
267
|
+
decoratedTools: Set<RuntimeToolDefinition>,
|
|
268
|
+
): void {
|
|
269
|
+
for (const tool of decoratedTools) {
|
|
270
|
+
const snapshot = descriptorSnapshots.get(tool) ?? {};
|
|
271
|
+
for (const property of TOOL_DISPLAY_DECORATED_PROPERTIES) {
|
|
272
|
+
const descriptor = snapshot[property];
|
|
273
|
+
if (descriptor) {
|
|
274
|
+
Object.defineProperty(tool, property, descriptor);
|
|
275
|
+
} else {
|
|
276
|
+
delete tool[property];
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
descriptorSnapshots.delete(tool);
|
|
280
|
+
}
|
|
281
|
+
decoratedTools.clear();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
function getToolPrepareArguments(tool: unknown): unknown {
|
|
286
|
+
const prepareArguments = toRecord(tool).prepareArguments;
|
|
287
|
+
return typeof prepareArguments === "function" ? prepareArguments : undefined;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function cloneToolParameters(parameters: unknown, seen = new WeakMap<object, unknown>()): unknown {
|
|
291
|
+
if (parameters === null || typeof parameters !== "object") {
|
|
292
|
+
return parameters;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (seen.has(parameters)) {
|
|
296
|
+
return seen.get(parameters);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const clone: object = Array.isArray(parameters)
|
|
300
|
+
? []
|
|
301
|
+
: Object.create(Object.getPrototypeOf(parameters) as object | null) as object;
|
|
302
|
+
seen.set(parameters, clone);
|
|
303
|
+
|
|
304
|
+
for (const key of Reflect.ownKeys(parameters)) {
|
|
305
|
+
const descriptor = Object.getOwnPropertyDescriptor(parameters, key);
|
|
306
|
+
if (!descriptor) {
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if ("value" in descriptor) {
|
|
311
|
+
descriptor.value = cloneToolParameters(descriptor.value as unknown, seen);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
Object.defineProperty(clone, key, descriptor);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return clone;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function clearBuiltInToolCache(): void {
|
|
321
|
+
builtInToolCache.clear();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function getStringSetting(value: unknown): string | undefined {
|
|
325
|
+
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function loadBashToolOverrideOptions(): BashToolOverrideOptions {
|
|
329
|
+
const settingsPath = join(resolvePiAgentDir(), "settings.json");
|
|
330
|
+
if (!existsSync(settingsPath)) {
|
|
331
|
+
return {};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
const rawSettings = JSON.parse(readFileSync(settingsPath, "utf-8")) as PiSettingsShellConfig;
|
|
336
|
+
return {
|
|
337
|
+
shellPath: getStringSetting(rawSettings.shellPath),
|
|
338
|
+
commandPrefix: getStringSetting(rawSettings.shellCommandPrefix),
|
|
339
|
+
};
|
|
340
|
+
} catch (error) {
|
|
341
|
+
logToolDisplayDebug("Failed to read Pi settings for bash tool overrides.", error);
|
|
342
|
+
return {};
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function getBuiltInTools(cwd: string): BuiltInTools {
|
|
347
|
+
let tools = builtInToolCache.get(cwd);
|
|
348
|
+
if (!tools) {
|
|
349
|
+
tools = createLazyBuiltInTools(cwd);
|
|
350
|
+
builtInToolCache.set(cwd, tools);
|
|
351
|
+
}
|
|
352
|
+
return tools;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function createLazyBuiltInTools(cwd: string): BuiltInTools {
|
|
356
|
+
const cache = new Map<string, unknown>();
|
|
357
|
+
const get = <K extends keyof BuiltInTools>(name: K, factory: () => BuiltInTools[K]): BuiltInTools[K] => {
|
|
358
|
+
if (!cache.has(name)) {
|
|
359
|
+
cache.set(name, factory());
|
|
360
|
+
}
|
|
361
|
+
return cache.get(name) as BuiltInTools[K];
|
|
362
|
+
};
|
|
363
|
+
return {
|
|
364
|
+
get read() { return get("read", () => createReadTool(cwd)); },
|
|
365
|
+
get grep() { return get("grep", () => createGrepTool(cwd)); },
|
|
366
|
+
get find() { return get("find", () => createFindTool(cwd)); },
|
|
367
|
+
get ls() { return get("ls", () => createLsTool(cwd)); },
|
|
368
|
+
get bash() { return get("bash", () => createBashTool(cwd, loadBashToolOverrideOptions())); },
|
|
369
|
+
get edit() { return get("edit", () => createEditTool(cwd)); },
|
|
370
|
+
get write() { return get("write", () => createWriteTool(cwd)); },
|
|
371
|
+
} as BuiltInTools;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function createLazyToolRecord<T>(
|
|
375
|
+
bootstrapTools: BuiltInTools,
|
|
376
|
+
factory: (tool: BuiltInTools[keyof BuiltInTools]) => T,
|
|
377
|
+
): Record<keyof BuiltInTools, T> {
|
|
378
|
+
const cache = new Map<string, unknown>();
|
|
379
|
+
const get = (name: keyof BuiltInTools): T => {
|
|
380
|
+
if (!cache.has(name)) {
|
|
381
|
+
cache.set(name, factory(bootstrapTools[name]));
|
|
382
|
+
}
|
|
383
|
+
return cache.get(name) as T;
|
|
384
|
+
};
|
|
385
|
+
return {
|
|
386
|
+
get read() { return get("read"); },
|
|
387
|
+
get grep() { return get("grep"); },
|
|
388
|
+
get find() { return get("find"); },
|
|
389
|
+
get ls() { return get("ls"); },
|
|
390
|
+
get bash() { return get("bash"); },
|
|
391
|
+
get edit() { return get("edit"); },
|
|
392
|
+
get write() { return get("write"); },
|
|
393
|
+
} as Record<keyof BuiltInTools, T>;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function createLazyPromptMetadata(bootstrapTools: BuiltInTools): Record<keyof BuiltInTools, ReturnType<typeof extractPromptMetadata>> {
|
|
397
|
+
return createLazyToolRecord(bootstrapTools, extractPromptMetadata);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function createLazyClonedParameters(bootstrapTools: BuiltInTools): Record<keyof BuiltInTools, unknown> {
|
|
401
|
+
return createLazyToolRecord(bootstrapTools, (tool) => cloneToolParameters(tool.parameters));
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function captureExistingWriteContent(
|
|
405
|
+
cwd: string,
|
|
406
|
+
rawPath: unknown,
|
|
407
|
+
): { existed: boolean; content?: string } {
|
|
408
|
+
if (typeof rawPath !== "string" || !rawPath.trim()) {
|
|
409
|
+
return { existed: false };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const existing = readWorkspaceUtf8File(cwd, rawPath);
|
|
413
|
+
return {
|
|
414
|
+
existed: existing.exists,
|
|
415
|
+
content: existing.content,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function formatExpandHint(theme: RenderTheme): string {
|
|
420
|
+
return theme.fg("muted", " • Ctrl+O to expand");
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function formatTruncationHint(remaining: number, expanded: boolean, theme: RenderTheme): string {
|
|
424
|
+
if (remaining <= 0) {
|
|
425
|
+
return "";
|
|
426
|
+
}
|
|
427
|
+
const hint = expanded ? "" : " • Ctrl+O to expand";
|
|
428
|
+
return `\n${theme.fg("muted", `... (${remaining} more ${pluralize(remaining, "line")}${hint})`)}`;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function buildPreviewText(
|
|
432
|
+
lines: string[],
|
|
433
|
+
maxLines: number,
|
|
434
|
+
theme: RenderTheme,
|
|
435
|
+
expanded: boolean,
|
|
436
|
+
): string {
|
|
437
|
+
if (lines.length === 0) {
|
|
438
|
+
return theme.fg("muted", "↳ (no output)");
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const { shown, remaining } = previewLines(lines, maxLines);
|
|
442
|
+
let text = shown
|
|
443
|
+
.map((line) => theme.fg("toolOutput", sanitizeAnsiForThemedOutput(line)))
|
|
444
|
+
.join("\n");
|
|
445
|
+
text += formatTruncationHint(remaining, expanded, theme);
|
|
446
|
+
return text;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function prepareOutputLines(
|
|
450
|
+
rawText: string,
|
|
451
|
+
options: ToolRenderResultOptions,
|
|
452
|
+
): string[] {
|
|
453
|
+
return compactOutputLines(splitLines(rawText), {
|
|
454
|
+
expanded: options.expanded,
|
|
455
|
+
maxCollapsedConsecutiveEmptyLines: 1,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function formatBashNoOutputLine(
|
|
460
|
+
command: string | undefined,
|
|
461
|
+
theme: RenderTheme,
|
|
462
|
+
): string {
|
|
463
|
+
if (isLikelyQuietCommand(command)) {
|
|
464
|
+
return theme.fg("muted", "↳ command completed (no output)");
|
|
465
|
+
}
|
|
466
|
+
return theme.fg("muted", "↳ (no output)");
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function truncationHint(
|
|
470
|
+
details: { truncation?: { truncated?: boolean } } | undefined,
|
|
471
|
+
): string {
|
|
472
|
+
return details?.truncation?.truncated ? " • truncated" : "";
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function countTextLines(value: unknown): number {
|
|
476
|
+
if (typeof value !== "string") {
|
|
477
|
+
return 0;
|
|
478
|
+
}
|
|
479
|
+
return splitLines(value).length;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function getStringField(value: unknown, field: string): string | undefined {
|
|
483
|
+
const raw = toRecord(value)[field];
|
|
484
|
+
return typeof raw === "string" ? raw : undefined;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function getNumericField(value: unknown, field: string): number | undefined {
|
|
488
|
+
const raw = toRecord(value)[field];
|
|
489
|
+
return typeof raw === "number" && Number.isFinite(raw) ? raw : undefined;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function getToolPathArg(value: unknown): string | undefined {
|
|
493
|
+
return getStringField(value, "file_path") ?? getStringField(value, "path");
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function getToolContentArg(value: unknown): string | undefined {
|
|
497
|
+
return getStringField(value, "content");
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function getEditPayloadLineCount(value: unknown): number {
|
|
501
|
+
const record = toRecord(value);
|
|
502
|
+
const lines = record.lines;
|
|
503
|
+
if (Array.isArray(lines)) {
|
|
504
|
+
return lines.filter((line): line is string => typeof line === "string").length;
|
|
505
|
+
}
|
|
506
|
+
if (typeof lines === "string") {
|
|
507
|
+
return countTextLines(lines);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return countTextLines(record.newText);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function getEditLineCount(value: unknown): number {
|
|
514
|
+
const record = toRecord(value);
|
|
515
|
+
const edits = Array.isArray(record.edits) ? record.edits as unknown[] : [];
|
|
516
|
+
if (edits.length > 0) {
|
|
517
|
+
return edits.reduce<number>((total, edit) => {
|
|
518
|
+
return total + getEditPayloadLineCount(edit);
|
|
519
|
+
}, 0);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return getEditPayloadLineCount(record);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function isToolError(
|
|
526
|
+
result: unknown,
|
|
527
|
+
context?: ToolRenderContextLike,
|
|
528
|
+
): boolean {
|
|
529
|
+
return context?.isError === true || toRecord(result).isError === true;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function toStateRecord(value: unknown): Record<string, unknown> | undefined {
|
|
533
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
534
|
+
return undefined;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
return value as Record<string, unknown>;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
export function recordWriteExecutionMeta(
|
|
541
|
+
pendingMetaByToolCallId: Map<string, WriteExecutionMeta>,
|
|
542
|
+
toolCallId: string,
|
|
543
|
+
meta: WriteExecutionMeta,
|
|
544
|
+
): void {
|
|
545
|
+
pendingMetaByToolCallId.delete(toolCallId);
|
|
546
|
+
pendingMetaByToolCallId.set(toolCallId, meta);
|
|
547
|
+
|
|
548
|
+
while (pendingMetaByToolCallId.size > WRITE_EXECUTION_META_LIMIT) {
|
|
549
|
+
const oldestToolCallId: string | undefined = pendingMetaByToolCallId.keys().next().value as string | undefined;
|
|
550
|
+
if (oldestToolCallId === undefined) {
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
pendingMetaByToolCallId.delete(oldestToolCallId);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
export function clearWriteExecutionMeta(
|
|
558
|
+
pendingMetaByToolCallId: Map<string, WriteExecutionMeta>,
|
|
559
|
+
): void {
|
|
560
|
+
pendingMetaByToolCallId.clear();
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
export function getWriteExecutionMeta(
|
|
564
|
+
context: ToolRenderContextLike | undefined,
|
|
565
|
+
pendingMetaByToolCallId: Map<string, WriteExecutionMeta>,
|
|
566
|
+
): WriteExecutionMeta | undefined {
|
|
567
|
+
if (!context) {
|
|
568
|
+
return undefined;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const carrier = toStateRecord(context.state);
|
|
572
|
+
const existing = carrier
|
|
573
|
+
? toRecord(carrier[WRITE_EXECUTION_META_STATE_KEY])
|
|
574
|
+
: undefined;
|
|
575
|
+
if (existing && Object.keys(existing).length > 0) {
|
|
576
|
+
return existing as unknown as WriteExecutionMeta;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (!context.toolCallId) {
|
|
580
|
+
return undefined;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const pending = pendingMetaByToolCallId.get(context.toolCallId);
|
|
584
|
+
if (!pending) {
|
|
585
|
+
return undefined;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (carrier) {
|
|
589
|
+
const storedMeta: WriteExecutionMeta = { ...pending };
|
|
590
|
+
carrier[WRITE_EXECUTION_META_STATE_KEY] = storedMeta;
|
|
591
|
+
pendingMetaByToolCallId.delete(context.toolCallId);
|
|
592
|
+
return storedMeta;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return pending;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function getPendingDiffPreviewState(
|
|
599
|
+
context: ToolRenderContextLike | undefined,
|
|
600
|
+
stateKey: string,
|
|
601
|
+
): PendingDiffPreviewState | undefined {
|
|
602
|
+
const carrier = toStateRecord(context?.state);
|
|
603
|
+
if (!carrier) {
|
|
604
|
+
return undefined;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const current = carrier[stateKey];
|
|
608
|
+
if (current && typeof current === "object" && !Array.isArray(current)) {
|
|
609
|
+
return current as PendingDiffPreviewState;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const next: PendingDiffPreviewState = {};
|
|
613
|
+
carrier[stateKey] = next;
|
|
614
|
+
return next;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function resolvePendingDiffPreview(
|
|
618
|
+
context: ToolRenderContextLike | undefined,
|
|
619
|
+
stateKey: string,
|
|
620
|
+
previewKey: string | undefined,
|
|
621
|
+
compute: () => PendingDiffPreviewData | undefined,
|
|
622
|
+
): PendingDiffPreviewData | undefined {
|
|
623
|
+
const previewState = getPendingDiffPreviewState(context, stateKey);
|
|
624
|
+
if (!previewState) {
|
|
625
|
+
return compute();
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (previewState.key !== previewKey) {
|
|
629
|
+
previewState.key = previewKey;
|
|
630
|
+
previewState.data = previewKey ? compute() : undefined;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
return previewState.data;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function buildPendingDiffCallComponent(
|
|
637
|
+
summaryText: string,
|
|
638
|
+
previewData: PendingDiffPreviewData | undefined,
|
|
639
|
+
context: ToolRenderContextLike | undefined,
|
|
640
|
+
config: ToolDisplayConfig,
|
|
641
|
+
theme: RenderTheme,
|
|
642
|
+
): Text | Container {
|
|
643
|
+
if (!context?.isPartial || !previewData) {
|
|
644
|
+
return textResult(summaryText);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const container = new Container();
|
|
648
|
+
container.addChild(new Text(summaryText, 0, 0));
|
|
649
|
+
container.addChild(new Spacer(1));
|
|
650
|
+
|
|
651
|
+
if (previewData.notice || typeof previewData.nextContent !== "string") {
|
|
652
|
+
container.addChild(new Text(theme.fg("warning", previewData.notice || "Preview unavailable."), 0, 0));
|
|
653
|
+
return container;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
container.addChild(
|
|
657
|
+
renderWriteDiffResult(
|
|
658
|
+
previewData.nextContent,
|
|
659
|
+
{
|
|
660
|
+
expanded: context.expanded === true,
|
|
661
|
+
filePath: previewData.filePath,
|
|
662
|
+
previousContent: previewData.previousContent,
|
|
663
|
+
fileExistedBeforeWrite: previewData.fileExistedBeforeWrite,
|
|
664
|
+
headerLabel: previewData.headerLabel,
|
|
665
|
+
},
|
|
666
|
+
config,
|
|
667
|
+
theme,
|
|
668
|
+
"",
|
|
669
|
+
),
|
|
670
|
+
);
|
|
671
|
+
return container;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function formatLineCountSuffix(
|
|
675
|
+
lineCount: number,
|
|
676
|
+
theme: RenderTheme,
|
|
677
|
+
): string {
|
|
678
|
+
return theme.fg("muted", ` (${lineCount} ${pluralize(lineCount, "line")})`);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function formatWriteCallSuffix(
|
|
682
|
+
lineCount: number,
|
|
683
|
+
sizeBytes: number,
|
|
684
|
+
theme: RenderTheme,
|
|
685
|
+
): string {
|
|
686
|
+
return theme.fg(
|
|
687
|
+
"muted",
|
|
688
|
+
` (${lineCount} ${pluralize(lineCount, "line")} • ${formatSize(sizeBytes)})`,
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function formatInProgressLineCount(
|
|
693
|
+
action: string,
|
|
694
|
+
lineCount: number,
|
|
695
|
+
theme: RenderTheme,
|
|
696
|
+
): string {
|
|
697
|
+
return theme.fg("warning", `${action}...`) + formatLineCountSuffix(lineCount, theme);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function normalizePositiveInteger(value: unknown): number | undefined {
|
|
701
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0
|
|
702
|
+
? Math.floor(value)
|
|
703
|
+
: undefined;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function toStringArray(value: unknown): string[] {
|
|
707
|
+
if (!Array.isArray(value)) {
|
|
708
|
+
return [];
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
return value
|
|
712
|
+
.filter((entry): entry is string => typeof entry === "string")
|
|
713
|
+
.map((entry) => entry.trim())
|
|
714
|
+
.filter((entry) => entry.length > 0);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function getRtkCompactionInfo(details: unknown): RtkCompactionInfo | undefined {
|
|
718
|
+
const detailRecord = toRecord(details);
|
|
719
|
+
const metadataRecord = toRecord(detailRecord.metadata);
|
|
720
|
+
const topLevel = toRecord(detailRecord.rtkCompaction);
|
|
721
|
+
const nested = toRecord(metadataRecord.rtkCompaction);
|
|
722
|
+
|
|
723
|
+
const source =
|
|
724
|
+
Object.keys(topLevel).length > 0
|
|
725
|
+
? topLevel
|
|
726
|
+
: Object.keys(nested).length > 0
|
|
727
|
+
? nested
|
|
728
|
+
: undefined;
|
|
729
|
+
|
|
730
|
+
if (!source) {
|
|
731
|
+
return undefined;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const techniques = toStringArray(source.techniques);
|
|
735
|
+
const info: RtkCompactionInfo = {
|
|
736
|
+
applied: source.applied === true,
|
|
737
|
+
techniques,
|
|
738
|
+
truncated: source.truncated === true,
|
|
739
|
+
originalLineCount: normalizePositiveInteger(source.originalLineCount),
|
|
740
|
+
compactedLineCount: normalizePositiveInteger(source.compactedLineCount),
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
if (
|
|
744
|
+
!info.applied &&
|
|
745
|
+
info.techniques.length === 0 &&
|
|
746
|
+
!info.truncated &&
|
|
747
|
+
info.originalLineCount === undefined &&
|
|
748
|
+
info.compactedLineCount === undefined
|
|
749
|
+
) {
|
|
750
|
+
return undefined;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
return info;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function formatRtkTechniqueList(techniques: string[]): string {
|
|
757
|
+
if (techniques.length === 0) {
|
|
758
|
+
return "";
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const visible = techniques.slice(0, 3).join(", ");
|
|
762
|
+
const hidden = techniques.length - 3;
|
|
763
|
+
return hidden > 0 ? `${visible}, +${hidden} more` : visible;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function getRtkCompactionInfoIfApplied(
|
|
767
|
+
details: unknown,
|
|
768
|
+
config: ToolDisplayConfig,
|
|
769
|
+
): RtkCompactionInfo | undefined {
|
|
770
|
+
if (!config.showRtkCompactionHints) {
|
|
771
|
+
return undefined;
|
|
772
|
+
}
|
|
773
|
+
const info = getRtkCompactionInfo(details);
|
|
774
|
+
return info?.applied ? info : undefined;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function withRtkCompactionInfo(
|
|
778
|
+
params: RtkHintParams,
|
|
779
|
+
handler: (info: RtkCompactionInfo) => string,
|
|
780
|
+
): string {
|
|
781
|
+
const info = getRtkCompactionInfoIfApplied(params.details, params.config);
|
|
782
|
+
return info ? handler(info) : "";
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
interface RtkHintParams {
|
|
786
|
+
details: unknown;
|
|
787
|
+
config: ToolDisplayConfig;
|
|
788
|
+
theme: RenderTheme;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
interface PreviewHintContext {
|
|
792
|
+
lines: string[];
|
|
793
|
+
config: ToolDisplayConfig;
|
|
794
|
+
theme: RenderTheme;
|
|
795
|
+
options: ToolRenderResultOptions;
|
|
796
|
+
details: unknown;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
interface McpPreviewHintContext extends PreviewHintContext {
|
|
800
|
+
truncation: { truncated: boolean; fullOutputPath?: string };
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function handlePartialResult(
|
|
804
|
+
options: ToolRenderResultOptions,
|
|
805
|
+
theme: RenderTheme,
|
|
806
|
+
message: string,
|
|
807
|
+
): Text | undefined {
|
|
808
|
+
return options.isPartial ? partialResultText(theme, message) : undefined;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function renderSearchPreview(ctx: PreviewHintContext, expandedOnly = false): Text {
|
|
812
|
+
return renderPreviewText(ctx.lines, ctx.config, ctx.theme, ctx.options, (p) => appendPreviewHints(p, ctx), expandedOnly);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function renderMcpPreview(ctx: McpPreviewHintContext, expandedOnly = false): Text {
|
|
816
|
+
return renderPreviewText(ctx.lines, ctx.config, ctx.theme, ctx.options, (p) => appendMcpPreviewHints(p, ctx), expandedOnly);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function formatRtkSummarySuffix(params: RtkHintParams): string {
|
|
820
|
+
const { theme } = params;
|
|
821
|
+
return withRtkCompactionInfo(params, (info) => {
|
|
822
|
+
const segments: string[] = [RTK_COMPACTION_LABEL];
|
|
823
|
+
|
|
824
|
+
const techniqueText = formatRtkTechniqueList(info.techniques);
|
|
825
|
+
if (techniqueText) {
|
|
826
|
+
segments.push(techniqueText);
|
|
827
|
+
}
|
|
828
|
+
if (info.truncated) {
|
|
829
|
+
segments.push("RTK removed content");
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
if (segments.length === 0) {
|
|
833
|
+
return "";
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
return theme.fg("warning", ` • ${segments.join(" • ")}`);
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function getExpandedPreviewLineLimit(
|
|
841
|
+
lines: string[],
|
|
842
|
+
config: ToolDisplayConfig,
|
|
843
|
+
): number {
|
|
844
|
+
const limit = Math.max(0, config.expandedPreviewMaxLines);
|
|
845
|
+
if (limit === 0) {
|
|
846
|
+
return lines.length;
|
|
847
|
+
}
|
|
848
|
+
return Math.min(lines.length, limit);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function formatExpandedPreviewCapHint(
|
|
852
|
+
lines: string[],
|
|
853
|
+
config: ToolDisplayConfig,
|
|
854
|
+
theme: RenderTheme,
|
|
855
|
+
): string {
|
|
856
|
+
const cap = Math.max(0, config.expandedPreviewMaxLines);
|
|
857
|
+
if (cap === 0 || lines.length <= cap) {
|
|
858
|
+
return "";
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
return `\n${theme.fg("warning", `(display capped at ${cap} lines by tool-display setting)`)}`;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function formatRtkPreviewHint(params: RtkHintParams): string {
|
|
865
|
+
const { theme } = params;
|
|
866
|
+
return withRtkCompactionInfo(params, (info) => {
|
|
867
|
+
const hints: string[] = [];
|
|
868
|
+
const techniqueText = formatRtkTechniqueList(info.techniques);
|
|
869
|
+
if (techniqueText) {
|
|
870
|
+
hints.push(`${RTK_COMPACTION_LABEL}: ${techniqueText}`);
|
|
871
|
+
} else {
|
|
872
|
+
hints.push(`${RTK_COMPACTION_LABEL} applied`);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
if (
|
|
876
|
+
info.originalLineCount !== undefined &&
|
|
877
|
+
info.compactedLineCount !== undefined &&
|
|
878
|
+
info.originalLineCount > info.compactedLineCount
|
|
879
|
+
) {
|
|
880
|
+
hints.push(`${info.compactedLineCount}/${info.originalLineCount} lines kept`);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
if (info.truncated) {
|
|
884
|
+
hints.push("RTK removed content");
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
return hints.length > 0
|
|
888
|
+
? `\n${theme.fg("warning", `(${hints.join(" • ")})`)}`
|
|
889
|
+
: "";
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function appendRtkAndExpandedHints(preview: string, ctx: PreviewHintContext): string {
|
|
894
|
+
preview += formatRtkPreviewHint(ctx);
|
|
895
|
+
if (ctx.options.expanded) {
|
|
896
|
+
preview += formatExpandedPreviewCapHint(ctx.lines, ctx.config, ctx.theme);
|
|
897
|
+
}
|
|
898
|
+
return preview;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function appendMcpPreviewHints(preview: string, ctx: McpPreviewHintContext): string {
|
|
902
|
+
const { config, theme, details, lines, options, truncation } = ctx;
|
|
903
|
+
if (config.showTruncationHints && (truncation.truncated || truncation.fullOutputPath)) {
|
|
904
|
+
const hints: string[] = [];
|
|
905
|
+
if (truncation.truncated) {
|
|
906
|
+
hints.push("truncated by backend limits");
|
|
907
|
+
}
|
|
908
|
+
if (truncation.fullOutputPath) {
|
|
909
|
+
hints.push(`full output: ${truncation.fullOutputPath}`);
|
|
910
|
+
}
|
|
911
|
+
preview += `\n${theme.fg("warning", `(${hints.join(" • ")})`)}`;
|
|
912
|
+
}
|
|
913
|
+
return appendRtkAndExpandedHints(preview, ctx);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function appendPreviewHints(preview: string, ctx: PreviewHintContext): string {
|
|
917
|
+
const { config, theme, details } = ctx;
|
|
918
|
+
if (config.showTruncationHints && toRecord(toRecord(details).truncation).truncated) {
|
|
919
|
+
preview += `\n${theme.fg("warning", "(truncated by backend limits)")}`;
|
|
920
|
+
}
|
|
921
|
+
return appendRtkAndExpandedHints(preview, ctx);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function renderPreviewText(
|
|
925
|
+
lines: string[],
|
|
926
|
+
config: ToolDisplayConfig,
|
|
927
|
+
theme: RenderTheme,
|
|
928
|
+
options: ToolRenderResultOptions,
|
|
929
|
+
appendHints: (preview: string) => string,
|
|
930
|
+
expandedOnly: boolean = false,
|
|
931
|
+
): Text {
|
|
932
|
+
const useExpanded = expandedOnly || options.expanded;
|
|
933
|
+
const maxLines = useExpanded
|
|
934
|
+
? getExpandedPreviewLineLimit(lines, config)
|
|
935
|
+
: config.previewLines;
|
|
936
|
+
const preview = buildPreviewText(lines, maxLines, theme, useExpanded);
|
|
937
|
+
return textResult(appendHints(preview));
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function formatReadSummary(
|
|
941
|
+
lines: string[],
|
|
942
|
+
details: ReadToolDetails | undefined,
|
|
943
|
+
theme: RenderTheme,
|
|
944
|
+
showTruncationHints: boolean,
|
|
945
|
+
): string {
|
|
946
|
+
const lineCount = lines.length;
|
|
947
|
+
let summary = theme.fg(
|
|
948
|
+
"muted",
|
|
949
|
+
`↳ loaded ${lineCount} ${pluralize(lineCount, "line")}`,
|
|
950
|
+
);
|
|
951
|
+
summary += theme.fg(
|
|
952
|
+
"warning",
|
|
953
|
+
showTruncationHints ? truncationHint(details) : "",
|
|
954
|
+
);
|
|
955
|
+
return summary;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function formatSearchSummary(
|
|
959
|
+
lines: string[],
|
|
960
|
+
unitLabel: string,
|
|
961
|
+
details: { truncation?: { truncated?: boolean } } | undefined,
|
|
962
|
+
theme: RenderTheme,
|
|
963
|
+
showTruncationHints: boolean,
|
|
964
|
+
pluralLabel?: string,
|
|
965
|
+
): string {
|
|
966
|
+
const count = countNonEmptyLines(lines);
|
|
967
|
+
let summary = theme.fg(
|
|
968
|
+
"muted",
|
|
969
|
+
`↳ ${count} ${pluralize(count, unitLabel, pluralLabel)} returned`,
|
|
970
|
+
);
|
|
971
|
+
summary += theme.fg(
|
|
972
|
+
"warning",
|
|
973
|
+
showTruncationHints ? truncationHint(details) : "",
|
|
974
|
+
);
|
|
975
|
+
return summary;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function formatBashSummary(
|
|
979
|
+
lines: string[],
|
|
980
|
+
_details: BashToolDetails | undefined,
|
|
981
|
+
theme: RenderTheme,
|
|
982
|
+
_showTruncationHints: boolean,
|
|
983
|
+
): string {
|
|
984
|
+
const lineCount = lines.length;
|
|
985
|
+
const summary = theme.fg(
|
|
986
|
+
"muted",
|
|
987
|
+
`↳ ${lineCount} ${pluralize(lineCount, "line")} returned`,
|
|
988
|
+
);
|
|
989
|
+
return summary;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
function formatBashTruncationHints(
|
|
993
|
+
details: BashToolDetails | undefined,
|
|
994
|
+
theme: RenderTheme,
|
|
995
|
+
): string {
|
|
996
|
+
if (!details) {
|
|
997
|
+
return "";
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
const hints: string[] = [];
|
|
1001
|
+
if (details.truncation?.truncated) {
|
|
1002
|
+
hints.push("output truncated");
|
|
1003
|
+
}
|
|
1004
|
+
if (details.fullOutputPath) {
|
|
1005
|
+
hints.push(`full output: ${details.fullOutputPath}`);
|
|
1006
|
+
}
|
|
1007
|
+
if (hints.length === 0) {
|
|
1008
|
+
return "";
|
|
1009
|
+
}
|
|
1010
|
+
return `\n${theme.fg("warning", `(${hints.join(" • ")})`)}`;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function getBashPreviewLineLimit(
|
|
1014
|
+
lines: string[],
|
|
1015
|
+
options: ToolRenderResultOptions,
|
|
1016
|
+
config: ToolDisplayConfig,
|
|
1017
|
+
): number {
|
|
1018
|
+
if (options.expanded) {
|
|
1019
|
+
return getExpandedPreviewLineLimit(lines, config);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
return config.bashOutputMode === "opencode"
|
|
1023
|
+
? config.bashCollapsedLines
|
|
1024
|
+
: config.previewLines;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
type ToolRenderInput = {
|
|
1028
|
+
content?: Array<{ type: string; text?: string }>;
|
|
1029
|
+
details?: unknown;
|
|
1030
|
+
};
|
|
1031
|
+
|
|
1032
|
+
function textResult(text: string): Text {
|
|
1033
|
+
return new Text(text, 0, 0);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
function partialResultText(theme: RenderTheme, label: string): Text {
|
|
1037
|
+
return textResult(theme.fg("warning", label));
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
function renderBashPreviewWithHints(
|
|
1041
|
+
lines: string[],
|
|
1042
|
+
maxLines: number,
|
|
1043
|
+
config: ToolDisplayConfig,
|
|
1044
|
+
theme: RenderTheme,
|
|
1045
|
+
options: ToolRenderResultOptions,
|
|
1046
|
+
details: BashToolDetails | undefined,
|
|
1047
|
+
): Text {
|
|
1048
|
+
let preview = buildPreviewText(lines, maxLines, theme, options.expanded);
|
|
1049
|
+
if (config.showTruncationHints) {
|
|
1050
|
+
preview += formatBashTruncationHints(details, theme);
|
|
1051
|
+
}
|
|
1052
|
+
if (options.expanded) {
|
|
1053
|
+
preview += formatExpandedPreviewCapHint(lines, config, theme);
|
|
1054
|
+
}
|
|
1055
|
+
return textResult(preview);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function prepareBashLivePreview(
|
|
1059
|
+
rawOutput: string,
|
|
1060
|
+
options: ToolRenderResultOptions,
|
|
1061
|
+
config: ToolDisplayConfig,
|
|
1062
|
+
): { lines: string[]; maxLines: number } | undefined {
|
|
1063
|
+
const lines = prepareOutputLines(rawOutput, options);
|
|
1064
|
+
if (lines.length === 0) {
|
|
1065
|
+
return undefined;
|
|
1066
|
+
}
|
|
1067
|
+
const maxLines = getBashPreviewLineLimit(lines, options, config);
|
|
1068
|
+
if (!options.expanded && maxLines === 0) {
|
|
1069
|
+
return undefined;
|
|
1070
|
+
}
|
|
1071
|
+
return { lines, maxLines };
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
function renderBashLivePreview(
|
|
1075
|
+
rawOutput: string,
|
|
1076
|
+
options: ToolRenderResultOptions,
|
|
1077
|
+
config: ToolDisplayConfig,
|
|
1078
|
+
theme: RenderTheme,
|
|
1079
|
+
details: BashToolDetails | undefined,
|
|
1080
|
+
): Text {
|
|
1081
|
+
const prepared = prepareBashLivePreview(rawOutput, options, config);
|
|
1082
|
+
if (!prepared) {
|
|
1083
|
+
return textResult("");
|
|
1084
|
+
}
|
|
1085
|
+
return renderBashPreviewWithHints(prepared.lines, prepared.maxLines, config, theme, options, details);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
function renderBashErrorResult(
|
|
1089
|
+
rawOutput: string,
|
|
1090
|
+
options: ToolRenderResultOptions,
|
|
1091
|
+
config: ToolDisplayConfig,
|
|
1092
|
+
theme: RenderTheme,
|
|
1093
|
+
details: BashToolDetails | undefined,
|
|
1094
|
+
): Text {
|
|
1095
|
+
const lines = prepareOutputLines(rawOutput, options);
|
|
1096
|
+
let text = theme.fg("error", "↳ command failed");
|
|
1097
|
+
|
|
1098
|
+
if (lines.length > 0) {
|
|
1099
|
+
const maxLines = getBashPreviewLineLimit(lines, options, config);
|
|
1100
|
+
if (options.expanded || maxLines > 0) {
|
|
1101
|
+
const { shown, remaining } = previewLines(lines, maxLines);
|
|
1102
|
+
text += `\n${shown
|
|
1103
|
+
.map((line) => theme.fg("error", sanitizeAnsiForThemedOutput(line)))
|
|
1104
|
+
.join("\n")}`;
|
|
1105
|
+
text += formatTruncationHint(remaining, options.expanded, theme);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
if (config.showTruncationHints) {
|
|
1110
|
+
text += formatBashTruncationHints(details, theme);
|
|
1111
|
+
}
|
|
1112
|
+
if (options.expanded && lines.length > 0) {
|
|
1113
|
+
text += formatExpandedPreviewCapHint(lines, config, theme);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
return textResult(text);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
function renderSearchResult(
|
|
1120
|
+
result: ToolRenderInput,
|
|
1121
|
+
options: ToolRenderResultOptions,
|
|
1122
|
+
config: ToolDisplayConfig,
|
|
1123
|
+
theme: RenderTheme,
|
|
1124
|
+
unitLabel: string,
|
|
1125
|
+
details: GrepToolDetails | FindToolDetails | LsToolDetails | undefined,
|
|
1126
|
+
pluralLabel?: string,
|
|
1127
|
+
): Text {
|
|
1128
|
+
if (options.isPartial) {
|
|
1129
|
+
return partialResultText(theme, "running...");
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
const lines = prepareOutputLines(extractTextOutput(result), options);
|
|
1133
|
+
|
|
1134
|
+
if (config.searchOutputMode === "hidden") {
|
|
1135
|
+
return textResult("");
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
const hintCtx: PreviewHintContext = { lines, config, theme, options, details };
|
|
1139
|
+
|
|
1140
|
+
if (config.searchOutputMode === "count") {
|
|
1141
|
+
if (options.expanded) {
|
|
1142
|
+
return renderSearchPreview(hintCtx, true);
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
let summary = formatSearchSummary(
|
|
1146
|
+
lines,
|
|
1147
|
+
unitLabel,
|
|
1148
|
+
details,
|
|
1149
|
+
theme,
|
|
1150
|
+
config.showTruncationHints,
|
|
1151
|
+
pluralLabel,
|
|
1152
|
+
);
|
|
1153
|
+
summary += formatExpandHint(theme);
|
|
1154
|
+
summary += formatRtkSummarySuffix({ details, config, theme });
|
|
1155
|
+
return textResult(summary);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
return renderSearchPreview(hintCtx);
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
function resolveMcpProxyCallTarget(args: Record<string, unknown>): string {
|
|
1162
|
+
const tool = getTextField(args, "tool");
|
|
1163
|
+
const connect = getTextField(args, "connect");
|
|
1164
|
+
const describe = getTextField(args, "describe");
|
|
1165
|
+
const search = getTextField(args, "search");
|
|
1166
|
+
const server = getTextField(args, "server");
|
|
1167
|
+
|
|
1168
|
+
if (tool) {
|
|
1169
|
+
return server ? `call ${server}:${tool}` : `call ${tool}`;
|
|
1170
|
+
}
|
|
1171
|
+
if (connect) {
|
|
1172
|
+
return `connect ${connect}`;
|
|
1173
|
+
}
|
|
1174
|
+
if (describe) {
|
|
1175
|
+
return server ? `describe ${describe} @${server}` : `describe ${describe}`;
|
|
1176
|
+
}
|
|
1177
|
+
if (search) {
|
|
1178
|
+
return server ? `search "${search}" @${server}` : `search "${search}"`;
|
|
1179
|
+
}
|
|
1180
|
+
if (server) {
|
|
1181
|
+
return `tools ${server}`;
|
|
1182
|
+
}
|
|
1183
|
+
return "status";
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
function formatArgCountSuffix(argCount: number, theme: RenderTheme): string {
|
|
1187
|
+
return argCount === 0
|
|
1188
|
+
? theme.fg("muted", " (no args)")
|
|
1189
|
+
: theme.fg("muted", ` (${argCount} ${pluralize(argCount, "arg")})`);
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
function formatMcpCallLine(
|
|
1193
|
+
toolName: string,
|
|
1194
|
+
toolLabel: string,
|
|
1195
|
+
args: Record<string, unknown>,
|
|
1196
|
+
theme: RenderTheme,
|
|
1197
|
+
): Text {
|
|
1198
|
+
const argCount = Object.keys(args).length;
|
|
1199
|
+
const argSuffix = formatArgCountSuffix(argCount, theme);
|
|
1200
|
+
const target =
|
|
1201
|
+
toolName === "mcp"
|
|
1202
|
+
? resolveMcpProxyCallTarget(args)
|
|
1203
|
+
: toolLabel.startsWith("MCP ")
|
|
1204
|
+
? toolLabel.slice("MCP ".length)
|
|
1205
|
+
: toolLabel;
|
|
1206
|
+
|
|
1207
|
+
return new Text(
|
|
1208
|
+
`${theme.fg("toolTitle", theme.bold("MCP"))} ${theme.fg("accent", target)}${argSuffix}`,
|
|
1209
|
+
0,
|
|
1210
|
+
0,
|
|
1211
|
+
);
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
function getMcpTruncationDetails(details: unknown): {
|
|
1215
|
+
truncated: boolean;
|
|
1216
|
+
fullOutputPath?: string;
|
|
1217
|
+
} {
|
|
1218
|
+
const detailRecord = toRecord(details);
|
|
1219
|
+
const truncation = toRecord(detailRecord.truncation);
|
|
1220
|
+
|
|
1221
|
+
const fullOutputPath =
|
|
1222
|
+
typeof truncation.fullOutputPath === "string"
|
|
1223
|
+
? truncation.fullOutputPath
|
|
1224
|
+
: typeof detailRecord.fullOutputPath === "string"
|
|
1225
|
+
? detailRecord.fullOutputPath
|
|
1226
|
+
: undefined;
|
|
1227
|
+
|
|
1228
|
+
return {
|
|
1229
|
+
truncated: truncation.truncated === true,
|
|
1230
|
+
fullOutputPath,
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
function renderMcpResult(
|
|
1235
|
+
result: ToolRenderInput,
|
|
1236
|
+
options: ToolRenderResultOptions,
|
|
1237
|
+
config: ToolDisplayConfig,
|
|
1238
|
+
theme: RenderTheme,
|
|
1239
|
+
): Text {
|
|
1240
|
+
const partial = handlePartialResult(options, theme, "running...");
|
|
1241
|
+
if (partial) {
|
|
1242
|
+
return partial;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
if (config.mcpOutputMode === "hidden") {
|
|
1246
|
+
return textResult("");
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
const lines = prepareOutputLines(extractTextOutput(result), options);
|
|
1250
|
+
const truncation = getMcpTruncationDetails(result.details);
|
|
1251
|
+
const mcpCtx: McpPreviewHintContext = { lines, config, theme, options, details: result.details, truncation };
|
|
1252
|
+
|
|
1253
|
+
if (config.mcpOutputMode === "summary") {
|
|
1254
|
+
if (options.expanded) {
|
|
1255
|
+
return renderMcpPreview(mcpCtx, true);
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
const lineCount = countNonEmptyLines(lines);
|
|
1259
|
+
let summary = theme.fg(
|
|
1260
|
+
"muted",
|
|
1261
|
+
`↳ ${lineCount} ${pluralize(lineCount, "line")} returned`,
|
|
1262
|
+
);
|
|
1263
|
+
summary += formatExpandHint(theme);
|
|
1264
|
+
if (config.showTruncationHints && truncation.truncated) {
|
|
1265
|
+
summary += theme.fg("warning", " • truncated");
|
|
1266
|
+
}
|
|
1267
|
+
summary += formatRtkSummarySuffix({ details: result.details, config, theme });
|
|
1268
|
+
return textResult(summary);
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
return renderMcpPreview(mcpCtx);
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
function isBuiltInToolName(toolName: string): boolean {
|
|
1275
|
+
return (BUILT_IN_TOOL_OVERRIDE_NAMES as readonly string[]).includes(toolName);
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
function getRuntimeCustomToolOverride(
|
|
1279
|
+
toolName: string,
|
|
1280
|
+
config: ToolDisplayConfig,
|
|
1281
|
+
): CustomToolOverrideConfig | undefined {
|
|
1282
|
+
if (!toolName || isBuiltInToolName(toolName)) {
|
|
1283
|
+
return undefined;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
const overrides = toRecord((config as unknown as Record<string, unknown>).customToolOverrides);
|
|
1287
|
+
return normalizeCustomToolOverrideEntry(overrides[toolName]);
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
function formatGenericToolCallLine(
|
|
1291
|
+
toolName: string,
|
|
1292
|
+
args: unknown,
|
|
1293
|
+
theme: RenderTheme,
|
|
1294
|
+
): Text {
|
|
1295
|
+
const argRecord = toRecord(args);
|
|
1296
|
+
const argCount = Object.keys(argRecord).length;
|
|
1297
|
+
const argSuffix = formatArgCountSuffix(argCount, theme);
|
|
1298
|
+
return new Text(
|
|
1299
|
+
`${theme.fg("toolTitle", theme.bold(toolName))}${argSuffix}`,
|
|
1300
|
+
0,
|
|
1301
|
+
0,
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
function getSearchScope(args: Record<string, unknown>): string {
|
|
1306
|
+
return shortenPath((args.path as string) || ".");
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
function formatSearchCallLine(
|
|
1310
|
+
toolName: string,
|
|
1311
|
+
accent: string,
|
|
1312
|
+
mutedSuffix: string,
|
|
1313
|
+
theme: RenderTheme,
|
|
1314
|
+
): string {
|
|
1315
|
+
return `${theme.fg("toolTitle", theme.bold(toolName))} ${theme.fg("accent", accent)}${theme.fg("muted", mutedSuffix)}`;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
function renderCustomToolResult(
|
|
1319
|
+
result: ToolRenderInput,
|
|
1320
|
+
options: ToolRenderResultOptions,
|
|
1321
|
+
config: ToolDisplayConfig,
|
|
1322
|
+
outputMode: CustomToolOverrideConfig["outputMode"],
|
|
1323
|
+
theme: RenderTheme,
|
|
1324
|
+
): Text {
|
|
1325
|
+
return renderMcpResult(
|
|
1326
|
+
result as ToolRenderInput,
|
|
1327
|
+
options,
|
|
1328
|
+
{ ...config, mcpOutputMode: outputMode },
|
|
1329
|
+
theme,
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
function getAdapterKind(tool: RuntimeToolDefinition, adapter: ToolDisplayAdapter): ToolDisplayKind {
|
|
1334
|
+
if (adapter.kind) {
|
|
1335
|
+
return adapter.kind;
|
|
1336
|
+
}
|
|
1337
|
+
if (tool.name === "read" || tool.name === "edit") {
|
|
1338
|
+
return tool.name;
|
|
1339
|
+
}
|
|
1340
|
+
return isMcpToolCandidate(tool) ? "mcp" : "generic";
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
function getAdapterPath(args: unknown, adapter: ToolDisplayAdapter): string | undefined {
|
|
1344
|
+
const explicitPath = adapter.getPath?.(args);
|
|
1345
|
+
if (explicitPath) {
|
|
1346
|
+
return explicitPath;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
for (const field of adapter.pathFields ?? ["file_path", "path"]) {
|
|
1350
|
+
const value = getStringField(args, field);
|
|
1351
|
+
if (value) {
|
|
1352
|
+
return value;
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
return undefined;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
function renderReadDisplayCall(
|
|
1360
|
+
args: unknown,
|
|
1361
|
+
theme: RenderTheme,
|
|
1362
|
+
adapter: ToolDisplayAdapter = {},
|
|
1363
|
+
): Text {
|
|
1364
|
+
const path = shortenPath(getAdapterPath(args, adapter));
|
|
1365
|
+
const offset = getNumericField(args, "offset");
|
|
1366
|
+
const limit = getNumericField(args, "limit");
|
|
1367
|
+
let suffix = "";
|
|
1368
|
+
if (offset !== undefined || limit !== undefined) {
|
|
1369
|
+
const from = offset ?? 1;
|
|
1370
|
+
const to = limit !== undefined ? from + limit - 1 : undefined;
|
|
1371
|
+
suffix = to ? `:${from}-${to}` : `:${from}`;
|
|
1372
|
+
}
|
|
1373
|
+
const line = `${theme.fg("toolTitle", theme.bold("read"))} ${theme.fg("accent", path || "...")}${theme.fg("warning", suffix)}`;
|
|
1374
|
+
return textResult(line);
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
function renderReadDisplayResult(
|
|
1378
|
+
result: ToolRenderInput,
|
|
1379
|
+
options: ToolRenderResultOptions,
|
|
1380
|
+
config: ToolDisplayConfig,
|
|
1381
|
+
theme: RenderTheme,
|
|
1382
|
+
): Text {
|
|
1383
|
+
if (options.isPartial) {
|
|
1384
|
+
return partialResultText(theme, "reading...");
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
if (config.readOutputMode === "hidden") {
|
|
1388
|
+
return textResult("");
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
const details = result.details as ReadToolDetails | undefined;
|
|
1392
|
+
const rawOutput = extractTextOutput(result);
|
|
1393
|
+
const lines = prepareOutputLines(rawOutput, options);
|
|
1394
|
+
const hintCtx: PreviewHintContext = { lines, config, theme, options, details };
|
|
1395
|
+
|
|
1396
|
+
if (config.readOutputMode === "summary") {
|
|
1397
|
+
if (options.expanded) {
|
|
1398
|
+
return renderSearchPreview(hintCtx, true);
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
const summaryLines = compactOutputLines(splitLines(rawOutput), {
|
|
1402
|
+
expanded: true,
|
|
1403
|
+
});
|
|
1404
|
+
let summary = formatReadSummary(
|
|
1405
|
+
summaryLines,
|
|
1406
|
+
details,
|
|
1407
|
+
theme,
|
|
1408
|
+
config.showTruncationHints,
|
|
1409
|
+
);
|
|
1410
|
+
summary += formatExpandHint(theme);
|
|
1411
|
+
summary += formatRtkSummarySuffix({ details: result.details, config, theme });
|
|
1412
|
+
return textResult(summary);
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
return renderSearchPreview(hintCtx);
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
function renderEditDisplayCall(
|
|
1419
|
+
args: unknown,
|
|
1420
|
+
theme: RenderTheme,
|
|
1421
|
+
context: ToolRenderContextLike | undefined,
|
|
1422
|
+
adapter: ToolDisplayAdapter = {},
|
|
1423
|
+
getConfig: ConfigGetter,
|
|
1424
|
+
): Text | Container {
|
|
1425
|
+
const path = shortenPath(getAdapterPath(args, adapter));
|
|
1426
|
+
const lineCount = adapter.getEditLineCount?.(args) ?? getEditLineCount(args);
|
|
1427
|
+
const summaryText = `${theme.fg("toolTitle", theme.bold("edit"))} ${theme.fg("accent", path || "...")}${formatLineCountSuffix(lineCount, theme)}`;
|
|
1428
|
+
if (!context?.argsComplete || !context.isPartial) {
|
|
1429
|
+
return textResult(summaryText);
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
const previewKey = JSON.stringify({
|
|
1433
|
+
path: getAdapterPath(args, adapter) ?? null,
|
|
1434
|
+
edits: toRecord(args).edits ?? null,
|
|
1435
|
+
oldText: getStringField(args, "oldText") ?? null,
|
|
1436
|
+
newText: getStringField(args, "newText") ?? null,
|
|
1437
|
+
});
|
|
1438
|
+
const previewData = resolvePendingDiffPreview(
|
|
1439
|
+
context,
|
|
1440
|
+
EDIT_PENDING_PREVIEW_STATE_KEY,
|
|
1441
|
+
previewKey,
|
|
1442
|
+
() => buildPendingEditPreviewData(args, context.cwd),
|
|
1443
|
+
);
|
|
1444
|
+
return buildPendingDiffCallComponent(summaryText, previewData, context, getConfig(), theme);
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
function renderEditDisplayResult(
|
|
1448
|
+
result: ToolRenderInput & { isError?: boolean },
|
|
1449
|
+
options: ToolRenderResultOptions,
|
|
1450
|
+
theme: RenderTheme,
|
|
1451
|
+
context: ToolRenderContextLike | undefined,
|
|
1452
|
+
adapter: ToolDisplayAdapter = {},
|
|
1453
|
+
getConfig: ConfigGetter,
|
|
1454
|
+
): unknown {
|
|
1455
|
+
const lineCount = adapter.getEditLineCount?.(context?.args) ?? getEditLineCount(context?.args);
|
|
1456
|
+
const { fallbackText, earlyResult } = handleEditOrWriteResult(result, options, context, theme, lineCount, "editing", "Edit failed.");
|
|
1457
|
+
if (earlyResult) {
|
|
1458
|
+
return earlyResult;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
const config = getConfig();
|
|
1462
|
+
const details = result.details as EditToolDetails | undefined;
|
|
1463
|
+
return renderEditDiffResult(
|
|
1464
|
+
details,
|
|
1465
|
+
{ expanded: options.expanded, filePath: getAdapterPath(context?.args, adapter) },
|
|
1466
|
+
config,
|
|
1467
|
+
theme,
|
|
1468
|
+
fallbackText,
|
|
1469
|
+
);
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
function handleEditOrWriteResult(
|
|
1473
|
+
result: ToolRenderInput,
|
|
1474
|
+
options: ToolRenderResultOptions,
|
|
1475
|
+
context: ToolRenderContextLike | undefined,
|
|
1476
|
+
theme: RenderTheme,
|
|
1477
|
+
lineCount: number,
|
|
1478
|
+
progressLabel: string,
|
|
1479
|
+
errorMessage: string,
|
|
1480
|
+
): { fallbackText: string; earlyResult: Text | undefined } {
|
|
1481
|
+
if (options.isPartial) {
|
|
1482
|
+
return { fallbackText: "", earlyResult: new Text(formatInProgressLineCount(progressLabel, lineCount, theme), 0, 0) };
|
|
1483
|
+
}
|
|
1484
|
+
const fallbackText = extractTextOutput(result);
|
|
1485
|
+
if (isToolError(result, context)) {
|
|
1486
|
+
return { fallbackText, earlyResult: textResult(theme.fg("error", fallbackText || errorMessage)) };
|
|
1487
|
+
}
|
|
1488
|
+
return { fallbackText, earlyResult: undefined };
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
function applyToolDisplayDecorationInPlace(
|
|
1492
|
+
tool: RuntimeToolDefinition,
|
|
1493
|
+
api: ToolDisplayApi,
|
|
1494
|
+
adapter?: ToolDisplayAdapter,
|
|
1495
|
+
): boolean {
|
|
1496
|
+
try {
|
|
1497
|
+
captureToolPropertyDescriptors(tool, decoratedToolDescriptors, decoratedTools);
|
|
1498
|
+
Object.assign(tool, api.decorateTool(tool, adapter));
|
|
1499
|
+
return true;
|
|
1500
|
+
} catch (error) {
|
|
1501
|
+
logToolDisplayDebug("Tool display decoration failed.", error);
|
|
1502
|
+
return false;
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
function drainPendingToolDisplayDecorations(api: ToolDisplayApi): void {
|
|
1507
|
+
const globalWithApi = globalThis as GlobalWithToolDisplayApi;
|
|
1508
|
+
const pendingDecorations = globalWithApi[TOOL_DISPLAY_PENDING_DECORATIONS_KEY];
|
|
1509
|
+
if (!Array.isArray(pendingDecorations) || pendingDecorations.length === 0) {
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
const entries = pendingDecorations.splice(0);
|
|
1514
|
+
for (const entry of entries) {
|
|
1515
|
+
if (!entry?.tool || typeof entry.tool !== "object") {
|
|
1516
|
+
continue;
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
applyToolDisplayDecorationInPlace(entry.tool, api, entry.adapter);
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
function drainPendingResultRenderMiddlewares(api: ToolDisplayApi): void {
|
|
1524
|
+
const globalWithApi = globalThis as GlobalWithToolDisplayApi;
|
|
1525
|
+
const pending = globalWithApi[TOOL_DISPLAY_PENDING_RESULT_MIDDLEWARES_KEY];
|
|
1526
|
+
if (!Array.isArray(pending) || pending.length === 0) return;
|
|
1527
|
+
|
|
1528
|
+
const entries = pending.splice(0);
|
|
1529
|
+
for (const entry of entries) {
|
|
1530
|
+
if (!entry?.id || !entry.toolName || typeof entry.middleware !== "function") continue;
|
|
1531
|
+
api.registerResultRenderMiddleware(entry);
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
function installToolDisplayApi(getConfig: ConfigGetter): ToolDisplayApi {
|
|
1536
|
+
const adapters = new Map<string, ToolDisplayAdapter>();
|
|
1537
|
+
const resultRenderMiddlewares = new Map<string, PendingResultRenderMiddleware>();
|
|
1538
|
+
const activeResultRenderPipelines = new Set<string>();
|
|
1539
|
+
let nextAdapterId = 0;
|
|
1540
|
+
let nextResultMiddlewareId = 0;
|
|
1541
|
+
|
|
1542
|
+
const resolveAdapter = (tool: RuntimeToolDefinition, adapter?: ToolDisplayAdapter): ToolDisplayAdapter => {
|
|
1543
|
+
if (adapter) {
|
|
1544
|
+
return adapter;
|
|
1545
|
+
}
|
|
1546
|
+
const toolName = getTextField(tool, "name");
|
|
1547
|
+
if (toolName) {
|
|
1548
|
+
return adapters.get(toolName) ?? {};
|
|
1549
|
+
}
|
|
1550
|
+
return {};
|
|
1551
|
+
};
|
|
1552
|
+
|
|
1553
|
+
const api: ToolDisplayApi = {
|
|
1554
|
+
version: 1,
|
|
1555
|
+
decorateTool<T extends RuntimeToolDefinition>(tool: T, adapter?: ToolDisplayAdapter): T {
|
|
1556
|
+
const resolvedAdapter = resolveAdapter(tool, adapter);
|
|
1557
|
+
const kind = getAdapterKind(tool, resolvedAdapter);
|
|
1558
|
+
const overrideExisting = resolvedAdapter.overrideExistingRenderers === true;
|
|
1559
|
+
const decorated: RuntimeToolDefinition = { ...tool };
|
|
1560
|
+
|
|
1561
|
+
if (resolvedAdapter.renderCall && (overrideExisting || typeof decorated.renderCall !== "function")) {
|
|
1562
|
+
decorated.renderCall = resolvedAdapter.renderCall;
|
|
1563
|
+
} else if (kind === "read" && (overrideExisting || typeof decorated.renderCall !== "function")) {
|
|
1564
|
+
decorated.renderCall = (args: unknown, theme: RenderTheme) => renderReadDisplayCall(args, theme, resolvedAdapter);
|
|
1565
|
+
} else if (kind === "edit" && (overrideExisting || typeof decorated.renderCall !== "function")) {
|
|
1566
|
+
decorated.renderCall = (args: unknown, theme: RenderTheme, context: ToolRenderContextLike) => renderEditDisplayCall(args, theme, context, resolvedAdapter, getConfig);
|
|
1567
|
+
} else if (kind === "mcp" && (overrideExisting || typeof decorated.renderCall !== "function")) {
|
|
1568
|
+
decorated.renderCall = (args: unknown, theme: RenderTheme) => {
|
|
1569
|
+
const toolName = getTextField(decorated, "name") ?? "mcp";
|
|
1570
|
+
const toolLabel = getTextField(decorated, "label") ?? (toolName === "mcp" ? "MCP Proxy" : `MCP ${toolName}`);
|
|
1571
|
+
return formatMcpCallLine(toolName, toolLabel, toRecord(args), theme);
|
|
1572
|
+
};
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
if (resolvedAdapter.renderResult && (overrideExisting || typeof decorated.renderResult !== "function")) {
|
|
1576
|
+
decorated.renderResult = resolvedAdapter.renderResult;
|
|
1577
|
+
} else if (kind === "read" && (overrideExisting || typeof decorated.renderResult !== "function")) {
|
|
1578
|
+
decorated.renderResult = (result: ToolRenderInput, options: ToolRenderResultOptions, theme: RenderTheme) =>
|
|
1579
|
+
renderReadDisplayResult(result, options, getConfig(), theme);
|
|
1580
|
+
} else if (kind === "edit" && (overrideExisting || typeof decorated.renderResult !== "function")) {
|
|
1581
|
+
decorated.renderResult = (result: ToolRenderInput & { isError?: boolean }, options: ToolRenderResultOptions, theme: RenderTheme, context?: ToolRenderContextLike) =>
|
|
1582
|
+
renderEditDisplayResult(result, options, theme, context, resolvedAdapter, getConfig);
|
|
1583
|
+
} else if (kind === "mcp" && (overrideExisting || typeof decorated.renderResult !== "function")) {
|
|
1584
|
+
decorated.renderResult = (result: ToolRenderInput, options: ToolRenderResultOptions, theme: RenderTheme) =>
|
|
1585
|
+
renderMcpResult(result, options, getConfig(), theme);
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
if (kind === "edit" && (overrideExisting || typeof decorated.renderShell !== "string")) {
|
|
1589
|
+
decorated.renderShell = "default";
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
return decorated as T;
|
|
1593
|
+
},
|
|
1594
|
+
registerAdapter(adapter: ToolDisplayAdapter): string {
|
|
1595
|
+
const id = adapter.id || adapter.toolName || `adapter-${++nextAdapterId}`;
|
|
1596
|
+
adapters.set(id, { ...adapter, id });
|
|
1597
|
+
if (adapter.toolName) {
|
|
1598
|
+
adapters.set(adapter.toolName, { ...adapter, id });
|
|
1599
|
+
}
|
|
1600
|
+
return id;
|
|
1601
|
+
},
|
|
1602
|
+
unregisterAdapter(id: string): boolean {
|
|
1603
|
+
const adapter = adapters.get(id);
|
|
1604
|
+
const removed = adapters.delete(id);
|
|
1605
|
+
if (adapter?.toolName) {
|
|
1606
|
+
adapters.delete(adapter.toolName);
|
|
1607
|
+
}
|
|
1608
|
+
return removed;
|
|
1609
|
+
},
|
|
1610
|
+
registerResultRenderMiddleware(registration: ToolResultRenderMiddlewareRegistration): string {
|
|
1611
|
+
const id = registration.id || `result-middleware-${++nextResultMiddlewareId}`;
|
|
1612
|
+
resultRenderMiddlewares.set(id, { ...registration, id });
|
|
1613
|
+
return id;
|
|
1614
|
+
},
|
|
1615
|
+
unregisterResultRenderMiddleware(id: string): boolean {
|
|
1616
|
+
return resultRenderMiddlewares.delete(id);
|
|
1617
|
+
},
|
|
1618
|
+
hasResultRenderMiddleware(id: string): boolean {
|
|
1619
|
+
return resultRenderMiddlewares.has(id);
|
|
1620
|
+
},
|
|
1621
|
+
activateResultRenderPipeline(toolName: string): void {
|
|
1622
|
+
activeResultRenderPipelines.add(toolName);
|
|
1623
|
+
},
|
|
1624
|
+
isResultRenderPipelineActive(toolName: string): boolean {
|
|
1625
|
+
return activeResultRenderPipelines.has(toolName);
|
|
1626
|
+
},
|
|
1627
|
+
renderResultWithMiddleware(
|
|
1628
|
+
context: ToolResultRenderMiddlewareContext,
|
|
1629
|
+
renderBase: () => unknown,
|
|
1630
|
+
): unknown {
|
|
1631
|
+
const middlewares = [...resultRenderMiddlewares.values()]
|
|
1632
|
+
.filter((registration) => registration.toolName === context.toolName || registration.toolName === "*");
|
|
1633
|
+
const dispatch = (index: number): unknown => {
|
|
1634
|
+
const registration = middlewares[index];
|
|
1635
|
+
if (!registration) return renderBase();
|
|
1636
|
+
let nextCalled = false;
|
|
1637
|
+
const next = (): unknown => {
|
|
1638
|
+
nextCalled = true;
|
|
1639
|
+
return dispatch(index + 1);
|
|
1640
|
+
};
|
|
1641
|
+
try {
|
|
1642
|
+
const rendered = registration.middleware(context, next);
|
|
1643
|
+
return rendered === undefined && !nextCalled ? dispatch(index + 1) : rendered;
|
|
1644
|
+
} catch (error) {
|
|
1645
|
+
logToolDisplayDebug(`Result render middleware '${registration.id}' failed.`, error);
|
|
1646
|
+
return nextCalled ? renderBase() : dispatch(index + 1);
|
|
1647
|
+
}
|
|
1648
|
+
};
|
|
1649
|
+
return dispatch(0);
|
|
1650
|
+
},
|
|
1651
|
+
};
|
|
1652
|
+
|
|
1653
|
+
(globalThis as GlobalWithToolDisplayApi)[TOOL_DISPLAY_API_KEY] = api;
|
|
1654
|
+
drainPendingToolDisplayDecorations(api);
|
|
1655
|
+
drainPendingResultRenderMiddlewares(api);
|
|
1656
|
+
return api;
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
function tryGetAllTools(pi: ExtensionAPI, debugMessage: string): unknown[] | undefined {
|
|
1660
|
+
try {
|
|
1661
|
+
return pi.getAllTools();
|
|
1662
|
+
} catch (error) {
|
|
1663
|
+
logToolDisplayDebug(debugMessage, error);
|
|
1664
|
+
return undefined;
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
export function registerToolDisplayOverrides(
|
|
1669
|
+
pi: ExtensionAPI,
|
|
1670
|
+
getConfig: ConfigGetter,
|
|
1671
|
+
): void {
|
|
1672
|
+
clearBuiltInToolCache();
|
|
1673
|
+
const toolDisplayApi = installToolDisplayApi(getConfig);
|
|
1674
|
+
registerCleanup(() => {
|
|
1675
|
+
restoreToolPropertyDescriptors(decoratedToolDescriptors, decoratedTools);
|
|
1676
|
+
const globalWithApi = globalThis as GlobalWithToolDisplayApi;
|
|
1677
|
+
if (globalWithApi[TOOL_DISPLAY_API_KEY] === toolDisplayApi) {
|
|
1678
|
+
delete globalWithApi[TOOL_DISPLAY_API_KEY];
|
|
1679
|
+
}
|
|
1680
|
+
});
|
|
1681
|
+
const bootstrapTools = getBuiltInTools(process.cwd());
|
|
1682
|
+
const builtInPromptMetadata = createLazyPromptMetadata(bootstrapTools);
|
|
1683
|
+
const clonedParameters = createLazyClonedParameters(bootstrapTools);
|
|
1684
|
+
const writeExecutionMetaByToolCallId = new Map<string, WriteExecutionMeta>();
|
|
1685
|
+
const registeredBuiltInToolOverrides = new Set<BuiltInToolOverrideName>();
|
|
1686
|
+
|
|
1687
|
+
const isExternallyOwnedBuiltInTool = (toolName: BuiltInToolOverrideName): boolean => {
|
|
1688
|
+
const allTools = tryGetAllTools(pi, "Built-in tool override ownership discovery unavailable during extension load; registering renderer for pre-bind history rendering.");
|
|
1689
|
+
if (!allTools) {
|
|
1690
|
+
return false;
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
const currentOwner = allTools.find((tool) => getTextField(tool, "name") === toolName);
|
|
1694
|
+
const sourceInfo = toRecord(toRecord(currentOwner).sourceInfo);
|
|
1695
|
+
const source = getTextField(sourceInfo, "source");
|
|
1696
|
+
if (currentOwner && source && source !== "builtin") {
|
|
1697
|
+
logToolDisplayDebug("Skipped built-in tool display override because another tool owner is active.", {
|
|
1698
|
+
toolName,
|
|
1699
|
+
source,
|
|
1700
|
+
path: getTextField(sourceInfo, "path") ?? "unknown",
|
|
1701
|
+
});
|
|
1702
|
+
return true;
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
return false;
|
|
1706
|
+
};
|
|
1707
|
+
|
|
1708
|
+
const registerIfOwned = (
|
|
1709
|
+
toolName: BuiltInToolOverrideName,
|
|
1710
|
+
register: () => void,
|
|
1711
|
+
): void => {
|
|
1712
|
+
if (
|
|
1713
|
+
registeredBuiltInToolOverrides.has(toolName) ||
|
|
1714
|
+
!getConfig().registerToolOverrides[toolName] ||
|
|
1715
|
+
isExternallyOwnedBuiltInTool(toolName)
|
|
1716
|
+
) {
|
|
1717
|
+
return;
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
register();
|
|
1721
|
+
registeredBuiltInToolOverrides.add(toolName);
|
|
1722
|
+
toolDisplayApi.activateResultRenderPipeline(toolName);
|
|
1723
|
+
};
|
|
1724
|
+
|
|
1725
|
+
const executeBuiltin = (
|
|
1726
|
+
toolName: keyof BuiltInTools,
|
|
1727
|
+
toolCallId: string,
|
|
1728
|
+
params: Record<string, unknown>,
|
|
1729
|
+
signal: unknown,
|
|
1730
|
+
onUpdate: unknown,
|
|
1731
|
+
ctx: { cwd: string },
|
|
1732
|
+
): Promise<unknown> => {
|
|
1733
|
+
return getBuiltInTools(ctx.cwd)[toolName].execute(
|
|
1734
|
+
toolCallId,
|
|
1735
|
+
params as never,
|
|
1736
|
+
signal as never,
|
|
1737
|
+
onUpdate as never,
|
|
1738
|
+
);
|
|
1739
|
+
};
|
|
1740
|
+
|
|
1741
|
+
function createBuiltinToolBase(toolName: keyof BuiltInTools) {
|
|
1742
|
+
return {
|
|
1743
|
+
description: bootstrapTools[toolName].description,
|
|
1744
|
+
...builtInPromptMetadata[toolName],
|
|
1745
|
+
parameters: clonedParameters[toolName],
|
|
1746
|
+
prepareArguments: getToolPrepareArguments(bootstrapTools[toolName]),
|
|
1747
|
+
async execute(toolCallId: string, params: Record<string, unknown>, signal: unknown, onUpdate: unknown, ctx: ExtensionContext) {
|
|
1748
|
+
return executeBuiltin(toolName, toolCallId, params, signal, onUpdate, ctx);
|
|
1749
|
+
},
|
|
1750
|
+
};
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
const renderSearchToolResult = (
|
|
1754
|
+
result: ToolRenderInput,
|
|
1755
|
+
options: ToolRenderResultOptions,
|
|
1756
|
+
theme: RenderTheme,
|
|
1757
|
+
unitLabel: string,
|
|
1758
|
+
pluralLabel?: string,
|
|
1759
|
+
): Text => {
|
|
1760
|
+
const config = getConfig();
|
|
1761
|
+
return renderSearchResult(result as never, options, config, theme, unitLabel, result.details, pluralLabel);
|
|
1762
|
+
};
|
|
1763
|
+
|
|
1764
|
+
const renderResultWithMiddleware = (
|
|
1765
|
+
toolName: string,
|
|
1766
|
+
result: unknown,
|
|
1767
|
+
options: ToolRenderResultOptions,
|
|
1768
|
+
theme: RenderTheme,
|
|
1769
|
+
renderContext: ToolRenderContextLike | undefined,
|
|
1770
|
+
renderBase: () => unknown,
|
|
1771
|
+
): unknown => toolDisplayApi.renderResultWithMiddleware(
|
|
1772
|
+
{ toolName, result, options, theme, renderContext },
|
|
1773
|
+
renderBase,
|
|
1774
|
+
);
|
|
1775
|
+
|
|
1776
|
+
const buildSearchCallSuffix = (args: Record<string, unknown>): { scope: string; limitSuffix: string } => {
|
|
1777
|
+
return { scope: getSearchScope(args), limitSuffix: args.limit !== undefined ? ` (limit ${args.limit})` : "" };
|
|
1778
|
+
};
|
|
1779
|
+
|
|
1780
|
+
registerIfOwned("read", () => {
|
|
1781
|
+
registerRuntimeTool(pi, {
|
|
1782
|
+
name: "read",
|
|
1783
|
+
label: "read",
|
|
1784
|
+
...createBuiltinToolBase("read"),
|
|
1785
|
+
renderCall(args, theme) {
|
|
1786
|
+
return renderReadDisplayCall(args, theme);
|
|
1787
|
+
},
|
|
1788
|
+
renderResult(result, options, theme) {
|
|
1789
|
+
return renderResultWithMiddleware("read", result, options, theme, undefined, () =>
|
|
1790
|
+
renderReadDisplayResult(result, options, getConfig(), theme));
|
|
1791
|
+
},
|
|
1792
|
+
});
|
|
1793
|
+
});
|
|
1794
|
+
|
|
1795
|
+
registerIfOwned("grep", () => {
|
|
1796
|
+
registerRuntimeTool(pi, {
|
|
1797
|
+
name: "grep",
|
|
1798
|
+
label: "grep",
|
|
1799
|
+
...createBuiltinToolBase("grep"),
|
|
1800
|
+
renderCall(args, theme) {
|
|
1801
|
+
const scope = getSearchScope(args);
|
|
1802
|
+
const globSuffix = args.glob ? ` (${args.glob})` : "";
|
|
1803
|
+
const limitSuffix =
|
|
1804
|
+
args.limit !== undefined ? ` limit ${args.limit}` : "";
|
|
1805
|
+
return textResult(
|
|
1806
|
+
formatSearchCallLine("grep", `/${args.pattern}/`, ` in ${scope}${globSuffix}${limitSuffix}`, theme),
|
|
1807
|
+
);
|
|
1808
|
+
},
|
|
1809
|
+
renderResult(result, options, theme) {
|
|
1810
|
+
return renderResultWithMiddleware("grep", result, options, theme, undefined, () =>
|
|
1811
|
+
renderSearchToolResult(result, options, theme, "match", "matches"));
|
|
1812
|
+
},
|
|
1813
|
+
});
|
|
1814
|
+
});
|
|
1815
|
+
|
|
1816
|
+
registerIfOwned("find", () => {
|
|
1817
|
+
registerRuntimeTool(pi, {
|
|
1818
|
+
name: "find",
|
|
1819
|
+
label: "find",
|
|
1820
|
+
...createBuiltinToolBase("find"),
|
|
1821
|
+
renderCall(args, theme) {
|
|
1822
|
+
const { scope, limitSuffix } = buildSearchCallSuffix(args);
|
|
1823
|
+
return textResult(
|
|
1824
|
+
formatSearchCallLine("find", args.pattern as string, ` in ${scope}${limitSuffix}`, theme),
|
|
1825
|
+
);
|
|
1826
|
+
},
|
|
1827
|
+
renderResult(result, options, theme) {
|
|
1828
|
+
return renderResultWithMiddleware("find", result, options, theme, undefined, () =>
|
|
1829
|
+
renderSearchToolResult(result, options, theme, "result"));
|
|
1830
|
+
},
|
|
1831
|
+
});
|
|
1832
|
+
});
|
|
1833
|
+
|
|
1834
|
+
registerIfOwned("ls", () => {
|
|
1835
|
+
registerRuntimeTool(pi, {
|
|
1836
|
+
name: "ls",
|
|
1837
|
+
label: "ls",
|
|
1838
|
+
...createBuiltinToolBase("ls"),
|
|
1839
|
+
renderCall(args, theme) {
|
|
1840
|
+
const { scope, limitSuffix } = buildSearchCallSuffix(args);
|
|
1841
|
+
return textResult(formatSearchCallLine("ls", scope, limitSuffix, theme));
|
|
1842
|
+
},
|
|
1843
|
+
renderResult(result, options, theme) {
|
|
1844
|
+
return renderResultWithMiddleware("ls", result, options, theme, undefined, () =>
|
|
1845
|
+
renderSearchToolResult(result, options, theme, "entry", "entries"));
|
|
1846
|
+
},
|
|
1847
|
+
});
|
|
1848
|
+
});
|
|
1849
|
+
|
|
1850
|
+
registerIfOwned("edit", () => {
|
|
1851
|
+
registerRuntimeTool(pi, {
|
|
1852
|
+
name: "edit",
|
|
1853
|
+
label: "edit",
|
|
1854
|
+
description: bootstrapTools.edit.description,
|
|
1855
|
+
...builtInPromptMetadata.edit,
|
|
1856
|
+
parameters: clonedParameters.edit,
|
|
1857
|
+
renderShell: "default",
|
|
1858
|
+
prepareArguments: getToolPrepareArguments(bootstrapTools.edit),
|
|
1859
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
1860
|
+
return executeBuiltin("edit", toolCallId, params, signal, onUpdate, ctx);
|
|
1861
|
+
},
|
|
1862
|
+
renderCall(args, theme, context) {
|
|
1863
|
+
return renderEditDisplayCall(args, theme, context, {}, getConfig);
|
|
1864
|
+
},
|
|
1865
|
+
renderResult(result, options, theme, context) {
|
|
1866
|
+
return renderResultWithMiddleware("edit", result, options, theme, context, () =>
|
|
1867
|
+
renderEditDisplayResult(result as never, options, theme, context, {}, getConfig));
|
|
1868
|
+
},
|
|
1869
|
+
});
|
|
1870
|
+
});
|
|
1871
|
+
|
|
1872
|
+
registerIfOwned("write", () => {
|
|
1873
|
+
registerRuntimeTool(pi, {
|
|
1874
|
+
name: "write",
|
|
1875
|
+
label: "write",
|
|
1876
|
+
description: bootstrapTools.write.description,
|
|
1877
|
+
...builtInPromptMetadata.write,
|
|
1878
|
+
parameters: clonedParameters.write,
|
|
1879
|
+
prepareArguments: getToolPrepareArguments(bootstrapTools.write),
|
|
1880
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
1881
|
+
const previous = captureExistingWriteContent(ctx.cwd, params.path);
|
|
1882
|
+
recordWriteExecutionMeta(writeExecutionMetaByToolCallId, toolCallId, {
|
|
1883
|
+
fileExistedBeforeWrite: previous.existed,
|
|
1884
|
+
previousContent: previous.content,
|
|
1885
|
+
});
|
|
1886
|
+
|
|
1887
|
+
return executeBuiltin("write", toolCallId, params, signal, onUpdate, ctx);
|
|
1888
|
+
},
|
|
1889
|
+
renderCall(args, theme, context) {
|
|
1890
|
+
const content = getToolContentArg(args);
|
|
1891
|
+
const lineCount = countWriteContentLines(content);
|
|
1892
|
+
const sizeBytes = getWriteContentSizeBytes(content);
|
|
1893
|
+
const path = shortenPath(getToolPathArg(args));
|
|
1894
|
+
const suffix = shouldRenderWriteCallSummary({
|
|
1895
|
+
hasContent: content !== undefined,
|
|
1896
|
+
hasDetailedResultHeader: false,
|
|
1897
|
+
})
|
|
1898
|
+
? formatWriteCallSuffix(lineCount, sizeBytes, theme)
|
|
1899
|
+
: "";
|
|
1900
|
+
const summaryText = `${theme.fg("toolTitle", theme.bold("write"))} ${theme.fg("accent", path || "...")}${suffix}`;
|
|
1901
|
+
if (!context.argsComplete || !context.isPartial) {
|
|
1902
|
+
return textResult(summaryText);
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
const previewKey = JSON.stringify({ path: getToolPathArg(args) ?? null, content: content ?? null });
|
|
1906
|
+
const previewData = resolvePendingDiffPreview(
|
|
1907
|
+
context,
|
|
1908
|
+
WRITE_PENDING_PREVIEW_STATE_KEY,
|
|
1909
|
+
previewKey,
|
|
1910
|
+
() => buildPendingWritePreviewData(args, context.cwd),
|
|
1911
|
+
);
|
|
1912
|
+
return buildPendingDiffCallComponent(summaryText, previewData, context, getConfig(), theme);
|
|
1913
|
+
},
|
|
1914
|
+
renderResult(result, options, theme, context) {
|
|
1915
|
+
return renderResultWithMiddleware("write", result, options, theme, context, () => {
|
|
1916
|
+
const content = getToolContentArg(context?.args);
|
|
1917
|
+
const lineCount = countWriteContentLines(content);
|
|
1918
|
+
const { fallbackText, earlyResult } = handleEditOrWriteResult(result, options, context, theme, lineCount, "writing", "Write failed.");
|
|
1919
|
+
if (earlyResult) {
|
|
1920
|
+
return earlyResult;
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
const config = getConfig();
|
|
1924
|
+
const executionMeta = getWriteExecutionMeta(
|
|
1925
|
+
context,
|
|
1926
|
+
writeExecutionMetaByToolCallId,
|
|
1927
|
+
);
|
|
1928
|
+
return renderWriteDiffResult(
|
|
1929
|
+
content,
|
|
1930
|
+
{
|
|
1931
|
+
expanded: options.expanded,
|
|
1932
|
+
filePath: getToolPathArg(context?.args),
|
|
1933
|
+
previousContent: executionMeta?.previousContent,
|
|
1934
|
+
fileExistedBeforeWrite: executionMeta?.fileExistedBeforeWrite ?? false,
|
|
1935
|
+
},
|
|
1936
|
+
config,
|
|
1937
|
+
theme,
|
|
1938
|
+
fallbackText,
|
|
1939
|
+
);
|
|
1940
|
+
});
|
|
1941
|
+
},
|
|
1942
|
+
});
|
|
1943
|
+
});
|
|
1944
|
+
|
|
1945
|
+
registerIfOwned("bash", () => {
|
|
1946
|
+
registerRuntimeTool(pi, {
|
|
1947
|
+
name: "bash",
|
|
1948
|
+
label: "bash",
|
|
1949
|
+
...createBuiltinToolBase("bash"),
|
|
1950
|
+
renderCall(args, theme, context) {
|
|
1951
|
+
return renderBashCall(args, theme, context as never);
|
|
1952
|
+
},
|
|
1953
|
+
renderResult(result, options, theme, context) {
|
|
1954
|
+
return renderResultWithMiddleware("bash", result, options, theme, context, () => {
|
|
1955
|
+
const config = getConfig();
|
|
1956
|
+
const details = result.details as BashToolDetails | undefined;
|
|
1957
|
+
const rawOutput = extractTextOutput(result);
|
|
1958
|
+
|
|
1959
|
+
if (options.isPartial) {
|
|
1960
|
+
return renderBashLivePreview(rawOutput, options, config, theme, details);
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
if (isToolError(result, context)) {
|
|
1964
|
+
return renderBashErrorResult(rawOutput, options, config, theme, details);
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
const lines = prepareOutputLines(rawOutput, options);
|
|
1968
|
+
|
|
1969
|
+
if (lines.length === 0) {
|
|
1970
|
+
let text = formatBashNoOutputLine(getStringField(context?.args, "command"), theme);
|
|
1971
|
+
if (config.showTruncationHints) {
|
|
1972
|
+
text += formatBashTruncationHints(details, theme);
|
|
1973
|
+
}
|
|
1974
|
+
return textResult(text);
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
if (config.bashOutputMode === "summary") {
|
|
1978
|
+
if (options.expanded) {
|
|
1979
|
+
const maxLines = getExpandedPreviewLineLimit(lines, config);
|
|
1980
|
+
return renderBashPreviewWithHints(lines, maxLines, config, theme, options, details);
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
let summary = formatBashSummary(
|
|
1984
|
+
lines,
|
|
1985
|
+
details,
|
|
1986
|
+
theme,
|
|
1987
|
+
config.showTruncationHints,
|
|
1988
|
+
);
|
|
1989
|
+
summary += formatExpandHint(theme);
|
|
1990
|
+
if (config.showTruncationHints) {
|
|
1991
|
+
summary += formatBashTruncationHints(details, theme);
|
|
1992
|
+
}
|
|
1993
|
+
return textResult(summary);
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
if (config.bashOutputMode === "preview") {
|
|
1997
|
+
const maxLines = options.expanded
|
|
1998
|
+
? getExpandedPreviewLineLimit(lines, config)
|
|
1999
|
+
: config.previewLines;
|
|
2000
|
+
return renderBashPreviewWithHints(lines, maxLines, config, theme, options, details);
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
if (!options.expanded && config.bashCollapsedLines === 0) {
|
|
2004
|
+
let hidden = theme.fg("muted", "↳ output hidden");
|
|
2005
|
+
if (config.showTruncationHints) {
|
|
2006
|
+
hidden += formatBashTruncationHints(details, theme);
|
|
2007
|
+
}
|
|
2008
|
+
return textResult(hidden);
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
const maxLines = options.expanded
|
|
2012
|
+
? lines.length
|
|
2013
|
+
: config.bashCollapsedLines;
|
|
2014
|
+
let text = buildPreviewText(lines, maxLines, theme, options.expanded);
|
|
2015
|
+
if (config.showTruncationHints) {
|
|
2016
|
+
text += formatBashTruncationHints(details, theme);
|
|
2017
|
+
}
|
|
2018
|
+
return textResult(text);
|
|
2019
|
+
});
|
|
2020
|
+
},
|
|
2021
|
+
});
|
|
2022
|
+
});
|
|
2023
|
+
|
|
2024
|
+
const wrappedCustomToolNames = new Set<string>();
|
|
2025
|
+
registerCleanup(() => wrappedCustomToolNames.clear());
|
|
2026
|
+
|
|
2027
|
+
const getCustomOverrideForCandidate = (candidate: unknown): {
|
|
2028
|
+
toolName: string;
|
|
2029
|
+
override: CustomToolOverrideConfig;
|
|
2030
|
+
} | undefined => {
|
|
2031
|
+
const toolName = getTextField(candidate, "name");
|
|
2032
|
+
if (!toolName) {
|
|
2033
|
+
return undefined;
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
const override = getRuntimeCustomToolOverride(toolName, getConfig());
|
|
2037
|
+
if (!override?.enabled) {
|
|
2038
|
+
return undefined;
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
return { toolName, override };
|
|
2042
|
+
};
|
|
2043
|
+
|
|
2044
|
+
const decorateCustomToolOverrideCandidate = (candidate: unknown): boolean => {
|
|
2045
|
+
const customOverride = getCustomOverrideForCandidate(candidate);
|
|
2046
|
+
if (!customOverride || wrappedCustomToolNames.has(customOverride.toolName)) {
|
|
2047
|
+
return customOverride !== undefined;
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
const { toolName, override } = customOverride;
|
|
2051
|
+
const runtimeTool = candidate as RuntimeToolDefinition;
|
|
2052
|
+
applyToolDisplayDecorationInPlace(
|
|
2053
|
+
runtimeTool,
|
|
2054
|
+
toolDisplayApi,
|
|
2055
|
+
{
|
|
2056
|
+
kind: override.kind,
|
|
2057
|
+
overrideExistingRenderers: true,
|
|
2058
|
+
renderCall(args, theme) {
|
|
2059
|
+
if (override.kind === "mcp") {
|
|
2060
|
+
return formatMcpCallLine("mcp", "MCP Proxy", toRecord(args), theme);
|
|
2061
|
+
}
|
|
2062
|
+
return formatGenericToolCallLine(toolName, args, theme);
|
|
2063
|
+
},
|
|
2064
|
+
renderResult(result, options, theme) {
|
|
2065
|
+
return renderCustomToolResult(
|
|
2066
|
+
result as ToolRenderInput,
|
|
2067
|
+
options,
|
|
2068
|
+
getConfig(),
|
|
2069
|
+
override.outputMode,
|
|
2070
|
+
theme,
|
|
2071
|
+
);
|
|
2072
|
+
},
|
|
2073
|
+
},
|
|
2074
|
+
);
|
|
2075
|
+
|
|
2076
|
+
wrappedCustomToolNames.add(toolName);
|
|
2077
|
+
return true;
|
|
2078
|
+
};
|
|
2079
|
+
|
|
2080
|
+
const wrappedMcpToolNames = new Set<string>();
|
|
2081
|
+
registerCleanup(() => wrappedMcpToolNames.clear());
|
|
2082
|
+
|
|
2083
|
+
const decorateMcpToolCandidate = (candidate: unknown): void => {
|
|
2084
|
+
if (getCustomOverrideForCandidate(candidate)) {
|
|
2085
|
+
return;
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
if (!isMcpToolCandidate(candidate)) {
|
|
2089
|
+
return;
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
const toolName = getTextField(candidate, "name");
|
|
2093
|
+
if (!toolName || wrappedMcpToolNames.has(toolName)) {
|
|
2094
|
+
return;
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
const toolRecord = toRecord(candidate);
|
|
2098
|
+
const prepareArgumentsDelegate =
|
|
2099
|
+
typeof toolRecord.prepareArguments === "function"
|
|
2100
|
+
? (toolRecord.prepareArguments as (args: unknown) => unknown)
|
|
2101
|
+
: undefined;
|
|
2102
|
+
const toolLabel =
|
|
2103
|
+
getTextField(candidate, "label") ||
|
|
2104
|
+
(toolName === "mcp" ? "MCP Proxy" : `MCP ${toolName}`);
|
|
2105
|
+
const toolDescription =
|
|
2106
|
+
getTextField(candidate, "description") || "MCP tool";
|
|
2107
|
+
const parameters = toRecord(toolRecord.parameters);
|
|
2108
|
+
|
|
2109
|
+
const promptMetadata =
|
|
2110
|
+
toolName === "mcp"
|
|
2111
|
+
? {
|
|
2112
|
+
promptSnippet: MCP_PROXY_PROMPT_SNIPPET,
|
|
2113
|
+
promptGuidelines: [...MCP_PROXY_PROMPT_GUIDELINES],
|
|
2114
|
+
}
|
|
2115
|
+
: {
|
|
2116
|
+
promptSnippet: buildPromptSnippetFromDescription(
|
|
2117
|
+
toolDescription,
|
|
2118
|
+
`Call MCP tool '${toolName}'.`,
|
|
2119
|
+
),
|
|
2120
|
+
};
|
|
2121
|
+
|
|
2122
|
+
const runtimeTool = candidate as RuntimeToolDefinition;
|
|
2123
|
+
applyToolDisplayDecorationInPlace(
|
|
2124
|
+
runtimeTool,
|
|
2125
|
+
toolDisplayApi,
|
|
2126
|
+
{
|
|
2127
|
+
kind: "mcp",
|
|
2128
|
+
overrideExistingRenderers: true,
|
|
2129
|
+
renderCall(args, theme) {
|
|
2130
|
+
return formatMcpCallLine(toolName, toolLabel, toRecord(args), theme);
|
|
2131
|
+
},
|
|
2132
|
+
renderResult(result, options, theme) {
|
|
2133
|
+
return renderMcpResult(
|
|
2134
|
+
result as ToolRenderInput,
|
|
2135
|
+
options,
|
|
2136
|
+
getConfig(),
|
|
2137
|
+
theme,
|
|
2138
|
+
);
|
|
2139
|
+
},
|
|
2140
|
+
},
|
|
2141
|
+
);
|
|
2142
|
+
Object.assign(runtimeTool, {
|
|
2143
|
+
label: toolLabel,
|
|
2144
|
+
description: toolDescription,
|
|
2145
|
+
...promptMetadata,
|
|
2146
|
+
parameters,
|
|
2147
|
+
prepareArguments: prepareArgumentsDelegate,
|
|
2148
|
+
});
|
|
2149
|
+
|
|
2150
|
+
wrappedMcpToolNames.add(toolName);
|
|
2151
|
+
};
|
|
2152
|
+
|
|
2153
|
+
const installMcpRegistrationInterceptor = (): void => {
|
|
2154
|
+
const piWithInterception = pi as PiWithRegisterToolInterception;
|
|
2155
|
+
const existingInterception = piWithInterception[TOOL_DISPLAY_REGISTER_TOOL_INTERCEPTOR_KEY];
|
|
2156
|
+
if (existingInterception && pi.registerTool === existingInterception.wrapped) {
|
|
2157
|
+
pi.registerTool = existingInterception.original;
|
|
2158
|
+
delete piWithInterception[TOOL_DISPLAY_REGISTER_TOOL_INTERCEPTOR_KEY];
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
const originalRegisterTool = pi.registerTool;
|
|
2162
|
+
const wrappedRegisterTool = function registerToolWithMcpDecoration(
|
|
2163
|
+
this: ExtensionAPI,
|
|
2164
|
+
tool: ToolDefinition,
|
|
2165
|
+
): void {
|
|
2166
|
+
originalRegisterTool.call(this, tool);
|
|
2167
|
+
try {
|
|
2168
|
+
if (!decorateCustomToolOverrideCandidate(tool)) {
|
|
2169
|
+
decorateMcpToolCandidate(tool);
|
|
2170
|
+
}
|
|
2171
|
+
} catch (error) {
|
|
2172
|
+
logToolDisplayDebug("Tool display registration decoration failed.", error);
|
|
2173
|
+
}
|
|
2174
|
+
} as ExtensionAPI["registerTool"];
|
|
2175
|
+
|
|
2176
|
+
pi.registerTool = wrappedRegisterTool;
|
|
2177
|
+
piWithInterception[TOOL_DISPLAY_REGISTER_TOOL_INTERCEPTOR_KEY] = {
|
|
2178
|
+
original: originalRegisterTool,
|
|
2179
|
+
wrapped: wrappedRegisterTool,
|
|
2180
|
+
};
|
|
2181
|
+
|
|
2182
|
+
registerCleanup(() => {
|
|
2183
|
+
if (pi.registerTool === wrappedRegisterTool) {
|
|
2184
|
+
pi.registerTool = originalRegisterTool;
|
|
2185
|
+
}
|
|
2186
|
+
const currentInterception = piWithInterception[TOOL_DISPLAY_REGISTER_TOOL_INTERCEPTOR_KEY];
|
|
2187
|
+
if (currentInterception?.wrapped === wrappedRegisterTool) {
|
|
2188
|
+
delete piWithInterception[TOOL_DISPLAY_REGISTER_TOOL_INTERCEPTOR_KEY];
|
|
2189
|
+
}
|
|
2190
|
+
});
|
|
2191
|
+
};
|
|
2192
|
+
|
|
2193
|
+
installMcpRegistrationInterceptor();
|
|
2194
|
+
|
|
2195
|
+
const registerMcpToolOverrides = (): void => {
|
|
2196
|
+
const allTools = tryGetAllTools(pi, "MCP tool override discovery failed.");
|
|
2197
|
+
if (!allTools) {
|
|
2198
|
+
return;
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
for (const candidate of allTools) {
|
|
2202
|
+
if (!decorateCustomToolOverrideCandidate(candidate)) {
|
|
2203
|
+
decorateMcpToolCandidate(candidate);
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
};
|
|
2207
|
+
|
|
2208
|
+
const mcpDiscoveryRetryTimers = new Set<ReturnType<typeof setTimeout> & { unref?: () => void }>();
|
|
2209
|
+
registerCleanup(() => {
|
|
2210
|
+
for (const timer of mcpDiscoveryRetryTimers) {
|
|
2211
|
+
clearTimeout(timer);
|
|
2212
|
+
}
|
|
2213
|
+
mcpDiscoveryRetryTimers.clear();
|
|
2214
|
+
});
|
|
2215
|
+
|
|
2216
|
+
const scheduleMcpToolOverrideDiscovery = (): void => {
|
|
2217
|
+
for (const delayMs of [25, 75, 150, 300]) {
|
|
2218
|
+
const timer = setTimeout(() => {
|
|
2219
|
+
mcpDiscoveryRetryTimers.delete(timer);
|
|
2220
|
+
registerMcpToolOverrides();
|
|
2221
|
+
}, delayMs) as ReturnType<typeof setTimeout> & { unref?: () => void };
|
|
2222
|
+
mcpDiscoveryRetryTimers.add(timer);
|
|
2223
|
+
timer.unref?.();
|
|
2224
|
+
}
|
|
2225
|
+
};
|
|
2226
|
+
|
|
2227
|
+
pi.on("session_start", async () => {
|
|
2228
|
+
clearWriteExecutionMeta(writeExecutionMetaByToolCallId);
|
|
2229
|
+
registerMcpToolOverrides();
|
|
2230
|
+
scheduleMcpToolOverrideDiscovery();
|
|
2231
|
+
});
|
|
2232
|
+
pi.on("before_agent_start", async () => {
|
|
2233
|
+
clearWriteExecutionMeta(writeExecutionMetaByToolCallId);
|
|
2234
|
+
registerMcpToolOverrides();
|
|
2235
|
+
scheduleMcpToolOverrideDiscovery();
|
|
2236
|
+
});
|
|
2237
|
+
}
|