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