pi-minimal 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +81 -1
- package/CONTEXT.md +107 -0
- package/README.md +9 -6
- package/dist/background-overlay.js +95 -0
- package/dist/background.js +206 -0
- package/dist/config.js +10 -1
- package/dist/cwd.js +2 -2
- package/dist/edit-diff.js +74 -0
- package/dist/footer-contribution-events.js +5 -3
- package/dist/footer-contributions.js +15 -13
- package/dist/footer.js +15 -3
- package/dist/git-state.js +31 -11
- package/dist/herdr.js +78 -0
- package/dist/index.js +79 -202
- package/dist/listeners.js +23 -0
- package/dist/prs-command.js +76 -0
- package/dist/prs.js +270 -0
- package/dist/reclaim.js +0 -3
- package/dist/rex.js +289 -0
- package/dist/session.js +168 -0
- package/dist/siblings.js +14 -0
- package/dist/tools.js +213 -374
- package/dist/tracker.js +2 -2
- package/dist/working-status.js +2 -1
- package/doc/archive/README.md +3 -0
- package/doc/footer-contributions.md +7 -7
- package/package.json +5 -3
- package/release-notes.md +33 -12
- package/src/background-overlay.ts +103 -0
- package/src/background.ts +250 -0
- package/src/config.ts +10 -1
- package/src/cwd.ts +5 -4
- package/src/edit-diff.ts +73 -0
- package/src/footer-contribution-events.ts +7 -4
- package/src/footer-contributions.ts +15 -13
- package/src/footer.ts +23 -3
- package/src/git-state.ts +29 -10
- package/src/herdr.ts +78 -5
- package/src/index.ts +81 -195
- package/src/listeners.ts +22 -0
- package/src/prs-command.ts +81 -0
- package/src/prs.ts +348 -0
- package/src/reclaim.ts +0 -4
- package/src/rex.ts +304 -0
- package/src/session.ts +185 -0
- package/src/siblings.ts +38 -0
- package/src/tools.ts +261 -389
- package/src/tracker.ts +11 -5
- package/src/working-status.ts +2 -1
- package/test/background.test.ts +198 -0
- package/test/config.test.ts +24 -11
- package/test/footer-composition.test.ts +61 -0
- package/test/footer-contribution-events.test.ts +15 -0
- package/test/git-state.test.ts +35 -0
- package/test/index.test.ts +202 -4
- package/test/prs.test.ts +272 -0
- package/test/rex.test.ts +249 -0
- package/test/tools.test.ts +36 -3
- package/test/working-status.test.ts +53 -0
- /package/doc/{plans/2026-09-22-pi-minimal-package.md → archive/2026-09-22-plan-pi-minimal-package.md} +0 -0
- /package/doc/{specs/2026-09-22-pi-minimal-package.md → archive/2026-09-22-spec-pi-minimal-package.md} +0 -0
package/dist/tools.js
CHANGED
|
@@ -2,7 +2,7 @@ import { Container, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui
|
|
|
2
2
|
import { rowIsRunning } from "./activity.js";
|
|
3
3
|
import * as chrome from "./chrome.js";
|
|
4
4
|
import { callSummary, codemodeOutcome, nestedCalls, nestedItem, scriptOutput } from "./codemode-rows.js";
|
|
5
|
-
import {
|
|
5
|
+
import { colorDiffLines, editDiffText } from "./edit-diff.js";
|
|
6
6
|
import { createLiveWatch } from "./live-watch.js";
|
|
7
7
|
import { bashBody, formatDuration, formatOutcome, layoutRow, resultText, rowGlyph, toolItem, } from "./tool-rows.js";
|
|
8
8
|
/** Tolerates a stale chrome module after /reload, where only CHROME_MARGIN may exist. */
|
|
@@ -14,6 +14,8 @@ const rowIndent = () => margin() + 1;
|
|
|
14
14
|
const EXPANDED_MAX_LINES = 400;
|
|
15
15
|
/** Max diff lines shown inline under an edit row with `editDiff: "always"`; ctrl+o shows the rest. */
|
|
16
16
|
const INLINE_DIFF_MAX_LINES = 40;
|
|
17
|
+
/** With `editDiff: "expanded"`, diffs this short are shown inline anyway: hiding them saves nothing. */
|
|
18
|
+
const SMALL_DIFF_MAX_LINES = 8;
|
|
17
19
|
let editDiffMode = "expanded";
|
|
18
20
|
/** Applies the `editDiff` setting; read at session start, so a change needs a new session or /reload. */
|
|
19
21
|
export function setEditDiffMode(mode) {
|
|
@@ -22,13 +24,10 @@ export function setEditDiffMode(mode) {
|
|
|
22
24
|
export function stopLiveWatch() {
|
|
23
25
|
live.stop();
|
|
24
26
|
}
|
|
25
|
-
function liveId(context, item) {
|
|
26
|
-
return context.toolCallId || item;
|
|
27
|
-
}
|
|
28
27
|
function rowStatus(context) {
|
|
29
28
|
if (context.isError)
|
|
30
29
|
return "error";
|
|
31
|
-
if (context.state.outcome?.errorLine)
|
|
30
|
+
if (context.state.outcome?.errorLine !== undefined)
|
|
32
31
|
return "error";
|
|
33
32
|
return rowIsRunning(context) ? "running" : "ok";
|
|
34
33
|
}
|
|
@@ -39,243 +38,176 @@ function statusColor(status) {
|
|
|
39
38
|
return "warning";
|
|
40
39
|
return "success";
|
|
41
40
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return [];
|
|
48
|
-
const lines = text.split("\n");
|
|
49
|
-
if (lines.length > EXPANDED_MAX_LINES) {
|
|
50
|
-
const hidden = lines.length - EXPANDED_MAX_LINES;
|
|
51
|
-
return [...lines.slice(0, EXPANDED_MAX_LINES), `… ${hidden} more lines`];
|
|
52
|
-
}
|
|
53
|
-
return lines;
|
|
54
|
-
}
|
|
55
|
-
function outputBlock(kind, result, theme) {
|
|
56
|
-
const lines = expandedLines(kind, result);
|
|
57
|
-
if (lines.length === 0)
|
|
58
|
-
return new Container();
|
|
59
|
-
const indent = " ".repeat(rowIndent() + 2);
|
|
60
|
-
const rule = theme.fg("borderMuted", "│");
|
|
41
|
+
// ---------------------------------------------------------------------------------------------
|
|
42
|
+
// Blocks under a row
|
|
43
|
+
const ruled = (lines, theme, indentCells, tone = "dim") => {
|
|
44
|
+
const indent = " ".repeat(rowIndent() + indentCells);
|
|
45
|
+
const rule = theme.fg(tone === "error" ? "error" : "borderMuted", "│");
|
|
61
46
|
return {
|
|
62
47
|
render(width) {
|
|
63
48
|
const inner = Math.max(0, width - indent.length - 2);
|
|
64
|
-
return lines.map((line) => `${indent}${rule} ${theme.fg(
|
|
49
|
+
return lines.map((line) => `${indent}${rule} ${tone ? theme.fg(tone, truncateToWidth(line, inner)) : truncateToWidth(line, inner)}`);
|
|
65
50
|
},
|
|
66
51
|
invalidate() { },
|
|
67
52
|
};
|
|
53
|
+
};
|
|
54
|
+
function capped(lines, max, more) {
|
|
55
|
+
if (lines.length <= max)
|
|
56
|
+
return lines;
|
|
57
|
+
return [...lines.slice(0, max), more(lines.length - max)];
|
|
68
58
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
/** pi's edit diff lines: "+123 content", "-123 content", " 123 context"; anything else (e.g. "...") is a separator. */
|
|
74
|
-
function parseDiffLine(line) {
|
|
75
|
-
const match = /^([+\- ])(\s*\d*) (.*)$/.exec(line);
|
|
76
|
-
if (!match)
|
|
59
|
+
/** Plain text (output, file content, script) in the ruled block, dim, capped at the expanded limit. */
|
|
60
|
+
function textBlock(text, theme, indentCells, label) {
|
|
61
|
+
if (!text)
|
|
77
62
|
return undefined;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
/** Word-level diff of a 1:1 replacement; changed tokens are inverted, leading indentation is left alone, as in pi. */
|
|
81
|
-
function highlightPair(removed, added, theme) {
|
|
82
|
-
let out = { removed: "", added: "" };
|
|
83
|
-
let firstRemoved = true;
|
|
84
|
-
let firstAdded = true;
|
|
85
|
-
for (const part of diffWords(removed, added)) {
|
|
86
|
-
if (part.removed || part.added) {
|
|
87
|
-
const key = part.removed ? "removed" : "added";
|
|
88
|
-
let value = part.value;
|
|
89
|
-
if (part.removed ? firstRemoved : firstAdded) {
|
|
90
|
-
const ws = /^(\s*)/.exec(value)?.[1] ?? "";
|
|
91
|
-
value = value.slice(ws.length);
|
|
92
|
-
out = { ...out, [key]: out[key] + ws };
|
|
93
|
-
if (part.removed)
|
|
94
|
-
firstRemoved = false;
|
|
95
|
-
else
|
|
96
|
-
firstAdded = false;
|
|
97
|
-
}
|
|
98
|
-
if (value)
|
|
99
|
-
out = { ...out, [key]: out[key] + theme.inverse(value) };
|
|
100
|
-
}
|
|
101
|
-
else {
|
|
102
|
-
out = { removed: out.removed + part.value, added: out.added + part.value };
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
return out;
|
|
63
|
+
const lines = capped(text.split("\n"), EXPANDED_MAX_LINES, (hidden) => `… ${hidden} more lines`);
|
|
64
|
+
return ruled(label ? [label, ...lines] : lines, theme, indentCells);
|
|
106
65
|
}
|
|
107
|
-
/**
|
|
108
|
-
function colorDiffLines(lines, theme) {
|
|
109
|
-
const out = [];
|
|
110
|
-
for (let i = 0; i < lines.length;) {
|
|
111
|
-
const parsed = parseDiffLine(lines[i]);
|
|
112
|
-
if (!parsed) {
|
|
113
|
-
out.push(theme.fg("toolDiffContext", lines[i]));
|
|
114
|
-
i++;
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
|
-
if (parsed.prefix === " ") {
|
|
118
|
-
out.push(theme.fg("toolDiffContext", ` ${parsed.lineNum} ${parsed.content}`));
|
|
119
|
-
i++;
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
const removed = [];
|
|
123
|
-
const added = [];
|
|
124
|
-
for (let p = parseDiffLine(lines[i] ?? ""); p?.prefix === "-"; p = parseDiffLine(lines[++i] ?? ""))
|
|
125
|
-
removed.push(p);
|
|
126
|
-
for (let p = parseDiffLine(lines[i] ?? ""); p?.prefix === "+"; p = parseDiffLine(lines[++i] ?? ""))
|
|
127
|
-
added.push(p);
|
|
128
|
-
if (removed.length === 1 && added.length === 1) {
|
|
129
|
-
const pair = highlightPair(removed[0].content, added[0].content, theme);
|
|
130
|
-
out.push(theme.fg("toolDiffRemoved", `-${removed[0].lineNum} ${pair.removed}`));
|
|
131
|
-
out.push(theme.fg("toolDiffAdded", `+${added[0].lineNum} ${pair.added}`));
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
for (const line of removed)
|
|
135
|
-
out.push(theme.fg("toolDiffRemoved", `-${line.lineNum} ${line.content}`));
|
|
136
|
-
for (const line of added)
|
|
137
|
-
out.push(theme.fg("toolDiffAdded", `+${line.lineNum} ${line.content}`));
|
|
138
|
-
}
|
|
139
|
-
return out;
|
|
140
|
-
}
|
|
141
|
-
/** The edit's diff in the `│`-ruled block. Inline views are capped; expanded views show up to the usual limit. */
|
|
66
|
+
/** The edit's diff in the ruled block, coloured per line; inline views are capped and point at ctrl+o. */
|
|
142
67
|
function diffBlock(diff, theme, expanded) {
|
|
143
68
|
const all = diff.replace(/\n$/, "").split("\n");
|
|
144
69
|
const max = expanded ? EXPANDED_MAX_LINES : INLINE_DIFF_MAX_LINES;
|
|
145
|
-
const
|
|
146
|
-
const hidden = all.length -
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
return
|
|
150
|
-
render(width) {
|
|
151
|
-
const inner = Math.max(0, width - indent.length - 2);
|
|
152
|
-
const out = lines.map((line) => `${indent}${rule} ${truncateToWidth(line, inner)}`);
|
|
153
|
-
if (hidden > 0)
|
|
154
|
-
out.push(`${indent}${rule} ${theme.fg("dim", `… ${hidden} more lines${expanded ? "" : " (ctrl+o)"}`)}`);
|
|
155
|
-
return out;
|
|
156
|
-
},
|
|
157
|
-
invalidate() { },
|
|
158
|
-
};
|
|
70
|
+
const shown = colorDiffLines(all.slice(0, max), theme);
|
|
71
|
+
const hidden = all.length - shown.length;
|
|
72
|
+
if (hidden > 0)
|
|
73
|
+
shown.push(theme.fg("dim", `… ${hidden} more lines${expanded ? "" : " (ctrl+o)"}`));
|
|
74
|
+
return ruled(shown, theme, 2, undefined);
|
|
159
75
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
},
|
|
170
|
-
invalidate() { },
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
function activityRow(kind, args, theme, context) {
|
|
174
|
-
const item = toolItem(kind, args);
|
|
175
|
-
const id = liveId(context, item);
|
|
176
|
-
const running = rowIsRunning(context);
|
|
177
|
-
context.state.running = running;
|
|
178
|
-
if (running) {
|
|
179
|
-
context.state.startedAt ??= Date.now();
|
|
180
|
-
live.watch(id, context.invalidate);
|
|
76
|
+
// ---------------------------------------------------------------------------------------------
|
|
77
|
+
// The one row lifecycle
|
|
78
|
+
/** `glyph verb item … outcome` on one line; an edit's `+N −M` is coloured success/error. */
|
|
79
|
+
function rowLine(theme, width, status, frame, verb, item, outcome, elapsed) {
|
|
80
|
+
const diff = status !== "running" ? outcome?.diff : undefined;
|
|
81
|
+
let right = status === "running" ? elapsed : (outcome?.text ?? "");
|
|
82
|
+
if (diff) {
|
|
83
|
+
const stats = `+${diff.added} −${diff.removed}`;
|
|
84
|
+
right = right ? `${stats} · ${right}` : stats;
|
|
181
85
|
}
|
|
182
|
-
|
|
183
|
-
|
|
86
|
+
const inner = Math.max(8, width - rowIndent());
|
|
87
|
+
const row = layoutRow(inner, verb, item, right);
|
|
88
|
+
let coloredRight;
|
|
89
|
+
if (status === "running")
|
|
90
|
+
coloredRight = theme.fg("warning", row.outcome);
|
|
91
|
+
else if (status === "error")
|
|
92
|
+
coloredRight = theme.fg("error", row.outcome);
|
|
93
|
+
else if (diff && row.outcome) {
|
|
94
|
+
const rest = row.outcome.slice(`+${diff.added} −${diff.removed}`.length);
|
|
95
|
+
coloredRight = theme.fg("success", `+${diff.added}`) + " " + theme.fg("error", `−${diff.removed}`) + theme.fg("dim", rest);
|
|
184
96
|
}
|
|
97
|
+
else
|
|
98
|
+
coloredRight = theme.fg("dim", row.outcome);
|
|
99
|
+
return (" ".repeat(rowIndent()) +
|
|
100
|
+
theme.fg(statusColor(status), rowGlyph(status, frame)) +
|
|
101
|
+
" " +
|
|
102
|
+
theme.fg("dim", row.verb) +
|
|
103
|
+
" " +
|
|
104
|
+
theme.fg(status === "error" ? "text" : "muted", row.item) +
|
|
105
|
+
" ".repeat(row.gap) +
|
|
106
|
+
coloredRight);
|
|
107
|
+
}
|
|
108
|
+
export function createRowRenderers(strategy) {
|
|
109
|
+
const detailIndent = strategy.detailIndent ?? 2;
|
|
185
110
|
return {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const
|
|
189
|
-
const
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
right = started ? formatDuration(Date.now() - started) : "";
|
|
111
|
+
renderShell: "self",
|
|
112
|
+
renderCall(args, theme, rawContext) {
|
|
113
|
+
const context = rawContext;
|
|
114
|
+
const id = (context.state.liveId ??= context.toolCallId || `${strategy.verb}:${strategy.item(args, context.state)}`);
|
|
115
|
+
const running = rowIsRunning(context);
|
|
116
|
+
context.state.running = running;
|
|
117
|
+
if (running) {
|
|
118
|
+
context.state.startedAt ??= Date.now();
|
|
119
|
+
live.watch(id, context.invalidate);
|
|
196
120
|
}
|
|
197
|
-
else
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
121
|
+
else
|
|
122
|
+
live.unwatch(id);
|
|
123
|
+
return {
|
|
124
|
+
render(width) {
|
|
125
|
+
const status = rowStatus(context);
|
|
126
|
+
const frame = status === "running" && live.has(id) ? live.frame : 0;
|
|
127
|
+
const elapsed = context.state.startedAt ? formatDuration(Date.now() - context.state.startedAt) : "";
|
|
128
|
+
const item = strategy.item(args, context.state);
|
|
129
|
+
const lines = [rowLine(theme, width, status, frame, strategy.verb, item, context.state.outcome, elapsed)];
|
|
130
|
+
if (strategy.childLines)
|
|
131
|
+
lines.push(...strategy.childLines(context.state, theme, width, frame, Boolean(context.expanded)));
|
|
132
|
+
return lines;
|
|
133
|
+
},
|
|
134
|
+
invalidate() { },
|
|
135
|
+
};
|
|
136
|
+
},
|
|
137
|
+
renderResult(result, options, theme, rawContext) {
|
|
138
|
+
const context = rawContext;
|
|
139
|
+
const typed = result;
|
|
140
|
+
const isError = Boolean(context.isError);
|
|
141
|
+
strategy.onResult?.(typed, context.state);
|
|
142
|
+
if (!context.isPartial) {
|
|
143
|
+
context.state.done = true;
|
|
144
|
+
context.state.running = false;
|
|
145
|
+
context.state.outcome = strategy.outcome(context.args, typed, isError, context.durationMs);
|
|
146
|
+
live.unwatch(context.state.liveId ?? context.toolCallId ?? `${strategy.verb}:${strategy.item(context.args, context.state)}`);
|
|
203
147
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
148
|
+
// No invalidate here: the host runs renderCall and renderResult in one updateDisplay() and
|
|
149
|
+
// paints afterwards, so the call row's lazy render() already sees this state.
|
|
150
|
+
const container = new Container();
|
|
151
|
+
const expanded = Boolean(options.expanded);
|
|
152
|
+
const errorLine = context.state.outcome?.errorLine;
|
|
153
|
+
if (errorLine && !expanded)
|
|
154
|
+
container.addChild(ruled([errorLine], theme, detailIndent, "error"));
|
|
155
|
+
if (!context.isPartial) {
|
|
156
|
+
const detail = strategy.detail?.(context.args, typed, context.state, theme, expanded, isError);
|
|
157
|
+
if (detail)
|
|
158
|
+
container.addChild(detail);
|
|
215
159
|
}
|
|
216
|
-
|
|
217
|
-
coloredRight = theme.fg("dim", row.outcome);
|
|
218
|
-
const line = " ".repeat(rowIndent()) +
|
|
219
|
-
theme.fg(statusColor(status), glyph) +
|
|
220
|
-
" " +
|
|
221
|
-
theme.fg("dim", row.verb) +
|
|
222
|
-
" " +
|
|
223
|
-
theme.fg(status === "error" ? "text" : "muted", row.item) +
|
|
224
|
-
" ".repeat(row.gap) +
|
|
225
|
-
coloredRight;
|
|
226
|
-
return [line];
|
|
160
|
+
return container;
|
|
227
161
|
},
|
|
228
|
-
invalidate() { },
|
|
229
162
|
};
|
|
230
163
|
}
|
|
164
|
+
// ---------------------------------------------------------------------------------------------
|
|
165
|
+
// Strategy: built-in tools (bash, read, edit, write, grep, find, ls)
|
|
231
166
|
const TOOL_KINDS = new Set(["bash", "read", "edit", "write", "grep", "find", "ls"]);
|
|
232
167
|
export function isToolKind(name) {
|
|
233
168
|
return TOOL_KINDS.has(name);
|
|
234
169
|
}
|
|
235
|
-
|
|
170
|
+
/** Which edit diffs show without ctrl+o under the current setting. */
|
|
171
|
+
function diffShownInline(diffLines) {
|
|
172
|
+
if (editDiffMode === "always")
|
|
173
|
+
return true;
|
|
174
|
+
if (editDiffMode === "expanded")
|
|
175
|
+
return diffLines <= SMALL_DIFF_MAX_LINES;
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
function compactStrategy(kind) {
|
|
236
179
|
return {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
180
|
+
verb: kind,
|
|
181
|
+
item: (args) => toolItem(kind, args),
|
|
182
|
+
outcome: (args, result, isError, durationMs) => formatOutcome(kind, args, result, isError, durationMs),
|
|
183
|
+
detail(args, result, _state, theme, expanded, isError) {
|
|
184
|
+
if (kind === "edit" && !isError) {
|
|
185
|
+
const diff = editDiffText(result);
|
|
186
|
+
if (diff) {
|
|
187
|
+
const lines = diff.replace(/\n$/, "").split("\n").length;
|
|
188
|
+
if (expanded ? editDiffMode !== "never" : diffShownInline(lines))
|
|
189
|
+
return diffBlock(diff, theme, expanded);
|
|
190
|
+
if (!expanded)
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
249
193
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
else if (expanded)
|
|
260
|
-
container.addChild(outputBlock(kind, typedResult, theme));
|
|
261
|
-
return container;
|
|
194
|
+
if (!expanded)
|
|
195
|
+
return undefined;
|
|
196
|
+
if (kind === "write" && !isError) {
|
|
197
|
+
const content = args?.content;
|
|
198
|
+
if (typeof content === "string")
|
|
199
|
+
return textBlock(content.replace(/\n$/, ""), theme, 2);
|
|
200
|
+
}
|
|
201
|
+
const text = kind === "bash" ? bashBody(resultText(result)).body : resultText(result);
|
|
202
|
+
return textBlock(text, theme, 2);
|
|
262
203
|
},
|
|
263
204
|
};
|
|
264
205
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const inner = Math.max(8, width - rowIndent());
|
|
268
|
-
const row = layoutRow(inner, verb, item, right);
|
|
269
|
-
const rightColor = status === "running" ? "warning" : status === "error" ? "error" : "dim";
|
|
270
|
-
return (" ".repeat(rowIndent()) +
|
|
271
|
-
theme.fg(statusColor(status), rowGlyph(status, frame)) +
|
|
272
|
-
" " +
|
|
273
|
-
theme.fg("dim", row.verb) +
|
|
274
|
-
" " +
|
|
275
|
-
theme.fg(status === "error" ? "text" : "muted", row.item) +
|
|
276
|
-
" ".repeat(row.gap) +
|
|
277
|
-
theme.fg(rightColor, row.outcome));
|
|
206
|
+
export function compactRenderers(kind) {
|
|
207
|
+
return createRowRenderers(compactStrategy(kind));
|
|
278
208
|
}
|
|
209
|
+
// ---------------------------------------------------------------------------------------------
|
|
210
|
+
// Strategy: tools without a renderer of their own (most MCP tools, subagents_enable)
|
|
279
211
|
/** Longest first result line shown verbatim as an outcome; longer results show a line count. */
|
|
280
212
|
const OUTCOME_TEXT_MAX = 24;
|
|
281
213
|
/** `mcp` for MCP tools, `agent` for subagent tools, `tool` for anything else. */
|
|
@@ -297,13 +229,13 @@ export function genericItem(toolName, args) {
|
|
|
297
229
|
}
|
|
298
230
|
const params = nestedItem({ name: toolName, args: json });
|
|
299
231
|
const mcp = /^mcp__(.+?)__(.+)$/.exec(toolName);
|
|
300
|
-
const label = mcp ? `${mcp[1]}
|
|
232
|
+
const label = mcp ? `${mcp[1]} · ${mcp[2]}` : toolName;
|
|
301
233
|
return params ? `${label} ${params}` : label;
|
|
302
234
|
}
|
|
303
235
|
export function genericOutcome(result, isError, durationMs) {
|
|
304
236
|
const text = resultText(result);
|
|
305
237
|
const dur = formatDuration(durationMs);
|
|
306
|
-
const join = (...parts) => parts.filter(Boolean).join("
|
|
238
|
+
const join = (...parts) => parts.filter(Boolean).join(" · ");
|
|
307
239
|
const lines = text.split("\n").filter((line) => line.trim().length > 0);
|
|
308
240
|
if (isError)
|
|
309
241
|
return { text: join("failed", dur), errorLine: lines[0]?.trim() ?? "" };
|
|
@@ -315,191 +247,98 @@ export function genericOutcome(result, isError, durationMs) {
|
|
|
315
247
|
return { text: join(`${lines.length} line${lines.length === 1 ? "" : "s"}`, dur) };
|
|
316
248
|
}
|
|
317
249
|
/**
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
* Without it they fall through to pi-core's default: bold name, JSON arguments, a text preview.
|
|
250
|
+
* `✓ mcp re_mcp · query_prometheus query=… 12 lines · 1.4s`. Without it such tools fall
|
|
251
|
+
* through to pi-core's default: bold name, JSON arguments, a text preview.
|
|
321
252
|
*/
|
|
322
253
|
export function genericRenderers(toolName) {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const item = genericItem(toolName, args);
|
|
330
|
-
const running = rowIsRunning(context);
|
|
331
|
-
context.state.running = running;
|
|
332
|
-
if (running) {
|
|
333
|
-
context.state.startedAt ??= Date.now();
|
|
334
|
-
live.watch(id, context.invalidate);
|
|
335
|
-
}
|
|
336
|
-
else
|
|
337
|
-
live.unwatch(id);
|
|
338
|
-
return {
|
|
339
|
-
render(width) {
|
|
340
|
-
const status = rowStatus(context);
|
|
341
|
-
const frame = status === "running" && live.has(id) ? live.frame : 0;
|
|
342
|
-
const right = status === "running"
|
|
343
|
-
? context.state.startedAt
|
|
344
|
-
? formatDuration(Date.now() - context.state.startedAt)
|
|
345
|
-
: ""
|
|
346
|
-
: (context.state.outcome?.text ?? "");
|
|
347
|
-
return [rowLine(theme, width, status, frame, verb, item, right)];
|
|
348
|
-
},
|
|
349
|
-
invalidate() { },
|
|
350
|
-
};
|
|
351
|
-
},
|
|
352
|
-
renderResult(result, options, theme, rawContext) {
|
|
353
|
-
const context = rawContext;
|
|
354
|
-
const typed = result;
|
|
355
|
-
if (!context.isPartial) {
|
|
356
|
-
context.state.done = true;
|
|
357
|
-
context.state.running = false;
|
|
358
|
-
context.state.outcome = genericOutcome(typed, Boolean(context.isError), context.durationMs);
|
|
359
|
-
live.unwatch(context.toolCallId || toolName);
|
|
360
|
-
}
|
|
361
|
-
const container = new Container();
|
|
362
|
-
const expanded = Boolean(options.expanded);
|
|
363
|
-
const errorLine = context.state.outcome?.errorLine;
|
|
364
|
-
if (errorLine && !expanded)
|
|
365
|
-
container.addChild(errorLineBlock(errorLine, theme));
|
|
366
|
-
if (expanded) {
|
|
367
|
-
const text = resultText(typed);
|
|
368
|
-
if (text) {
|
|
369
|
-
const indent = " ".repeat(rowIndent() + 2);
|
|
370
|
-
container.addChild({ render: (width) => ruledLines(text, width, theme, indent), invalidate() { } });
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
return container;
|
|
374
|
-
},
|
|
375
|
-
};
|
|
254
|
+
return createRowRenderers({
|
|
255
|
+
verb: genericVerb(toolName),
|
|
256
|
+
item: (args) => genericItem(toolName, args),
|
|
257
|
+
outcome: (_args, result, isError, durationMs) => genericOutcome(result, isError, durationMs),
|
|
258
|
+
detail: (_args, result, _state, theme, expanded) => (expanded ? textBlock(resultText(result), theme, 2) : undefined),
|
|
259
|
+
});
|
|
376
260
|
}
|
|
377
261
|
/** True when a tool definition draws its own call or result. */
|
|
378
262
|
function hasOwnRenderer(definition) {
|
|
379
263
|
return typeof definition?.renderCall === "function" || typeof definition?.renderResult === "function";
|
|
380
264
|
}
|
|
265
|
+
// ---------------------------------------------------------------------------------------------
|
|
266
|
+
// Strategy: codemode, one row per nested call
|
|
381
267
|
/** Max nested calls shown under a collapsed codemode row; the rest are counted. */
|
|
382
268
|
const NESTED_PREVIEW = 8;
|
|
383
269
|
/** Width of the nested tool-name column. */
|
|
384
270
|
const NESTED_NAME_MAX = 14;
|
|
271
|
+
const codemodeExtra = (state) => (state.extra ??= {});
|
|
385
272
|
function nestedGlyph(call, frame) {
|
|
386
273
|
if (call.status === "running")
|
|
387
274
|
return { glyph: rowGlyph("running", frame), color: "warning" };
|
|
388
275
|
if (call.status === "error")
|
|
389
|
-
return { glyph: "
|
|
276
|
+
return { glyph: "✗", color: "error" };
|
|
390
277
|
if (call.status === "cancelled")
|
|
391
|
-
return { glyph: "
|
|
392
|
-
return { glyph: "
|
|
278
|
+
return { glyph: "⊘", color: "dim" };
|
|
279
|
+
return { glyph: "✓", color: "success" };
|
|
393
280
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
const
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
const
|
|
402
|
-
|
|
281
|
+
function nestedCallLines(calls, theme, width, frame, expanded) {
|
|
282
|
+
const shown = expanded ? calls : calls.slice(-NESTED_PREVIEW);
|
|
283
|
+
const childIndent = " ".repeat(rowIndent() + 4);
|
|
284
|
+
const lines = [];
|
|
285
|
+
if (shown.length < calls.length)
|
|
286
|
+
lines.push(`${childIndent}${theme.fg("dim", `… ${calls.length - shown.length} earlier calls`)}`);
|
|
287
|
+
const nameWidth = Math.min(NESTED_NAME_MAX, Math.max(0, ...shown.map((call) => (call.name ?? "tool").length)));
|
|
288
|
+
for (const call of shown) {
|
|
289
|
+
const { glyph, color } = nestedGlyph(call, frame);
|
|
290
|
+
const name = truncateToWidth(call.name ?? "tool", nameWidth, "…").padEnd(nameWidth);
|
|
291
|
+
const dur = call.status === "error" ? `failed · ${formatDuration(call.durationMs)}` : formatDuration(call.durationMs);
|
|
292
|
+
const room = Math.max(8, width - childIndent.length);
|
|
293
|
+
const prefixWidth = 2 + nameWidth + 1;
|
|
294
|
+
const itemRoom = Math.max(0, room - prefixWidth - (dur ? visibleWidth(dur) + 2 : 0));
|
|
295
|
+
const item = truncateToWidth(nestedItem(call), itemRoom, "…");
|
|
296
|
+
const gap = dur ? Math.max(2, room - prefixWidth - visibleWidth(item) - visibleWidth(dur)) : 0;
|
|
297
|
+
lines.push(childIndent +
|
|
298
|
+
theme.fg(color, glyph) +
|
|
299
|
+
" " +
|
|
300
|
+
theme.fg("dim", name) +
|
|
301
|
+
" " +
|
|
302
|
+
theme.fg(call.status === "error" ? "text" : "muted", item) +
|
|
303
|
+
" ".repeat(gap) +
|
|
304
|
+
theme.fg(call.status === "error" ? "error" : "dim", dur));
|
|
305
|
+
}
|
|
306
|
+
return lines;
|
|
403
307
|
}
|
|
404
308
|
/**
|
|
405
|
-
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
309
|
+
* `✓ code 3 calls · plan_tracker ×2, bash … 54 lines · 1.2s`, then one child row per nested
|
|
310
|
+
* call. The script and its output appear only when expanded, undecorated, so syntax colours
|
|
311
|
+
* never compete with status colours in the transcript.
|
|
408
312
|
*/
|
|
409
313
|
export function codemodeRenderers() {
|
|
410
|
-
return {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
context.state.running = running;
|
|
419
|
-
if (running) {
|
|
420
|
-
context.state.startedAt ??= Date.now();
|
|
421
|
-
live.watch(id, context.invalidate);
|
|
422
|
-
}
|
|
423
|
-
else
|
|
424
|
-
live.unwatch(id);
|
|
425
|
-
return {
|
|
426
|
-
render(width) {
|
|
427
|
-
const state = context.state;
|
|
428
|
-
const status = state.outcome?.errorLine !== undefined || context.isError ? "error" : rowIsRunning(context) ? "running" : "ok";
|
|
429
|
-
const frame = status === "running" && live.has(id) ? live.frame : 0;
|
|
430
|
-
const calls = state.calls ?? [];
|
|
431
|
-
const right = status === "running" ? (state.startedAt ? formatDuration(Date.now() - state.startedAt) : "") : (state.outcome?.text ?? "");
|
|
432
|
-
const lines = [rowLine(theme, width, status, frame, "code", callSummary(calls), right)];
|
|
433
|
-
const expanded = Boolean(context.expanded);
|
|
434
|
-
const shown = expanded ? calls : calls.slice(-NESTED_PREVIEW);
|
|
435
|
-
const childIndent = " ".repeat(rowIndent() + 4);
|
|
436
|
-
if (shown.length < calls.length) {
|
|
437
|
-
lines.push(`${childIndent}${theme.fg("dim", `\u2026 ${calls.length - shown.length} earlier calls`)}`);
|
|
438
|
-
}
|
|
439
|
-
const nameWidth = Math.min(NESTED_NAME_MAX, Math.max(0, ...shown.map((call) => (call.name ?? "tool").length)));
|
|
440
|
-
for (const call of shown) {
|
|
441
|
-
const { glyph, color } = nestedGlyph(call, frame);
|
|
442
|
-
const name = truncateToWidth(call.name ?? "tool", nameWidth, "\u2026").padEnd(nameWidth);
|
|
443
|
-
const dur = call.status === "error" ? `failed \u00b7 ${formatDuration(call.durationMs)}` : formatDuration(call.durationMs);
|
|
444
|
-
const room = Math.max(8, width - childIndent.length);
|
|
445
|
-
// Reuse the row grid with the name as the "verb" column, then fix the glyph slot.
|
|
446
|
-
const prefixWidth = 2 + nameWidth + 1;
|
|
447
|
-
const itemRoom = Math.max(0, room - prefixWidth - (dur ? visibleWidth(dur) + 2 : 0));
|
|
448
|
-
const item = truncateToWidth(nestedItem(call), itemRoom, "\u2026");
|
|
449
|
-
const gap = dur ? Math.max(2, room - prefixWidth - visibleWidth(item) - visibleWidth(dur)) : 0;
|
|
450
|
-
lines.push(childIndent +
|
|
451
|
-
theme.fg(color, glyph) +
|
|
452
|
-
" " +
|
|
453
|
-
theme.fg("dim", name) +
|
|
454
|
-
" " +
|
|
455
|
-
theme.fg(call.status === "error" ? "text" : "muted", item) +
|
|
456
|
-
" ".repeat(gap) +
|
|
457
|
-
theme.fg(call.status === "error" ? "error" : "dim", dur));
|
|
458
|
-
}
|
|
459
|
-
return lines;
|
|
460
|
-
},
|
|
461
|
-
invalidate() { },
|
|
462
|
-
};
|
|
314
|
+
return createRowRenderers({
|
|
315
|
+
verb: "code",
|
|
316
|
+
item: (_args, state) => callSummary(codemodeExtra(state).calls ?? []),
|
|
317
|
+
detailIndent: 4,
|
|
318
|
+
onResult(result, state) {
|
|
319
|
+
const extra = codemodeExtra(state);
|
|
320
|
+
extra.calls = nestedCalls(result);
|
|
321
|
+
extra.output = scriptOutput(result);
|
|
463
322
|
},
|
|
464
|
-
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
context.state.calls = nestedCalls(typed);
|
|
468
|
-
if (!context.isPartial) {
|
|
469
|
-
context.state.done = true;
|
|
470
|
-
context.state.running = false;
|
|
471
|
-
const outcome = codemodeOutcome(typed, Boolean(context.isError), context.durationMs);
|
|
472
|
-
context.state.outcome = { text: outcome.text, errorLine: outcome.failed ? (outcome.errorLine ?? "") : undefined };
|
|
473
|
-
context.state.output = scriptOutput(typed);
|
|
474
|
-
live.unwatch(context.toolCallId || "codemode");
|
|
475
|
-
}
|
|
476
|
-
// No invalidate here: the host runs renderCall and renderResult in one updateDisplay() and
|
|
477
|
-
// paints afterwards, so the call row's lazy render() already sees these calls. Calling
|
|
478
|
-
// invalidate() would re-enter updateDisplay() and recurse.
|
|
479
|
-
const container = new Container();
|
|
480
|
-
const expanded = Boolean(options.expanded);
|
|
481
|
-
const errorLine = context.state.outcome?.errorLine;
|
|
482
|
-
if (errorLine && !expanded)
|
|
483
|
-
container.addChild(errorLineBlock(errorLine, theme));
|
|
484
|
-
if (expanded) {
|
|
485
|
-
const indent = " ".repeat(rowIndent() + 4);
|
|
486
|
-
const code = context.state.code ?? "";
|
|
487
|
-
const output = context.state.output ?? "";
|
|
488
|
-
container.addChild({
|
|
489
|
-
render(width) {
|
|
490
|
-
const lines = [];
|
|
491
|
-
if (code)
|
|
492
|
-
lines.push(...ruledLines(code.trimEnd(), width, theme, indent, `// script \u00b7 ${code.trimEnd().split("\n").length} lines`));
|
|
493
|
-
if (output)
|
|
494
|
-
lines.push(...ruledLines(output, width, theme, indent, "// output"));
|
|
495
|
-
return lines;
|
|
496
|
-
},
|
|
497
|
-
invalidate() { },
|
|
498
|
-
});
|
|
499
|
-
}
|
|
500
|
-
return container;
|
|
323
|
+
outcome(_args, result, isError, durationMs) {
|
|
324
|
+
const outcome = codemodeOutcome(result, isError, durationMs);
|
|
325
|
+
return { text: outcome.text, errorLine: outcome.failed ? (outcome.errorLine ?? "") : undefined };
|
|
501
326
|
},
|
|
502
|
-
|
|
327
|
+
childLines: (state, theme, width, frame, expanded) => nestedCallLines(codemodeExtra(state).calls ?? [], theme, width, frame, expanded),
|
|
328
|
+
detail(args, _result, state, theme, expanded) {
|
|
329
|
+
if (!expanded)
|
|
330
|
+
return undefined;
|
|
331
|
+
const code = typeof args?.code === "string" ? args.code.trimEnd() : "";
|
|
332
|
+
const output = codemodeExtra(state).output ?? "";
|
|
333
|
+
const parts = [
|
|
334
|
+
code ? textBlock(code, theme, 4, `// script · ${code.split("\n").length} lines`) : undefined,
|
|
335
|
+
output ? textBlock(output, theme, 4, "// output") : undefined,
|
|
336
|
+
].filter((part) => part !== undefined);
|
|
337
|
+
if (parts.length === 0)
|
|
338
|
+
return undefined;
|
|
339
|
+
return { render: (width) => parts.flatMap((part) => part.render(width)), invalidate() { } };
|
|
340
|
+
},
|
|
341
|
+
});
|
|
503
342
|
}
|
|
504
343
|
/**
|
|
505
344
|
* Attach the compact renderers to the built-in tools without re-registering them. Registered at
|