pi-compact-tools 0.6.3 → 0.7.8

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.
@@ -1,322 +1,350 @@
1
- import type {
2
- AgentToolResult,
3
- BashToolDetails,
4
- BashToolInput,
5
- ExtensionAPI,
6
- ExtensionContext,
7
- Theme,
8
- ToolDefinition,
9
- ToolRenderResultOptions,
10
- } from "@earendil-works/pi-coding-agent";
11
- import {
12
- createBashToolDefinition,
13
- createEditToolDefinition,
14
- createFindToolDefinition,
15
- createGrepToolDefinition,
16
- createLsToolDefinition,
17
- createPowerShellToolDefinition,
18
- createReadToolDefinition,
19
- createWriteToolDefinition,
20
- } from "@earendil-works/pi-coding-agent";
21
- import type { Component, Container } from "@earendil-works/pi-tui";
22
- import {
23
- classifyCallStatus,
24
- formatDurationMs,
25
- normalizeLineEndings,
26
- type RowStatus,
27
- } from "./compact-tools-core.ts";
28
- import { isFullscreenMode, loadConfig } from "./compact-tools-config.ts";
29
- import { classifyToggleInput } from "./compact-tools-input.ts";
30
- import {
31
- formatResultLineSummary,
32
- getArgumentDetails,
33
- getCallDetails,
34
- getFileOutput,
35
- getTextResult,
36
- } from "./compact-tools-invocation.ts";
37
- import {
38
- CachedContainer,
39
- prefixedText,
40
- renderArguments,
41
- renderOutput,
42
- renderToolCall,
43
- styleMultiline,
44
- wrapEditResult,
45
- } from "./compact-tools-layout.ts";
46
- import { ToolRuntime } from "./compact-tools-runtime.ts";
47
- import { ThinkingCycleController } from "./compact-tools-thinking.ts";
48
- import type {
49
- BuiltInDefinition,
50
- CompactToolName,
51
- RenderContext,
52
- RowState,
53
- ShellToolName,
54
- ToolArgs,
55
- } from "./compact-tools-types.ts";
56
-
57
- const runtime = new ToolRuntime();
58
- const registeredTools = new Set<CompactToolName>();
59
- let unsubscribeTerminalInput: (() => void) | undefined;
60
-
61
- function bindTerminalInput(ctx: ExtensionContext): void {
62
- unsubscribeTerminalInput?.();
63
- unsubscribeTerminalInput = ctx.ui.onTerminalInput((data) => {
64
- const input = classifyToggleInput(data);
65
- if (!input) return undefined;
66
- if (input === "release") return { consume: true };
67
- runtime.toggleTrackedRows();
68
- return { consume: true };
69
- });
70
- }
71
-
72
- function spinnerTone(frameIndex: number, frameCount: number): "muted" | "dim" | "border" {
73
- if (frameCount < 3) return "muted";
74
- const distanceFromCenter = Math.abs(frameIndex / (frameCount - 1) - 0.5) * 2;
75
- if (distanceFromCenter < 0.34) return "border";
76
- return distanceFromCenter < 0.75 ? "dim" : "muted";
77
- }
78
-
79
- function callStatus(ctx: RenderContext, state: RowState): RowStatus {
80
- return classifyCallStatus(ctx.isError, ctx.executionStarted, state.endedAt !== undefined);
81
- }
82
-
83
- function renderIndicator(theme: Theme, state: RowState, status: RowStatus): string {
84
- if (status === "error") return theme.fg("error", "●");
85
- if (status === "success") return theme.fg("success", "●");
86
- const frames = runtime.config.spinner.frames;
87
- const frameIndex = state.frame ?? 0;
88
- return theme.fg(spinnerTone(frameIndex, frames.length), frames[frameIndex] ?? frames[0] ?? "◐");
89
- }
90
-
91
- function formatDuration(state: RowState): string | undefined {
92
- if (state.startedAt === undefined) return undefined;
93
- return formatDurationMs((state.endedAt ?? Date.now()) - state.startedAt);
94
- }
95
-
96
- function renderControls(
97
- theme: Theme,
98
- state: RowState,
99
- running: boolean,
100
- isError: boolean,
101
- lineSummary?: string,
102
- ): Component {
103
- const duration = running && !runtime.animatesRows ? undefined : formatDuration(state);
104
- const status = running
105
- ? (duration ?? "Running")
106
- : `${isError ? "Failed" : "Done"}${duration ? ` in ${duration}` : ""}`;
107
- let details = theme.fg("borderAccent", status);
108
- if (lineSummary && !running) details += theme.fg("borderAccent", ` (${lineSummary})`);
109
- if (state.hasResult) {
110
- const clickAction = state.expanded ? "to hide" : "for result";
111
- details += theme.fg(
112
- "borderAccent",
113
- `, ${theme.italic("ctrl+o")} toggle all • ${theme.italic("click")} ${clickAction}`,
114
- );
115
- }
116
- return prefixedText(details, theme.fg("border", " └─ "), " ");
117
- }
118
-
119
- function renderCallTitle(name: string, theme: Theme, ctx: RenderContext, state: RowState): string {
120
- return `${renderIndicator(theme, state, callStatus(ctx, state))} ${theme.fg("toolTitle", theme.bold(name))}`;
121
- }
122
-
123
- function renderFileCall(
124
- definition: BuiltInDefinition,
125
- args: ToolArgs,
126
- theme: Theme,
127
- ctx: RenderContext,
128
- ): Container {
129
- const name = definition.name as CompactToolName;
130
- const state = runtime.syncRow(ctx, ctx.state.endedAt === undefined);
131
- runtime.track(ctx, name, state);
132
- runtime.syncExpansion(state, ctx.expanded, name);
133
- const callDetails = getCallDetails(name, args);
134
- const details = callDetails
135
- ? styleMultiline(callDetails, (line) => theme.fg("toolOutput", line))
136
- : undefined;
137
- const container = new CachedContainer();
138
- container.addChild(renderToolCall(renderCallTitle(name, theme, ctx, state), details, theme));
139
- const arguments_ = getArgumentDetails(name, args);
140
- if (Object.keys(arguments_).length > 0) container.addChild(renderArguments(arguments_, theme));
141
- return container;
142
- }
143
-
144
- function updateEditResult(
145
- definition: BuiltInDefinition,
146
- result: AgentToolResult<unknown>,
147
- options: ToolRenderResultOptions,
148
- theme: Theme,
149
- ctx: RenderContext,
150
- state: RowState,
151
- ): void {
152
- if (definition.name !== "edit" || !definition.renderResult) return;
153
- state.originalResultComponent = definition.renderResult(
154
- result,
155
- { ...options, expanded: state.expanded ?? false },
156
- theme,
157
- { ...ctx, state: {}, lastComponent: state.originalResultComponent },
158
- );
159
- }
160
-
161
- function appendFileResult(
162
- container: Container,
163
- definition: BuiltInDefinition,
164
- state: RowState,
165
- output: string,
166
- theme: Theme,
167
- isError: boolean,
168
- ): void {
169
- if (!state.expanded) return;
170
- if (definition.name === "edit") {
171
- if (state.originalResultComponent) container.addChild(wrapEditResult(state.originalResultComponent, theme));
172
- return;
173
- }
174
- const component = renderOutput(output, theme, isError);
175
- if (component) container.addChild(component);
176
- }
177
-
178
- function renderFileResult(
179
- definition: BuiltInDefinition,
180
- result: AgentToolResult<unknown>,
181
- options: ToolRenderResultOptions,
182
- theme: Theme,
183
- ctx: RenderContext,
184
- ): Container {
185
- const name = definition.name as CompactToolName;
186
- const state = runtime.syncRow(ctx, options.isPartial, !options.isPartial);
187
- runtime.track(ctx, name, state);
188
- const output = getFileOutput(name, ctx.args, result, ctx.isError);
189
- runtime.setResultAvailable(state, name, name === "edit" || output.length > 0);
190
- updateEditResult(definition, result, options, theme, ctx, state);
191
- const container = new CachedContainer();
192
- appendFileResult(container, definition, state, output, theme, ctx.isError);
193
- if (!options.isPartial && !state.resultLineSummaryComputed) {
194
- state.resultLineSummary = formatResultLineSummary(name, ctx.args, result, output);
195
- state.resultLineSummaryComputed = true;
196
- }
197
- container.addChild(renderControls(theme, state, options.isPartial, ctx.isError, state.resultLineSummary));
198
- return container;
199
- }
200
-
201
- function renderShellCall(
202
- name: ShellToolName,
203
- args: BashToolInput,
204
- theme: Theme,
205
- ctx: RenderContext<BashToolInput>,
206
- ): Component {
207
- const state = runtime.syncRow(ctx, ctx.state.endedAt === undefined);
208
- runtime.track(ctx, name, state);
209
- runtime.syncExpansion(state, ctx.expanded, name);
210
- const command = normalizeLineEndings(args.command ?? "") || "…";
211
- const details = styleMultiline(command, (line) => theme.fg("toolOutput", line));
212
- return renderToolCall(renderCallTitle(name, theme, ctx, state), details, theme);
213
- }
214
-
215
- function renderShellResult(
216
- name: ShellToolName,
217
- result: AgentToolResult<BashToolDetails | undefined>,
218
- options: ToolRenderResultOptions,
219
- theme: Theme,
220
- ctx: RenderContext<BashToolInput>,
221
- ): Component {
222
- const state = runtime.syncRow(ctx, options.isPartial, !options.isPartial);
223
- runtime.track(ctx, name, state);
224
- const output = getTextResult(result);
225
- runtime.setResultAvailable(state, name, output.length > 0);
226
- if (!options.isPartial && !state.resultLineSummaryComputed) {
227
- state.resultLineSummary = formatResultLineSummary(name, ctx.args, result, output);
228
- state.resultLineSummaryComputed = true;
229
- }
230
- if (!state.expanded) {
231
- return renderControls(theme, state, options.isPartial, ctx.isError, state.resultLineSummary);
232
- }
233
- const container = new CachedContainer();
234
- const component = renderOutput(output, theme, ctx.isError);
235
- if (component) container.addChild(component);
236
- container.addChild(renderControls(theme, state, options.isPartial, ctx.isError, state.resultLineSummary));
237
- return container;
238
- }
239
-
240
- const toolFactories: Record<CompactToolName, (cwd: string) => BuiltInDefinition> = {
241
- read: createReadToolDefinition,
242
- write: createWriteToolDefinition,
243
- edit: createEditToolDefinition,
244
- bash: createBashToolDefinition,
245
- powershell: createPowerShellToolDefinition,
246
- grep: createGrepToolDefinition,
247
- find: createFindToolDefinition,
248
- ls: createLsToolDefinition,
249
- };
250
-
251
- function createBuiltInDefinition(name: CompactToolName, cwd: string): BuiltInDefinition {
252
- return toolFactories[name](cwd);
253
- }
254
-
255
- function registerFileTool(pi: ExtensionAPI, definition: BuiltInDefinition): void {
256
- pi.registerTool({
257
- ...definition,
258
- execute: runtime.createTimedExecute(definition),
259
- renderShell: "self",
260
- renderCall: (args: ToolArgs, theme: Theme, ctx: RenderContext) => renderFileCall(definition, args, theme, ctx),
261
- renderResult: (
262
- result: AgentToolResult<unknown>,
263
- options: ToolRenderResultOptions,
264
- theme: Theme,
265
- ctx: RenderContext,
266
- ) => renderFileResult(definition, result, options, theme, ctx),
267
- } as ToolDefinition<any, any, RowState>);
268
- }
269
-
270
- function registerShellTool(pi: ExtensionAPI, definition: BuiltInDefinition): void {
271
- const name = definition.name as ShellToolName;
272
- pi.registerTool({
273
- ...definition,
274
- execute: runtime.createTimedExecute(definition),
275
- renderShell: "self",
276
- renderCall: (args: BashToolInput, theme: Theme, ctx: RenderContext<BashToolInput>) =>
277
- renderShellCall(name, args, theme, ctx),
278
- renderResult: (
279
- result: AgentToolResult<BashToolDetails | undefined>,
280
- options: ToolRenderResultOptions,
281
- theme: Theme,
282
- ctx: RenderContext<BashToolInput>,
283
- ) => renderShellResult(name, result, options, theme, ctx),
284
- } as ToolDefinition<any, BashToolDetails | undefined, RowState>);
285
- }
286
-
287
- function registerTools(pi: ExtensionAPI, cwd: string): void {
288
- const enabledTools = new Set(runtime.config.tools);
289
- for (const name of registeredTools) {
290
- if (!enabledTools.has(name)) pi.registerTool(createBuiltInDefinition(name, cwd));
291
- }
292
- registeredTools.clear();
293
- for (const name of runtime.config.tools) {
294
- const definition = createBuiltInDefinition(name, cwd);
295
- if (name === "bash" || name === "powershell") registerShellTool(pi, definition);
296
- else registerFileTool(pi, definition);
297
- registeredTools.add(name);
298
- }
299
- }
300
-
301
- function configure(pi: ExtensionAPI, cwd?: string, projectTrusted = false): void {
302
- runtime.configure(loadConfig(cwd, projectTrusted), isFullscreenMode());
303
- registerTools(pi, cwd ?? process.cwd());
304
- }
305
-
306
- export default function compactTools(pi: ExtensionAPI): void {
307
- const thinkingCycle = new ThinkingCycleController(pi);
308
- configure(pi);
309
- pi.on("session_start", (event, ctx) => {
310
- if (event.reason !== "reload") runtime.clearTimings();
311
- configure(pi, ctx.cwd, ctx.isProjectTrusted());
312
- bindTerminalInput(ctx);
313
- thinkingCycle.bind(ctx);
314
- });
315
- pi.on("resources_discover", (_event, ctx) => configure(pi, ctx.cwd, ctx.isProjectTrusted()));
316
- pi.on("session_shutdown", (event) => {
317
- thinkingCycle.dispose();
318
- unsubscribeTerminalInput?.();
319
- unsubscribeTerminalInput = undefined;
320
- runtime.reset(event.reason !== "reload");
321
- });
322
- }
1
+ import type {
2
+ AgentToolResult,
3
+ BashToolDetails,
4
+ BashToolInput,
5
+ ExtensionAPI,
6
+ Theme,
7
+ ToolDefinition,
8
+ ToolRenderResultOptions,
9
+ } from "@earendil-works/pi-coding-agent";
10
+ import {
11
+ createBashToolDefinition,
12
+ createEditToolDefinition,
13
+ createFindToolDefinition,
14
+ createGrepToolDefinition,
15
+ createLsToolDefinition,
16
+ createPowerShellToolDefinition,
17
+ createReadToolDefinition,
18
+ createWriteToolDefinition,
19
+ } from "@earendil-works/pi-coding-agent";
20
+ import type { Component, Container } from "@earendil-works/pi-tui";
21
+ import {
22
+ classifyCallStatus,
23
+ formatDurationMs,
24
+ indicatorGlyph,
25
+ indicatorStrength,
26
+ indicatorTone,
27
+ normalizeLineEndings,
28
+ type RowStatus,
29
+ } from "./compact-tools-core.ts";
30
+ import { colorizeRgb, interpolateRgb, themeColorRgb } from "./compact-tools-color.ts";
31
+ import { loadConfig } from "./compact-tools-config.ts";
32
+ import {
33
+ formatResultLineSummary,
34
+ getArgumentDetails,
35
+ getCallDetails,
36
+ getEditChanges,
37
+ getFileOutput,
38
+ getTextResult,
39
+ summarizeShellCommand,
40
+ } from "./compact-tools-invocation.ts";
41
+ import {
42
+ CachedContainer,
43
+ limitComponentLines,
44
+ prefixedText,
45
+ renderArguments,
46
+ renderEditChanges,
47
+ renderOutput,
48
+ renderToolCall,
49
+ styleMultiline,
50
+ } from "./compact-tools-layout.ts";
51
+ import { ProgressController } from "./compact-tools-progress.ts";
52
+ import { ToolRuntime } from "./compact-tools-runtime.ts";
53
+ import { ThinkingCycleController } from "./compact-tools-thinking.ts";
54
+ import type {
55
+ BuiltInDefinition,
56
+ CompactToolName,
57
+ RenderContext,
58
+ RowState,
59
+ ShellToolName,
60
+ ToolArgs,
61
+ } from "./compact-tools-types.ts";
62
+
63
+ const runtime = new ToolRuntime();
64
+ const registeredTools = new Set<CompactToolName>();
65
+ let registeredConfiguration: string | undefined;
66
+
67
+ function callStatus(ctx: RenderContext, state: RowState): RowStatus {
68
+ // write/edit arguments can stream for much longer than their eventual filesystem
69
+ // operation. Treat that active tool-call phase as running so every built-in row
70
+ // animates consistently instead of waiting for execute() to begin.
71
+ const active = ctx.executionStarted || !ctx.argsComplete;
72
+ return classifyCallStatus(ctx.isError, active, state.endedAt !== undefined);
73
+ }
74
+
75
+ function renderIndicator(theme: Theme, ctx: RenderContext, status: RowStatus): string {
76
+ const frame = runtime.syncIndicator(ctx.toolCallId, status === "running", () => ctx.invalidate());
77
+ const glyph = indicatorGlyph(status, frame);
78
+ if (status === "running") {
79
+ const bright = themeColorRgb(theme, "borderAccent");
80
+ const dim = themeColorRgb(theme, "borderMuted");
81
+ if (bright && dim) {
82
+ return colorizeRgb(theme, interpolateRgb(dim, bright, indicatorStrength(status, frame)), glyph);
83
+ }
84
+ }
85
+ return theme.fg(indicatorTone(status, frame), glyph);
86
+ }
87
+
88
+ function formatDuration(state: RowState): string | undefined {
89
+ if (state.startedAt === undefined) return undefined;
90
+ return formatDurationMs((state.endedAt ?? Date.now()) - state.startedAt);
91
+ }
92
+
93
+ function canReuseResult<TDetails, TArgs>(
94
+ state: RowState,
95
+ result: AgentToolResult<TDetails>,
96
+ options: ToolRenderResultOptions,
97
+ ctx: RenderContext<TArgs>,
98
+ ): ctx is RenderContext<TArgs> & { lastComponent: Component } {
99
+ return ctx.lastComponent !== undefined
100
+ && state.lastResultContent === result.content
101
+ && state.lastResultDetails === result.details
102
+ && state.lastResultPartial === options.isPartial
103
+ && state.lastResultExpanded === state.expanded
104
+ && state.lastResultPreview === state.preview
105
+ && state.lastResultError === ctx.isError
106
+ && state.lastResultConfigRevision === state.configRevision;
107
+ }
108
+
109
+ function rememberResult<TDetails, TArgs>(
110
+ state: RowState,
111
+ result: AgentToolResult<TDetails>,
112
+ options: ToolRenderResultOptions,
113
+ ctx: RenderContext<TArgs>,
114
+ ): void {
115
+ state.lastResultContent = result.content;
116
+ state.lastResultDetails = result.details;
117
+ state.lastResultPartial = options.isPartial;
118
+ state.lastResultExpanded = state.expanded;
119
+ state.lastResultPreview = state.preview;
120
+ state.lastResultError = ctx.isError;
121
+ state.lastResultConfigRevision = state.configRevision;
122
+ }
123
+
124
+ function renderControls(
125
+ theme: Theme,
126
+ state: RowState,
127
+ running: boolean,
128
+ isError: boolean,
129
+ lineSummary?: string,
130
+ ): Component {
131
+ const duration = running ? undefined : formatDuration(state);
132
+ const status = running
133
+ ? (duration ?? "Running")
134
+ : `${isError ? "Failed" : "Done"}${duration ? ` in ${duration}` : ""}`;
135
+ let details = theme.fg("borderAccent", status);
136
+ if (lineSummary && !running) details += theme.fg("borderAccent", ` (${lineSummary})`);
137
+ return prefixedText(details, theme.fg("border", " └─ "), " ");
138
+ }
139
+
140
+ function renderCallTitle(name: string, theme: Theme, ctx: RenderContext, state: RowState): string {
141
+ return `${renderIndicator(theme, ctx, callStatus(ctx, state))} ${theme.fg("toolTitle", theme.bold(name))}`;
142
+ }
143
+
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.syncExpansion(state, ctx.expanded, name);
153
+ const callDetails = getCallDetails(name, args);
154
+ const details = callDetails
155
+ ? styleMultiline(callDetails, (line) => theme.fg("toolOutput", line))
156
+ : undefined;
157
+ const container = new CachedContainer();
158
+ container.addChild(renderToolCall(renderCallTitle(name, theme, ctx, state), details, theme));
159
+ const arguments_ = getArgumentDetails(name, args);
160
+ if (Object.keys(arguments_).length > 0) container.addChild(renderArguments(arguments_, theme));
161
+ return container;
162
+ }
163
+
164
+ function appendFileResult(
165
+ container: Container,
166
+ definition: BuiltInDefinition,
167
+ state: RowState,
168
+ output: string,
169
+ editChanges: string,
170
+ theme: Theme,
171
+ isError: boolean,
172
+ ): void {
173
+ if (!state.expanded && !state.preview) return;
174
+ const component = definition.name === "edit" && editChanges
175
+ ? renderEditChanges(editChanges, theme)
176
+ : renderOutput(output, theme, isError);
177
+ if (!component) return;
178
+ container.addChild(state.expanded ? component : limitComponentLines(component, runtime.config.previewLines, theme));
179
+ }
180
+
181
+ function renderFileResult(
182
+ definition: BuiltInDefinition,
183
+ result: AgentToolResult<unknown>,
184
+ options: ToolRenderResultOptions,
185
+ theme: Theme,
186
+ ctx: RenderContext,
187
+ ): Component {
188
+ const name = definition.name as CompactToolName;
189
+ const state = runtime.syncRow(ctx, options.isPartial, !options.isPartial);
190
+ runtime.syncExpansion(state, ctx.expanded, name);
191
+ if (canReuseResult(state, result, options, ctx)) return ctx.lastComponent;
192
+ const output = getFileOutput(name, ctx.args, result, ctx.isError);
193
+ const editChanges = name === "edit" ? getEditChanges(result) : "";
194
+ runtime.setResultAvailable(state, name, editChanges.length > 0 || output.length > 0);
195
+ const container = new CachedContainer();
196
+ appendFileResult(container, definition, state, output, editChanges, theme, ctx.isError);
197
+ if (!options.isPartial && !state.resultLineSummaryComputed) {
198
+ state.resultLineSummary = formatResultLineSummary(name, ctx.args, result, output);
199
+ state.resultLineSummaryComputed = true;
200
+ }
201
+ container.addChild(renderControls(theme, state, options.isPartial, ctx.isError, state.resultLineSummary));
202
+ rememberResult(state, result, options, ctx);
203
+ return container;
204
+ }
205
+
206
+ function renderShellCall(
207
+ name: ShellToolName,
208
+ args: BashToolInput,
209
+ theme: Theme,
210
+ ctx: RenderContext<BashToolInput>,
211
+ ): Component {
212
+ const state = runtime.syncRow(ctx, ctx.state.endedAt === undefined);
213
+ runtime.syncExpansion(state, ctx.expanded, name);
214
+ const command = normalizeLineEndings(args.command ?? "");
215
+ const details = state.expanded
216
+ ? styleMultiline(command || "…", (line) => theme.fg("toolOutput", line))
217
+ : theme.fg("toolOutput", summarizeShellCommand(name, command));
218
+ return renderToolCall(renderCallTitle(name, theme, ctx, state), details, theme);
219
+ }
220
+
221
+ function renderShellResult(
222
+ name: ShellToolName,
223
+ result: AgentToolResult<BashToolDetails | undefined>,
224
+ options: ToolRenderResultOptions,
225
+ theme: Theme,
226
+ ctx: RenderContext<BashToolInput>,
227
+ ): Component {
228
+ const state = runtime.syncRow(ctx, options.isPartial, !options.isPartial);
229
+ runtime.syncExpansion(state, ctx.expanded, name);
230
+ if (canReuseResult(state, result, options, ctx)) return ctx.lastComponent;
231
+ const output = getTextResult(result);
232
+ runtime.setResultAvailable(state, name, output.length > 0);
233
+ if (!options.isPartial && !state.resultLineSummaryComputed) {
234
+ state.resultLineSummary = formatResultLineSummary(name, ctx.args, result, output);
235
+ state.resultLineSummaryComputed = true;
236
+ }
237
+ if (!state.expanded && !state.preview) {
238
+ const controls = renderControls(theme, state, options.isPartial, ctx.isError, state.resultLineSummary);
239
+ rememberResult(state, result, options, ctx);
240
+ return controls;
241
+ }
242
+ const container = new CachedContainer();
243
+ const component = renderOutput(output, theme, ctx.isError);
244
+ if (component) {
245
+ container.addChild(state.expanded ? component : limitComponentLines(component, runtime.config.previewLines, theme));
246
+ }
247
+ container.addChild(renderControls(theme, state, options.isPartial, ctx.isError, state.resultLineSummary));
248
+ rememberResult(state, result, options, ctx);
249
+ return container;
250
+ }
251
+
252
+ const toolFactories: Record<CompactToolName, (cwd: string) => BuiltInDefinition> = {
253
+ read: createReadToolDefinition,
254
+ write: createWriteToolDefinition,
255
+ edit: createEditToolDefinition,
256
+ bash: createBashToolDefinition,
257
+ powershell: createPowerShellToolDefinition,
258
+ grep: createGrepToolDefinition,
259
+ find: createFindToolDefinition,
260
+ ls: createLsToolDefinition,
261
+ };
262
+
263
+ function createBuiltInDefinition(name: CompactToolName, cwd: string): BuiltInDefinition {
264
+ return toolFactories[name](cwd);
265
+ }
266
+
267
+ function registerFileTool(pi: ExtensionAPI, definition: BuiltInDefinition): void {
268
+ pi.registerTool({
269
+ ...definition,
270
+ execute: runtime.createTimedExecute(definition),
271
+ renderShell: "self",
272
+ renderCall: (args: ToolArgs, theme: Theme, ctx: RenderContext) => renderFileCall(definition, args, theme, ctx),
273
+ renderResult: (
274
+ result: AgentToolResult<unknown>,
275
+ options: ToolRenderResultOptions,
276
+ theme: Theme,
277
+ ctx: RenderContext,
278
+ ) => renderFileResult(definition, result, options, theme, ctx),
279
+ } as ToolDefinition<any, any, RowState>);
280
+ }
281
+
282
+ function registerShellTool(pi: ExtensionAPI, definition: BuiltInDefinition): void {
283
+ const name = definition.name as ShellToolName;
284
+ pi.registerTool({
285
+ ...definition,
286
+ execute: runtime.createTimedExecute(definition),
287
+ renderShell: "self",
288
+ renderCall: (args: BashToolInput, theme: Theme, ctx: RenderContext<BashToolInput>) =>
289
+ renderShellCall(name, args, theme, ctx),
290
+ renderResult: (
291
+ result: AgentToolResult<BashToolDetails | undefined>,
292
+ options: ToolRenderResultOptions,
293
+ theme: Theme,
294
+ ctx: RenderContext<BashToolInput>,
295
+ ) => renderShellResult(name, result, options, theme, ctx),
296
+ } as ToolDefinition<any, BashToolDetails | undefined, RowState>);
297
+ }
298
+
299
+ function registerTools(pi: ExtensionAPI, cwd: string): void {
300
+ const enabledTools = new Set(runtime.config.tools);
301
+ for (const name of registeredTools) {
302
+ // Pi exposes no unregister API, so restoring a disabled tool means re-registering
303
+ // the built-in definition to release this extension's renderers.
304
+ if (!enabledTools.has(name)) pi.registerTool(createBuiltInDefinition(name, cwd));
305
+ }
306
+ registeredTools.clear();
307
+ for (const name of runtime.config.tools) {
308
+ const definition = createBuiltInDefinition(name, cwd);
309
+ if (name === "bash" || name === "powershell") registerShellTool(pi, definition);
310
+ else registerFileTool(pi, definition);
311
+ registeredTools.add(name);
312
+ }
313
+ }
314
+
315
+ function configure(pi: ExtensionAPI, cwd?: string, projectTrusted = false): void {
316
+ const resolvedCwd = cwd ?? process.cwd();
317
+ const config = loadConfig(resolvedCwd, projectTrusted);
318
+ const signature = JSON.stringify([resolvedCwd, config]);
319
+ if (signature === registeredConfiguration) return;
320
+ runtime.configure(config);
321
+ registerTools(pi, resolvedCwd);
322
+ registeredConfiguration = signature;
323
+ }
324
+
325
+ export default function compactTools(pi: ExtensionAPI): void {
326
+ const thinkingCycle = new ThinkingCycleController(pi);
327
+ const progress = new ProgressController(pi);
328
+ // Register once while the extension runtime is being built. In particular, this
329
+ // makes the overrides available before Pi restores the active tool set on /reload.
330
+ configure(pi, process.cwd());
331
+ pi.on("session_start", (event, ctx) => {
332
+ if (event.reason !== "reload") runtime.clearTimings();
333
+ configure(pi, ctx.cwd, ctx.isProjectTrusted());
334
+ if (ctx.mode === "tui") {
335
+ progress.bind(ctx);
336
+ thinkingCycle.bind(ctx);
337
+ } else {
338
+ progress.dispose();
339
+ thinkingCycle.dispose();
340
+ }
341
+ });
342
+ pi.on("session_shutdown", (event) => {
343
+ thinkingCycle.dispose();
344
+ progress.dispose();
345
+ runtime.reset(event.reason !== "reload");
346
+ // Some Pi hosts rebuild the active tool registry during reload while keeping
347
+ // this module instance. Force the next session to restore our renderers.
348
+ if (event.reason === "reload") registeredConfiguration = undefined;
349
+ });
350
+ }