pi-compact-tools 0.5.0 → 0.5.7
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/LICENSE +21 -21
- package/README.md +11 -10
- package/examples/compact-tools-2.json +0 -1
- package/examples/compact-tools.json +0 -1
- package/extensions/compact-tools-config.ts +163 -0
- package/extensions/compact-tools-core.ts +0 -9
- package/extensions/compact-tools-input.ts +8 -0
- package/extensions/compact-tools-invocation.ts +59 -0
- package/extensions/compact-tools-layout.ts +123 -0
- package/extensions/compact-tools-runtime.ts +187 -0
- package/extensions/compact-tools-types.ts +39 -0
- package/extensions/compact-tools.ts +149 -639
- package/package.json +64 -64
- package/schemas/compact-tools.schema.json +0 -7
- package/scripts/check-resources.mjs +6 -0
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
/** Compact rendering and shared status indicators for built-in tools. */
|
|
2
|
-
|
|
3
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
-
import { join } from "node:path";
|
|
5
1
|
import type {
|
|
6
2
|
AgentToolResult,
|
|
7
3
|
BashToolDetails,
|
|
@@ -13,7 +9,6 @@ import type {
|
|
|
13
9
|
ToolRenderResultOptions,
|
|
14
10
|
} from "@earendil-works/pi-coding-agent";
|
|
15
11
|
import {
|
|
16
|
-
CONFIG_DIR_NAME,
|
|
17
12
|
createBashToolDefinition,
|
|
18
13
|
createEditToolDefinition,
|
|
19
14
|
createFindToolDefinition,
|
|
@@ -22,395 +17,57 @@ import {
|
|
|
22
17
|
createPowerShellToolDefinition,
|
|
23
18
|
createReadToolDefinition,
|
|
24
19
|
createWriteToolDefinition,
|
|
25
|
-
getAgentDir,
|
|
26
20
|
} from "@earendil-works/pi-coding-agent";
|
|
27
21
|
import type { Component } from "@earendil-works/pi-tui";
|
|
28
|
-
import { Container
|
|
22
|
+
import { Container } from "@earendil-works/pi-tui";
|
|
29
23
|
import {
|
|
30
24
|
classifyCallStatus,
|
|
31
25
|
formatDurationMs,
|
|
32
26
|
HEX_COLOR_PATTERN,
|
|
33
27
|
normalizeLineEndings,
|
|
34
|
-
parseDurationIndicators,
|
|
35
28
|
rgbToAnsi256,
|
|
36
29
|
selectDurationIndicator,
|
|
37
|
-
shouldExpandAll,
|
|
38
30
|
type DurationIndicatorConfig,
|
|
39
31
|
type RowStatus,
|
|
40
32
|
type ThemeDurationIndicatorColor,
|
|
41
33
|
} from "./compact-tools-core.ts";
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
find: true,
|
|
71
|
-
ls: true,
|
|
72
|
-
},
|
|
73
|
-
previewLines: 10,
|
|
74
|
-
spinner: {
|
|
75
|
-
frames: ["◐", "◓", "◑", "◒"],
|
|
76
|
-
intervalMs: 120,
|
|
77
|
-
},
|
|
78
|
-
durationIndicators: [
|
|
79
|
-
{ underMs: 1_000, icon: "⚡️", color: "warning" },
|
|
80
|
-
{ underMs: 10_000, icon: "🔥", color: "#D95C3F" },
|
|
81
|
-
{ underMs: 30_000, icon: "○", color: "#79C0FF" },
|
|
82
|
-
{ icon: "⏳", color: "#D2A8FF" },
|
|
83
|
-
],
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
let config = DEFAULT_CONFIG;
|
|
87
|
-
let configRevision: object = {};
|
|
88
|
-
let animateRows = false;
|
|
34
|
+
import { isFullscreenMode, loadConfig } from "./compact-tools-config.ts";
|
|
35
|
+
import { classifyToggleInput } from "./compact-tools-input.ts";
|
|
36
|
+
import {
|
|
37
|
+
getArgumentDetails,
|
|
38
|
+
getCallDetails,
|
|
39
|
+
getFileOutput,
|
|
40
|
+
getTextResult,
|
|
41
|
+
} from "./compact-tools-invocation.ts";
|
|
42
|
+
import {
|
|
43
|
+
prefixedText,
|
|
44
|
+
renderArguments,
|
|
45
|
+
renderOutput,
|
|
46
|
+
renderToolCall,
|
|
47
|
+
styleMultiline,
|
|
48
|
+
wrapEditResult,
|
|
49
|
+
} from "./compact-tools-layout.ts";
|
|
50
|
+
import { ToolRuntime } from "./compact-tools-runtime.ts";
|
|
51
|
+
import type {
|
|
52
|
+
BuiltInDefinition,
|
|
53
|
+
CompactToolName,
|
|
54
|
+
RenderContext,
|
|
55
|
+
RowState,
|
|
56
|
+
ShellToolName,
|
|
57
|
+
ToolArgs,
|
|
58
|
+
} from "./compact-tools-types.ts";
|
|
59
|
+
|
|
60
|
+
const runtime = new ToolRuntime();
|
|
61
|
+
const registeredTools = new Set<CompactToolName>();
|
|
89
62
|
let unsubscribeTerminalInput: (() => void) | undefined;
|
|
90
|
-
const registeredCompactTools = new Set<CompactToolName>();
|
|
91
|
-
|
|
92
|
-
interface RowState {
|
|
93
|
-
level?: number;
|
|
94
|
-
availableLevels?: number[];
|
|
95
|
-
lastExpanded?: boolean;
|
|
96
|
-
configRevision?: object;
|
|
97
|
-
frame?: number;
|
|
98
|
-
startedAt?: number;
|
|
99
|
-
endedAt?: number;
|
|
100
|
-
timer?: ReturnType<typeof setInterval>;
|
|
101
|
-
originalResultComponent?: Component;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
type ToolArgs = Record<string, unknown>;
|
|
105
|
-
type BuiltInDefinition = ToolDefinition<any, any, any>;
|
|
106
|
-
type BaseRenderContext = Parameters<NonNullable<BuiltInDefinition["renderCall"]>>[2];
|
|
107
|
-
type RenderContext<TArgs = ToolArgs> = Omit<BaseRenderContext, "args" | "state"> & {
|
|
108
|
-
args: TArgs;
|
|
109
|
-
state: RowState;
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
type JsonObject = Record<string, unknown>;
|
|
113
|
-
|
|
114
|
-
function isObject(value: unknown): value is JsonObject {
|
|
115
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function warnConfig(path: string, message: string): void {
|
|
119
|
-
console.error(`[compact-tools] ${path}: ${message}`);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function isIntegerInRange(value: unknown, minimum: number, maximum: number): value is number {
|
|
123
|
-
return typeof value === "number" && Number.isInteger(value) && value >= minimum && value <= maximum;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function isNonEmptyStringArray(value: unknown): value is string[] {
|
|
127
|
-
return Array.isArray(value) && value.length > 0 && value.every((item) => typeof item === "string" && item.length > 0);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function parseTools(value: unknown, path: string): CompactToolName[] | undefined {
|
|
131
|
-
if (!Array.isArray(value)) {
|
|
132
|
-
warnConfig(path, "tools must be an array; using previous values");
|
|
133
|
-
return undefined;
|
|
134
|
-
}
|
|
135
|
-
const valid = value.filter((item): item is CompactToolName => typeof item === "string" && SUPPORTED_TOOL_SET.has(item));
|
|
136
|
-
const invalid = value.filter((item) => typeof item !== "string" || !SUPPORTED_TOOL_SET.has(item));
|
|
137
|
-
if (invalid.length > 0) warnConfig(path, `ignoring unsupported tools: ${invalid.map(String).join(", ")}`);
|
|
138
|
-
return [...new Set(valid)];
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function parseAutoCompact(
|
|
142
|
-
base: CompactToolsConfig["auto_compact"],
|
|
143
|
-
value: unknown,
|
|
144
|
-
path: string,
|
|
145
|
-
): CompactToolsConfig["auto_compact"] {
|
|
146
|
-
if (value === undefined) return base;
|
|
147
|
-
if (!isObject(value)) {
|
|
148
|
-
warnConfig(path, "auto_compact must be an object; using previous values");
|
|
149
|
-
return base;
|
|
150
|
-
}
|
|
151
|
-
const autoCompact = { ...base };
|
|
152
|
-
for (const [name, enabled] of Object.entries(value)) {
|
|
153
|
-
if (!SUPPORTED_TOOL_SET.has(name)) {
|
|
154
|
-
warnConfig(path, `ignoring unsupported auto_compact tool: ${name}`);
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
if (typeof enabled !== "boolean") {
|
|
158
|
-
warnConfig(path, `auto_compact.${name} must be true or false; using previous value`);
|
|
159
|
-
continue;
|
|
160
|
-
}
|
|
161
|
-
autoCompact[name as CompactToolName] = enabled;
|
|
162
|
-
}
|
|
163
|
-
return autoCompact;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function mergeConfig(base: CompactToolsConfig, value: unknown, path: string): CompactToolsConfig {
|
|
167
|
-
if (value === undefined) return base;
|
|
168
|
-
if (!isObject(value)) {
|
|
169
|
-
warnConfig(path, "expected a JSON object; using previous values");
|
|
170
|
-
return base;
|
|
171
|
-
}
|
|
172
|
-
const tools = value.tools === undefined ? base.tools : (parseTools(value.tools, path) ?? base.tools);
|
|
173
|
-
const auto_compact = parseAutoCompact(base.auto_compact, value.auto_compact, path);
|
|
174
|
-
const previewValue = value.previewLines;
|
|
175
|
-
const validPreviewLines = isIntegerInRange(previewValue, 1, 1_000);
|
|
176
|
-
const previewLines = validPreviewLines ? previewValue : base.previewLines;
|
|
177
|
-
if (previewValue !== undefined && !validPreviewLines) {
|
|
178
|
-
warnConfig(path, "previewLines must be an integer from 1 to 1000; using previous value");
|
|
179
|
-
}
|
|
180
|
-
const spinner = parseSpinnerConfig(base.spinner, value.spinner, path);
|
|
181
|
-
const durationIndicators = parseDurationIndicators(value.durationIndicators) ?? base.durationIndicators;
|
|
182
|
-
if (value.durationIndicators !== undefined && durationIndicators === base.durationIndicators) {
|
|
183
|
-
warnConfig(path, "invalid durationIndicators; using previous values");
|
|
184
|
-
}
|
|
185
|
-
return { tools, auto_compact, previewLines, spinner, durationIndicators };
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function parseSpinnerConfig(
|
|
189
|
-
base: CompactToolsConfig["spinner"],
|
|
190
|
-
value: unknown,
|
|
191
|
-
path: string,
|
|
192
|
-
): CompactToolsConfig["spinner"] {
|
|
193
|
-
if (value === undefined) return base;
|
|
194
|
-
if (!isObject(value)) {
|
|
195
|
-
warnConfig(path, "spinner must be an object; using previous values");
|
|
196
|
-
return base;
|
|
197
|
-
}
|
|
198
|
-
const framesValue = value.frames;
|
|
199
|
-
const intervalValue = value.intervalMs;
|
|
200
|
-
const validFrames = isNonEmptyStringArray(framesValue);
|
|
201
|
-
const validInterval = isIntegerInRange(intervalValue, 40, 5_000);
|
|
202
|
-
if (framesValue !== undefined && !validFrames) warnConfig(path, "spinner.frames must be a non-empty string array");
|
|
203
|
-
if (intervalValue !== undefined && !validInterval) warnConfig(path, "spinner.intervalMs must be 40–5000");
|
|
204
|
-
return {
|
|
205
|
-
frames: validFrames ? framesValue : base.frames,
|
|
206
|
-
intervalMs: validInterval ? intervalValue : base.intervalMs,
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function readConfig(path: string): unknown {
|
|
211
|
-
if (!existsSync(path)) return undefined;
|
|
212
|
-
try {
|
|
213
|
-
return JSON.parse(readFileSync(path, "utf8"));
|
|
214
|
-
} catch (error) {
|
|
215
|
-
warnConfig(path, error instanceof Error ? error.message : String(error));
|
|
216
|
-
return undefined;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function getConfiguredTuiMode(): "regular" | "fullscreen" {
|
|
221
|
-
for (let index = 0; index < process.argv.length; index++) {
|
|
222
|
-
const argument = process.argv[index];
|
|
223
|
-
if (argument === "--tui-mode") {
|
|
224
|
-
const value = process.argv[index + 1];
|
|
225
|
-
if (value === "regular" || value === "fullscreen") return value;
|
|
226
|
-
}
|
|
227
|
-
if (argument?.startsWith("--tui-mode=")) {
|
|
228
|
-
const value = argument.slice("--tui-mode=".length);
|
|
229
|
-
if (value === "regular" || value === "fullscreen") return value;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const settings = readConfig(join(getAgentDir(), "settings.json"));
|
|
234
|
-
return isObject(settings) && settings.tuiMode === "fullscreen" ? "fullscreen" : "regular";
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
export function loadConfig(cwd?: string, projectTrusted = false): CompactToolsConfig {
|
|
238
|
-
const globalPath = join(getAgentDir(), CONFIG_FILE);
|
|
239
|
-
let loaded = mergeConfig(DEFAULT_CONFIG, readConfig(globalPath), globalPath);
|
|
240
|
-
if (!cwd || !projectTrusted) return loaded;
|
|
241
|
-
const projectPath = join(cwd, CONFIG_DIR_NAME, CONFIG_FILE);
|
|
242
|
-
loaded = mergeConfig(loaded, readConfig(projectPath), projectPath);
|
|
243
|
-
return loaded;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const SPINNING_ROWS_KEY = Symbol.for("pi.compact-tools.spinning-rows");
|
|
247
|
-
const EXECUTION_TIMINGS_KEY = Symbol.for("pi.compact-tools.execution-timings");
|
|
248
|
-
const MAX_TRACKED_ROWS = 2_000;
|
|
249
|
-
type ExecutionTiming = { startedAt: number; endedAt?: number };
|
|
250
|
-
type TrackedRow = {
|
|
251
|
-
state: RowState;
|
|
252
|
-
name: CompactToolName;
|
|
253
|
-
invalidate: () => void;
|
|
254
|
-
};
|
|
255
|
-
const globalState = globalThis as typeof globalThis & {
|
|
256
|
-
[SPINNING_ROWS_KEY]?: Set<RowState>;
|
|
257
|
-
[EXECUTION_TIMINGS_KEY]?: Map<string, ExecutionTiming>;
|
|
258
|
-
};
|
|
259
|
-
for (const staleState of globalState[SPINNING_ROWS_KEY] ?? []) {
|
|
260
|
-
if (staleState.timer) clearInterval(staleState.timer);
|
|
261
|
-
}
|
|
262
|
-
const spinningRows = new Set<RowState>();
|
|
263
|
-
const executionTimings = globalState[EXECUTION_TIMINGS_KEY] ?? new Map<string, ExecutionTiming>();
|
|
264
|
-
const trackedRows = new Map<string, TrackedRow>();
|
|
265
|
-
globalState[SPINNING_ROWS_KEY] = spinningRows;
|
|
266
|
-
globalState[EXECUTION_TIMINGS_KEY] = executionTimings;
|
|
267
|
-
|
|
268
|
-
function trackRow(ctx: RenderContext, name: CompactToolName, state: RowState): void {
|
|
269
|
-
trackedRows.delete(ctx.toolCallId);
|
|
270
|
-
trackedRows.set(ctx.toolCallId, { state, name, invalidate: ctx.invalidate });
|
|
271
|
-
if (trackedRows.size <= MAX_TRACKED_ROWS) return;
|
|
272
|
-
const oldestToolCallId = trackedRows.keys().next().value;
|
|
273
|
-
if (oldestToolCallId !== undefined) trackedRows.delete(oldestToolCallId);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function toggleTrackedRows(): "expanded" | "collapsed" | undefined {
|
|
277
|
-
const rows = [...trackedRows.values()].filter(({ name, state }) => {
|
|
278
|
-
const levels = state.availableLevels ?? [0, 1];
|
|
279
|
-
return config.auto_compact[name] && levels.length > 1;
|
|
280
|
-
});
|
|
281
|
-
if (rows.length === 0) return undefined;
|
|
282
|
-
|
|
283
|
-
const expand = shouldExpandAll(rows.map(({ state }) => ({
|
|
284
|
-
level: state.level,
|
|
285
|
-
levels: state.availableLevels ?? [0, 1],
|
|
286
|
-
})));
|
|
287
|
-
for (const { state, invalidate } of rows) {
|
|
288
|
-
const levels = state.availableLevels ?? [0, 1];
|
|
289
|
-
state.level = expand ? (levels.at(-1) ?? 0) : (levels[0] ?? 0);
|
|
290
|
-
invalidate();
|
|
291
|
-
}
|
|
292
|
-
return expand ? "expanded" : "collapsed";
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
function initializeRowLevel(state: RowState, name: CompactToolName, levels = state.availableLevels): boolean {
|
|
296
|
-
if (state.configRevision === configRevision) return false;
|
|
297
|
-
state.configRevision = configRevision;
|
|
298
|
-
state.level = config.auto_compact[name] ? (levels?.[0] ?? 0) : (levels?.at(-1) ?? MAX_LEVEL);
|
|
299
|
-
return true;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function advanceLevel(state: RowState, expanded: boolean, name: CompactToolName): number {
|
|
303
|
-
const initialized = initializeRowLevel(state, name);
|
|
304
|
-
state.level ??= config.auto_compact[name] ? 0 : (state.availableLevels?.at(-1) ?? MAX_LEVEL);
|
|
305
|
-
if (initialized || state.lastExpanded === undefined) {
|
|
306
|
-
state.lastExpanded = expanded;
|
|
307
|
-
return state.level;
|
|
308
|
-
}
|
|
309
|
-
if (state.lastExpanded !== expanded) {
|
|
310
|
-
state.lastExpanded = expanded;
|
|
311
|
-
const levels = state.availableLevels ?? [0, 1];
|
|
312
|
-
const index = levels.indexOf(state.level);
|
|
313
|
-
state.level = levels[(index + 1) % levels.length] ?? 0;
|
|
314
|
-
}
|
|
315
|
-
return state.level;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function setAvailableLevels(state: RowState, levels: number[], name: CompactToolName): void {
|
|
319
|
-
state.availableLevels = levels;
|
|
320
|
-
if (initializeRowLevel(state, name, levels)) return;
|
|
321
|
-
if (state.level === undefined) {
|
|
322
|
-
state.level = config.auto_compact[name] ? 0 : (levels.at(-1) ?? 0);
|
|
323
|
-
} else if (!levels.includes(state.level)) {
|
|
324
|
-
state.level = !config.auto_compact[name] && state.level === MAX_LEVEL ? (levels.at(-1) ?? 0) : 0;
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
function getOutputLevels(output: string): number[] {
|
|
329
|
-
const normalized = normalizeLineEndings(output).trimEnd();
|
|
330
|
-
if (!normalized) return [];
|
|
331
|
-
return normalized.split("\n").length > config.previewLines ? [2, 3] : [2];
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function stopSpinner(state: RowState): void {
|
|
335
|
-
if (state.timer) clearInterval(state.timer);
|
|
336
|
-
state.timer = undefined;
|
|
337
|
-
spinningRows.delete(state);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
function syncSpinner(state: RowState, running: boolean, invalidate: () => void): void {
|
|
341
|
-
// Regular mode cannot safely redraw a transcript row after it has scrolled
|
|
342
|
-
// above the viewport. Keep active rows static there to avoid full transcript
|
|
343
|
-
// redraws and duplicated-looking terminal output.
|
|
344
|
-
if (!running || !animateRows || config.spinner.frames.length < 2) {
|
|
345
|
-
stopSpinner(state);
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
if (state.timer) return;
|
|
349
|
-
|
|
350
|
-
state.frame ??= 0;
|
|
351
|
-
state.timer = setInterval(() => {
|
|
352
|
-
state.frame = ((state.frame ?? 0) + 1) % config.spinner.frames.length;
|
|
353
|
-
try {
|
|
354
|
-
invalidate();
|
|
355
|
-
} catch {
|
|
356
|
-
stopSpinner(state);
|
|
357
|
-
}
|
|
358
|
-
}, config.spinner.intervalMs);
|
|
359
|
-
state.timer.unref?.();
|
|
360
|
-
spinningRows.add(state);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
function syncTiming(state: RowState, callStarted: boolean, finished: boolean): void {
|
|
364
|
-
if (callStarted && state.startedAt === undefined) state.startedAt = Date.now();
|
|
365
|
-
if (finished && state.startedAt !== undefined && state.endedAt === undefined) {
|
|
366
|
-
state.endedAt = Date.now();
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
function restoreExecutionTiming(state: RowState, toolCallId: string): boolean {
|
|
371
|
-
const timing = executionTimings.get(toolCallId);
|
|
372
|
-
if (!timing) return false;
|
|
373
|
-
state.startedAt = timing.startedAt;
|
|
374
|
-
state.endedAt = timing.endedAt;
|
|
375
|
-
return timing.endedAt !== undefined;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
function persistExecutionTiming(state: RowState, toolCallId: string): void {
|
|
379
|
-
if (state.startedAt === undefined) return;
|
|
380
|
-
const timing = executionTimings.get(toolCallId) ?? { startedAt: state.startedAt };
|
|
381
|
-
timing.startedAt = state.startedAt;
|
|
382
|
-
if (state.endedAt !== undefined) timing.endedAt = state.endedAt;
|
|
383
|
-
executionTimings.set(toolCallId, timing);
|
|
384
|
-
if (executionTimings.size <= 2_000) return;
|
|
385
|
-
const oldestToolCallId = executionTimings.keys().next().value;
|
|
386
|
-
if (oldestToolCallId !== undefined) executionTimings.delete(oldestToolCallId);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
function syncRow(
|
|
390
|
-
ctx: RenderContext,
|
|
391
|
-
running = ctx.executionStarted && ctx.state.endedAt === undefined,
|
|
392
|
-
finished = false,
|
|
393
|
-
): RowState {
|
|
394
|
-
const state = ctx.state;
|
|
395
|
-
if (restoreExecutionTiming(state, ctx.toolCallId)) running = false;
|
|
396
|
-
const callStarted = !ctx.argsComplete || ctx.executionStarted || running;
|
|
397
|
-
syncTiming(state, callStarted, finished);
|
|
398
|
-
persistExecutionTiming(state, ctx.toolCallId);
|
|
399
|
-
syncSpinner(state, running, ctx.invalidate);
|
|
400
|
-
return state;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
function resetUiState(clearTimings = false): void {
|
|
404
|
-
for (const state of [...spinningRows]) stopSpinner(state);
|
|
405
|
-
trackedRows.clear();
|
|
406
|
-
if (clearTimings) executionTimings.clear();
|
|
407
|
-
}
|
|
408
63
|
|
|
409
64
|
function bindTerminalInput(ctx: ExtensionContext): void {
|
|
410
65
|
unsubscribeTerminalInput?.();
|
|
411
66
|
unsubscribeTerminalInput = ctx.ui.onTerminalInput((data) => {
|
|
412
|
-
|
|
413
|
-
|
|
67
|
+
const input = classifyToggleInput(data);
|
|
68
|
+
if (!input) return undefined;
|
|
69
|
+
if (input === "release") return { consume: true };
|
|
70
|
+
const result = runtime.toggleTrackedRows();
|
|
414
71
|
if (result) ctx.ui.notify(`Compact tool rows: ${result}`, "info");
|
|
415
72
|
return { consume: true };
|
|
416
73
|
});
|
|
@@ -418,24 +75,21 @@ function bindTerminalInput(ctx: ExtensionContext): void {
|
|
|
418
75
|
|
|
419
76
|
function spinnerTone(frameIndex: number, frameCount: number): "muted" | "dim" | "border" {
|
|
420
77
|
if (frameCount < 3) return "muted";
|
|
421
|
-
const
|
|
422
|
-
const distanceFromCenter = Math.abs(position - 0.5) * 2;
|
|
78
|
+
const distanceFromCenter = Math.abs(frameIndex / (frameCount - 1) - 0.5) * 2;
|
|
423
79
|
if (distanceFromCenter < 0.34) return "border";
|
|
424
|
-
|
|
425
|
-
return "muted";
|
|
80
|
+
return distanceFromCenter < 0.75 ? "dim" : "muted";
|
|
426
81
|
}
|
|
427
82
|
|
|
428
|
-
function
|
|
83
|
+
function callStatus(ctx: RenderContext, state: RowState): RowStatus {
|
|
429
84
|
return classifyCallStatus(ctx.isError, ctx.executionStarted, state.endedAt !== undefined);
|
|
430
85
|
}
|
|
431
86
|
|
|
432
87
|
function renderIndicator(theme: Theme, state: RowState, status: RowStatus): string {
|
|
433
88
|
if (status === "error") return theme.fg("error", "⊗");
|
|
434
89
|
if (status === "success") return theme.fg("success", "●");
|
|
435
|
-
|
|
90
|
+
const frames = runtime.config.spinner.frames;
|
|
436
91
|
const frameIndex = state.frame ?? 0;
|
|
437
|
-
|
|
438
|
-
return theme.fg(spinnerTone(frameIndex, config.spinner.frames.length), frame);
|
|
92
|
+
return theme.fg(spinnerTone(frameIndex, frames.length), frames[frameIndex] ?? frames[0] ?? "◐");
|
|
439
93
|
}
|
|
440
94
|
|
|
441
95
|
function formatDuration(state: RowState): string | undefined {
|
|
@@ -444,20 +98,10 @@ function formatDuration(state: RowState): string | undefined {
|
|
|
444
98
|
}
|
|
445
99
|
|
|
446
100
|
function durationIndicator(state: RowState): DurationIndicatorConfig | undefined {
|
|
447
|
-
|
|
448
|
-
// have timing data. Keep their configured indicator visible using the first rule.
|
|
449
|
-
const elapsedMs = state.startedAt !== undefined && state.endedAt !== undefined
|
|
101
|
+
const elapsed = state.startedAt !== undefined && state.endedAt !== undefined
|
|
450
102
|
? state.endedAt - state.startedAt
|
|
451
103
|
: undefined;
|
|
452
|
-
return selectDurationIndicator(config.durationIndicators,
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
function parseHexColor(color: string): [number, number, number] {
|
|
456
|
-
return [
|
|
457
|
-
Number.parseInt(color.slice(1, 3), 16),
|
|
458
|
-
Number.parseInt(color.slice(3, 5), 16),
|
|
459
|
-
Number.parseInt(color.slice(5, 7), 16),
|
|
460
|
-
];
|
|
104
|
+
return selectDurationIndicator(runtime.config.durationIndicators, elapsed);
|
|
461
105
|
}
|
|
462
106
|
|
|
463
107
|
function styleDurationIcon(theme: Theme, indicator: DurationIndicatorConfig): string {
|
|
@@ -465,21 +109,17 @@ function styleDurationIcon(theme: Theme, indicator: DurationIndicatorConfig): st
|
|
|
465
109
|
if (!HEX_COLOR_PATTERN.test(color)) {
|
|
466
110
|
return theme.fg(color as ThemeDurationIndicatorColor, indicator.icon);
|
|
467
111
|
}
|
|
468
|
-
const
|
|
112
|
+
const red = Number.parseInt(color.slice(1, 3), 16);
|
|
113
|
+
const green = Number.parseInt(color.slice(3, 5), 16);
|
|
114
|
+
const blue = Number.parseInt(color.slice(5, 7), 16);
|
|
469
115
|
const ansi = theme.getColorMode() === "truecolor"
|
|
470
116
|
? `\x1b[38;2;${red};${green};${blue}m`
|
|
471
117
|
: `\x1b[38;5;${rgbToAnsi256(red, green, blue)}m`;
|
|
472
118
|
return `${ansi}${indicator.icon}\x1b[39m`;
|
|
473
119
|
}
|
|
474
120
|
|
|
475
|
-
function renderControls(theme: Theme, state: RowState, running: boolean, isError: boolean):
|
|
476
|
-
const duration = running && !
|
|
477
|
-
const levels = state.availableLevels ?? [0, 1];
|
|
478
|
-
const expandable = levels.length > 1;
|
|
479
|
-
const atLastLevel = state.level === levels.at(-1);
|
|
480
|
-
const action = expandable
|
|
481
|
-
? `${theme.italic("ctrl+o")} toggle all • ${theme.italic("click")} ${atLastLevel ? "to collapse" : "for more"}`
|
|
482
|
-
: undefined;
|
|
121
|
+
function renderControls(theme: Theme, state: RowState, running: boolean, isError: boolean): Component {
|
|
122
|
+
const duration = running && !runtime.animatesRows ? undefined : formatDuration(state);
|
|
483
123
|
const indicator = !running && !isError ? durationIndicator(state) : undefined;
|
|
484
124
|
const status = running
|
|
485
125
|
? (duration ?? "Running")
|
|
@@ -487,162 +127,73 @@ function renderControls(theme: Theme, state: RowState, running: boolean, isError
|
|
|
487
127
|
let details = indicator
|
|
488
128
|
? `${styleDurationIcon(theme, indicator)} ${theme.fg("borderAccent", status)}`
|
|
489
129
|
: theme.fg("borderAccent", status);
|
|
490
|
-
if (
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
return line.length > maxLength ? `${line.slice(0, maxLength - 1)}…` : line;
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
function getTextResult(result: AgentToolResult<unknown>): string {
|
|
500
|
-
const text = result.content
|
|
501
|
-
.filter((item) => item.type === "text")
|
|
502
|
-
.map((item) => item.text)
|
|
503
|
-
.join("\n");
|
|
504
|
-
return normalizeLineEndings(text).trimEnd();
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
function withoutLeadingBlankLines(component: Component, theme: Theme): Component {
|
|
508
|
-
return {
|
|
509
|
-
render(width: number) {
|
|
510
|
-
const lines = component.render(Math.max(1, width - 4));
|
|
511
|
-
const firstContentLine = lines.findIndex((line) => visibleWidth(line.trim()) > 0);
|
|
512
|
-
const prefix = theme.fg("border", " │ ");
|
|
513
|
-
return firstContentLine < 0 ? [] : lines.slice(firstContentLine).map((line) => `${prefix}${line}`);
|
|
514
|
-
},
|
|
515
|
-
invalidate() {
|
|
516
|
-
component.invalidate?.();
|
|
517
|
-
},
|
|
518
|
-
};
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
function styleToolOutput(text: string, theme: Theme, isError: boolean): string {
|
|
522
|
-
const color = isError ? "error" : "toolOutput";
|
|
523
|
-
return text
|
|
524
|
-
.split(/(⚡️?)/u)
|
|
525
|
-
.map((part) => theme.fg(!isError && part.startsWith("⚡") ? "warning" : color, part))
|
|
526
|
-
.join("");
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
function renderPreview(output: string, level: number, theme: Theme, isError: boolean): Text | undefined {
|
|
530
|
-
const normalized = normalizeLineEndings(output).trimEnd();
|
|
531
|
-
if (!normalized) return undefined;
|
|
532
|
-
|
|
533
|
-
const lines = normalized.split("\n");
|
|
534
|
-
const shown = level >= MAX_LEVEL ? lines : lines.slice(0, config.previewLines);
|
|
535
|
-
const prefix = theme.fg("border", " │ ");
|
|
536
|
-
let text = shown.map((line) => `${prefix}${styleToolOutput(line, theme, isError)}`).join("\n");
|
|
537
|
-
if (shown.length < lines.length) {
|
|
538
|
-
text += `\n${prefix}${theme.fg("dim", `… ${lines.length - shown.length} more lines`)}`;
|
|
130
|
+
if (state.hasResult) {
|
|
131
|
+
const clickAction = state.expanded ? "to hide" : "for result";
|
|
132
|
+
details += theme.fg(
|
|
133
|
+
"borderAccent",
|
|
134
|
+
`, ${theme.italic("ctrl+o")} toggle all • ${theme.italic("click")} ${clickAction}`,
|
|
135
|
+
);
|
|
539
136
|
}
|
|
540
|
-
return
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
function getPathArg(args: ToolArgs): string {
|
|
544
|
-
const value = args.path ?? args.file_path;
|
|
545
|
-
return typeof value === "string" ? firstLine(value) : "";
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
function getCallDetails(name: string, args: ToolArgs): string {
|
|
549
|
-
const path = getPathArg(args) || (name === "grep" || name === "find" || name === "ls" ? "." : "");
|
|
550
|
-
const pattern = typeof args.pattern === "string" ? firstLine(args.pattern) : "…";
|
|
551
|
-
if (name === "grep") return `/${pattern}/ in ${path}`;
|
|
552
|
-
if (name === "find") return `${pattern} in ${path}`;
|
|
553
|
-
return path;
|
|
137
|
+
return prefixedText(details, theme.fg("border", " └─ "), " ");
|
|
554
138
|
}
|
|
555
139
|
|
|
556
|
-
function
|
|
557
|
-
|
|
558
|
-
const omitted = new Set(["path", "file_path"]);
|
|
559
|
-
if (name === "write") omitted.add("content");
|
|
560
|
-
if (name === "grep" || name === "find") omitted.add("pattern");
|
|
561
|
-
return Object.fromEntries(Object.entries(args).filter(([key, value]) => !omitted.has(key) && value !== undefined));
|
|
140
|
+
function renderCallTitle(name: string, theme: Theme, ctx: RenderContext, state: RowState): string {
|
|
141
|
+
return `${renderIndicator(theme, state, callStatus(ctx, state))} ${theme.fg("toolTitle", theme.bold(name))}`;
|
|
562
142
|
}
|
|
563
143
|
|
|
564
|
-
function
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
144
|
+
function renderFileCall(
|
|
145
|
+
definition: BuiltInDefinition,
|
|
146
|
+
args: ToolArgs,
|
|
147
|
+
theme: Theme,
|
|
148
|
+
ctx: RenderContext,
|
|
149
|
+
): Container {
|
|
150
|
+
const name = definition.name as CompactToolName;
|
|
151
|
+
const state = runtime.syncRow(ctx, ctx.state.endedAt === undefined);
|
|
152
|
+
runtime.track(ctx, name, state);
|
|
153
|
+
runtime.syncExpansion(state, ctx.expanded, name);
|
|
154
|
+
const callDetails = getCallDetails(name, args);
|
|
155
|
+
const details = callDetails
|
|
156
|
+
? styleMultiline(callDetails, (line) => theme.fg("toolOutput", line))
|
|
157
|
+
: undefined;
|
|
158
|
+
const container = new Container();
|
|
159
|
+
container.addChild(renderToolCall(renderCallTitle(name, theme, ctx, state), details, theme));
|
|
160
|
+
const arguments_ = getArgumentDetails(name, args);
|
|
161
|
+
if (Object.keys(arguments_).length > 0) container.addChild(renderArguments(arguments_, theme));
|
|
162
|
+
return container;
|
|
572
163
|
}
|
|
573
164
|
|
|
574
|
-
function
|
|
165
|
+
function updateEditResult(
|
|
575
166
|
definition: BuiltInDefinition,
|
|
576
167
|
result: AgentToolResult<unknown>,
|
|
577
168
|
options: ToolRenderResultOptions,
|
|
578
169
|
theme: Theme,
|
|
579
170
|
ctx: RenderContext,
|
|
580
171
|
state: RowState,
|
|
581
|
-
level: number,
|
|
582
172
|
): void {
|
|
583
173
|
if (definition.name !== "edit" || !definition.renderResult) return;
|
|
584
174
|
state.originalResultComponent = definition.renderResult(
|
|
585
175
|
result,
|
|
586
|
-
{ ...options, expanded:
|
|
176
|
+
{ ...options, expanded: state.expanded ?? false },
|
|
587
177
|
theme,
|
|
588
178
|
{ ...ctx, state: {}, lastComponent: state.originalResultComponent },
|
|
589
179
|
);
|
|
590
180
|
}
|
|
591
181
|
|
|
592
|
-
function
|
|
593
|
-
if (isError || name === "read" || name === "grep" || name === "find" || name === "ls") {
|
|
594
|
-
return getTextResult(result);
|
|
595
|
-
}
|
|
596
|
-
if (name === "write") return String(args.content ?? "");
|
|
597
|
-
return "";
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
function renderFileCall(
|
|
601
|
-
definition: BuiltInDefinition,
|
|
602
|
-
args: ToolArgs,
|
|
603
|
-
theme: Theme,
|
|
604
|
-
ctx: RenderContext,
|
|
605
|
-
): Container {
|
|
606
|
-
// Animate and time from the first rendered call so large write/edit calls
|
|
607
|
-
// represent the full lifecycle, including streamed arguments and filesystem work.
|
|
608
|
-
const state = syncRow(ctx, ctx.state.endedAt === undefined);
|
|
609
|
-
trackRow(ctx, definition.name as CompactToolName, state);
|
|
610
|
-
// syncRow may restore a completed timing after /reload. Recompute from the
|
|
611
|
-
// synchronized state so a stale local value cannot leave the spinner visible.
|
|
612
|
-
const status = resolveCallStatus(ctx, state);
|
|
613
|
-
const level = advanceLevel(state, ctx.expanded, definition.name as CompactToolName);
|
|
614
|
-
const callDetails = getCallDetails(definition.name, args);
|
|
615
|
-
const argumentDetails = getFileArgumentDetails(definition.name, args);
|
|
616
|
-
let text = `${renderIndicator(theme, state, status)} `;
|
|
617
|
-
text += theme.fg("toolTitle", theme.bold(definition.name));
|
|
618
|
-
if (callDetails) text += ` ${theme.fg("toolOutput", callDetails)}`;
|
|
619
|
-
|
|
620
|
-
const container = new Container();
|
|
621
|
-
container.addChild(new Text(text, 1, 0));
|
|
622
|
-
if (level >= 1 && Object.keys(argumentDetails).length > 0) {
|
|
623
|
-
container.addChild(renderArguments(argumentDetails, theme));
|
|
624
|
-
}
|
|
625
|
-
return container;
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
function addFileResultPreview(
|
|
182
|
+
function appendFileResult(
|
|
629
183
|
container: Container,
|
|
630
184
|
definition: BuiltInDefinition,
|
|
631
185
|
state: RowState,
|
|
632
186
|
output: string,
|
|
633
|
-
level: number,
|
|
634
187
|
theme: Theme,
|
|
635
188
|
isError: boolean,
|
|
636
189
|
): void {
|
|
637
|
-
if (
|
|
190
|
+
if (!state.expanded) return;
|
|
638
191
|
if (definition.name === "edit") {
|
|
639
|
-
if (state.originalResultComponent)
|
|
640
|
-
container.addChild(withoutLeadingBlankLines(state.originalResultComponent, theme));
|
|
641
|
-
}
|
|
192
|
+
if (state.originalResultComponent) container.addChild(wrapEditResult(state.originalResultComponent, theme));
|
|
642
193
|
return;
|
|
643
194
|
}
|
|
644
|
-
const
|
|
645
|
-
if (
|
|
195
|
+
const component = renderOutput(output, theme, isError);
|
|
196
|
+
if (component) container.addChild(component);
|
|
646
197
|
}
|
|
647
198
|
|
|
648
199
|
function renderFileResult(
|
|
@@ -652,169 +203,128 @@ function renderFileResult(
|
|
|
652
203
|
theme: Theme,
|
|
653
204
|
ctx: RenderContext,
|
|
654
205
|
): Container {
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
const
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
const level = state.level ?? 0;
|
|
663
|
-
callOriginalEditResult(definition, result, options, theme, ctx, state, level);
|
|
206
|
+
const name = definition.name as CompactToolName;
|
|
207
|
+
const state = runtime.syncRow(ctx, options.isPartial, !options.isPartial);
|
|
208
|
+
runtime.track(ctx, name, state);
|
|
209
|
+
const output = getFileOutput(name, ctx.args, result, ctx.isError);
|
|
210
|
+
runtime.setResultAvailable(state, name, name === "edit" || output.length > 0);
|
|
211
|
+
updateEditResult(definition, result, options, theme, ctx, state);
|
|
664
212
|
const container = new Container();
|
|
665
|
-
|
|
213
|
+
appendFileResult(container, definition, state, output, theme, ctx.isError);
|
|
666
214
|
container.addChild(renderControls(theme, state, options.isPartial, ctx.isError));
|
|
667
215
|
return container;
|
|
668
216
|
}
|
|
669
217
|
|
|
670
|
-
function createFileCallRenderer(definition: BuiltInDefinition) {
|
|
671
|
-
return (args: ToolArgs, theme: Theme, ctx: RenderContext) => renderFileCall(definition, args, theme, ctx);
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
function createFileResultRenderer(definition: BuiltInDefinition) {
|
|
675
|
-
return (result: AgentToolResult<unknown>, options: ToolRenderResultOptions, theme: Theme, ctx: RenderContext) =>
|
|
676
|
-
renderFileResult(definition, result, options, theme, ctx);
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
type TimedExecute = (...args: any[]) => Promise<AgentToolResult<unknown>>;
|
|
680
|
-
|
|
681
|
-
function createTimedExecute(definition: BuiltInDefinition): TimedExecute {
|
|
682
|
-
const execute = definition.execute as TimedExecute;
|
|
683
|
-
return async (...args: any[]) => {
|
|
684
|
-
const toolCallId = typeof args[0] === "string" ? args[0] : undefined;
|
|
685
|
-
const startedAt = (toolCallId ? executionTimings.get(toolCallId)?.startedAt : undefined) ?? Date.now();
|
|
686
|
-
if (toolCallId && !executionTimings.has(toolCallId)) executionTimings.set(toolCallId, { startedAt });
|
|
687
|
-
try {
|
|
688
|
-
return await execute.apply(definition, args);
|
|
689
|
-
} finally {
|
|
690
|
-
const endedAt = Date.now();
|
|
691
|
-
const timing = toolCallId ? executionTimings.get(toolCallId) : undefined;
|
|
692
|
-
if (timing) timing.endedAt = endedAt;
|
|
693
|
-
}
|
|
694
|
-
};
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
function registerFileTool(pi: ExtensionAPI, definition: BuiltInDefinition): void {
|
|
698
|
-
const tool = {
|
|
699
|
-
...definition,
|
|
700
|
-
execute: createTimedExecute(definition),
|
|
701
|
-
renderShell: "self" as const,
|
|
702
|
-
renderCall: createFileCallRenderer(definition),
|
|
703
|
-
renderResult: createFileResultRenderer(definition),
|
|
704
|
-
};
|
|
705
|
-
// Built-in file schemas are intentionally erased at this shared renderer boundary.
|
|
706
|
-
pi.registerTool(tool as ToolDefinition<any, any, RowState>);
|
|
707
|
-
}
|
|
708
|
-
|
|
709
218
|
function renderShellCall(
|
|
710
|
-
name:
|
|
219
|
+
name: ShellToolName,
|
|
711
220
|
args: BashToolInput,
|
|
712
221
|
theme: Theme,
|
|
713
222
|
ctx: RenderContext<BashToolInput>,
|
|
714
|
-
):
|
|
715
|
-
const state = syncRow(ctx, ctx.state.endedAt === undefined);
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
const
|
|
720
|
-
|
|
721
|
-
const command = normalizeLineEndings(args.command ?? "");
|
|
722
|
-
const displayedCommand = (level >= 1 ? command : firstLine(command)) || "…";
|
|
723
|
-
let text = `${renderIndicator(theme, state, status)} `;
|
|
724
|
-
text += `${theme.fg("toolTitle", theme.bold(name))} ${theme.fg("toolOutput", displayedCommand)}`;
|
|
725
|
-
return new Text(text, 1, 0);
|
|
223
|
+
): Component {
|
|
224
|
+
const state = runtime.syncRow(ctx, ctx.state.endedAt === undefined);
|
|
225
|
+
runtime.track(ctx, name, state);
|
|
226
|
+
runtime.syncExpansion(state, ctx.expanded, name);
|
|
227
|
+
const command = normalizeLineEndings(args.command ?? "") || "…";
|
|
228
|
+
const details = styleMultiline(command, (line) => theme.fg("toolOutput", line));
|
|
229
|
+
return renderToolCall(renderCallTitle(name, theme, ctx, state), details, theme);
|
|
726
230
|
}
|
|
727
231
|
|
|
728
232
|
function renderShellResult(
|
|
729
|
-
name:
|
|
233
|
+
name: ShellToolName,
|
|
730
234
|
result: AgentToolResult<BashToolDetails | undefined>,
|
|
731
235
|
options: ToolRenderResultOptions,
|
|
732
236
|
theme: Theme,
|
|
733
237
|
ctx: RenderContext<BashToolInput>,
|
|
734
238
|
): Component {
|
|
735
|
-
const state = syncRow(ctx, options.isPartial, !options.isPartial);
|
|
736
|
-
|
|
239
|
+
const state = runtime.syncRow(ctx, options.isPartial, !options.isPartial);
|
|
240
|
+
runtime.track(ctx, name, state);
|
|
737
241
|
const output = getTextResult(result);
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
const level = state.level ?? 0;
|
|
741
|
-
if (level < 2) return renderControls(theme, state, options.isPartial, ctx.isError);
|
|
742
|
-
|
|
242
|
+
runtime.setResultAvailable(state, name, output.length > 0);
|
|
243
|
+
if (!state.expanded) return renderControls(theme, state, options.isPartial, ctx.isError);
|
|
743
244
|
const container = new Container();
|
|
744
|
-
const
|
|
745
|
-
if (
|
|
746
|
-
else {
|
|
747
|
-
const prefix = theme.fg("border", " │ ");
|
|
748
|
-
const message = theme.fg("dim", options.isPartial ? "…" : "(no output)");
|
|
749
|
-
container.addChild(new Text(`${prefix}${message}`, 0, 0));
|
|
750
|
-
}
|
|
245
|
+
const component = renderOutput(output, theme, ctx.isError);
|
|
246
|
+
if (component) container.addChild(component);
|
|
751
247
|
container.addChild(renderControls(theme, state, options.isPartial, ctx.isError));
|
|
752
248
|
return container;
|
|
753
249
|
}
|
|
754
250
|
|
|
251
|
+
const toolFactories: Record<CompactToolName, (cwd: string) => BuiltInDefinition> = {
|
|
252
|
+
read: createReadToolDefinition,
|
|
253
|
+
write: createWriteToolDefinition,
|
|
254
|
+
edit: createEditToolDefinition,
|
|
255
|
+
bash: createBashToolDefinition,
|
|
256
|
+
powershell: createPowerShellToolDefinition,
|
|
257
|
+
grep: createGrepToolDefinition,
|
|
258
|
+
find: createFindToolDefinition,
|
|
259
|
+
ls: createLsToolDefinition,
|
|
260
|
+
};
|
|
261
|
+
|
|
755
262
|
function createBuiltInDefinition(name: CompactToolName, cwd: string): BuiltInDefinition {
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
263
|
+
return toolFactories[name](cwd);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function registerFileTool(pi: ExtensionAPI, definition: BuiltInDefinition): void {
|
|
267
|
+
pi.registerTool({
|
|
268
|
+
...definition,
|
|
269
|
+
execute: runtime.createTimedExecute(definition),
|
|
270
|
+
renderShell: "self",
|
|
271
|
+
renderCall: (args: ToolArgs, theme: Theme, ctx: RenderContext) => renderFileCall(definition, args, theme, ctx),
|
|
272
|
+
renderResult: (
|
|
273
|
+
result: AgentToolResult<unknown>,
|
|
274
|
+
options: ToolRenderResultOptions,
|
|
275
|
+
theme: Theme,
|
|
276
|
+
ctx: RenderContext,
|
|
277
|
+
) => renderFileResult(definition, result, options, theme, ctx),
|
|
278
|
+
} as ToolDefinition<any, any, RowState>);
|
|
766
279
|
}
|
|
767
280
|
|
|
768
281
|
function registerShellTool(pi: ExtensionAPI, definition: BuiltInDefinition): void {
|
|
282
|
+
const name = definition.name as ShellToolName;
|
|
769
283
|
pi.registerTool({
|
|
770
284
|
...definition,
|
|
771
|
-
execute: createTimedExecute(definition),
|
|
285
|
+
execute: runtime.createTimedExecute(definition),
|
|
772
286
|
renderShell: "self",
|
|
773
287
|
renderCall: (args: BashToolInput, theme: Theme, ctx: RenderContext<BashToolInput>) =>
|
|
774
|
-
renderShellCall(
|
|
288
|
+
renderShellCall(name, args, theme, ctx),
|
|
775
289
|
renderResult: (
|
|
776
290
|
result: AgentToolResult<BashToolDetails | undefined>,
|
|
777
291
|
options: ToolRenderResultOptions,
|
|
778
292
|
theme: Theme,
|
|
779
293
|
ctx: RenderContext<BashToolInput>,
|
|
780
|
-
) => renderShellResult(
|
|
294
|
+
) => renderShellResult(name, result, options, theme, ctx),
|
|
781
295
|
} as ToolDefinition<any, BashToolDetails | undefined, RowState>);
|
|
782
296
|
}
|
|
783
297
|
|
|
784
|
-
function
|
|
785
|
-
|
|
786
|
-
|
|
298
|
+
function registerTools(pi: ExtensionAPI, cwd: string): void {
|
|
299
|
+
const enabledTools = new Set(runtime.config.tools);
|
|
300
|
+
for (const name of registeredTools) {
|
|
301
|
+
if (!enabledTools.has(name)) pi.registerTool(createBuiltInDefinition(name, cwd));
|
|
787
302
|
}
|
|
788
|
-
|
|
789
|
-
for (const name of config.tools) {
|
|
303
|
+
registeredTools.clear();
|
|
304
|
+
for (const name of runtime.config.tools) {
|
|
790
305
|
const definition = createBuiltInDefinition(name, cwd);
|
|
791
306
|
if (name === "bash" || name === "powershell") registerShellTool(pi, definition);
|
|
792
307
|
else registerFileTool(pi, definition);
|
|
793
|
-
|
|
308
|
+
registeredTools.add(name);
|
|
794
309
|
}
|
|
795
310
|
}
|
|
796
311
|
|
|
797
|
-
function
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
config = loadConfig(cwd, projectTrusted);
|
|
801
|
-
configRevision = {};
|
|
802
|
-
registerBuiltInTools(pi, cwd);
|
|
312
|
+
function configure(pi: ExtensionAPI, cwd?: string, projectTrusted = false): void {
|
|
313
|
+
runtime.configure(loadConfig(cwd, projectTrusted), isFullscreenMode());
|
|
314
|
+
registerTools(pi, cwd ?? process.cwd());
|
|
803
315
|
}
|
|
804
316
|
|
|
805
|
-
export default function (pi: ExtensionAPI): void {
|
|
806
|
-
|
|
807
|
-
config = loadConfig();
|
|
808
|
-
registerBuiltInTools(pi, process.cwd());
|
|
317
|
+
export default function compactTools(pi: ExtensionAPI): void {
|
|
318
|
+
configure(pi);
|
|
809
319
|
pi.on("session_start", (event, ctx) => {
|
|
810
|
-
if (event.reason !== "reload")
|
|
811
|
-
|
|
320
|
+
if (event.reason !== "reload") runtime.clearTimings();
|
|
321
|
+
configure(pi, ctx.cwd, ctx.isProjectTrusted());
|
|
812
322
|
bindTerminalInput(ctx);
|
|
813
323
|
});
|
|
814
|
-
pi.on("resources_discover", (_event, ctx) =>
|
|
324
|
+
pi.on("resources_discover", (_event, ctx) => configure(pi, ctx.cwd, ctx.isProjectTrusted()));
|
|
815
325
|
pi.on("session_shutdown", (event) => {
|
|
816
326
|
unsubscribeTerminalInput?.();
|
|
817
327
|
unsubscribeTerminalInput = undefined;
|
|
818
|
-
|
|
328
|
+
runtime.reset(event.reason !== "reload");
|
|
819
329
|
});
|
|
820
330
|
}
|