@tt-a1i/openpi 0.3.0 → 0.4.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/README.md +87 -24
- package/SETUP.md +3 -3
- package/extensions/ask-user/index.ts +30 -14
- package/extensions/background-terminals/src/prompt.ts +1 -1
- package/extensions/background-terminals/src/ui/ps.ts +132 -129
- package/extensions/capabilities/index.ts +35 -44
- package/extensions/capabilities/src/ui.ts +93 -0
- package/extensions/file-mutation-display/index.ts +34 -76
- package/extensions/file-mutation-display/render.ts +387 -88
- package/extensions/file-search/index.ts +8 -7
- package/extensions/file-search/src/binaries.ts +18 -18
- package/extensions/git-info/src/changed-files-view.ts +47 -14
- package/extensions/git-read/index.ts +330 -0
- package/extensions/git-read/src/args.ts +171 -0
- package/extensions/git-read/src/process.ts +81 -0
- package/extensions/git-read/src/prompt.ts +56 -0
- package/extensions/sessions/index.ts +70 -55
- package/extensions/setup/index.ts +6 -6
- package/extensions/shared/activity-status.ts +6 -5
- package/extensions/shared/below-editor-navigation.ts +26 -0
- package/extensions/shared/capability-intent.ts +53 -0
- package/extensions/shared/child-session.ts +7 -1
- package/extensions/shared/result-budget.ts +134 -0
- package/extensions/shared/screen-chrome.ts +133 -0
- package/extensions/shared/setup-config.ts +24 -5
- package/extensions/shared/spinner.ts +28 -0
- package/extensions/shared/text-projection.ts +56 -0
- package/extensions/shared/tool-surface.ts +13 -6
- package/extensions/subagents/index.ts +216 -170
- package/extensions/subagents/navigation.ts +52 -23
- package/extensions/subagents/src/agent-types.ts +37 -15
- package/extensions/subagents/src/backends/stub.ts +7 -0
- package/extensions/subagents/src/id-sequence.ts +84 -0
- package/extensions/subagents/src/manager.ts +620 -537
- package/extensions/subagents/src/prompt.ts +153 -38
- package/extensions/subagents/src/result-artifact.ts +142 -0
- package/extensions/subagents/src/result-delivery.ts +50 -5
- package/extensions/subagents/src/runtime.ts +8 -5
- package/extensions/subagents/src/ui/takeover.ts +84 -109
- package/extensions/subagents/src/ui/transcript.ts +76 -42
- package/extensions/subagents/src/ui/wait-result.ts +1 -1
- package/extensions/tasks/ui.ts +79 -62
- package/extensions/ui-customization/footer.ts +7 -4
- package/extensions/user-input-fold/index.ts +185 -0
- package/extensions/workflows/artifacts.ts +35 -0
- package/extensions/workflows/controller.ts +14 -2
- package/extensions/workflows/coordinator.ts +64 -0
- package/extensions/workflows/dashboard.ts +353 -173
- package/extensions/workflows/handoff.ts +62 -20
- package/extensions/workflows/index.ts +647 -387
- package/extensions/workflows/model.ts +57 -15
- package/extensions/workflows/navigation.ts +33 -14
- package/extensions/workflows/prompt.ts +104 -8
- package/extensions/workflows/replay-safety.ts +16 -6
- package/extensions/workflows/result-delivery.ts +189 -0
- package/extensions/workflows/sandbox-child.cjs +11 -0
- package/package.json +1 -1
- package/skills/subagents/SKILL.md +2 -2
- package/skills/workflows/REFERENCE.md +7 -4
- package/skills/workflows/SKILL.md +53 -10
- package/extensions/subagents/src/format.ts +0 -48
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Takeover UI for subagents (ported from v1, rendering from the synchronous
|
|
3
3
|
* SubagentReadModel instead of live pi sessions):
|
|
4
|
-
* - SubagentDashboard:
|
|
4
|
+
* - SubagentDashboard: compact picker docked above the input, listing all subagents.
|
|
5
5
|
* - TakeoverView: full interactive view of one subagent with an input line
|
|
6
6
|
* to steer/continue it.
|
|
7
7
|
*/
|
|
@@ -13,15 +13,20 @@ import type {
|
|
|
13
13
|
} from "@earendil-works/pi-coding-agent";
|
|
14
14
|
import type { Component, Focusable, TUI } from "@earendil-works/pi-tui";
|
|
15
15
|
import { Input, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
16
|
+
import {
|
|
17
|
+
hintLine,
|
|
18
|
+
panelFrame,
|
|
19
|
+
type ScreenHint,
|
|
20
|
+
} from "../../../shared/screen-chrome.ts";
|
|
16
21
|
import { sanitizeTerminalText } from "../../../shared/terminal-text.ts";
|
|
17
22
|
import { formatElapsed, type SubagentSnapshot } from "../domain.ts";
|
|
18
|
-
import { formatContextUtilization } from "
|
|
23
|
+
import { formatContextUtilization } from "../../../shared/context-utilization.ts";
|
|
19
24
|
import type { SubagentReadModel } from "../manager.ts";
|
|
20
25
|
import {
|
|
21
|
-
SPINNER_INTERVAL_MS,
|
|
22
|
-
TranscriptRenderer,
|
|
23
26
|
buildTranscriptLines,
|
|
27
|
+
SPINNER_INTERVAL_MS,
|
|
24
28
|
spinnerFrame,
|
|
29
|
+
TranscriptRenderer,
|
|
25
30
|
} from "./transcript.ts";
|
|
26
31
|
|
|
27
32
|
export function sanitizeSubagentDisplayLine(value: string) {
|
|
@@ -44,48 +49,22 @@ function statusGlyph(
|
|
|
44
49
|
snap: SubagentSnapshot,
|
|
45
50
|
theme: Theme,
|
|
46
51
|
now = Date.now(),
|
|
52
|
+
selected = false,
|
|
47
53
|
): string {
|
|
54
|
+
// A selected row keeps its state glyph and borrows the accent tone, so the
|
|
55
|
+
// list never hides what is still running behind a selection marker.
|
|
56
|
+
const tone = (color: "warning" | "success" | "error") =>
|
|
57
|
+
selected ? ("accent" as const) : color;
|
|
48
58
|
switch (snap.status) {
|
|
49
59
|
case "running":
|
|
50
|
-
return theme.fg("warning", spinnerFrame(now));
|
|
60
|
+
return theme.fg(tone("warning"), spinnerFrame(now));
|
|
51
61
|
case "done":
|
|
52
|
-
return theme.fg("success", "✓");
|
|
62
|
+
return theme.fg(tone("success"), "✓");
|
|
53
63
|
case "error":
|
|
54
|
-
return theme.fg("error", "✗");
|
|
64
|
+
return theme.fg(tone("error"), "✗");
|
|
55
65
|
}
|
|
56
66
|
}
|
|
57
67
|
|
|
58
|
-
/**
|
|
59
|
-
* Tool names come from the child's own tool-call events, so they are as
|
|
60
|
-
* untrusted as their arguments and must be sanitized before reaching the
|
|
61
|
-
* terminal: `truncateToWidth` only ignores escape sequences while measuring.
|
|
62
|
-
*/
|
|
63
|
-
function runningActivity(snap: SubagentSnapshot) {
|
|
64
|
-
if (snap.status !== "running") return "";
|
|
65
|
-
const liveTool = snap.liveTools.at(-1);
|
|
66
|
-
if (liveTool) {
|
|
67
|
-
const name = sanitizeSubagentDisplayLine(liveTool.name);
|
|
68
|
-
const args = truncateToWidth(
|
|
69
|
-
sanitizeSubagentDisplayLine(liveTool.argsPreview ?? ""),
|
|
70
|
-
48,
|
|
71
|
-
);
|
|
72
|
-
return args ? `${name} · ${args}` : name;
|
|
73
|
-
}
|
|
74
|
-
for (const item of [...snap.transcript].reverse()) {
|
|
75
|
-
if (item.kind === "toolResult") {
|
|
76
|
-
return sanitizeSubagentDisplayLine(item.name);
|
|
77
|
-
}
|
|
78
|
-
if (item.kind !== "assistant") continue;
|
|
79
|
-
const tool = [...item.parts]
|
|
80
|
-
.reverse()
|
|
81
|
-
.find((part) => part.type === "toolCall");
|
|
82
|
-
if (tool?.type === "toolCall") {
|
|
83
|
-
return sanitizeSubagentDisplayLine(tool.name);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return "";
|
|
87
|
-
}
|
|
88
|
-
|
|
89
68
|
// --- Entry points --------------------------------------------------------------
|
|
90
69
|
|
|
91
70
|
export interface TakeoverOptions {
|
|
@@ -127,7 +106,14 @@ export async function openSubagentPicker(
|
|
|
127
106
|
new SubagentDashboard(tui, theme, keybindings, view, selection, done),
|
|
128
107
|
{
|
|
129
108
|
overlay: true,
|
|
130
|
-
|
|
109
|
+
// Dock the picker just above the editor (editor + strip + footer ≈ 5
|
|
110
|
+
// rows) like a command palette, instead of covering the conversation.
|
|
111
|
+
overlayOptions: {
|
|
112
|
+
anchor: "bottom-center",
|
|
113
|
+
width: "100%",
|
|
114
|
+
maxHeight: "60%",
|
|
115
|
+
margin: { bottom: 5 },
|
|
116
|
+
},
|
|
131
117
|
},
|
|
132
118
|
);
|
|
133
119
|
|
|
@@ -139,7 +125,10 @@ export async function openSubagentPicker(
|
|
|
139
125
|
}
|
|
140
126
|
}
|
|
141
127
|
|
|
142
|
-
// --- Dashboard (
|
|
128
|
+
// --- Dashboard (picker docked above the input) ---------------------------------
|
|
129
|
+
|
|
130
|
+
/** A picker is a glance, not a workspace: cap the list window and scroll. */
|
|
131
|
+
const MAX_PICKER_ROWS = 10;
|
|
143
132
|
|
|
144
133
|
export interface DashboardSelection {
|
|
145
134
|
id?: string;
|
|
@@ -258,24 +247,6 @@ export class SubagentDashboard implements Component {
|
|
|
258
247
|
}
|
|
259
248
|
}
|
|
260
249
|
|
|
261
|
-
private pad(text: string, width: number): string {
|
|
262
|
-
const truncated = truncateToWidth(text, width);
|
|
263
|
-
return truncated + " ".repeat(Math.max(0, width - visibleWidth(truncated)));
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
private borderSegment(width: number, title: string): string {
|
|
267
|
-
const theme = this.theme;
|
|
268
|
-
const label = title
|
|
269
|
-
? ` ${truncateToWidth(title, Math.max(0, width - 3))} `
|
|
270
|
-
: "";
|
|
271
|
-
const labelWidth = visibleWidth(label);
|
|
272
|
-
return (
|
|
273
|
-
theme.fg("border", "─") +
|
|
274
|
-
(label ? theme.fg("text", label) : "") +
|
|
275
|
-
theme.fg("border", "─".repeat(Math.max(0, width - 1 - labelWidth)))
|
|
276
|
-
);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
250
|
render(width: number): string[] {
|
|
280
251
|
const theme = this.theme;
|
|
281
252
|
const subs = this.subs();
|
|
@@ -283,13 +254,14 @@ export class SubagentDashboard implements Component {
|
|
|
283
254
|
|
|
284
255
|
// One timestamp per frame so every row's spinner shows the same frame.
|
|
285
256
|
const now = Date.now();
|
|
257
|
+
// Docked above the editor, the panel borrows conversation space: cap the
|
|
258
|
+
// list window and scroll instead of growing toward the top of the screen.
|
|
286
259
|
const rows = this.tui.terminal.rows || 30;
|
|
287
|
-
const maxBodyHeight = Math.max(1, rows - 5);
|
|
260
|
+
const maxBodyHeight = Math.min(Math.max(1, rows - 5), MAX_PICKER_ROWS);
|
|
288
261
|
const bodyHeight =
|
|
289
262
|
subs.length > maxBodyHeight ? maxBodyHeight : Math.max(1, subs.length);
|
|
290
263
|
const innerWidth = Math.max(0, width - 2);
|
|
291
264
|
|
|
292
|
-
const lines: string[] = [];
|
|
293
265
|
const running = subs.filter((snap) => snap.status === "running").length;
|
|
294
266
|
const done = subs.filter((snap) => snap.status === "done").length;
|
|
295
267
|
const failed = subs.filter((snap) => snap.status === "error").length;
|
|
@@ -301,37 +273,32 @@ export class SubagentDashboard implements Component {
|
|
|
301
273
|
]
|
|
302
274
|
.filter(Boolean)
|
|
303
275
|
.join(" · ") || "no agents";
|
|
304
|
-
lines.push(
|
|
305
|
-
theme.fg("border", "╭") +
|
|
306
|
-
this.borderSegment(innerWidth, `Subagents · ${summary}`) +
|
|
307
|
-
theme.fg("border", "╮"),
|
|
308
|
-
);
|
|
309
|
-
|
|
310
|
-
const divider = theme.fg("border", "│");
|
|
311
276
|
const rowLines = this.renderRows(subs, innerWidth, bodyHeight, now);
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
//
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
277
|
+
const keys = (binding: Parameters<KeybindingsManager["getKeys"]>[0]) =>
|
|
278
|
+
configuredKeys(this.keybindings, binding);
|
|
279
|
+
// Shared chrome, so this panel and its hints read the same as /ps and
|
|
280
|
+
// /workflows. The frame is padded to the rows it was given, which keeps
|
|
281
|
+
// this view's content-fit height rather than reintroducing a fixed one.
|
|
282
|
+
return [
|
|
283
|
+
// One empty row of air between the conversation and the docked panel.
|
|
284
|
+
"",
|
|
285
|
+
...panelFrame(theme, {
|
|
286
|
+
label: `Subagents · ${summary}`,
|
|
287
|
+
rows: rowLines,
|
|
288
|
+
width,
|
|
289
|
+
height: rowLines.length + 2,
|
|
290
|
+
}),
|
|
291
|
+
hintLine(
|
|
292
|
+
theme,
|
|
293
|
+
[
|
|
294
|
+
[`${keys("tui.select.up")}/${keys("tui.select.down")}/jk`, "select"],
|
|
295
|
+
[keys("tui.select.confirm"), "take over"],
|
|
296
|
+
["x", "abort"],
|
|
297
|
+
[keys("tui.select.cancel"), "close"],
|
|
298
|
+
],
|
|
330
299
|
width,
|
|
331
300
|
),
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
return lines;
|
|
301
|
+
];
|
|
335
302
|
}
|
|
336
303
|
|
|
337
304
|
private renderRows(
|
|
@@ -362,13 +329,16 @@ export class SubagentDashboard implements Component {
|
|
|
362
329
|
const index = start + i;
|
|
363
330
|
const isSelected = index === this.selection.index;
|
|
364
331
|
|
|
365
|
-
|
|
332
|
+
// One glyph column: a selected row tints its status glyph with the
|
|
333
|
+
// accent tone instead of stacking a second marker. The live tool name
|
|
334
|
+
// is deliberately absent: it flickers with every tool call and says
|
|
335
|
+
// nothing the user acts on.
|
|
336
|
+
const glyph = statusGlyph(snap, theme, now, isSelected);
|
|
366
337
|
const safeTitle = sanitizeSubagentDisplayLine(snap.title) || snap.id;
|
|
367
338
|
const title = isSelected
|
|
368
339
|
? theme.fg("accent", safeTitle)
|
|
369
340
|
: theme.fg("text", safeTitle);
|
|
370
|
-
const
|
|
371
|
-
const prefix = ` ${marker} ${statusGlyph(snap, theme, now)} `;
|
|
341
|
+
const prefix = ` ${glyph} `;
|
|
372
342
|
|
|
373
343
|
const utilization = formatContextUtilization(snap.usage);
|
|
374
344
|
const metadata = [
|
|
@@ -392,20 +362,9 @@ export class SubagentDashboard implements Component {
|
|
|
392
362
|
const right = metadata.join(dot);
|
|
393
363
|
const rightWidth = visibleWidth(right);
|
|
394
364
|
const leftMax = Math.max(0, width - rightWidth - (right ? 2 : 0));
|
|
395
|
-
const activityLabel = activity ? theme.fg("muted", ` · ${activity}`) : "";
|
|
396
365
|
const left =
|
|
397
366
|
prefix +
|
|
398
|
-
truncateToWidth(
|
|
399
|
-
title,
|
|
400
|
-
Math.max(
|
|
401
|
-
0,
|
|
402
|
-
leftMax - visibleWidth(prefix) - visibleWidth(activityLabel),
|
|
403
|
-
),
|
|
404
|
-
) +
|
|
405
|
-
truncateToWidth(
|
|
406
|
-
activityLabel,
|
|
407
|
-
Math.max(0, leftMax - visibleWidth(prefix)),
|
|
408
|
-
);
|
|
367
|
+
truncateToWidth(title, Math.max(0, leftMax - visibleWidth(prefix)));
|
|
409
368
|
const gap = right
|
|
410
369
|
? Math.max(1, width - visibleWidth(left) - rightWidth)
|
|
411
370
|
: 0;
|
|
@@ -694,13 +653,29 @@ export class TakeoverView implements Component, Focusable {
|
|
|
694
653
|
),
|
|
695
654
|
);
|
|
696
655
|
lines.push(...this.input.render(width));
|
|
697
|
-
const
|
|
698
|
-
|
|
656
|
+
const keys = (binding: Parameters<KeybindingsManager["getKeys"]>[0]) =>
|
|
657
|
+
configuredKeys(this.keybindings, binding);
|
|
658
|
+
const editing: ScreenHint[] = [
|
|
659
|
+
[keys("tui.input.submit"), "send"],
|
|
660
|
+
[keys("app.interrupt"), "back"],
|
|
661
|
+
[keys("app.clear"), "abort run"],
|
|
662
|
+
[
|
|
663
|
+
`${keys("tui.editor.cursorUp")}/${keys("tui.editor.cursorDown")}`,
|
|
664
|
+
"scroll",
|
|
665
|
+
],
|
|
666
|
+
];
|
|
667
|
+
// Same drop-the-page-hint fallback as before, measured on the styled line
|
|
668
|
+
// so the keys-brighter-than-labels styling cannot change what fits.
|
|
669
|
+
const full = hintLine(
|
|
670
|
+
theme,
|
|
671
|
+
[
|
|
672
|
+
...editing,
|
|
673
|
+
[`${keys("tui.editor.pageUp")}/${keys("tui.editor.pageDown")}`, "page"],
|
|
674
|
+
],
|
|
675
|
+
width,
|
|
676
|
+
);
|
|
699
677
|
lines.push(
|
|
700
|
-
|
|
701
|
-
theme.fg("dim", visibleWidth(hints) <= width ? hints : compactHints),
|
|
702
|
-
width,
|
|
703
|
-
),
|
|
678
|
+
visibleWidth(full) <= width ? full : hintLine(theme, editing, width),
|
|
704
679
|
);
|
|
705
680
|
lines.push(this.rule(width, theme.fg("borderAccent", "─")));
|
|
706
681
|
return lines;
|
|
@@ -18,28 +18,14 @@ import type { SubagentSnapshot, TranscriptItem } from "../domain.ts";
|
|
|
18
18
|
|
|
19
19
|
const MAX_CACHED_WIDTHS_PER_ITEM = 2;
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"⠧",
|
|
30
|
-
"⠇",
|
|
31
|
-
"⠏",
|
|
32
|
-
] as const;
|
|
33
|
-
|
|
34
|
-
/** Frame cadence, shared with the dashboard and takeover headers. */
|
|
35
|
-
export const SPINNER_INTERVAL_MS = 120;
|
|
36
|
-
|
|
37
|
-
export function spinnerFrame(now: number) {
|
|
38
|
-
const frame = Math.floor(now / SPINNER_INTERVAL_MS) % SPINNER_FRAMES.length;
|
|
39
|
-
return SPINNER_FRAMES[
|
|
40
|
-
(frame + SPINNER_FRAMES.length) % SPINNER_FRAMES.length
|
|
41
|
-
];
|
|
42
|
-
}
|
|
21
|
+
// The spinner lives in shared/ so strips outside this extension animate in
|
|
22
|
+
// step; the re-export keeps this module's historical import surface intact.
|
|
23
|
+
import { spinnerFrame } from "../../../shared/spinner.ts";
|
|
24
|
+
export {
|
|
25
|
+
SPINNER_FRAMES,
|
|
26
|
+
SPINNER_INTERVAL_MS,
|
|
27
|
+
spinnerFrame,
|
|
28
|
+
} from "../../../shared/spinner.ts";
|
|
43
29
|
|
|
44
30
|
/**
|
|
45
31
|
* Strip raw ANSI codes, expand tabs, and drop control chars. Terminal-expanded
|
|
@@ -79,7 +65,11 @@ function parsedArgs(preview: string) {
|
|
|
79
65
|
}
|
|
80
66
|
|
|
81
67
|
/** Turn common tool arguments into a useful, bounded summary without retaining raw args. */
|
|
82
|
-
export function summarizeToolArgs(
|
|
68
|
+
export function summarizeToolArgs(
|
|
69
|
+
name: string,
|
|
70
|
+
argsPreview?: string,
|
|
71
|
+
cwd?: string,
|
|
72
|
+
) {
|
|
83
73
|
if (!argsPreview) return undefined;
|
|
84
74
|
|
|
85
75
|
const fallback = compactPreview(argsPreview);
|
|
@@ -88,20 +78,33 @@ export function summarizeToolArgs(name: string, argsPreview?: string) {
|
|
|
88
78
|
const args = parsedArgs(fallback);
|
|
89
79
|
if (!args) return fallback;
|
|
90
80
|
|
|
81
|
+
const path = (field: string) => {
|
|
82
|
+
const value = stringField(args, field);
|
|
83
|
+
return value ? relativeToCwd(value, cwd) : undefined;
|
|
84
|
+
};
|
|
85
|
+
|
|
91
86
|
const tool = name.toLowerCase();
|
|
92
87
|
if (tool === "bash") return stringField(args, "command") ?? fallback;
|
|
93
88
|
if (tool === "read" || tool === "write" || tool === "edit") {
|
|
94
|
-
return
|
|
89
|
+
return path("path") ?? fallback;
|
|
95
90
|
}
|
|
96
91
|
if (tool === "rg" || tool === "fd") {
|
|
97
92
|
const pattern = stringField(args, "pattern");
|
|
98
|
-
const
|
|
99
|
-
if (pattern &&
|
|
100
|
-
return pattern ??
|
|
93
|
+
const searchPath = path("path");
|
|
94
|
+
if (pattern && searchPath) return `${pattern} · ${searchPath}`;
|
|
95
|
+
return pattern ?? searchPath ?? fallback;
|
|
101
96
|
}
|
|
102
97
|
return fallback;
|
|
103
98
|
}
|
|
104
99
|
|
|
100
|
+
/** Absolute paths inside the child's own checkout read as noise; relativize. */
|
|
101
|
+
function relativeToCwd(path: string, cwd?: string) {
|
|
102
|
+
if (!cwd) return path;
|
|
103
|
+
if (path === cwd) return ".";
|
|
104
|
+
const prefix = cwd.endsWith("/") ? cwd : `${cwd}/`;
|
|
105
|
+
return path.startsWith(prefix) ? path.slice(prefix.length) : path;
|
|
106
|
+
}
|
|
107
|
+
|
|
105
108
|
function transcriptMarkdownTheme() {
|
|
106
109
|
const theme = getMarkdownTheme();
|
|
107
110
|
return {
|
|
@@ -169,14 +172,18 @@ function renderThinking(theme: Theme, text: string, width: number) {
|
|
|
169
172
|
return out;
|
|
170
173
|
}
|
|
171
174
|
|
|
172
|
-
function renderToolBody(
|
|
175
|
+
function renderToolBody(
|
|
176
|
+
theme: Theme,
|
|
177
|
+
name: string,
|
|
178
|
+
argsPreview?: string,
|
|
179
|
+
cwd?: string,
|
|
180
|
+
) {
|
|
173
181
|
const toolName = sanitizeText(name);
|
|
174
|
-
const preview = summarizeToolArgs(toolName, argsPreview);
|
|
182
|
+
const preview = summarizeToolArgs(toolName, argsPreview, cwd);
|
|
175
183
|
// The `$` form only earns its prompt when there is a command to show; a bare
|
|
176
184
|
// `$ ` would read as an empty shell line.
|
|
177
185
|
if (toolName === "bash" && preview) return theme.fg("dim", `$ ${preview}`);
|
|
178
186
|
return (
|
|
179
|
-
theme.fg("dim", "→ ") +
|
|
180
187
|
theme.fg("toolTitle", toolName) +
|
|
181
188
|
(preview ? theme.fg("dim", ` ${preview}`) : "")
|
|
182
189
|
);
|
|
@@ -210,7 +217,7 @@ function phaseGlyph(theme: Theme, phase: ToolPhase, now: number) {
|
|
|
210
217
|
}
|
|
211
218
|
}
|
|
212
219
|
|
|
213
|
-
/** Command line: `<glyph> $ cmd` for bash, `<glyph>
|
|
220
|
+
/** Command line: `<glyph> $ cmd` for bash, `<glyph> name args` otherwise. */
|
|
214
221
|
function renderToolLine(
|
|
215
222
|
theme: Theme,
|
|
216
223
|
phase: ToolPhase,
|
|
@@ -218,9 +225,10 @@ function renderToolLine(
|
|
|
218
225
|
argsPreview: string | undefined,
|
|
219
226
|
width: number,
|
|
220
227
|
now: number,
|
|
228
|
+
cwd?: string,
|
|
221
229
|
) {
|
|
222
230
|
return truncateToWidth(
|
|
223
|
-
`${phaseGlyph(theme, phase, now)} ${renderToolBody(theme, name, argsPreview)}`,
|
|
231
|
+
`${phaseGlyph(theme, phase, now)} ${renderToolBody(theme, name, argsPreview, cwd)}`,
|
|
224
232
|
width,
|
|
225
233
|
);
|
|
226
234
|
}
|
|
@@ -231,10 +239,14 @@ function renderOutputLine(
|
|
|
231
239
|
isError: boolean,
|
|
232
240
|
outputPreview: string,
|
|
233
241
|
width: number,
|
|
242
|
+
cwd?: string,
|
|
234
243
|
) {
|
|
235
|
-
|
|
244
|
+
// Tool output echoes the absolute search path back (fd/rg print what they
|
|
245
|
+
// were given); inside the child's own checkout the relative form is enough.
|
|
246
|
+
const text = cwd ? outputPreview.split(`${cwd}/`).join("") : outputPreview;
|
|
247
|
+
const preview = text || "(no output)";
|
|
236
248
|
const content = isError
|
|
237
|
-
? theme.fg(
|
|
249
|
+
? theme.fg(text ? "error" : "dim", preview)
|
|
238
250
|
: theme.fg("dim", preview);
|
|
239
251
|
return truncateToWidth(` ${content}`, width);
|
|
240
252
|
}
|
|
@@ -245,6 +257,7 @@ function renderAssistantItem(
|
|
|
245
257
|
width: number,
|
|
246
258
|
phases: ReadonlyMap<string, ToolPhase>,
|
|
247
259
|
now: number,
|
|
260
|
+
cwd?: string,
|
|
248
261
|
) {
|
|
249
262
|
const out: string[] = [];
|
|
250
263
|
for (const part of item.parts) {
|
|
@@ -265,7 +278,15 @@ function renderAssistantItem(
|
|
|
265
278
|
// command twice and make the block reflow when the tool settles.
|
|
266
279
|
if (phase === "live") continue;
|
|
267
280
|
out.push(
|
|
268
|
-
renderToolLine(
|
|
281
|
+
renderToolLine(
|
|
282
|
+
theme,
|
|
283
|
+
phase,
|
|
284
|
+
part.name,
|
|
285
|
+
part.argsPreview,
|
|
286
|
+
width,
|
|
287
|
+
now,
|
|
288
|
+
cwd,
|
|
289
|
+
),
|
|
269
290
|
);
|
|
270
291
|
}
|
|
271
292
|
}
|
|
@@ -278,6 +299,7 @@ function renderToolResultItem(
|
|
|
278
299
|
width: number,
|
|
279
300
|
paired: boolean,
|
|
280
301
|
now: number,
|
|
302
|
+
cwd?: string,
|
|
281
303
|
) {
|
|
282
304
|
const preview = firstOutputPreview(item.outputPreview);
|
|
283
305
|
// An orphan result (its call is not the previous item) still needs a glyph:
|
|
@@ -293,11 +315,11 @@ function renderToolResultItem(
|
|
|
293
315
|
now,
|
|
294
316
|
),
|
|
295
317
|
...(preview
|
|
296
|
-
? [renderOutputLine(theme, item.isError, preview, width)]
|
|
318
|
+
? [renderOutputLine(theme, item.isError, preview, width, cwd)]
|
|
297
319
|
: []),
|
|
298
320
|
];
|
|
299
321
|
}
|
|
300
|
-
return [renderOutputLine(theme, item.isError, preview, width)];
|
|
322
|
+
return [renderOutputLine(theme, item.isError, preview, width, cwd)];
|
|
301
323
|
}
|
|
302
324
|
|
|
303
325
|
function isPairedToolResult(
|
|
@@ -321,12 +343,13 @@ function renderTranscriptItem(
|
|
|
321
343
|
width: number,
|
|
322
344
|
context: ItemContext,
|
|
323
345
|
now: number,
|
|
346
|
+
cwd?: string,
|
|
324
347
|
) {
|
|
325
348
|
if (item.kind === "user") return renderUserText(theme, item.text, width);
|
|
326
349
|
if (item.kind === "assistant") {
|
|
327
|
-
return renderAssistantItem(theme, item, width, context.phases, now);
|
|
350
|
+
return renderAssistantItem(theme, item, width, context.phases, now, cwd);
|
|
328
351
|
}
|
|
329
|
-
return renderToolResultItem(theme, item, width, context.paired, now);
|
|
352
|
+
return renderToolResultItem(theme, item, width, context.paired, now, cwd);
|
|
330
353
|
}
|
|
331
354
|
|
|
332
355
|
interface ItemContext {
|
|
@@ -413,7 +436,8 @@ export class TranscriptRenderer {
|
|
|
413
436
|
const key = `${width}|${context.token}`;
|
|
414
437
|
const cached = this.itemCache.get(item)?.get(key);
|
|
415
438
|
const lines =
|
|
416
|
-
cached ??
|
|
439
|
+
cached ??
|
|
440
|
+
renderTranscriptItem(theme, item, width, context, now, snap.cwd);
|
|
417
441
|
if (!cached) {
|
|
418
442
|
const widths = this.itemCache.get(item) ?? new Map<string, string[]>();
|
|
419
443
|
if (widths.size >= MAX_CACHED_WIDTHS_PER_ITEM) {
|
|
@@ -456,11 +480,21 @@ export class TranscriptRenderer {
|
|
|
456
480
|
: "ok"
|
|
457
481
|
: "live";
|
|
458
482
|
out.push(
|
|
459
|
-
renderToolLine(
|
|
483
|
+
renderToolLine(
|
|
484
|
+
theme,
|
|
485
|
+
phase,
|
|
486
|
+
tool.name,
|
|
487
|
+
tool.argsPreview,
|
|
488
|
+
width,
|
|
489
|
+
now,
|
|
490
|
+
snap.cwd,
|
|
491
|
+
),
|
|
460
492
|
);
|
|
461
493
|
const preview = firstOutputPreview(tool.outputPreview);
|
|
462
494
|
if (preview)
|
|
463
|
-
out.push(
|
|
495
|
+
out.push(
|
|
496
|
+
renderOutputLine(theme, !!tool.isError, preview, width, snap.cwd),
|
|
497
|
+
);
|
|
464
498
|
}
|
|
465
499
|
|
|
466
500
|
// Queued steering/follow-up messages: show them immediately so Enter
|
|
@@ -26,7 +26,7 @@ export function buildWaitResultPreview(
|
|
|
26
26
|
const results = details?.results ?? [];
|
|
27
27
|
const failed = results.filter((result) => result.status === "error").length;
|
|
28
28
|
const header =
|
|
29
|
-
theme.fg(failed > 0 ? "warning" : "success", "
|
|
29
|
+
theme.fg(failed > 0 ? "warning" : "success", failed > 0 ? "!" : "✓") +
|
|
30
30
|
` ${theme.fg("accent", theme.bold(`${results.length} subagent${results.length === 1 ? "" : "s"} settled`))}` +
|
|
31
31
|
(failed > 0 ? theme.fg("error", ` · ${failed} failed`) : "");
|
|
32
32
|
const lines = [header];
|