pi-compact-tools 0.10.1 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -53
- package/extensions/compact-tools-claude.ts +227 -0
- package/extensions/compact-tools-config.ts +9 -5
- package/extensions/compact-tools-grouping.ts +282 -0
- package/extensions/compact-tools-invocation.ts +1 -1
- package/extensions/compact-tools-layout.ts +64 -17
- package/extensions/compact-tools-palette.ts +10 -0
- package/extensions/compact-tools-runtime.ts +10 -0
- package/extensions/compact-tools-silent.ts +9 -3
- package/extensions/compact-tools-types.ts +4 -0
- package/extensions/compact-tools.ts +218 -223
- package/package.json +3 -3
- package/release-notes.json +7 -0
- package/schemas/compact-tools.schema.json +7 -1
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AgentToolResult,
|
|
3
|
-
BashToolDetails,
|
|
4
|
-
BashToolInput,
|
|
5
3
|
ExtensionAPI,
|
|
6
4
|
Theme,
|
|
7
5
|
ToolDefinition,
|
|
@@ -17,17 +15,8 @@ import {
|
|
|
17
15
|
createReadToolDefinition,
|
|
18
16
|
createWriteToolDefinition,
|
|
19
17
|
} from "@earendil-works/pi-coding-agent";
|
|
20
|
-
import type { Component
|
|
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 } from "./compact-tools-color.ts";
|
|
18
|
+
import type { Component } from "@earendil-works/pi-tui";
|
|
19
|
+
import { classifyCallStatus, formatDurationMs, normalizeLineEndings, type RowStatus } from "./compact-tools-core.ts";
|
|
31
20
|
import { loadConfig } from "./compact-tools-config.ts";
|
|
32
21
|
import { installToolRowPatch, setRowResolver, type RowRenderers, type ToolRow } from "./compact-tools-custom.ts";
|
|
33
22
|
import {
|
|
@@ -47,7 +36,9 @@ import {
|
|
|
47
36
|
} from "./compact-tools-invocation.ts";
|
|
48
37
|
import {
|
|
49
38
|
CachedContainer,
|
|
39
|
+
claudeRows,
|
|
50
40
|
limitComponentLines,
|
|
41
|
+
prefixedLines,
|
|
51
42
|
prefixedText,
|
|
52
43
|
railComponent,
|
|
53
44
|
renderArguments,
|
|
@@ -57,7 +48,9 @@ import {
|
|
|
57
48
|
renderToolCall,
|
|
58
49
|
styleMultiline,
|
|
59
50
|
} from "./compact-tools-layout.ts";
|
|
60
|
-
import {
|
|
51
|
+
import { CLAUDE_DIFF_CONTEXT_LINES, CLAUDE_OUTPUT_ROWS, CLAUDE_WRITE_ROWS, claudeFailure, claudeOutcome, claudeTitle } from "./compact-tools-claude.ts";
|
|
52
|
+
import { ToolGroupController } from "./compact-tools-grouping.ts";
|
|
53
|
+
import { chromePainter, paintIndicator } from "./compact-tools-palette.ts";
|
|
61
54
|
import { ProgressController } from "./compact-tools-progress.ts";
|
|
62
55
|
import { showReleaseNotice } from "./compact-tools-release.ts";
|
|
63
56
|
import { ToolRuntime } from "./compact-tools-runtime.ts";
|
|
@@ -70,17 +63,34 @@ import {
|
|
|
70
63
|
type CompactToolName,
|
|
71
64
|
type RenderContext,
|
|
72
65
|
type RowState,
|
|
73
|
-
type ShellToolName,
|
|
74
66
|
type ToolArgs,
|
|
75
67
|
} from "./compact-tools-types.ts";
|
|
76
68
|
|
|
77
69
|
/** Context lines kept around each change while an edit result is collapsed. */
|
|
78
70
|
const PREVIEW_DIFF_CONTEXT_LINES = 1;
|
|
71
|
+
/** Claude Code sets what a call returned under the "⎿" rather than beside a rail. */
|
|
72
|
+
const CLAUDE_INDENT = " ";
|
|
79
73
|
|
|
80
74
|
const runtime = new ToolRuntime();
|
|
81
75
|
const registeredTools = new Set<CompactToolName>();
|
|
82
76
|
let registeredConfiguration: string | undefined;
|
|
83
77
|
|
|
78
|
+
/** How a tool's row is drawn: a file tool's result is code, a shell's is output, a custom tool's is its author's. */
|
|
79
|
+
type RowKind = "file" | "shell" | "custom";
|
|
80
|
+
type AuthorResultRenderer = (expanded: boolean) => Component | undefined;
|
|
81
|
+
|
|
82
|
+
function rowKind(name: string): RowKind {
|
|
83
|
+
return name === "bash" || name === "powershell" ? "shell" : SUPPORTED_TOOL_SET.has(name) ? "file" : "custom";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function isClaude(): boolean {
|
|
87
|
+
return runtime.config.style === "claude";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function pathArgument(args: ToolArgs): string {
|
|
91
|
+
return typeof args.path === "string" ? args.path : typeof args.file_path === "string" ? args.file_path : "";
|
|
92
|
+
}
|
|
93
|
+
|
|
84
94
|
function callStatus(ctx: RenderContext, state: RowState): RowStatus {
|
|
85
95
|
// write/edit arguments can stream for much longer than their eventual filesystem
|
|
86
96
|
// operation. Treat that active tool-call phase as running so every built-in row
|
|
@@ -89,16 +99,9 @@ function callStatus(ctx: RenderContext, state: RowState): RowStatus {
|
|
|
89
99
|
return classifyCallStatus(ctx.isError, active, state.endedAt !== undefined);
|
|
90
100
|
}
|
|
91
101
|
|
|
92
|
-
function renderIndicator(theme: Theme, ctx: RenderContext,
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
if (status === "running") {
|
|
96
|
-
const pulse = indicatorPulse(theme);
|
|
97
|
-
if (pulse) {
|
|
98
|
-
return colorizeRgb(theme, interpolateRgb(pulse.from, pulse.to, indicatorStrength(status, frame)), glyph);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
return theme.fg(indicatorTone(status, frame), glyph);
|
|
102
|
+
function renderIndicator(theme: Theme, ctx: RenderContext, state: RowState): string {
|
|
103
|
+
const status = callStatus(ctx, state);
|
|
104
|
+
return paintIndicator(theme, status, runtime.syncIndicator(ctx.toolCallId, status === "running", () => ctx.invalidate()));
|
|
102
105
|
}
|
|
103
106
|
|
|
104
107
|
function formatDuration(state: RowState): string | undefined {
|
|
@@ -106,9 +109,9 @@ function formatDuration(state: RowState): string | undefined {
|
|
|
106
109
|
return formatDurationMs((state.endedAt ?? Date.now()) - state.startedAt);
|
|
107
110
|
}
|
|
108
111
|
|
|
109
|
-
function canReuseResult<
|
|
112
|
+
function canReuseResult<TArgs>(
|
|
110
113
|
state: RowState,
|
|
111
|
-
result: AgentToolResult<
|
|
114
|
+
result: AgentToolResult<unknown>,
|
|
112
115
|
options: ToolRenderResultOptions,
|
|
113
116
|
ctx: RenderContext<TArgs>,
|
|
114
117
|
): ctx is RenderContext<TArgs> & { lastComponent: Component } {
|
|
@@ -122,18 +125,13 @@ function canReuseResult<TDetails, TArgs>(
|
|
|
122
125
|
&& state.lastResultConfigRevision === state.configRevision;
|
|
123
126
|
}
|
|
124
127
|
|
|
125
|
-
function rememberResult<
|
|
126
|
-
state: RowState,
|
|
127
|
-
result: AgentToolResult<TDetails>,
|
|
128
|
-
options: ToolRenderResultOptions,
|
|
129
|
-
ctx: RenderContext<TArgs>,
|
|
130
|
-
): void {
|
|
128
|
+
function rememberResult(state: RowState, result: AgentToolResult<unknown>, options: ToolRenderResultOptions, isError: boolean): void {
|
|
131
129
|
state.lastResultContent = result.content;
|
|
132
130
|
state.lastResultDetails = result.details;
|
|
133
131
|
state.lastResultPartial = options.isPartial;
|
|
134
132
|
state.lastResultExpanded = state.expanded;
|
|
135
133
|
state.lastResultPreview = state.preview;
|
|
136
|
-
state.lastResultError =
|
|
134
|
+
state.lastResultError = isError;
|
|
137
135
|
state.lastResultConfigRevision = state.configRevision;
|
|
138
136
|
}
|
|
139
137
|
|
|
@@ -142,7 +140,6 @@ function renderControls(
|
|
|
142
140
|
state: RowState,
|
|
143
141
|
running: boolean,
|
|
144
142
|
isError: boolean,
|
|
145
|
-
lineSummary?: string,
|
|
146
143
|
failureReason?: string,
|
|
147
144
|
changes?: { added: number; removed: number },
|
|
148
145
|
): Component {
|
|
@@ -161,45 +158,53 @@ function renderControls(
|
|
|
161
158
|
+ theme.fg("toolDiffRemoved", `-${changes.removed}`) + chrome(")");
|
|
162
159
|
}
|
|
163
160
|
// A failed call produced no result worth counting; "(0 lines)" would only mislead.
|
|
164
|
-
else if (
|
|
161
|
+
else if (state.resultLineSummary && !running && !isError) details += chrome(` (${state.resultLineSummary})`);
|
|
165
162
|
return prefixedText(details, chrome(" └ "), " ");
|
|
166
163
|
}
|
|
167
164
|
|
|
168
|
-
/**
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
*/
|
|
173
|
-
function hiddenFailureReason(name: string, state: RowState, isError: boolean, output: string): string | undefined {
|
|
174
|
-
return isError && !state.expanded && !state.preview ? summarizeFailure(name, output) : undefined;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function renderCallTitle(name: string, theme: Theme, ctx: RenderContext, state: RowState): string {
|
|
178
|
-
return `${renderIndicator(theme, ctx, callStatus(ctx, state))} ${theme.fg("toolTitle", theme.bold(name))}`;
|
|
165
|
+
/** Arguments a one-line summary cannot show: objects, or lists of them. */
|
|
166
|
+
function hasNestedArguments(args: ToolArgs): boolean {
|
|
167
|
+
return Object.values(args).some((value) => typeof value === "object" && value !== null
|
|
168
|
+
&& (!Array.isArray(value) || value.some((item) => typeof item === "object" && item !== null)));
|
|
179
169
|
}
|
|
180
170
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
args: ToolArgs,
|
|
184
|
-
theme: Theme,
|
|
185
|
-
ctx: RenderContext,
|
|
186
|
-
): Container {
|
|
187
|
-
const name = definition.name as CompactToolName;
|
|
171
|
+
/** The call line: a status dot, the tool, and what it was asked, then any arguments the line leaves out. */
|
|
172
|
+
function renderRowCall(name: string, args: ToolArgs, theme: Theme, ctx: RenderContext): Component {
|
|
188
173
|
const state = runtime.syncRow(ctx, ctx.state.endedAt === undefined);
|
|
189
174
|
runtime.syncExpansion(state, ctx.expanded, name);
|
|
190
|
-
const
|
|
191
|
-
const details = callDetails
|
|
192
|
-
? styleMultiline(callDetails, (line) => theme.fg("toolOutput", line))
|
|
193
|
-
: undefined;
|
|
175
|
+
const kind = rowKind(name);
|
|
194
176
|
const container = new CachedContainer();
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
177
|
+
if (isClaude()) {
|
|
178
|
+
// Claude Code's call line: `⦁ Bash(npm test)`, `⦁ Update(src/app.ts)`.
|
|
179
|
+
const { label, argument } = claudeTitle(name, args);
|
|
180
|
+
const title = `${renderIndicator(theme, ctx, state)} ${theme.fg("toolTitle", theme.bold(label))}`
|
|
181
|
+
+ (argument ? theme.fg("toolOutput", `(${argument})`) : "");
|
|
182
|
+
// Collapsed, a long command keeps to one row; opened, it shows in full.
|
|
183
|
+
container.addChild(state.expanded ? renderToolCall(title, undefined, theme) : prefixedLines(title, " ", " ", true));
|
|
184
|
+
// The title already lists plain arguments; only nested ones need the full view.
|
|
185
|
+
if (kind === "custom" && state.expanded && hasNestedArguments(args)) container.addChild(renderArguments(args, theme));
|
|
186
|
+
return container;
|
|
187
|
+
}
|
|
188
|
+
const title = `${renderIndicator(theme, ctx, state)} ${theme.fg("toolTitle", theme.bold(name))}`;
|
|
189
|
+
let details: string | undefined;
|
|
190
|
+
if (kind === "shell") {
|
|
191
|
+
const command = normalizeLineEndings(typeof args.command === "string" ? args.command : "");
|
|
192
|
+
details = state.expanded
|
|
193
|
+
? styleMultiline(command || "…", (line) => theme.fg("toolOutput", line))
|
|
194
|
+
: theme.fg("toolOutput", summarizeShellCommand(name, command));
|
|
195
|
+
} else {
|
|
196
|
+
const summary = kind === "file" ? getCallDetails(name, args) : summarizeCustomArguments(args);
|
|
197
|
+
details = summary ? styleMultiline(summary, (line) => theme.fg("toolOutput", line)) : undefined;
|
|
198
|
+
}
|
|
199
|
+
container.addChild(renderToolCall(title, details, theme));
|
|
200
|
+
const extra = kind === "file" ? getArgumentDetails(name, args) : kind === "custom" && state.expanded ? args : {};
|
|
201
|
+
if (Object.keys(extra).length > 0) container.addChild(renderArguments(extra, theme));
|
|
198
202
|
return container;
|
|
199
203
|
}
|
|
200
204
|
|
|
205
|
+
/** A result body as code where it is code: numbered file text, or an edit's diff. */
|
|
201
206
|
function renderFileBody(
|
|
202
|
-
name:
|
|
207
|
+
name: string,
|
|
203
208
|
args: ToolArgs,
|
|
204
209
|
result: AgentToolResult<unknown>,
|
|
205
210
|
output: string,
|
|
@@ -208,13 +213,10 @@ function renderFileBody(
|
|
|
208
213
|
isError: boolean,
|
|
209
214
|
): Component | undefined {
|
|
210
215
|
if (isError) return renderOutput(output, theme, isError);
|
|
211
|
-
const path =
|
|
212
|
-
if (name === "edit") {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
? renderCodeDiff(getEditPatch(result), diff, path, theme, expanded ? {} : { contextLines: PREVIEW_DIFF_CONTEXT_LINES })
|
|
216
|
-
: undefined;
|
|
217
|
-
return component ?? renderOutput(output, theme, isError);
|
|
216
|
+
const path = pathArgument(args);
|
|
217
|
+
if (name === "edit" && getEditDiff(result)) {
|
|
218
|
+
return renderCodeDiff(getEditPatch(result), getEditDiff(result), path, theme,
|
|
219
|
+
expanded ? {} : { contextLines: PREVIEW_DIFF_CONTEXT_LINES }) ?? renderOutput(output, theme, isError);
|
|
218
220
|
}
|
|
219
221
|
if (name === "read" && isReadTextResult(result)) {
|
|
220
222
|
const { body, footer } = splitReadFooter(output);
|
|
@@ -225,155 +227,166 @@ function renderFileBody(
|
|
|
225
227
|
return renderOutput(output, theme, isError);
|
|
226
228
|
}
|
|
227
229
|
|
|
228
|
-
function
|
|
229
|
-
|
|
230
|
-
name: CompactToolName,
|
|
231
|
-
state: RowState,
|
|
230
|
+
function renderRowBody(
|
|
231
|
+
name: string,
|
|
232
232
|
args: ToolArgs,
|
|
233
233
|
result: AgentToolResult<unknown>,
|
|
234
234
|
output: string,
|
|
235
|
+
expanded: boolean,
|
|
235
236
|
theme: Theme,
|
|
236
237
|
isError: boolean,
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
if (
|
|
241
|
-
|
|
238
|
+
author: AuthorResultRenderer | undefined,
|
|
239
|
+
): Component | undefined {
|
|
240
|
+
if (rowKind(name) !== "custom") return renderFileBody(name, args, result, output, expanded, theme, isError);
|
|
241
|
+
if (author) {
|
|
242
|
+
try {
|
|
243
|
+
const component = author(expanded);
|
|
244
|
+
if (component) return railComponent(component, theme);
|
|
245
|
+
} catch {
|
|
246
|
+
// A failing third-party renderer degrades to the plain text result.
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return renderOutput(output, theme, isError);
|
|
242
250
|
}
|
|
243
251
|
|
|
244
|
-
function
|
|
245
|
-
|
|
246
|
-
result: AgentToolResult<unknown>,
|
|
247
|
-
options: ToolRenderResultOptions,
|
|
248
|
-
theme: Theme,
|
|
249
|
-
ctx: RenderContext,
|
|
250
|
-
): Component {
|
|
251
|
-
const name = definition.name as CompactToolName;
|
|
252
|
-
const state = runtime.syncRow(ctx, options.isPartial, !options.isPartial);
|
|
253
|
-
runtime.syncExpansion(state, ctx.expanded, name);
|
|
254
|
-
if (canReuseResult(state, result, options, ctx)) return ctx.lastComponent;
|
|
255
|
-
const output = getFileOutput(name, ctx.args, result, ctx.isError);
|
|
256
|
-
const hasEditDiff = name === "edit" && getEditDiff(result).length > 0;
|
|
257
|
-
runtime.setResultAvailable(state, name, hasEditDiff || output.length > 0);
|
|
258
|
-
const container = new CachedContainer();
|
|
259
|
-
appendFileResult(container, name, state, ctx.args, result, output, theme, ctx.isError);
|
|
260
|
-
if (!options.isPartial && !state.resultLineSummaryComputed) {
|
|
261
|
-
state.resultLineSummary = formatResultLineSummary(name, ctx.args, result, output);
|
|
262
|
-
state.resultLineSummaryComputed = true;
|
|
263
|
-
}
|
|
264
|
-
container.addChild(renderControls(theme, state, options.isPartial, ctx.isError, state.resultLineSummary,
|
|
265
|
-
hiddenFailureReason(name, state, ctx.isError, output), name === "edit" ? countEditChanges(result) : undefined));
|
|
266
|
-
rememberResult(state, result, options, ctx);
|
|
267
|
-
return container;
|
|
252
|
+
function claudeMoreLines(theme: Theme, hidden: number): string {
|
|
253
|
+
return chromePainter(theme)(`… +${hidden} ${hidden === 1 ? "line" : "lines"} (ctrl+o to expand)`);
|
|
268
254
|
}
|
|
269
255
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
)
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const details = state.expanded
|
|
280
|
-
? styleMultiline(command || "…", (line) => theme.fg("toolOutput", line))
|
|
281
|
-
: theme.fg("toolOutput", summarizeShellCommand(name, command));
|
|
282
|
-
return renderToolCall(renderCallTitle(name, theme, ctx, state), details, theme);
|
|
256
|
+
/** Output previewed in rows the way Claude Code previews it, all of it once opened. */
|
|
257
|
+
function claudeOutput(output: string, expanded: boolean, theme: Theme, isError: boolean, empty: string, firstPrefix: string): Component {
|
|
258
|
+
const normalized = normalizeLineEndings(output).trimEnd();
|
|
259
|
+
// Pi's bash reports an empty run as "(no output)"; Claude Code dims its own words for it.
|
|
260
|
+
const lines = !normalized || normalized === "(no output)"
|
|
261
|
+
? [chromePainter(theme)(empty)]
|
|
262
|
+
: normalized.split("\n").map((line) => theme.fg(isError ? "error" : "toolOutput", line));
|
|
263
|
+
return claudeRows(lines, firstPrefix, CLAUDE_INDENT, expanded ? undefined : CLAUDE_OUTPUT_ROWS,
|
|
264
|
+
(hidden) => claudeMoreLines(theme, hidden));
|
|
283
265
|
}
|
|
284
266
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
267
|
+
/**
|
|
268
|
+
* What Claude Code draws beneath a finished call: an edit's every hunk, a write's
|
|
269
|
+
* first rows, the author's view of a custom tool once opened, and otherwise
|
|
270
|
+
* nothing until the row is opened.
|
|
271
|
+
*/
|
|
272
|
+
function claudeBody(
|
|
273
|
+
name: string,
|
|
274
|
+
args: ToolArgs,
|
|
275
|
+
result: AgentToolResult<unknown>,
|
|
276
|
+
output: string,
|
|
277
|
+
expanded: boolean,
|
|
289
278
|
theme: Theme,
|
|
290
|
-
|
|
291
|
-
): Component {
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
const failureReason = hiddenFailureReason(name, state, ctx.isError, output);
|
|
297
|
-
runtime.setResultAvailable(state, name, output.length > 0);
|
|
298
|
-
if (!options.isPartial && !state.resultLineSummaryComputed) {
|
|
299
|
-
state.resultLineSummary = formatResultLineSummary(name, ctx.args, result, output);
|
|
300
|
-
state.resultLineSummaryComputed = true;
|
|
301
|
-
}
|
|
302
|
-
if (!state.expanded && !state.preview) {
|
|
303
|
-
const controls = renderControls(theme, state, options.isPartial, ctx.isError, state.resultLineSummary, failureReason);
|
|
304
|
-
rememberResult(state, result, options, ctx);
|
|
305
|
-
return controls;
|
|
279
|
+
author: AuthorResultRenderer | undefined,
|
|
280
|
+
): Component | undefined {
|
|
281
|
+
const path = pathArgument(args);
|
|
282
|
+
if (name === "edit") {
|
|
283
|
+
return renderCodeDiff(getEditPatch(result), getEditDiff(result), path, theme,
|
|
284
|
+
{ contextLines: CLAUDE_DIFF_CONTEXT_LINES, rail: CLAUDE_INDENT });
|
|
306
285
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
286
|
+
if (name === "write") {
|
|
287
|
+
const component = renderCodeView(output, path, theme, { rail: CLAUDE_INDENT });
|
|
288
|
+
return component && !expanded
|
|
289
|
+
? limitComponentLines(component, CLAUDE_WRITE_ROWS, theme, (hidden) => CLAUDE_INDENT + claudeMoreLines(theme, hidden))
|
|
290
|
+
: component;
|
|
311
291
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
return
|
|
292
|
+
// A command's output is already the result line itself.
|
|
293
|
+
const kind = rowKind(name);
|
|
294
|
+
if (!expanded || kind === "shell") return undefined;
|
|
295
|
+
if (kind === "custom") return author ? renderRowBody(name, args, result, output, true, theme, false, author) : undefined;
|
|
296
|
+
return renderFileBody(name, args, result, output, true, theme, false);
|
|
315
297
|
}
|
|
316
298
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
));
|
|
327
|
-
if (state.expanded && Object.keys(args).length > 0) container.addChild(renderArguments(args, theme));
|
|
328
|
-
return container;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
type AuthorResultRenderer = (expanded: boolean) => Component | undefined;
|
|
332
|
-
|
|
333
|
-
function renderCustomBody(
|
|
334
|
-
output: string,
|
|
335
|
-
expanded: boolean,
|
|
299
|
+
/**
|
|
300
|
+
* Claude Code's result block: a "└" line saying what happened, or the output
|
|
301
|
+
* itself for commands and custom tools, then any diff or code beneath it.
|
|
302
|
+
*/
|
|
303
|
+
function renderClaudeResult(
|
|
304
|
+
name: string,
|
|
305
|
+
args: ToolArgs,
|
|
306
|
+
result: AgentToolResult<unknown>,
|
|
307
|
+
options: ToolRenderResultOptions,
|
|
336
308
|
theme: Theme,
|
|
337
309
|
isError: boolean,
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
310
|
+
expanded: boolean,
|
|
311
|
+
output: string,
|
|
312
|
+
author: AuthorResultRenderer | undefined,
|
|
313
|
+
): Component {
|
|
314
|
+
const chrome = chromePainter(theme);
|
|
315
|
+
const container = new CachedContainer();
|
|
316
|
+
// One line under the call; collapsed it keeps to one row.
|
|
317
|
+
const line = (text: string) => prefixedLines(text, chrome(" └ "), CLAUDE_INDENT, !expanded);
|
|
318
|
+
if (options.isPartial) {
|
|
319
|
+
container.addChild(line(chrome("Running…")));
|
|
320
|
+
return container;
|
|
321
|
+
}
|
|
322
|
+
if (isError) {
|
|
323
|
+
const failure = claudeFailure(name, output);
|
|
324
|
+
// A command's output says why beneath its exit code; another tool's message is
|
|
325
|
+
// shortened to one line, and a click shows it whole in its place.
|
|
326
|
+
if (failure.detail) {
|
|
327
|
+
container.addChild(line(theme.fg("error", failure.headline)));
|
|
328
|
+
container.addChild(claudeOutput(failure.detail, expanded, theme, true, "", CLAUDE_INDENT));
|
|
329
|
+
} else if (expanded && output.trim()) {
|
|
330
|
+
container.addChild(claudeOutput(`Error: ${output.trim()}`, true, theme, true, "", chrome(" └ ")));
|
|
331
|
+
} else {
|
|
332
|
+
container.addChild(line(theme.fg("error", failure.headline)));
|
|
346
333
|
}
|
|
334
|
+
return container;
|
|
347
335
|
}
|
|
348
|
-
|
|
336
|
+
const body = claudeBody(name, args, result, output, expanded, theme, author);
|
|
337
|
+
// The author's view already shows a custom tool's output, so the line above it only counts it.
|
|
338
|
+
const outcome = body && rowKind(name) === "custom"
|
|
339
|
+
? formatResultLineSummary(name, args, result, output) ?? "Done"
|
|
340
|
+
: claudeOutcome(name, args, result, output, (value) => theme.bold(value));
|
|
341
|
+
if (outcome !== undefined) container.addChild(line(theme.fg("toolOutput", outcome)));
|
|
342
|
+
else container.addChild(claudeOutput(output, expanded, theme, false, rowKind(name) === "shell" ? "(No output)" : "(No content)", chrome(" └ ")));
|
|
343
|
+
if (body) container.addChild(body);
|
|
344
|
+
return container;
|
|
349
345
|
}
|
|
350
346
|
|
|
351
|
-
|
|
347
|
+
/**
|
|
348
|
+
* The result under a call: a preview or the whole of it when shown, and a status
|
|
349
|
+
* line with how long it took and how much it returned. An unchanged result reuses
|
|
350
|
+
* the component it drew last, so a large output is laid out once.
|
|
351
|
+
*/
|
|
352
|
+
function renderRowResult(
|
|
352
353
|
name: string,
|
|
353
354
|
result: AgentToolResult<unknown>,
|
|
354
355
|
options: ToolRenderResultOptions,
|
|
355
356
|
theme: Theme,
|
|
356
357
|
ctx: RenderContext,
|
|
357
|
-
|
|
358
|
+
author?: AuthorResultRenderer,
|
|
358
359
|
): Component {
|
|
359
360
|
const state = runtime.syncRow(ctx, options.isPartial, !options.isPartial);
|
|
360
361
|
runtime.syncExpansion(state, ctx.expanded, name);
|
|
361
362
|
if (canReuseResult(state, result, options, ctx)) return ctx.lastComponent;
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
363
|
+
const kind = rowKind(name);
|
|
364
|
+
const output = kind === "file" ? getFileOutput(name, ctx.args, result, ctx.isError) : getTextResult(result);
|
|
365
|
+
const hasEditDiff = name === "edit" && getEditDiff(result).length > 0;
|
|
366
|
+
runtime.setResultAvailable(state, name, hasEditDiff || output.length > 0 || author !== undefined);
|
|
367
|
+
let component: Component;
|
|
368
|
+
if (isClaude()) {
|
|
369
|
+
component = renderClaudeResult(name, ctx.args, result, options, theme, ctx.isError, state.expanded === true, output, author);
|
|
370
|
+
} else {
|
|
371
|
+
if (!options.isPartial && !state.resultLineSummaryComputed) {
|
|
372
|
+
// A custom tool with no text has nothing to count.
|
|
373
|
+
state.resultLineSummary = kind === "custom" && !output ? undefined : formatResultLineSummary(name, ctx.args, result, output);
|
|
374
|
+
state.resultLineSummaryComputed = true;
|
|
375
|
+
}
|
|
376
|
+
const container = new CachedContainer();
|
|
377
|
+
if (state.expanded || state.preview) {
|
|
378
|
+
const body = renderRowBody(name, ctx.args, result, output, state.expanded === true, theme, ctx.isError, author);
|
|
379
|
+
if (body) container.addChild(state.expanded ? body : limitComponentLines(body, runtime.config.previewLines, theme));
|
|
380
|
+
}
|
|
381
|
+
// A failed row whose output is hidden gets its reason on the status line. When
|
|
382
|
+
// the output shows, it already says why, and repeating it only doubles the error.
|
|
383
|
+
const failureReason = ctx.isError && !state.expanded && !state.preview ? summarizeFailure(name, output) : undefined;
|
|
384
|
+
container.addChild(renderControls(theme, state, options.isPartial, ctx.isError, failureReason,
|
|
385
|
+
name === "edit" ? countEditChanges(result) : undefined));
|
|
386
|
+
component = container;
|
|
372
387
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
rememberResult(state, result, options, ctx);
|
|
376
|
-
return container;
|
|
388
|
+
rememberResult(state, result, options, ctx.isError);
|
|
389
|
+
return component;
|
|
377
390
|
}
|
|
378
391
|
|
|
379
392
|
/**
|
|
@@ -388,7 +401,7 @@ function createCustomRenderers(name: string, author: ToolDefinition<any, any, an
|
|
|
388
401
|
let authorResult: Component | undefined;
|
|
389
402
|
const own = (ctx: RenderContext<any>): RenderContext => ({ ...ctx, args: ctx.args ?? {}, state });
|
|
390
403
|
return {
|
|
391
|
-
renderCall: (args, theme, ctx) =>
|
|
404
|
+
renderCall: (args, theme, ctx) => renderRowCall(name, (args ?? {}) as ToolArgs, theme, own(ctx)),
|
|
392
405
|
renderResult: (result, options, theme, ctx) => {
|
|
393
406
|
const authorRenderer = author?.renderResult;
|
|
394
407
|
const renderAuthorResult = authorRenderer
|
|
@@ -397,7 +410,7 @@ function createCustomRenderers(name: string, author: ToolDefinition<any, any, an
|
|
|
397
410
|
return authorResult;
|
|
398
411
|
}
|
|
399
412
|
: undefined;
|
|
400
|
-
return
|
|
413
|
+
return renderRowResult(name, result, options, theme, own(ctx), renderAuthorResult);
|
|
401
414
|
},
|
|
402
415
|
};
|
|
403
416
|
}
|
|
@@ -405,7 +418,7 @@ function createCustomRenderers(name: string, author: ToolDefinition<any, any, an
|
|
|
405
418
|
function resolveCustomRow(row: ToolRow): RowRenderers | undefined {
|
|
406
419
|
const customTools = runtime.config.custom_tools;
|
|
407
420
|
// Built-ins are governed by `tools`: ones left out keep Pi's own renderer.
|
|
408
|
-
if (!customTools.enabled || SUPPORTED_TOOL_SET.has(row.toolName)) return undefined;
|
|
421
|
+
if (runtime.config.style === "off" || !customTools.enabled || SUPPORTED_TOOL_SET.has(row.toolName)) return undefined;
|
|
409
422
|
if (customTools.exclude.includes(row.toolName)) return undefined;
|
|
410
423
|
return createCustomRenderers(row.toolName, row.toolDefinition);
|
|
411
424
|
}
|
|
@@ -421,54 +434,32 @@ const toolFactories: Record<CompactToolName, (cwd: string) => BuiltInDefinition>
|
|
|
421
434
|
ls: createLsToolDefinition,
|
|
422
435
|
};
|
|
423
436
|
|
|
424
|
-
function
|
|
425
|
-
return toolFactories[name](cwd);
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
function registerFileTool(pi: ExtensionAPI, definition: BuiltInDefinition): void {
|
|
437
|
+
function registerCompactTool(pi: ExtensionAPI, definition: BuiltInDefinition): void {
|
|
429
438
|
pi.registerTool({
|
|
430
439
|
...definition,
|
|
431
440
|
execute: runtime.createTimedExecute(definition),
|
|
432
441
|
renderShell: "self",
|
|
433
|
-
renderCall: (args: ToolArgs, theme: Theme, ctx: RenderContext) =>
|
|
434
|
-
renderResult: (
|
|
435
|
-
result
|
|
436
|
-
options: ToolRenderResultOptions,
|
|
437
|
-
theme: Theme,
|
|
438
|
-
ctx: RenderContext,
|
|
439
|
-
) => renderFileResult(definition, result, options, theme, ctx),
|
|
442
|
+
renderCall: (args: ToolArgs, theme: Theme, ctx: RenderContext) => renderRowCall(definition.name, args, theme, ctx),
|
|
443
|
+
renderResult: (result: AgentToolResult<unknown>, options: ToolRenderResultOptions, theme: Theme, ctx: RenderContext) =>
|
|
444
|
+
renderRowResult(definition.name, result, options, theme, ctx),
|
|
440
445
|
} as ToolDefinition<any, any, RowState>);
|
|
441
446
|
}
|
|
442
447
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
...definition,
|
|
447
|
-
execute: runtime.createTimedExecute(definition),
|
|
448
|
-
renderShell: "self",
|
|
449
|
-
renderCall: (args: BashToolInput, theme: Theme, ctx: RenderContext<BashToolInput>) =>
|
|
450
|
-
renderShellCall(name, args, theme, ctx),
|
|
451
|
-
renderResult: (
|
|
452
|
-
result: AgentToolResult<BashToolDetails | undefined>,
|
|
453
|
-
options: ToolRenderResultOptions,
|
|
454
|
-
theme: Theme,
|
|
455
|
-
ctx: RenderContext<BashToolInput>,
|
|
456
|
-
) => renderShellResult(name, result, options, theme, ctx),
|
|
457
|
-
} as ToolDefinition<any, BashToolDetails | undefined, RowState>);
|
|
448
|
+
/** Off leaves every tool with Pi's own renderer. */
|
|
449
|
+
function compactedTools(): CompactToolName[] {
|
|
450
|
+
return runtime.config.style === "off" ? [] : runtime.config.tools;
|
|
458
451
|
}
|
|
459
452
|
|
|
460
453
|
function registerTools(pi: ExtensionAPI, cwd: string): void {
|
|
461
|
-
const enabledTools = new Set(
|
|
454
|
+
const enabledTools = new Set(compactedTools());
|
|
462
455
|
for (const name of registeredTools) {
|
|
463
456
|
// Pi exposes no unregister API, so restoring a disabled tool means re-registering
|
|
464
457
|
// the built-in definition to release this extension's renderers.
|
|
465
|
-
if (!enabledTools.has(name)) pi.registerTool(
|
|
458
|
+
if (!enabledTools.has(name)) pi.registerTool(toolFactories[name](cwd));
|
|
466
459
|
}
|
|
467
460
|
registeredTools.clear();
|
|
468
|
-
for (const name of
|
|
469
|
-
|
|
470
|
-
if (name === "bash" || name === "powershell") registerShellTool(pi, definition);
|
|
471
|
-
else registerFileTool(pi, definition);
|
|
461
|
+
for (const name of compactedTools()) {
|
|
462
|
+
registerCompactTool(pi, toolFactories[name](cwd));
|
|
472
463
|
registeredTools.add(name);
|
|
473
464
|
}
|
|
474
465
|
}
|
|
@@ -496,6 +487,7 @@ export default function compactTools(pi: ExtensionAPI): void {
|
|
|
496
487
|
const progress = new ProgressController(pi);
|
|
497
488
|
const viewport = new ViewportKeeper();
|
|
498
489
|
const silent = new SilentModeController(pi, showReleaseNotice);
|
|
490
|
+
const groups = new ToolGroupController(runtime);
|
|
499
491
|
const customRowsAvailable = installToolRowPatch();
|
|
500
492
|
setRowResolver(resolveCustomRow);
|
|
501
493
|
let customRowsWarned = false;
|
|
@@ -505,12 +497,13 @@ export default function compactTools(pi: ExtensionAPI): void {
|
|
|
505
497
|
pi.on("session_start", (event, ctx) => {
|
|
506
498
|
if (event.reason !== "reload") runtime.clearTimings();
|
|
507
499
|
configure(pi, ctx.cwd, ctx.isProjectTrusted());
|
|
508
|
-
if (ctx.mode === "tui") {
|
|
500
|
+
if (ctx.mode === "tui" && runtime.config.style !== "off") {
|
|
509
501
|
// First, so its input listener sees relayout keys before the thinking controller consumes them.
|
|
510
502
|
viewport.bind(ctx);
|
|
511
503
|
progress.bind(ctx);
|
|
512
504
|
thinkingCycle.bind(ctx);
|
|
513
505
|
silent.bind(ctx, runtime.config.mode, event.reason === "reload");
|
|
506
|
+
groups.bind(ctx);
|
|
514
507
|
if (!silent.isEnabled()) showReleaseNotice(ctx);
|
|
515
508
|
if (runtime.config.custom_tools.enabled && !customRowsAvailable && !customRowsWarned) {
|
|
516
509
|
customRowsWarned = true;
|
|
@@ -521,6 +514,7 @@ export default function compactTools(pi: ExtensionAPI): void {
|
|
|
521
514
|
progress.dispose();
|
|
522
515
|
thinkingCycle.dispose();
|
|
523
516
|
silent.dispose();
|
|
517
|
+
groups.dispose();
|
|
524
518
|
}
|
|
525
519
|
});
|
|
526
520
|
pi.on("tool_execution_start", (event) => runtime.noteExecutionStart(event.toolCallId));
|
|
@@ -529,6 +523,7 @@ export default function compactTools(pi: ExtensionAPI): void {
|
|
|
529
523
|
thinkingCycle.dispose();
|
|
530
524
|
progress.dispose();
|
|
531
525
|
silent.dispose();
|
|
526
|
+
groups.dispose();
|
|
532
527
|
viewport.dispose();
|
|
533
528
|
runtime.reset(event.reason !== "reload");
|
|
534
529
|
});
|