@gavin-hu/my-pi-agent 0.8.1 → 0.10.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 +35 -8
- package/README.zh-CN.md +33 -8
- package/assets/bytefly.schema.json +17 -1
- package/assets/chrome.png +0 -0
- package/assets/file-browse.png +0 -0
- package/assets/job.png +0 -0
- package/assets/plan-review.png +0 -0
- package/assets/question.png +0 -0
- package/assets/rewind.png +0 -0
- package/assets/todo.png +0 -0
- package/assets/tool-renderings.png +0 -0
- package/assets/tools/ask_user_question.png +0 -0
- package/assets/tools/bash.png +0 -0
- package/assets/tools/edit.png +0 -0
- package/assets/tools/enter_plan_mode.png +0 -0
- package/assets/tools/enter_worktree.png +0 -0
- package/assets/tools/exit_plan_mode.png +0 -0
- package/assets/tools/exit_worktree.png +0 -0
- package/assets/tools/find.png +0 -0
- package/assets/tools/goal.png +0 -0
- package/assets/tools/grep.png +0 -0
- package/assets/tools/job.png +0 -0
- package/assets/tools/list_worktrees.png +0 -0
- package/assets/tools/ls.png +0 -0
- package/assets/tools/memory.png +0 -0
- package/assets/tools/notice.png +0 -0
- package/assets/tools/open_path.png +0 -0
- package/assets/tools/prune_worktrees.png +0 -0
- package/assets/tools/read.png +0 -0
- package/assets/tools/read_doc.png +0 -0
- package/assets/tools/run_doctor.png +0 -0
- package/assets/tools/send_wechat.png +0 -0
- package/assets/tools/set_plan_steps.png +0 -0
- package/assets/tools/subagent.png +0 -0
- package/assets/tools/todo.png +0 -0
- package/assets/tools/turn-rule.png +0 -0
- package/assets/tools/user-message.png +0 -0
- package/assets/tools/watch.png +0 -0
- package/assets/tools/web_fetch.png +0 -0
- package/assets/tools/web_search.png +0 -0
- package/assets/tools/write.png +0 -0
- package/assets/tools/write_plan.png +0 -0
- package/extensions/bytefly/README.md +306 -68
- package/extensions/bytefly/block/component.ts +282 -0
- package/extensions/bytefly/block/config.ts +24 -0
- package/extensions/bytefly/block/fallback.ts +120 -0
- package/extensions/bytefly/block/register.ts +164 -0
- package/extensions/bytefly/chrome.ts +97 -0
- package/extensions/bytefly/doctor/format.ts +36 -10
- package/extensions/bytefly/doctor/tools.ts +10 -6
- package/extensions/bytefly/footer/config.ts +1 -1
- package/extensions/bytefly/footer/render/format.ts +5 -18
- package/extensions/bytefly/footer/render/layout.ts +2 -1
- package/extensions/bytefly/footer/render/segments.ts +51 -78
- package/extensions/bytefly/header/component.ts +49 -21
- package/extensions/bytefly/header/config.ts +28 -21
- package/extensions/bytefly/header/register.ts +6 -1
- package/extensions/bytefly/index.ts +32 -12
- package/extensions/bytefly/message/config.ts +15 -0
- package/extensions/bytefly/message/format.ts +42 -0
- package/extensions/bytefly/message/register.ts +15 -0
- package/extensions/bytefly/notice/component.ts +107 -0
- package/extensions/bytefly/notice/config.ts +41 -0
- package/extensions/bytefly/notice/register.ts +27 -0
- package/extensions/bytefly/pad.ts +42 -0
- package/extensions/bytefly/panel/commands.ts +69 -35
- package/extensions/bytefly/panel/format.ts +8 -8
- package/extensions/bytefly/panel/tui.ts +45 -70
- package/extensions/bytefly/separator/component.ts +2 -1
- package/extensions/bytefly/separator/config.ts +16 -8
- package/extensions/bytefly/separator/format.ts +18 -38
- package/extensions/bytefly/separator/register.ts +3 -2
- package/extensions/bytefly/separator/types.ts +0 -10
- package/extensions/bytefly/working/config.ts +20 -0
- package/extensions/bytefly/working/format.ts +25 -0
- package/extensions/bytefly/working/register.ts +88 -0
- package/extensions/document/README.md +12 -5
- package/extensions/document/paging.ts +10 -10
- package/extensions/document/render.ts +17 -16
- package/extensions/document/tool.ts +1 -1
- package/extensions/file-browse/README.md +54 -10
- package/extensions/file-browse/client.ts +110 -2
- package/extensions/file-browse/config.ts +12 -0
- package/extensions/file-browse/diff.ts +187 -0
- package/extensions/file-browse/git.ts +180 -8
- package/extensions/file-browse/html.ts +401 -25
- package/extensions/file-browse/index.ts +15 -4
- package/extensions/file-browse/markdown.ts +33 -0
- package/extensions/file-browse/router.ts +94 -2
- package/extensions/file-browse/server.ts +4 -1
- package/extensions/file-open/README.md +6 -0
- package/extensions/goal/README.md +16 -10
- package/extensions/goal/format.ts +13 -25
- package/extensions/goal/tools.ts +11 -22
- package/extensions/goal/tui.ts +12 -40
- package/extensions/job/README.md +22 -13
- package/extensions/job/format.ts +36 -13
- package/extensions/job/index.ts +26 -21
- package/extensions/job/tools.ts +15 -12
- package/extensions/job/tui.ts +148 -222
- package/extensions/job/ui.ts +24 -49
- package/extensions/memory/README.md +7 -1
- package/extensions/memory/format.ts +5 -3
- package/extensions/memory/tools.ts +4 -3
- package/extensions/memory/tui.ts +32 -36
- package/extensions/plan/README.md +86 -36
- package/extensions/plan/blocks.ts +44 -2
- package/extensions/plan/feedback.ts +20 -5
- package/extensions/plan/index.ts +1 -1
- package/extensions/plan/runtime.ts +3 -2
- package/extensions/plan/tools.ts +43 -27
- package/extensions/plan/tui.ts +21 -27
- package/extensions/plan/web-client.ts +386 -40
- package/extensions/plan/web-page.ts +138 -33
- package/extensions/plan/web-router.ts +46 -13
- package/extensions/plan/web.ts +63 -12
- package/extensions/question/README.md +14 -2
- package/extensions/question/answers.ts +25 -18
- package/extensions/question/tools.ts +22 -16
- package/extensions/question/tui.ts +132 -76
- package/extensions/rewind/README.md +3 -1
- package/extensions/rewind/format.ts +35 -2
- package/extensions/rewind/index.ts +3 -1
- package/extensions/rewind/rewind-tui.ts +66 -62
- package/extensions/rewind/snapshot-run.ts +1 -1
- package/extensions/rewind/status.ts +3 -2
- package/extensions/subagent/README.md +6 -0
- package/extensions/subagent/index.ts +1 -1
- package/extensions/subagent/render.ts +108 -66
- package/extensions/todo/README.md +20 -13
- package/extensions/todo/format.ts +47 -34
- package/extensions/todo/tools.ts +6 -4
- package/extensions/todo/tui.ts +46 -26
- package/extensions/watch/README.md +16 -8
- package/extensions/watch/format.ts +33 -13
- package/extensions/watch/index.ts +29 -24
- package/extensions/watch/tools.ts +16 -9
- package/extensions/watch/tui.ts +142 -196
- package/extensions/watch/ui.ts +24 -46
- package/extensions/web-access/README.md +16 -7
- package/extensions/web-access/fetch/tool.ts +12 -11
- package/extensions/web-access/search/tool.ts +16 -13
- package/extensions/wechat/README.md +8 -2
- package/extensions/wechat/index.ts +4 -3
- package/extensions/wechat/login-screen.ts +8 -5
- package/extensions/wechat/status-screen.ts +41 -22
- package/extensions/wechat/tools.ts +6 -5
- package/extensions/worktree/README.md +6 -0
- package/extensions/worktree/enter.ts +1 -1
- package/extensions/worktree/index.ts +1 -1
- package/extensions/worktree/runtime.ts +1 -1
- package/lib/README.md +11 -7
- package/lib/format.ts +17 -0
- package/lib/notice.ts +87 -0
- package/lib/requirements.ts +2 -2
- package/lib/{tui.ts → tui/chrome.ts} +17 -34
- package/lib/tui/columns.ts +96 -0
- package/lib/{list-cursor.ts → tui/cursor.ts} +58 -4
- package/lib/tui/gauge.ts +78 -0
- package/lib/{ui.ts → tui/glyphs.ts} +10 -11
- package/lib/tui/indent.ts +166 -0
- package/lib/tui/index.ts +27 -0
- package/lib/tui/line-pane.ts +173 -0
- package/lib/tui/rows.ts +143 -0
- package/lib/tui/rules.ts +94 -0
- package/lib/tui/status-controller.ts +107 -0
- package/lib/tui/tokens.ts +72 -0
- package/lib/web/base.ts +2 -1
- package/lib/web/components.ts +3 -1
- package/lib/web/layout.ts +5 -1
- package/package.json +7 -3
- package/themes/bytefly-dark.json +7 -8
- package/themes/bytefly-light.json +10 -11
- package/assets/bytefly-chrome.png +0 -0
|
@@ -9,26 +9,15 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
} from "
|
|
20
|
-
import {
|
|
21
|
-
fitRows,
|
|
22
|
-
formatRange,
|
|
23
|
-
isCloseKey,
|
|
24
|
-
ListCursor,
|
|
25
|
-
navigateList,
|
|
26
|
-
scrollList,
|
|
27
|
-
selectionMarker,
|
|
28
|
-
} from "../../lib/list-cursor.ts";
|
|
29
|
-
import { screenHeader, screenHint, type ViewportRowsSource } from "../../lib/tui.ts";
|
|
30
|
-
import { SEPARATORS } from "../../lib/ui.ts";
|
|
31
|
-
import { formatRewindDetail, formatRewindRow } from "./format.ts";
|
|
12
|
+
import { Key, matchesKey, type Component, type TuiMouseEvent, type TuiMouseEventResult } from "@earendil-works/pi-tui";
|
|
13
|
+
import { fitRows, formatRange, isCloseKey, ListCursor, navigateList, scrollList } from "../../lib/tui/cursor.ts";
|
|
14
|
+
import { dockListFrame, screenHint, type ViewportRowsSource } from "../../lib/tui/chrome.ts";
|
|
15
|
+
import { clip } from "../../lib/tui/columns.ts";
|
|
16
|
+
import { SEPARATORS } from "../../lib/tui/glyphs.ts";
|
|
17
|
+
import { emptyState, listRow } from "../../lib/tui/rows.ts";
|
|
18
|
+
import { titleRule } from "../../lib/tui/rules.ts";
|
|
19
|
+
import { paint } from "../../lib/tui/tokens.ts";
|
|
20
|
+
import { formatRewindDetail, formatRewindParts } from "./format.ts";
|
|
32
21
|
import type { RewindPoint } from "./timeline.ts";
|
|
33
22
|
|
|
34
23
|
export interface RewindListOptions {
|
|
@@ -43,7 +32,7 @@ export interface RewindListOptions {
|
|
|
43
32
|
/** Rows the screen shows when the terminal height is unknown. */
|
|
44
33
|
const SCREEN_DEFAULT_ITEMS = 12;
|
|
45
34
|
/** Header, summary, blanks, and footer rows around the list (excluding the optional range/detail rows). */
|
|
46
|
-
const SCREEN_CHROME_ROWS =
|
|
35
|
+
const SCREEN_CHROME_ROWS = 6;
|
|
47
36
|
|
|
48
37
|
/** Selectable, scrollable timeline of prompts opened by `/rewind`. */
|
|
49
38
|
export class RewindListComponent implements Component {
|
|
@@ -83,53 +72,68 @@ export class RewindListComponent implements Component {
|
|
|
83
72
|
|
|
84
73
|
invalidate(): void {}
|
|
85
74
|
|
|
86
|
-
//
|
|
87
|
-
// has no blank before the hint, so the shared frame would change its output.
|
|
88
|
-
// Its blank-line convention is deliberately local.
|
|
75
|
+
// The shared dock frame, like every other list screen.
|
|
89
76
|
render(width: number): string[] {
|
|
90
77
|
const w = Math.max(1, width);
|
|
91
|
-
const
|
|
78
|
+
const header = titleRule(this.theme, w, "Rewind");
|
|
92
79
|
|
|
93
80
|
if (this.points.length === 0) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const cursor = this.cursor;
|
|
100
|
-
const focused = this.points[cursor.selected];
|
|
101
|
-
const detail = focused ? formatRewindDetail(Boolean(focused.snapshot), focused.snapshot?.id) : "";
|
|
102
|
-
cursor.visible = fitRows(
|
|
103
|
-
this.options.viewportRows,
|
|
104
|
-
this.points.length,
|
|
105
|
-
SCREEN_CHROME_ROWS + (detail ? 1 : 0),
|
|
106
|
-
SCREEN_DEFAULT_ITEMS,
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
const { start, end } = cursor.window(this.points.length);
|
|
110
|
-
for (let i = start; i < end; i++) {
|
|
111
|
-
const selected = i === cursor.selected;
|
|
112
|
-
const row = formatRewindRow(
|
|
113
|
-
this.points[i].summary,
|
|
114
|
-
Boolean(this.points[i].snapshot),
|
|
115
|
-
this.points[i].timestamp,
|
|
116
|
-
Date.now(),
|
|
117
|
-
Math.max(1, w - 2),
|
|
118
|
-
);
|
|
119
|
-
const body = selected ? this.theme.fg("accent", row) : this.theme.fg("muted", row);
|
|
120
|
-
lines.push(truncateToWidth(selectionMarker(this.theme, selected) + body, w));
|
|
121
|
-
}
|
|
122
|
-
if (cursor.clipped(this.points.length)) {
|
|
123
|
-
lines.push(formatRange(this.theme, w, { start, end, total: this.points.length }));
|
|
124
|
-
}
|
|
125
|
-
lines.push("");
|
|
126
|
-
if (detail) lines.push(truncateToWidth(` ${this.theme.fg("muted", detail)}`, w));
|
|
81
|
+
return dockListFrame({
|
|
82
|
+
header,
|
|
83
|
+
body: emptyState(this.theme, w, { label: "No prompts on this branch yet." }),
|
|
84
|
+
hint: this.hint(w),
|
|
85
|
+
});
|
|
127
86
|
}
|
|
128
87
|
|
|
129
|
-
|
|
130
|
-
|
|
88
|
+
const count = `${this.points.length} prompt${this.points.length === 1 ? "" : "s"}`;
|
|
89
|
+
const cursor = this.cursor;
|
|
90
|
+
const focused = this.points[cursor.selected];
|
|
91
|
+
const detail = focused ? formatRewindDetail(Boolean(focused.snapshot), focused.snapshot?.id) : "";
|
|
92
|
+
cursor.visible = fitRows(
|
|
93
|
+
this.options.viewportRows,
|
|
94
|
+
this.points.length,
|
|
95
|
+
SCREEN_CHROME_ROWS + (detail ? 1 : 0),
|
|
96
|
+
SCREEN_DEFAULT_ITEMS,
|
|
131
97
|
);
|
|
132
|
-
|
|
133
|
-
|
|
98
|
+
|
|
99
|
+
const { start, end } = cursor.window(this.points.length);
|
|
100
|
+
const body: string[] = [];
|
|
101
|
+
for (let i = start; i < end; i++) {
|
|
102
|
+
const selected = i === cursor.selected;
|
|
103
|
+
const parts = formatRewindParts(
|
|
104
|
+
this.points[i].summary,
|
|
105
|
+
Boolean(this.points[i].snapshot),
|
|
106
|
+
this.points[i].timestamp,
|
|
107
|
+
Date.now(),
|
|
108
|
+
);
|
|
109
|
+
const tone = selected ? "accent" : "meta";
|
|
110
|
+
body.push(
|
|
111
|
+
listRow(this.theme, w, {
|
|
112
|
+
selected,
|
|
113
|
+
glyph: parts.glyph,
|
|
114
|
+
glyphRole: tone,
|
|
115
|
+
label: parts.label,
|
|
116
|
+
labelRole: tone,
|
|
117
|
+
trailing: parts.trailing,
|
|
118
|
+
trailingRole: tone,
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return dockListFrame({
|
|
124
|
+
header,
|
|
125
|
+
summary: clip(` ${paint(this.theme, "meta", `${count}${SEPARATORS.item}newest first`)}`, w),
|
|
126
|
+
body,
|
|
127
|
+
range: cursor.clipped(this.points.length)
|
|
128
|
+
? formatRange(this.theme, w, { start, end, total: this.points.length })
|
|
129
|
+
: undefined,
|
|
130
|
+
detail: detail ? [clip(` ${paint(this.theme, "meta", detail)}`, w)] : undefined,
|
|
131
|
+
hint: this.hint(w),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** The key-hint row, with the primary action in the accent role. */
|
|
136
|
+
private hint(width: number): string {
|
|
137
|
+
return screenHint(this.theme, width, ["Esc close", { text: "Enter choose", color: "accent" }, "↑/↓ or k/j select"]);
|
|
134
138
|
}
|
|
135
139
|
}
|
|
@@ -54,7 +54,7 @@ export function createSnapshotRun(pi: ExtensionAPI, runtime: RewindRuntime): Sna
|
|
|
54
54
|
runAutoSnapshots = false;
|
|
55
55
|
};
|
|
56
56
|
|
|
57
|
-
const beginRun = async (prompt: string, ctx: ExtensionContext): Promise<void> => {
|
|
57
|
+
const beginRun = async (prompt: string | undefined, ctx: ExtensionContext): Promise<void> => {
|
|
58
58
|
reset();
|
|
59
59
|
pendingPrompt = summarizePrompt(prompt);
|
|
60
60
|
try {
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
11
|
-
import { GLYPHS, STATUS_KEYS } from "../../lib/
|
|
11
|
+
import { GLYPHS, STATUS_KEYS } from "../../lib/tui/glyphs.ts";
|
|
12
|
+
import { paint } from "../../lib/tui/tokens.ts";
|
|
12
13
|
import type { RewindConfig } from "./config.ts";
|
|
13
14
|
import { userMessagesFromBranch } from "./timeline.ts";
|
|
14
15
|
|
|
@@ -45,7 +46,7 @@ export function createStatusChip(deps: StatusChipDeps): StatusChip {
|
|
|
45
46
|
const label = `${STATUS_GLYPH} ${count}`;
|
|
46
47
|
let themed = label;
|
|
47
48
|
try {
|
|
48
|
-
themed = ctx.ui.theme
|
|
49
|
+
themed = paint(ctx.ui.theme, "accent", label);
|
|
49
50
|
} catch {
|
|
50
51
|
// No initialized theme in a headless run.
|
|
51
52
|
}
|
|
@@ -10,6 +10,12 @@ pi -e . # load the whole @gavin-hu/my-pi-agent p
|
|
|
10
10
|
pi install ./ # install the package
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
## Tool rendering
|
|
14
|
+
|
|
15
|
+
How the `subagent` call and its result look in the transcript — the call line as a headline and the body indented under it:
|
|
16
|
+
|
|
17
|
+
<img src="../../assets/tools/subagent.png" alt="The subagent tool's call and result: the call line and an indented body with the two tasks, each agent's section rule, its summary, and the usage total" width="100%">
|
|
18
|
+
|
|
13
19
|
## What it does
|
|
14
20
|
|
|
15
21
|
- **Isolated context.** Each subagent is a separate `pi` process with its own
|
|
@@ -168,7 +168,7 @@ export default function subagent(pi: ExtensionAPI, deps: SubagentDeps = {}): voi
|
|
|
168
168
|
},
|
|
169
169
|
|
|
170
170
|
renderCall(args, theme, context) {
|
|
171
|
-
return renderSubagentCall(args
|
|
171
|
+
return renderSubagentCall(args, theme, {
|
|
172
172
|
cwd: context.cwd,
|
|
173
173
|
lastComponent: context.lastComponent,
|
|
174
174
|
});
|
|
@@ -12,10 +12,13 @@
|
|
|
12
12
|
|
|
13
13
|
import { getMarkdownTheme, type Theme } from "@earendil-works/pi-coding-agent";
|
|
14
14
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
15
|
-
import { Container, Markdown, Spacer, Text, type Component } from "@earendil-works/pi-tui";
|
|
15
|
+
import { Container, Markdown, Spacer, Text, visibleWidth, type Component } from "@earendil-works/pi-tui";
|
|
16
16
|
import { sanitizeLine, shortenPath, stripControlChars } from "../../lib/format.ts";
|
|
17
|
-
import { reuseText, toolDetails } from "../../lib/tui.ts";
|
|
18
|
-
import { expandHint, GLYPHS } from "../../lib/
|
|
17
|
+
import { reuseText, toolDetails } from "../../lib/tui/chrome.ts";
|
|
18
|
+
import { expandHint, GLYPHS, SEPARATORS } from "../../lib/tui/glyphs.ts";
|
|
19
|
+
import { centeredRule } from "../../lib/tui/rules.ts";
|
|
20
|
+
import { BODY_INDENT } from "../../lib/tui/indent.ts";
|
|
21
|
+
import { paint, type TuiRole } from "../../lib/tui/tokens.ts";
|
|
19
22
|
import { aggregateUsage, clip, formatToolCall, formatUsageStats } from "./format.ts";
|
|
20
23
|
import { COLLAPSED_ERROR_MAX, COLLAPSED_ITEM_COUNT, COLLAPSED_TEXT_LINES, type SubagentArgs } from "./schema.ts";
|
|
21
24
|
import { getFinalOutput, isFailedResult } from "./stream.ts";
|
|
@@ -24,6 +27,11 @@ import type { SingleResult, SubagentDetails } from "./types.ts";
|
|
|
24
27
|
type DisplayItem = { type: "text"; text: string } | { type: "toolCall"; name: string; args: Record<string, unknown> };
|
|
25
28
|
type ResultStatus = "running" | "aborted" | "failed" | "success";
|
|
26
29
|
|
|
30
|
+
/** Columns a section rule adds around its label: a space and three dashes each side. */
|
|
31
|
+
const SECTION_RULE_PAD = 8;
|
|
32
|
+
/** A section rule keeps the `meta` colour the hand-rolled `─── label ───` used. */
|
|
33
|
+
const SECTION_RULE_STYLE = { pad: 1, fillRole: "meta", labelRole: "meta" } as const;
|
|
34
|
+
|
|
27
35
|
/** Text and tool-call parts of every assistant message, in order. */
|
|
28
36
|
function getDisplayItems(messages: SingleResult["messages"]): DisplayItem[] {
|
|
29
37
|
const items: DisplayItem[] = [];
|
|
@@ -45,12 +53,12 @@ function hasExpandableContent(items: DisplayItem[]): boolean {
|
|
|
45
53
|
);
|
|
46
54
|
}
|
|
47
55
|
|
|
48
|
-
/**
|
|
49
|
-
const
|
|
50
|
-
running: "
|
|
51
|
-
aborted: "
|
|
52
|
-
failed: "
|
|
53
|
-
success: "
|
|
56
|
+
/** Design roles per status; the running glyph is time-based, so it is not a value here. */
|
|
57
|
+
const STATUS_ROLE: Record<ResultStatus, TuiRole> = {
|
|
58
|
+
running: "warn",
|
|
59
|
+
aborted: "warn",
|
|
60
|
+
failed: "bad",
|
|
61
|
+
success: "ok",
|
|
54
62
|
};
|
|
55
63
|
|
|
56
64
|
/** Static glyphs for terminal states, from the shared narrow-glyph vocabulary. */
|
|
@@ -77,12 +85,12 @@ function resultStatus(result: SingleResult, isPartial = false): ResultStatus {
|
|
|
77
85
|
}
|
|
78
86
|
|
|
79
87
|
function statusIcon(status: ResultStatus, theme: Theme, now: number): string {
|
|
80
|
-
return theme
|
|
88
|
+
return paint(theme, STATUS_ROLE[status], statusGlyph(status, now));
|
|
81
89
|
}
|
|
82
90
|
|
|
83
|
-
/** The
|
|
84
|
-
function
|
|
85
|
-
return
|
|
91
|
+
/** The role the status uses, so the `[stopReason]` bracket matches its icon. */
|
|
92
|
+
function statusRole(status: ResultStatus): TuiRole {
|
|
93
|
+
return STATUS_ROLE[status];
|
|
86
94
|
}
|
|
87
95
|
|
|
88
96
|
/** The message a failed/aborted result should show, falling back to stderr. */
|
|
@@ -99,7 +107,7 @@ function renderError(result: SingleResult, status: ResultStatus, theme: Theme, m
|
|
|
99
107
|
const detail = errorDetail(result, status, max);
|
|
100
108
|
if (!detail) return undefined;
|
|
101
109
|
const label = status === "aborted" ? "Aborted" : "Error";
|
|
102
|
-
return theme
|
|
110
|
+
return paint(theme, status === "aborted" ? "warn" : "bad", `${label}: ${detail}`);
|
|
103
111
|
}
|
|
104
112
|
|
|
105
113
|
/** Whether the collapsed error/stderr text is truncated, so expanding shows more. */
|
|
@@ -114,7 +122,7 @@ function hasClippedError(result: SingleResult, status: ResultStatus): boolean {
|
|
|
114
122
|
function collapsedBody(result: SingleResult, status: ResultStatus, items: DisplayItem[], theme: Theme): string {
|
|
115
123
|
const error = renderError(result, status, theme, COLLAPSED_ERROR_MAX);
|
|
116
124
|
if (error) return error;
|
|
117
|
-
if (items.length === 0) return theme
|
|
125
|
+
if (items.length === 0) return paint(theme, "meta", status === "running" ? "(running…)" : "(no output)");
|
|
118
126
|
return renderDisplayItems(items, theme, COLLAPSED_ITEM_COUNT);
|
|
119
127
|
}
|
|
120
128
|
|
|
@@ -164,11 +172,11 @@ function durationSuffix(result: SingleResult, status: ResultStatus, now: number)
|
|
|
164
172
|
function statusSuffix(result: SingleResult, status: ResultStatus, theme: Theme, now: number): string {
|
|
165
173
|
let suffix = "";
|
|
166
174
|
const duration = durationSuffix(result, status, now);
|
|
167
|
-
if (duration) suffix += theme
|
|
175
|
+
if (duration) suffix += paint(theme, "faint", duration);
|
|
168
176
|
const showReason = status === "failed" || status === "aborted";
|
|
169
177
|
const stop = showReason && result.stopReason && result.stopReason !== "end" ? result.stopReason : undefined;
|
|
170
178
|
const reason = stop ?? (status === "failed" ? `exit ${result.exitCode}` : undefined);
|
|
171
|
-
if (reason) suffix += ` ${theme
|
|
179
|
+
if (reason) suffix += ` ${paint(theme, statusRole(status), `[${reason}]`)}`;
|
|
172
180
|
return suffix;
|
|
173
181
|
}
|
|
174
182
|
|
|
@@ -180,20 +188,21 @@ function resultHeader(
|
|
|
180
188
|
stepPrefix: string,
|
|
181
189
|
now: number,
|
|
182
190
|
): string {
|
|
183
|
-
let header = `${statusIcon(status, theme, now)} ${theme
|
|
191
|
+
let header = `${statusIcon(status, theme, now)} ${paint(theme, "accent", theme.bold(sanitizeLine(result.agent)))}`;
|
|
184
192
|
if (result.agentSource && result.agentSource !== "builtin" && result.agentSource !== "unknown") {
|
|
185
|
-
header += theme
|
|
193
|
+
header += paint(theme, "faint", ` [${sanitizeLine(result.agentSource)}]`);
|
|
186
194
|
}
|
|
187
|
-
if (result.step) header = `${theme
|
|
195
|
+
if (result.step) header = `${paint(theme, "meta", `${stepPrefix} ${result.step} `)}` + header;
|
|
188
196
|
return header + statusSuffix(result, status, theme, now);
|
|
189
197
|
}
|
|
190
198
|
|
|
191
|
-
/** Prefix each line of intermediate assistant text with
|
|
192
|
-
function
|
|
199
|
+
/** Prefix each line of intermediate assistant text with the block's body indent. */
|
|
200
|
+
function indentBody(text: string, theme: Theme): string {
|
|
201
|
+
const indent = " ".repeat(BODY_INDENT);
|
|
193
202
|
return stripControlChars(text)
|
|
194
203
|
.trim()
|
|
195
204
|
.split("\n")
|
|
196
|
-
.map((line) =>
|
|
205
|
+
.map((line) => indent + theme.fg("toolOutput", line))
|
|
197
206
|
.join("\n");
|
|
198
207
|
}
|
|
199
208
|
|
|
@@ -201,14 +210,14 @@ function renderDisplayItems(items: DisplayItem[], theme: Theme, limit?: number,
|
|
|
201
210
|
const shown = limit ? items.slice(-limit) : items;
|
|
202
211
|
const skipped = limit && items.length > limit ? items.length - limit : 0;
|
|
203
212
|
const lines: string[] = [];
|
|
204
|
-
if (skipped > 0) lines.push(theme
|
|
213
|
+
if (skipped > 0) lines.push(paint(theme, "meta", `… ${skipped} earlier items`));
|
|
205
214
|
for (const item of shown) {
|
|
206
215
|
if (item.type === "text") {
|
|
207
216
|
const safe = stripControlChars(item.text);
|
|
208
217
|
const text = preview ? safe.split("\n").slice(0, COLLAPSED_TEXT_LINES).join("\n") : safe;
|
|
209
218
|
lines.push(theme.fg("toolOutput", text));
|
|
210
219
|
} else {
|
|
211
|
-
lines.push(theme
|
|
220
|
+
lines.push(paint(theme, "meta", "→ ") + formatToolCall(item.name, item.args, theme, preview));
|
|
212
221
|
}
|
|
213
222
|
}
|
|
214
223
|
return lines.join("\n");
|
|
@@ -226,45 +235,72 @@ export interface SubagentCallContext {
|
|
|
226
235
|
lastComponent?: Component;
|
|
227
236
|
}
|
|
228
237
|
|
|
238
|
+
/**
|
|
239
|
+
* The call's arguments as Pi hands them to `renderCall`. While the model streams
|
|
240
|
+
* a call, a field can still be missing, or arrive with a type the schema will
|
|
241
|
+
* later reject, so the call line reads every one defensively and never throws: a
|
|
242
|
+
* throwing renderer makes Pi fall back to its own generic call line.
|
|
243
|
+
*/
|
|
244
|
+
export type StreamedSubagentArgs = Partial<Record<keyof SubagentArgs, unknown>>;
|
|
245
|
+
|
|
246
|
+
/** One field of a partially-streamed element, or undefined when it has not arrived. */
|
|
247
|
+
function streamedText(element: unknown, key: "agent" | "task" | "cwd"): string | undefined {
|
|
248
|
+
if (typeof element !== "object" || element === null) return undefined;
|
|
249
|
+
const value = (element as Record<string, unknown>)[key];
|
|
250
|
+
return typeof value === "string" ? value : undefined;
|
|
251
|
+
}
|
|
252
|
+
|
|
229
253
|
/** Preview the requested mode and its agents, reusing the slot's `Text`. */
|
|
230
|
-
export function renderSubagentCall(
|
|
254
|
+
export function renderSubagentCall(
|
|
255
|
+
args: StreamedSubagentArgs | undefined,
|
|
256
|
+
theme: Theme,
|
|
257
|
+
context?: SubagentCallContext,
|
|
258
|
+
): Component {
|
|
231
259
|
const text = reuseText(context?.lastComponent);
|
|
232
|
-
text.setText(callText(args, theme, context));
|
|
260
|
+
text.setText(callText(args ?? {}, theme, context));
|
|
233
261
|
return text;
|
|
234
262
|
}
|
|
235
263
|
|
|
236
|
-
/**
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
264
|
+
/**
|
|
265
|
+
* The call-line string for the requested mode. Continuation lines carry no
|
|
266
|
+
* indent of their own: the tool-block frame headlines the first line and
|
|
267
|
+
* indents the rest by `BODY_INDENT`.
|
|
268
|
+
*/
|
|
269
|
+
function callText(args: StreamedSubagentArgs, theme: Theme, context?: { cwd?: string }): string {
|
|
270
|
+
const title =
|
|
271
|
+
theme.fg("toolTitle", theme.bold("subagent ")) + (args.readOnly ? paint(theme, "faint", "read-only ") : "");
|
|
272
|
+
const cwdNote = (cwd?: unknown) => {
|
|
273
|
+
if (typeof cwd !== "string" || !cwd || cwd === context?.cwd) return "";
|
|
274
|
+
return paint(theme, "faint", ` in ${shortenPath(cwd)}`);
|
|
242
275
|
};
|
|
243
|
-
|
|
244
|
-
if (
|
|
245
|
-
let text =
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const
|
|
249
|
-
|
|
276
|
+
const chain = Array.isArray(args.chain) ? args.chain : [];
|
|
277
|
+
if (chain.length > 0) {
|
|
278
|
+
let text =
|
|
279
|
+
title + paint(theme, "label", `chain${SEPARATORS.item}${chain.length} step${chain.length > 1 ? "s" : ""}`);
|
|
280
|
+
for (let i = 0; i < Math.min(chain.length, 3); i++) {
|
|
281
|
+
const step = chain[i];
|
|
282
|
+
const task = (streamedText(step, "task") ?? "…").replace(/\{previous\}/g, "");
|
|
283
|
+
text += `\n${paint(theme, "meta", `${i + 1}.`)} ${paint(theme, "label", sanitizeLine(streamedText(step, "agent") || "…"))}${paint(theme, "meta", ` ${clip(sanitizeLine(task), 40)}`)}${cwdNote(streamedText(step, "cwd"))}`;
|
|
250
284
|
}
|
|
251
|
-
if (
|
|
285
|
+
if (chain.length > 3) text += `\n${paint(theme, "meta", `… +${chain.length - 3} more`)}`;
|
|
252
286
|
return text;
|
|
253
287
|
}
|
|
254
288
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
289
|
+
const tasks = Array.isArray(args.tasks) ? args.tasks : [];
|
|
290
|
+
if (tasks.length > 0) {
|
|
291
|
+
let text =
|
|
292
|
+
title + paint(theme, "label", `parallel${SEPARATORS.item}${tasks.length} task${tasks.length > 1 ? "s" : ""}`);
|
|
293
|
+
for (let i = 0; i < Math.min(tasks.length, 3); i++) {
|
|
294
|
+
const task = tasks[i];
|
|
295
|
+
text += `\n${paint(theme, "meta", `${i + 1}.`)} ${paint(theme, "label", sanitizeLine(streamedText(task, "agent") || "…"))}${paint(theme, "meta", ` ${clip(sanitizeLine(streamedText(task, "task") ?? "…"), 40)}`)}${cwdNote(streamedText(task, "cwd"))}`;
|
|
260
296
|
}
|
|
261
|
-
if (
|
|
297
|
+
if (tasks.length > 3) text += `\n${paint(theme, "meta", `… +${tasks.length - 3} more`)}`;
|
|
262
298
|
return text;
|
|
263
299
|
}
|
|
264
300
|
|
|
265
|
-
const agentName = sanitizeLine(args
|
|
266
|
-
let text = title + theme
|
|
267
|
-
text += `\n
|
|
301
|
+
const agentName = sanitizeLine(streamedText(args, "agent") || "…");
|
|
302
|
+
let text = title + paint(theme, "label", agentName) + cwdNote(streamedText(args, "cwd"));
|
|
303
|
+
text += `\n${paint(theme, "meta", clip(sanitizeLine(streamedText(args, "task") ?? "…"), 60))}`;
|
|
268
304
|
return text;
|
|
269
305
|
}
|
|
270
306
|
|
|
@@ -283,8 +319,10 @@ function addExpandedResult(
|
|
|
283
319
|
if (error) container.addChild(new Text(error, 0, 0));
|
|
284
320
|
|
|
285
321
|
container.addChild(new Spacer(1));
|
|
286
|
-
container.addChild(
|
|
287
|
-
|
|
322
|
+
container.addChild(
|
|
323
|
+
new Text(centeredRule(theme, visibleWidth("Task") + SECTION_RULE_PAD, "Task", SECTION_RULE_STYLE), 0, 0),
|
|
324
|
+
);
|
|
325
|
+
container.addChild(new Text(paint(theme, "faint", stripControlChars(result.taskTemplate ?? result.task)), 0, 0));
|
|
288
326
|
|
|
289
327
|
const displayItems = getDisplayItems(result.messages);
|
|
290
328
|
const lastItem = displayItems[displayItems.length - 1];
|
|
@@ -294,14 +332,18 @@ function addExpandedResult(
|
|
|
294
332
|
// order and do not surface a stale earlier message as the answer.
|
|
295
333
|
const trailingOutput = lastItem?.type === "text" && lastItem.text === finalOutput ? lastItem.text : "";
|
|
296
334
|
container.addChild(new Spacer(1));
|
|
297
|
-
container.addChild(
|
|
335
|
+
container.addChild(
|
|
336
|
+
new Text(centeredRule(theme, visibleWidth("Output") + SECTION_RULE_PAD, "Output", SECTION_RULE_STYLE), 0, 0),
|
|
337
|
+
);
|
|
298
338
|
let renderedOutput = false;
|
|
299
339
|
for (const item of displayItems) {
|
|
300
340
|
if (item.type === "toolCall") {
|
|
301
|
-
container.addChild(
|
|
341
|
+
container.addChild(
|
|
342
|
+
new Text(paint(theme, "meta", "→ ") + formatToolCall(item.name, item.args, theme, false), 0, 0),
|
|
343
|
+
);
|
|
302
344
|
renderedOutput = true;
|
|
303
345
|
} else if (item.text.trim() && item.text !== trailingOutput) {
|
|
304
|
-
container.addChild(new Text(
|
|
346
|
+
container.addChild(new Text(indentBody(item.text, theme), 0, 0));
|
|
305
347
|
renderedOutput = true;
|
|
306
348
|
}
|
|
307
349
|
}
|
|
@@ -311,13 +353,13 @@ function addExpandedResult(
|
|
|
311
353
|
renderedOutput = true;
|
|
312
354
|
}
|
|
313
355
|
if (!renderedOutput) {
|
|
314
|
-
container.addChild(new Text(theme
|
|
356
|
+
container.addChild(new Text(paint(theme, "meta", status === "running" ? "(running…)" : "(no output)"), 0, 0));
|
|
315
357
|
}
|
|
316
358
|
|
|
317
359
|
const usage = usageLine(result);
|
|
318
360
|
if (usage) {
|
|
319
361
|
container.addChild(new Spacer(1));
|
|
320
|
-
container.addChild(new Text(theme
|
|
362
|
+
container.addChild(new Text(paint(theme, "faint", usage), 0, 0));
|
|
321
363
|
}
|
|
322
364
|
}
|
|
323
365
|
|
|
@@ -333,7 +375,7 @@ function collapsedSingleText(result: SingleResult, theme: Theme, isPartial: bool
|
|
|
333
375
|
text += `\n\n${collapsedBody(result, status, displayItems, theme)}`;
|
|
334
376
|
if (isExpandable(result, status, displayItems)) text += `\n${expandHint(theme)}`;
|
|
335
377
|
const usage = usageLine(result);
|
|
336
|
-
if (usage) text += `\n${theme
|
|
378
|
+
if (usage) text += `\n${paint(theme, "faint", usage)}`;
|
|
337
379
|
return text;
|
|
338
380
|
}
|
|
339
381
|
|
|
@@ -375,11 +417,11 @@ function multiHeader(details: SubagentDetails, theme: Theme, now: number): strin
|
|
|
375
417
|
const bad = failed + aborted;
|
|
376
418
|
const icon =
|
|
377
419
|
running > 0
|
|
378
|
-
? theme
|
|
420
|
+
? paint(theme, "warn", statusGlyph("running", now))
|
|
379
421
|
: bad > 0
|
|
380
|
-
? theme
|
|
381
|
-
: theme
|
|
382
|
-
return `${icon} ${theme.fg("toolTitle", theme.bold(`${details.mode} · `))}${theme
|
|
422
|
+
? paint(theme, bad === total ? "bad" : "warn", bad === total ? GLYPHS.jobsFailure : GLYPHS.half)
|
|
423
|
+
: paint(theme, "ok", GLYPHS.check);
|
|
424
|
+
return `${icon} ${theme.fg("toolTitle", theme.bold(`${details.mode} · `))}${paint(theme, "accent", status)}`;
|
|
383
425
|
}
|
|
384
426
|
|
|
385
427
|
function collapsedMultiText(details: SubagentDetails, theme: Theme, now: number): string {
|
|
@@ -394,14 +436,14 @@ function collapsedMultiText(details: SubagentDetails, theme: Theme, now: number)
|
|
|
394
436
|
const displayItems = getDisplayItems(result.messages);
|
|
395
437
|
if (isExpandable(result, status, displayItems)) hasHidden = true;
|
|
396
438
|
const taskText = result.taskTemplate ?? result.task;
|
|
397
|
-
const taskNote = taskText ? theme
|
|
398
|
-
text += `\n\n${theme
|
|
439
|
+
const taskNote = taskText ? paint(theme, "faint", ` ${clip(sanitizeLine(taskText), 50)}`) : "";
|
|
440
|
+
text += `\n\n${paint(theme, "meta", "─── ")}${paint(theme, "accent", resultLabel(result, i))}${taskNote} ${statusIcon(status, theme, now)}`;
|
|
399
441
|
text += statusSuffix(result, status, theme, now);
|
|
400
442
|
text += `\n${collapsedBody(result, status, displayItems, theme)}`;
|
|
401
443
|
}
|
|
402
444
|
if (running === 0) {
|
|
403
445
|
const usage = totalUsageLine(results);
|
|
404
|
-
if (usage) text += `\n\n${theme
|
|
446
|
+
if (usage) text += `\n\n${paint(theme, "faint", `Total: ${usage}`)}`;
|
|
405
447
|
}
|
|
406
448
|
if (running > 0 || hasHidden) text += `\n${expandHint(theme)}`;
|
|
407
449
|
return text;
|
|
@@ -421,7 +463,7 @@ function fillExpandedMulti(container: Container, details: SubagentDetails, theme
|
|
|
421
463
|
const usage = totalUsageLine(details.results);
|
|
422
464
|
if (usage) {
|
|
423
465
|
container.addChild(new Spacer(1));
|
|
424
|
-
container.addChild(new Text(theme
|
|
466
|
+
container.addChild(new Text(paint(theme, "faint", `Total: ${usage}`), 0, 0));
|
|
425
467
|
}
|
|
426
468
|
}
|
|
427
469
|
}
|
|
@@ -8,12 +8,20 @@ about to end after mutating work without a todo update, the extension injects
|
|
|
8
8
|
one short reminder and forces a continuation, so the list does not silently
|
|
9
9
|
drift behind the work.
|
|
10
10
|
|
|
11
|
+
<img src="../../assets/todo.png" alt="The /todos screen: a Todos title rule, a 3/7 completed summary, and the list with check, spinner, and circle glyphs" width="100%">
|
|
12
|
+
|
|
11
13
|
```
|
|
12
14
|
pi --extension ./extensions/todo # load just this extension
|
|
13
15
|
pi -e . # load the whole @gavin-hu/my-pi-agent package
|
|
14
16
|
pi install ./ # install the package
|
|
15
17
|
```
|
|
16
18
|
|
|
19
|
+
## Tool rendering
|
|
20
|
+
|
|
21
|
+
How the `todo` call and its result look in the transcript — the call line as a headline and the body indented under it:
|
|
22
|
+
|
|
23
|
+
<img src="../../assets/tools/todo.png" alt="The todo tool's result: the call line and an indented body with the progress line and the task rows below it, each led by a check, spinner, or circle glyph" width="100%">
|
|
24
|
+
|
|
17
25
|
## What it does
|
|
18
26
|
|
|
19
27
|
- Registers one model-callable tool, `todo`, that **replaces** the task list.
|
|
@@ -49,22 +57,20 @@ Todos · 1/3 · Writing the tests
|
|
|
49
57
|
`/todos` is the expanded view and keeps the model's list order (use it to see
|
|
50
58
|
the plan as written).
|
|
51
59
|
|
|
52
|
-
In the transcript, the call line
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
submitted order:
|
|
60
|
+
In the transcript, the call line carries progress only (`todo → 1/4`); the
|
|
61
|
+
result keeps the `✓` / `◒` / `◌` rows, directly under the call line. The item
|
|
62
|
+
text lives on the rows, so the call line never repeats it. Collapsed, it leads
|
|
63
|
+
with active work; expanded, it keeps the submitted order:
|
|
57
64
|
|
|
58
65
|
```
|
|
59
|
-
todo → 1/4
|
|
60
|
-
|
|
66
|
+
todo → 1/4
|
|
61
67
|
◒ Writing tests
|
|
62
68
|
◌ Ship it
|
|
63
69
|
◌ Write docs
|
|
64
70
|
✓ Write schema
|
|
65
71
|
```
|
|
66
72
|
|
|
67
|
-
Progress lives on the call line, so the
|
|
73
|
+
Progress lives on the call line, so the rows have no footer; the persistent
|
|
68
74
|
widget and `/todos` still show the count. A capped collapsed rail ends with
|
|
69
75
|
`… N more` and the `app.tools.expand` hint.
|
|
70
76
|
|
|
@@ -204,10 +210,11 @@ depends on config.
|
|
|
204
210
|
widget is a single line (`currentTodo` picks the `in_progress` item, else the
|
|
205
211
|
first pending); `/todos` keeps the submitted order, and the transcript result
|
|
206
212
|
orders by status via `compareByActivity` so active work is never hidden. The
|
|
207
|
-
transcript result draws the
|
|
208
|
-
|
|
209
|
-
long items with
|
|
210
|
-
the
|
|
213
|
+
transcript result draws the glyph rows at the block's body indent (the frame
|
|
214
|
+
supplies `BODY_INDENT`; `GLYPH_GAP` comes from
|
|
215
|
+
[`lib/tui/glyphs.ts`](../../lib/tui/glyphs.ts)), wrapping long items with
|
|
216
|
+
continuation rows aligned under the text. Progress lives on the call line, so
|
|
217
|
+
the rows carry no footer.
|
|
211
218
|
- **The transcript reuses its components.** `renderCall` reuses the slot's
|
|
212
219
|
`Text`, and `renderResult` reuses the `TodoResult` view via
|
|
213
220
|
`context.lastComponent`, updating each in place instead of allocating on every
|
|
@@ -223,7 +230,7 @@ depends on config.
|
|
|
223
230
|
| `state.ts` | Branch reconstruction and status queries (pure). |
|
|
224
231
|
| `nudge.ts` | Lag detection: mutating-tool classification, due check, reminder text (pure). |
|
|
225
232
|
| `format.ts` | Model-facing and transcript text (pure). |
|
|
226
|
-
| `tui.ts` | Widget and scrollable `/todos` components. |
|
|
233
|
+
| `tui.ts` | Widget and scrollable `/todos` components, drawn from the `lib/tui/` kit (`titleRule`, `dockListFrame`, `listRow`, `emptyState`). |
|
|
227
234
|
| `config.ts` | Widget config load/validation (the `todo` section of `bytefly.json`). |
|
|
228
235
|
| `runtime.ts` | Session-scoped state, lag tracking, and widget synchronization. |
|
|
229
236
|
| `tools.ts` | `todo` tool registration and rendering. |
|