pi-minimal 0.3.1 → 0.5.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 +60 -0
- package/CONTEXT.md +83 -0
- package/README.md +6 -3
- package/dist/config.js +21 -9
- package/dist/edit-diff.js +74 -0
- package/dist/footer-contribution-events.js +5 -3
- package/dist/footer-contributions.js +15 -13
- package/dist/git-state.js +31 -11
- package/dist/index.js +23 -206
- package/dist/reclaim.js +0 -3
- package/dist/session.js +155 -0
- package/dist/tools.js +224 -281
- 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 +4 -1
- package/release-notes.md +45 -19
- package/src/config.ts +20 -9
- package/src/edit-diff.ts +73 -0
- package/src/footer-contribution-events.ts +7 -4
- package/src/footer-contributions.ts +15 -13
- package/src/git-state.ts +29 -10
- package/src/index.ts +25 -199
- package/src/reclaim.ts +0 -4
- package/src/session.ts +159 -0
- package/src/tools.ts +279 -312
- package/src/tracker.ts +11 -5
- package/src/working-status.ts +2 -1
- package/test/config.test.ts +41 -20
- package/test/footer-contribution-events.test.ts +15 -0
- package/test/git-state.test.ts +35 -0
- package/test/index.test.ts +45 -3
- package/test/tools.test.ts +98 -0
- 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,6 +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 { colorDiffLines, editDiffText } from "./edit-diff.js";
|
|
5
6
|
import { createLiveWatch } from "./live-watch.js";
|
|
6
7
|
import { bashBody, formatDuration, formatOutcome, layoutRow, resultText, rowGlyph, toolItem, } from "./tool-rows.js";
|
|
7
8
|
/** Tolerates a stale chrome module after /reload, where only CHROME_MARGIN may exist. */
|
|
@@ -11,16 +12,22 @@ const live = createLiveWatch(400);
|
|
|
11
12
|
const rowIndent = () => margin() + 1;
|
|
12
13
|
/** Max lines of output shown when a row is expanded. */
|
|
13
14
|
const EXPANDED_MAX_LINES = 400;
|
|
15
|
+
/** Max diff lines shown inline under an edit row with `editDiff: "always"`; ctrl+o shows the rest. */
|
|
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;
|
|
19
|
+
let editDiffMode = "expanded";
|
|
20
|
+
/** Applies the `editDiff` setting; read at session start, so a change needs a new session or /reload. */
|
|
21
|
+
export function setEditDiffMode(mode) {
|
|
22
|
+
editDiffMode = mode;
|
|
23
|
+
}
|
|
14
24
|
export function stopLiveWatch() {
|
|
15
25
|
live.stop();
|
|
16
26
|
}
|
|
17
|
-
function liveId(context, item) {
|
|
18
|
-
return context.toolCallId || item;
|
|
19
|
-
}
|
|
20
27
|
function rowStatus(context) {
|
|
21
28
|
if (context.isError)
|
|
22
29
|
return "error";
|
|
23
|
-
if (context.state.outcome?.errorLine)
|
|
30
|
+
if (context.state.outcome?.errorLine !== undefined)
|
|
24
31
|
return "error";
|
|
25
32
|
return rowIsRunning(context) ? "running" : "ok";
|
|
26
33
|
}
|
|
@@ -31,147 +38,176 @@ function statusColor(status) {
|
|
|
31
38
|
return "warning";
|
|
32
39
|
return "success";
|
|
33
40
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return [];
|
|
40
|
-
const lines = text.split("\n");
|
|
41
|
-
if (lines.length > EXPANDED_MAX_LINES) {
|
|
42
|
-
const hidden = lines.length - EXPANDED_MAX_LINES;
|
|
43
|
-
return [...lines.slice(0, EXPANDED_MAX_LINES), `… ${hidden} more lines`];
|
|
44
|
-
}
|
|
45
|
-
return lines;
|
|
46
|
-
}
|
|
47
|
-
function outputBlock(kind, result, theme) {
|
|
48
|
-
const lines = expandedLines(kind, result);
|
|
49
|
-
if (lines.length === 0)
|
|
50
|
-
return new Container();
|
|
51
|
-
const indent = " ".repeat(rowIndent() + 2);
|
|
52
|
-
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", "│");
|
|
53
46
|
return {
|
|
54
47
|
render(width) {
|
|
55
48
|
const inner = Math.max(0, width - indent.length - 2);
|
|
56
|
-
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)}`);
|
|
57
50
|
},
|
|
58
51
|
invalidate() { },
|
|
59
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)];
|
|
60
58
|
}
|
|
61
|
-
|
|
62
|
-
|
|
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)
|
|
63
62
|
return undefined;
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
63
|
+
const lines = capped(text.split("\n"), EXPANDED_MAX_LINES, (hidden) => `… ${hidden} more lines`);
|
|
64
|
+
return ruled(label ? [label, ...lines] : lines, theme, indentCells);
|
|
65
|
+
}
|
|
66
|
+
/** The edit's diff in the ruled block, coloured per line; inline views are capped and point at ctrl+o. */
|
|
67
|
+
function diffBlock(diff, theme, expanded) {
|
|
68
|
+
const all = diff.replace(/\n$/, "").split("\n");
|
|
69
|
+
const max = expanded ? EXPANDED_MAX_LINES : INLINE_DIFF_MAX_LINES;
|
|
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);
|
|
73
75
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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;
|
|
82
85
|
}
|
|
83
|
-
|
|
84
|
-
|
|
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);
|
|
85
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;
|
|
86
110
|
return {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
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);
|
|
97
120
|
}
|
|
98
|
-
else
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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)}`);
|
|
104
147
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
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);
|
|
116
159
|
}
|
|
117
|
-
|
|
118
|
-
coloredRight = theme.fg("dim", row.outcome);
|
|
119
|
-
const line = " ".repeat(rowIndent()) +
|
|
120
|
-
theme.fg(statusColor(status), glyph) +
|
|
121
|
-
" " +
|
|
122
|
-
theme.fg("dim", row.verb) +
|
|
123
|
-
" " +
|
|
124
|
-
theme.fg(status === "error" ? "text" : "muted", row.item) +
|
|
125
|
-
" ".repeat(row.gap) +
|
|
126
|
-
coloredRight;
|
|
127
|
-
return [line];
|
|
160
|
+
return container;
|
|
128
161
|
},
|
|
129
|
-
invalidate() { },
|
|
130
162
|
};
|
|
131
163
|
}
|
|
164
|
+
// ---------------------------------------------------------------------------------------------
|
|
165
|
+
// Strategy: built-in tools (bash, read, edit, write, grep, find, ls)
|
|
132
166
|
const TOOL_KINDS = new Set(["bash", "read", "edit", "write", "grep", "find", "ls"]);
|
|
133
167
|
export function isToolKind(name) {
|
|
134
168
|
return TOOL_KINDS.has(name);
|
|
135
169
|
}
|
|
136
|
-
|
|
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) {
|
|
137
179
|
return {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
+
}
|
|
150
193
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if (
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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);
|
|
158
203
|
},
|
|
159
204
|
};
|
|
160
205
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const inner = Math.max(8, width - rowIndent());
|
|
164
|
-
const row = layoutRow(inner, verb, item, right);
|
|
165
|
-
const rightColor = status === "running" ? "warning" : status === "error" ? "error" : "dim";
|
|
166
|
-
return (" ".repeat(rowIndent()) +
|
|
167
|
-
theme.fg(statusColor(status), rowGlyph(status, frame)) +
|
|
168
|
-
" " +
|
|
169
|
-
theme.fg("dim", row.verb) +
|
|
170
|
-
" " +
|
|
171
|
-
theme.fg(status === "error" ? "text" : "muted", row.item) +
|
|
172
|
-
" ".repeat(row.gap) +
|
|
173
|
-
theme.fg(rightColor, row.outcome));
|
|
206
|
+
export function compactRenderers(kind) {
|
|
207
|
+
return createRowRenderers(compactStrategy(kind));
|
|
174
208
|
}
|
|
209
|
+
// ---------------------------------------------------------------------------------------------
|
|
210
|
+
// Strategy: tools without a renderer of their own (most MCP tools, subagents_enable)
|
|
175
211
|
/** Longest first result line shown verbatim as an outcome; longer results show a line count. */
|
|
176
212
|
const OUTCOME_TEXT_MAX = 24;
|
|
177
213
|
/** `mcp` for MCP tools, `agent` for subagent tools, `tool` for anything else. */
|
|
@@ -193,13 +229,13 @@ export function genericItem(toolName, args) {
|
|
|
193
229
|
}
|
|
194
230
|
const params = nestedItem({ name: toolName, args: json });
|
|
195
231
|
const mcp = /^mcp__(.+?)__(.+)$/.exec(toolName);
|
|
196
|
-
const label = mcp ? `${mcp[1]}
|
|
232
|
+
const label = mcp ? `${mcp[1]} · ${mcp[2]}` : toolName;
|
|
197
233
|
return params ? `${label} ${params}` : label;
|
|
198
234
|
}
|
|
199
235
|
export function genericOutcome(result, isError, durationMs) {
|
|
200
236
|
const text = resultText(result);
|
|
201
237
|
const dur = formatDuration(durationMs);
|
|
202
|
-
const join = (...parts) => parts.filter(Boolean).join("
|
|
238
|
+
const join = (...parts) => parts.filter(Boolean).join(" · ");
|
|
203
239
|
const lines = text.split("\n").filter((line) => line.trim().length > 0);
|
|
204
240
|
if (isError)
|
|
205
241
|
return { text: join("failed", dur), errorLine: lines[0]?.trim() ?? "" };
|
|
@@ -211,191 +247,98 @@ export function genericOutcome(result, isError, durationMs) {
|
|
|
211
247
|
return { text: join(`${lines.length} line${lines.length === 1 ? "" : "s"}`, dur) };
|
|
212
248
|
}
|
|
213
249
|
/**
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
* 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.
|
|
217
252
|
*/
|
|
218
253
|
export function genericRenderers(toolName) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const item = genericItem(toolName, args);
|
|
226
|
-
const running = rowIsRunning(context);
|
|
227
|
-
context.state.running = running;
|
|
228
|
-
if (running) {
|
|
229
|
-
context.state.startedAt ??= Date.now();
|
|
230
|
-
live.watch(id, context.invalidate);
|
|
231
|
-
}
|
|
232
|
-
else
|
|
233
|
-
live.unwatch(id);
|
|
234
|
-
return {
|
|
235
|
-
render(width) {
|
|
236
|
-
const status = rowStatus(context);
|
|
237
|
-
const frame = status === "running" && live.has(id) ? live.frame : 0;
|
|
238
|
-
const right = status === "running"
|
|
239
|
-
? context.state.startedAt
|
|
240
|
-
? formatDuration(Date.now() - context.state.startedAt)
|
|
241
|
-
: ""
|
|
242
|
-
: (context.state.outcome?.text ?? "");
|
|
243
|
-
return [rowLine(theme, width, status, frame, verb, item, right)];
|
|
244
|
-
},
|
|
245
|
-
invalidate() { },
|
|
246
|
-
};
|
|
247
|
-
},
|
|
248
|
-
renderResult(result, options, theme, rawContext) {
|
|
249
|
-
const context = rawContext;
|
|
250
|
-
const typed = result;
|
|
251
|
-
if (!context.isPartial) {
|
|
252
|
-
context.state.done = true;
|
|
253
|
-
context.state.running = false;
|
|
254
|
-
context.state.outcome = genericOutcome(typed, Boolean(context.isError), context.durationMs);
|
|
255
|
-
live.unwatch(context.toolCallId || toolName);
|
|
256
|
-
}
|
|
257
|
-
const container = new Container();
|
|
258
|
-
const expanded = Boolean(options.expanded);
|
|
259
|
-
const errorLine = context.state.outcome?.errorLine;
|
|
260
|
-
if (errorLine && !expanded)
|
|
261
|
-
container.addChild(errorLineBlock(errorLine, theme));
|
|
262
|
-
if (expanded) {
|
|
263
|
-
const text = resultText(typed);
|
|
264
|
-
if (text) {
|
|
265
|
-
const indent = " ".repeat(rowIndent() + 2);
|
|
266
|
-
container.addChild({ render: (width) => ruledLines(text, width, theme, indent), invalidate() { } });
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
return container;
|
|
270
|
-
},
|
|
271
|
-
};
|
|
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
|
+
});
|
|
272
260
|
}
|
|
273
261
|
/** True when a tool definition draws its own call or result. */
|
|
274
262
|
function hasOwnRenderer(definition) {
|
|
275
263
|
return typeof definition?.renderCall === "function" || typeof definition?.renderResult === "function";
|
|
276
264
|
}
|
|
265
|
+
// ---------------------------------------------------------------------------------------------
|
|
266
|
+
// Strategy: codemode, one row per nested call
|
|
277
267
|
/** Max nested calls shown under a collapsed codemode row; the rest are counted. */
|
|
278
268
|
const NESTED_PREVIEW = 8;
|
|
279
269
|
/** Width of the nested tool-name column. */
|
|
280
270
|
const NESTED_NAME_MAX = 14;
|
|
271
|
+
const codemodeExtra = (state) => (state.extra ??= {});
|
|
281
272
|
function nestedGlyph(call, frame) {
|
|
282
273
|
if (call.status === "running")
|
|
283
274
|
return { glyph: rowGlyph("running", frame), color: "warning" };
|
|
284
275
|
if (call.status === "error")
|
|
285
|
-
return { glyph: "
|
|
276
|
+
return { glyph: "✗", color: "error" };
|
|
286
277
|
if (call.status === "cancelled")
|
|
287
|
-
return { glyph: "
|
|
288
|
-
return { glyph: "
|
|
278
|
+
return { glyph: "⊘", color: "dim" };
|
|
279
|
+
return { glyph: "✓", color: "success" };
|
|
289
280
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
const
|
|
298
|
-
|
|
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;
|
|
299
307
|
}
|
|
300
308
|
/**
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
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.
|
|
304
312
|
*/
|
|
305
313
|
export function codemodeRenderers() {
|
|
306
|
-
return {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
context.state.running = running;
|
|
315
|
-
if (running) {
|
|
316
|
-
context.state.startedAt ??= Date.now();
|
|
317
|
-
live.watch(id, context.invalidate);
|
|
318
|
-
}
|
|
319
|
-
else
|
|
320
|
-
live.unwatch(id);
|
|
321
|
-
return {
|
|
322
|
-
render(width) {
|
|
323
|
-
const state = context.state;
|
|
324
|
-
const status = state.outcome?.errorLine !== undefined || context.isError ? "error" : rowIsRunning(context) ? "running" : "ok";
|
|
325
|
-
const frame = status === "running" && live.has(id) ? live.frame : 0;
|
|
326
|
-
const calls = state.calls ?? [];
|
|
327
|
-
const right = status === "running" ? (state.startedAt ? formatDuration(Date.now() - state.startedAt) : "") : (state.outcome?.text ?? "");
|
|
328
|
-
const lines = [rowLine(theme, width, status, frame, "code", callSummary(calls), right)];
|
|
329
|
-
const expanded = Boolean(context.expanded);
|
|
330
|
-
const shown = expanded ? calls : calls.slice(-NESTED_PREVIEW);
|
|
331
|
-
const childIndent = " ".repeat(rowIndent() + 4);
|
|
332
|
-
if (shown.length < calls.length) {
|
|
333
|
-
lines.push(`${childIndent}${theme.fg("dim", `\u2026 ${calls.length - shown.length} earlier calls`)}`);
|
|
334
|
-
}
|
|
335
|
-
const nameWidth = Math.min(NESTED_NAME_MAX, Math.max(0, ...shown.map((call) => (call.name ?? "tool").length)));
|
|
336
|
-
for (const call of shown) {
|
|
337
|
-
const { glyph, color } = nestedGlyph(call, frame);
|
|
338
|
-
const name = truncateToWidth(call.name ?? "tool", nameWidth, "\u2026").padEnd(nameWidth);
|
|
339
|
-
const dur = call.status === "error" ? `failed \u00b7 ${formatDuration(call.durationMs)}` : formatDuration(call.durationMs);
|
|
340
|
-
const room = Math.max(8, width - childIndent.length);
|
|
341
|
-
// Reuse the row grid with the name as the "verb" column, then fix the glyph slot.
|
|
342
|
-
const prefixWidth = 2 + nameWidth + 1;
|
|
343
|
-
const itemRoom = Math.max(0, room - prefixWidth - (dur ? visibleWidth(dur) + 2 : 0));
|
|
344
|
-
const item = truncateToWidth(nestedItem(call), itemRoom, "\u2026");
|
|
345
|
-
const gap = dur ? Math.max(2, room - prefixWidth - visibleWidth(item) - visibleWidth(dur)) : 0;
|
|
346
|
-
lines.push(childIndent +
|
|
347
|
-
theme.fg(color, glyph) +
|
|
348
|
-
" " +
|
|
349
|
-
theme.fg("dim", name) +
|
|
350
|
-
" " +
|
|
351
|
-
theme.fg(call.status === "error" ? "text" : "muted", item) +
|
|
352
|
-
" ".repeat(gap) +
|
|
353
|
-
theme.fg(call.status === "error" ? "error" : "dim", dur));
|
|
354
|
-
}
|
|
355
|
-
return lines;
|
|
356
|
-
},
|
|
357
|
-
invalidate() { },
|
|
358
|
-
};
|
|
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);
|
|
359
322
|
},
|
|
360
|
-
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
context.state.calls = nestedCalls(typed);
|
|
364
|
-
if (!context.isPartial) {
|
|
365
|
-
context.state.done = true;
|
|
366
|
-
context.state.running = false;
|
|
367
|
-
const outcome = codemodeOutcome(typed, Boolean(context.isError), context.durationMs);
|
|
368
|
-
context.state.outcome = { text: outcome.text, errorLine: outcome.failed ? (outcome.errorLine ?? "") : undefined };
|
|
369
|
-
context.state.output = scriptOutput(typed);
|
|
370
|
-
live.unwatch(context.toolCallId || "codemode");
|
|
371
|
-
}
|
|
372
|
-
// No invalidate here: the host runs renderCall and renderResult in one updateDisplay() and
|
|
373
|
-
// paints afterwards, so the call row's lazy render() already sees these calls. Calling
|
|
374
|
-
// invalidate() would re-enter updateDisplay() and recurse.
|
|
375
|
-
const container = new Container();
|
|
376
|
-
const expanded = Boolean(options.expanded);
|
|
377
|
-
const errorLine = context.state.outcome?.errorLine;
|
|
378
|
-
if (errorLine && !expanded)
|
|
379
|
-
container.addChild(errorLineBlock(errorLine, theme));
|
|
380
|
-
if (expanded) {
|
|
381
|
-
const indent = " ".repeat(rowIndent() + 4);
|
|
382
|
-
const code = context.state.code ?? "";
|
|
383
|
-
const output = context.state.output ?? "";
|
|
384
|
-
container.addChild({
|
|
385
|
-
render(width) {
|
|
386
|
-
const lines = [];
|
|
387
|
-
if (code)
|
|
388
|
-
lines.push(...ruledLines(code.trimEnd(), width, theme, indent, `// script \u00b7 ${code.trimEnd().split("\n").length} lines`));
|
|
389
|
-
if (output)
|
|
390
|
-
lines.push(...ruledLines(output, width, theme, indent, "// output"));
|
|
391
|
-
return lines;
|
|
392
|
-
},
|
|
393
|
-
invalidate() { },
|
|
394
|
-
});
|
|
395
|
-
}
|
|
396
|
-
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 };
|
|
397
326
|
},
|
|
398
|
-
|
|
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
|
+
});
|
|
399
342
|
}
|
|
400
343
|
/**
|
|
401
344
|
* Attach the compact renderers to the built-in tools without re-registering them. Registered at
|
package/dist/tracker.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const PHASES = ["brainstorm", "plan", "implement", "verify", "ship"];
|
|
2
|
-
function
|
|
2
|
+
export function readGauntletProjection(ctx) {
|
|
3
3
|
let phases;
|
|
4
4
|
let tasks;
|
|
5
5
|
for (const entry of ctx.sessionManager.getBranch()) {
|
|
@@ -21,7 +21,7 @@ function phaseActive(phases) {
|
|
|
21
21
|
return PHASES.some((p) => phases[p]?.status && phases[p].status !== "pending");
|
|
22
22
|
}
|
|
23
23
|
export function formatTrackerLines(ctx, theme) {
|
|
24
|
-
const { phases, tasks } =
|
|
24
|
+
const { phases, tasks } = readGauntletProjection(ctx);
|
|
25
25
|
const lines = [];
|
|
26
26
|
if (phases && phaseActive(phases)) {
|
|
27
27
|
const parts = PHASES.map((p) => {
|
package/dist/working-status.js
CHANGED
|
@@ -184,8 +184,9 @@ export function installWorkingStatus(pi) {
|
|
|
184
184
|
stop();
|
|
185
185
|
});
|
|
186
186
|
pi.on("session_shutdown", () => {
|
|
187
|
-
ctx
|
|
187
|
+
// Clear the message while ctx is still known; dropping ctx first left it on screen.
|
|
188
188
|
stop();
|
|
189
|
+
ctx = undefined;
|
|
189
190
|
});
|
|
190
191
|
return stop;
|
|
191
192
|
}
|