@gavin-hu/my-pi-agent 0.8.1 → 0.9.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 +8 -7
- package/README.zh-CN.md +7 -7
- package/assets/file-browse.png +0 -0
- package/assets/plan-review.png +0 -0
- package/extensions/bytefly/README.md +49 -36
- package/extensions/bytefly/doctor/format.ts +36 -10
- package/extensions/bytefly/doctor/tools.ts +6 -5
- 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 +33 -16
- package/extensions/bytefly/header/config.ts +28 -18
- package/extensions/bytefly/index.ts +1 -1
- 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/config.ts +16 -8
- package/extensions/bytefly/separator/format.ts +13 -40
- package/extensions/bytefly/separator/types.ts +0 -10
- package/extensions/document/README.md +1 -1
- package/extensions/document/render.ts +11 -10
- package/extensions/document/tool.ts +1 -1
- package/extensions/file-browse/README.md +51 -9
- package/extensions/file-browse/client.ts +103 -2
- package/extensions/file-browse/config.ts +12 -0
- package/extensions/file-browse/diff.ts +187 -0
- package/extensions/file-browse/git.ts +176 -1
- package/extensions/file-browse/html.ts +385 -19
- 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/goal/format.ts +11 -10
- package/extensions/goal/tools.ts +6 -5
- package/extensions/goal/tui.ts +1 -1
- package/extensions/job/README.md +2 -2
- package/extensions/job/format.ts +36 -13
- package/extensions/job/index.ts +1 -1
- package/extensions/job/tools.ts +11 -10
- package/extensions/job/tui.ts +141 -220
- package/extensions/job/ui.ts +24 -49
- package/extensions/memory/README.md +1 -1
- package/extensions/memory/tools.ts +4 -3
- package/extensions/memory/tui.ts +32 -36
- package/extensions/plan/README.md +64 -34
- 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 +23 -22
- package/extensions/plan/tui.ts +21 -27
- package/extensions/plan/web-client.ts +335 -30
- package/extensions/plan/web-page.ts +110 -30
- package/extensions/plan/web-router.ts +6 -4
- package/extensions/plan/web.ts +41 -10
- package/extensions/question/README.md +1 -1
- package/extensions/question/tools.ts +12 -11
- package/extensions/question/tui.ts +132 -76
- package/extensions/rewind/README.md +1 -1
- package/extensions/rewind/format.ts +29 -1
- package/extensions/rewind/rewind-tui.ts +66 -62
- package/extensions/rewind/status.ts +3 -2
- package/extensions/subagent/render.ts +62 -47
- package/extensions/todo/README.md +2 -2
- package/extensions/todo/format.ts +31 -10
- package/extensions/todo/tools.ts +5 -4
- package/extensions/todo/tui.ts +45 -26
- package/extensions/watch/README.md +2 -2
- package/extensions/watch/format.ts +33 -13
- package/extensions/watch/index.ts +1 -1
- package/extensions/watch/tools.ts +12 -7
- package/extensions/watch/tui.ts +135 -194
- package/extensions/watch/ui.ts +24 -46
- package/extensions/web-access/fetch/tool.ts +12 -11
- package/extensions/web-access/search/tool.ts +16 -13
- package/extensions/wechat/README.md +2 -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/enter.ts +1 -1
- package/extensions/worktree/index.ts +1 -1
- package/extensions/worktree/runtime.ts +1 -1
- package/lib/README.md +10 -7
- package/lib/requirements.ts +2 -2
- package/lib/{tui.ts → tui/chrome.ts} +9 -30
- package/lib/tui/columns.ts +96 -0
- package/lib/{list-cursor.ts → tui/cursor.ts} +56 -3
- package/lib/tui/gauge.ts +78 -0
- package/lib/tui/index.ts +25 -0
- package/lib/tui/line-pane.ts +173 -0
- package/lib/tui/rows.ts +109 -0
- package/lib/tui/rules.ts +88 -0
- package/lib/tui/status-controller.ts +107 -0
- package/lib/tui/tokens.ts +58 -0
- package/package.json +1 -1
- /package/lib/{ui.ts → tui/glyphs.ts} +0 -0
|
@@ -12,10 +12,12 @@
|
|
|
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 } from "../../lib/tui/glyphs.ts";
|
|
19
|
+
import { centeredRule } from "../../lib/tui/rules.ts";
|
|
20
|
+
import { paint, type TuiRole } from "../../lib/tui/tokens.ts";
|
|
19
21
|
import { aggregateUsage, clip, formatToolCall, formatUsageStats } from "./format.ts";
|
|
20
22
|
import { COLLAPSED_ERROR_MAX, COLLAPSED_ITEM_COUNT, COLLAPSED_TEXT_LINES, type SubagentArgs } from "./schema.ts";
|
|
21
23
|
import { getFinalOutput, isFailedResult } from "./stream.ts";
|
|
@@ -24,6 +26,11 @@ import type { SingleResult, SubagentDetails } from "./types.ts";
|
|
|
24
26
|
type DisplayItem = { type: "text"; text: string } | { type: "toolCall"; name: string; args: Record<string, unknown> };
|
|
25
27
|
type ResultStatus = "running" | "aborted" | "failed" | "success";
|
|
26
28
|
|
|
29
|
+
/** Columns a section rule adds around its label: a space and three dashes each side. */
|
|
30
|
+
const SECTION_RULE_PAD = 8;
|
|
31
|
+
/** A section rule keeps the `meta` colour the hand-rolled `─── label ───` used. */
|
|
32
|
+
const SECTION_RULE_STYLE = { pad: 1, fillRole: "meta", labelRole: "meta" } as const;
|
|
33
|
+
|
|
27
34
|
/** Text and tool-call parts of every assistant message, in order. */
|
|
28
35
|
function getDisplayItems(messages: SingleResult["messages"]): DisplayItem[] {
|
|
29
36
|
const items: DisplayItem[] = [];
|
|
@@ -45,12 +52,12 @@ function hasExpandableContent(items: DisplayItem[]): boolean {
|
|
|
45
52
|
);
|
|
46
53
|
}
|
|
47
54
|
|
|
48
|
-
/**
|
|
49
|
-
const
|
|
50
|
-
running: "
|
|
51
|
-
aborted: "
|
|
52
|
-
failed: "
|
|
53
|
-
success: "
|
|
55
|
+
/** Design roles per status; the running glyph is time-based, so it is not a value here. */
|
|
56
|
+
const STATUS_ROLE: Record<ResultStatus, TuiRole> = {
|
|
57
|
+
running: "warn",
|
|
58
|
+
aborted: "warn",
|
|
59
|
+
failed: "bad",
|
|
60
|
+
success: "ok",
|
|
54
61
|
};
|
|
55
62
|
|
|
56
63
|
/** Static glyphs for terminal states, from the shared narrow-glyph vocabulary. */
|
|
@@ -77,12 +84,12 @@ function resultStatus(result: SingleResult, isPartial = false): ResultStatus {
|
|
|
77
84
|
}
|
|
78
85
|
|
|
79
86
|
function statusIcon(status: ResultStatus, theme: Theme, now: number): string {
|
|
80
|
-
return theme
|
|
87
|
+
return paint(theme, STATUS_ROLE[status], statusGlyph(status, now));
|
|
81
88
|
}
|
|
82
89
|
|
|
83
|
-
/** The
|
|
84
|
-
function
|
|
85
|
-
return
|
|
90
|
+
/** The role the status uses, so the `[stopReason]` bracket matches its icon. */
|
|
91
|
+
function statusRole(status: ResultStatus): TuiRole {
|
|
92
|
+
return STATUS_ROLE[status];
|
|
86
93
|
}
|
|
87
94
|
|
|
88
95
|
/** The message a failed/aborted result should show, falling back to stderr. */
|
|
@@ -99,7 +106,7 @@ function renderError(result: SingleResult, status: ResultStatus, theme: Theme, m
|
|
|
99
106
|
const detail = errorDetail(result, status, max);
|
|
100
107
|
if (!detail) return undefined;
|
|
101
108
|
const label = status === "aborted" ? "Aborted" : "Error";
|
|
102
|
-
return theme
|
|
109
|
+
return paint(theme, status === "aborted" ? "warn" : "bad", `${label}: ${detail}`);
|
|
103
110
|
}
|
|
104
111
|
|
|
105
112
|
/** Whether the collapsed error/stderr text is truncated, so expanding shows more. */
|
|
@@ -114,7 +121,7 @@ function hasClippedError(result: SingleResult, status: ResultStatus): boolean {
|
|
|
114
121
|
function collapsedBody(result: SingleResult, status: ResultStatus, items: DisplayItem[], theme: Theme): string {
|
|
115
122
|
const error = renderError(result, status, theme, COLLAPSED_ERROR_MAX);
|
|
116
123
|
if (error) return error;
|
|
117
|
-
if (items.length === 0) return theme
|
|
124
|
+
if (items.length === 0) return paint(theme, "meta", status === "running" ? "(running…)" : "(no output)");
|
|
118
125
|
return renderDisplayItems(items, theme, COLLAPSED_ITEM_COUNT);
|
|
119
126
|
}
|
|
120
127
|
|
|
@@ -164,11 +171,11 @@ function durationSuffix(result: SingleResult, status: ResultStatus, now: number)
|
|
|
164
171
|
function statusSuffix(result: SingleResult, status: ResultStatus, theme: Theme, now: number): string {
|
|
165
172
|
let suffix = "";
|
|
166
173
|
const duration = durationSuffix(result, status, now);
|
|
167
|
-
if (duration) suffix += theme
|
|
174
|
+
if (duration) suffix += paint(theme, "faint", duration);
|
|
168
175
|
const showReason = status === "failed" || status === "aborted";
|
|
169
176
|
const stop = showReason && result.stopReason && result.stopReason !== "end" ? result.stopReason : undefined;
|
|
170
177
|
const reason = stop ?? (status === "failed" ? `exit ${result.exitCode}` : undefined);
|
|
171
|
-
if (reason) suffix += ` ${theme
|
|
178
|
+
if (reason) suffix += ` ${paint(theme, statusRole(status), `[${reason}]`)}`;
|
|
172
179
|
return suffix;
|
|
173
180
|
}
|
|
174
181
|
|
|
@@ -182,9 +189,9 @@ function resultHeader(
|
|
|
182
189
|
): string {
|
|
183
190
|
let header = `${statusIcon(status, theme, now)} ${theme.fg("toolTitle", theme.bold(sanitizeLine(result.agent)))}`;
|
|
184
191
|
if (result.agentSource && result.agentSource !== "builtin" && result.agentSource !== "unknown") {
|
|
185
|
-
header += theme
|
|
192
|
+
header += paint(theme, "faint", ` [${sanitizeLine(result.agentSource)}]`);
|
|
186
193
|
}
|
|
187
|
-
if (result.step) header = `${theme
|
|
194
|
+
if (result.step) header = `${paint(theme, "meta", `${stepPrefix} ${result.step} `)}` + header;
|
|
188
195
|
return header + statusSuffix(result, status, theme, now);
|
|
189
196
|
}
|
|
190
197
|
|
|
@@ -193,7 +200,7 @@ function quoteLines(text: string, theme: Theme): string {
|
|
|
193
200
|
return stripControlChars(text)
|
|
194
201
|
.trim()
|
|
195
202
|
.split("\n")
|
|
196
|
-
.map((line) => theme
|
|
203
|
+
.map((line) => paint(theme, "meta", "│ ") + theme.fg("toolOutput", line))
|
|
197
204
|
.join("\n");
|
|
198
205
|
}
|
|
199
206
|
|
|
@@ -201,14 +208,14 @@ function renderDisplayItems(items: DisplayItem[], theme: Theme, limit?: number,
|
|
|
201
208
|
const shown = limit ? items.slice(-limit) : items;
|
|
202
209
|
const skipped = limit && items.length > limit ? items.length - limit : 0;
|
|
203
210
|
const lines: string[] = [];
|
|
204
|
-
if (skipped > 0) lines.push(theme
|
|
211
|
+
if (skipped > 0) lines.push(paint(theme, "meta", `… ${skipped} earlier items`));
|
|
205
212
|
for (const item of shown) {
|
|
206
213
|
if (item.type === "text") {
|
|
207
214
|
const safe = stripControlChars(item.text);
|
|
208
215
|
const text = preview ? safe.split("\n").slice(0, COLLAPSED_TEXT_LINES).join("\n") : safe;
|
|
209
216
|
lines.push(theme.fg("toolOutput", text));
|
|
210
217
|
} else {
|
|
211
|
-
lines.push(theme
|
|
218
|
+
lines.push(paint(theme, "meta", "→ ") + formatToolCall(item.name, item.args, theme, preview));
|
|
212
219
|
}
|
|
213
220
|
}
|
|
214
221
|
return lines.join("\n");
|
|
@@ -235,36 +242,38 @@ export function renderSubagentCall(args: SubagentArgs, theme: Theme, context?: S
|
|
|
235
242
|
|
|
236
243
|
/** The call-line string for the requested mode. */
|
|
237
244
|
function callText(args: SubagentArgs, theme: Theme, context?: { cwd?: string }): string {
|
|
238
|
-
const title =
|
|
245
|
+
const title =
|
|
246
|
+
theme.fg("toolTitle", theme.bold("subagent ")) + (args.readOnly ? paint(theme, "faint", "read-only ") : "");
|
|
239
247
|
const cwdNote = (cwd?: string) => {
|
|
240
248
|
if (!cwd || cwd === context?.cwd) return "";
|
|
241
|
-
return theme
|
|
249
|
+
return paint(theme, "faint", ` in ${shortenPath(cwd)}`);
|
|
242
250
|
};
|
|
243
251
|
|
|
244
252
|
if (args.chain && args.chain.length > 0) {
|
|
245
|
-
let text = title + theme
|
|
253
|
+
let text = title + paint(theme, "accent", `chain · ${args.chain.length} step${args.chain.length > 1 ? "s" : ""}`);
|
|
246
254
|
for (let i = 0; i < Math.min(args.chain.length, 3); i++) {
|
|
247
255
|
const step = args.chain[i];
|
|
248
256
|
const clean = sanitizeLine(step.task.replace(/\{previous\}/g, ""));
|
|
249
|
-
text += `\n ${theme
|
|
257
|
+
text += `\n ${paint(theme, "meta", `${i + 1}.`)} ${paint(theme, "accent", sanitizeLine(step.agent))}${paint(theme, "faint", ` ${clip(clean, 40)}`)}${cwdNote(step.cwd)}`;
|
|
250
258
|
}
|
|
251
|
-
if (args.chain.length > 3) text += `\n ${theme
|
|
259
|
+
if (args.chain.length > 3) text += `\n ${paint(theme, "meta", `… +${args.chain.length - 3} more`)}`;
|
|
252
260
|
return text;
|
|
253
261
|
}
|
|
254
262
|
|
|
255
263
|
if (args.tasks && args.tasks.length > 0) {
|
|
256
|
-
let text =
|
|
264
|
+
let text =
|
|
265
|
+
title + paint(theme, "accent", `parallel · ${args.tasks.length} task${args.tasks.length > 1 ? "s" : ""}`);
|
|
257
266
|
for (let i = 0; i < Math.min(args.tasks.length, 3); i++) {
|
|
258
267
|
const task = args.tasks[i];
|
|
259
|
-
text += `\n ${theme
|
|
268
|
+
text += `\n ${paint(theme, "meta", `${i + 1}.`)} ${paint(theme, "accent", sanitizeLine(task.agent))}${paint(theme, "faint", ` ${clip(sanitizeLine(task.task), 40)}`)}${cwdNote(task.cwd)}`;
|
|
260
269
|
}
|
|
261
|
-
if (args.tasks.length > 3) text += `\n ${theme
|
|
270
|
+
if (args.tasks.length > 3) text += `\n ${paint(theme, "meta", `… +${args.tasks.length - 3} more`)}`;
|
|
262
271
|
return text;
|
|
263
272
|
}
|
|
264
273
|
|
|
265
274
|
const agentName = sanitizeLine(args.agent || "…");
|
|
266
|
-
let text = title + theme
|
|
267
|
-
text += `\n ${theme
|
|
275
|
+
let text = title + paint(theme, "accent", agentName) + cwdNote(args.cwd);
|
|
276
|
+
text += `\n ${paint(theme, "faint", clip(sanitizeLine(args.task ?? "…"), 60))}`;
|
|
268
277
|
return text;
|
|
269
278
|
}
|
|
270
279
|
|
|
@@ -283,8 +292,10 @@ function addExpandedResult(
|
|
|
283
292
|
if (error) container.addChild(new Text(error, 0, 0));
|
|
284
293
|
|
|
285
294
|
container.addChild(new Spacer(1));
|
|
286
|
-
container.addChild(
|
|
287
|
-
|
|
295
|
+
container.addChild(
|
|
296
|
+
new Text(centeredRule(theme, visibleWidth("Task") + SECTION_RULE_PAD, "Task", SECTION_RULE_STYLE), 0, 0),
|
|
297
|
+
);
|
|
298
|
+
container.addChild(new Text(paint(theme, "faint", stripControlChars(result.taskTemplate ?? result.task)), 0, 0));
|
|
288
299
|
|
|
289
300
|
const displayItems = getDisplayItems(result.messages);
|
|
290
301
|
const lastItem = displayItems[displayItems.length - 1];
|
|
@@ -294,11 +305,15 @@ function addExpandedResult(
|
|
|
294
305
|
// order and do not surface a stale earlier message as the answer.
|
|
295
306
|
const trailingOutput = lastItem?.type === "text" && lastItem.text === finalOutput ? lastItem.text : "";
|
|
296
307
|
container.addChild(new Spacer(1));
|
|
297
|
-
container.addChild(
|
|
308
|
+
container.addChild(
|
|
309
|
+
new Text(centeredRule(theme, visibleWidth("Output") + SECTION_RULE_PAD, "Output", SECTION_RULE_STYLE), 0, 0),
|
|
310
|
+
);
|
|
298
311
|
let renderedOutput = false;
|
|
299
312
|
for (const item of displayItems) {
|
|
300
313
|
if (item.type === "toolCall") {
|
|
301
|
-
container.addChild(
|
|
314
|
+
container.addChild(
|
|
315
|
+
new Text(paint(theme, "meta", "→ ") + formatToolCall(item.name, item.args, theme, false), 0, 0),
|
|
316
|
+
);
|
|
302
317
|
renderedOutput = true;
|
|
303
318
|
} else if (item.text.trim() && item.text !== trailingOutput) {
|
|
304
319
|
container.addChild(new Text(quoteLines(item.text, theme), 0, 0));
|
|
@@ -311,13 +326,13 @@ function addExpandedResult(
|
|
|
311
326
|
renderedOutput = true;
|
|
312
327
|
}
|
|
313
328
|
if (!renderedOutput) {
|
|
314
|
-
container.addChild(new Text(theme
|
|
329
|
+
container.addChild(new Text(paint(theme, "meta", status === "running" ? "(running…)" : "(no output)"), 0, 0));
|
|
315
330
|
}
|
|
316
331
|
|
|
317
332
|
const usage = usageLine(result);
|
|
318
333
|
if (usage) {
|
|
319
334
|
container.addChild(new Spacer(1));
|
|
320
|
-
container.addChild(new Text(theme
|
|
335
|
+
container.addChild(new Text(paint(theme, "faint", usage), 0, 0));
|
|
321
336
|
}
|
|
322
337
|
}
|
|
323
338
|
|
|
@@ -333,7 +348,7 @@ function collapsedSingleText(result: SingleResult, theme: Theme, isPartial: bool
|
|
|
333
348
|
text += `\n\n${collapsedBody(result, status, displayItems, theme)}`;
|
|
334
349
|
if (isExpandable(result, status, displayItems)) text += `\n${expandHint(theme)}`;
|
|
335
350
|
const usage = usageLine(result);
|
|
336
|
-
if (usage) text += `\n${theme
|
|
351
|
+
if (usage) text += `\n${paint(theme, "faint", usage)}`;
|
|
337
352
|
return text;
|
|
338
353
|
}
|
|
339
354
|
|
|
@@ -375,11 +390,11 @@ function multiHeader(details: SubagentDetails, theme: Theme, now: number): strin
|
|
|
375
390
|
const bad = failed + aborted;
|
|
376
391
|
const icon =
|
|
377
392
|
running > 0
|
|
378
|
-
? theme
|
|
393
|
+
? paint(theme, "warn", statusGlyph("running", now))
|
|
379
394
|
: bad > 0
|
|
380
|
-
? theme
|
|
381
|
-
: theme
|
|
382
|
-
return `${icon} ${theme.fg("toolTitle", theme.bold(`${details.mode} · `))}${theme
|
|
395
|
+
? paint(theme, bad === total ? "bad" : "warn", bad === total ? GLYPHS.jobsFailure : GLYPHS.half)
|
|
396
|
+
: paint(theme, "ok", GLYPHS.check);
|
|
397
|
+
return `${icon} ${theme.fg("toolTitle", theme.bold(`${details.mode} · `))}${paint(theme, "accent", status)}`;
|
|
383
398
|
}
|
|
384
399
|
|
|
385
400
|
function collapsedMultiText(details: SubagentDetails, theme: Theme, now: number): string {
|
|
@@ -394,14 +409,14 @@ function collapsedMultiText(details: SubagentDetails, theme: Theme, now: number)
|
|
|
394
409
|
const displayItems = getDisplayItems(result.messages);
|
|
395
410
|
if (isExpandable(result, status, displayItems)) hasHidden = true;
|
|
396
411
|
const taskText = result.taskTemplate ?? result.task;
|
|
397
|
-
const taskNote = taskText ? theme
|
|
398
|
-
text += `\n\n${theme
|
|
412
|
+
const taskNote = taskText ? paint(theme, "faint", ` ${clip(sanitizeLine(taskText), 50)}`) : "";
|
|
413
|
+
text += `\n\n${paint(theme, "meta", "─── ")}${paint(theme, "accent", resultLabel(result, i))}${taskNote} ${statusIcon(status, theme, now)}`;
|
|
399
414
|
text += statusSuffix(result, status, theme, now);
|
|
400
415
|
text += `\n${collapsedBody(result, status, displayItems, theme)}`;
|
|
401
416
|
}
|
|
402
417
|
if (running === 0) {
|
|
403
418
|
const usage = totalUsageLine(results);
|
|
404
|
-
if (usage) text += `\n\n${theme
|
|
419
|
+
if (usage) text += `\n\n${paint(theme, "faint", `Total: ${usage}`)}`;
|
|
405
420
|
}
|
|
406
421
|
if (running > 0 || hasHidden) text += `\n${expandHint(theme)}`;
|
|
407
422
|
return text;
|
|
@@ -421,7 +436,7 @@ function fillExpandedMulti(container: Container, details: SubagentDetails, theme
|
|
|
421
436
|
const usage = totalUsageLine(details.results);
|
|
422
437
|
if (usage) {
|
|
423
438
|
container.addChild(new Spacer(1));
|
|
424
|
-
container.addChild(new Text(theme
|
|
439
|
+
container.addChild(new Text(paint(theme, "faint", `Total: ${usage}`), 0, 0));
|
|
425
440
|
}
|
|
426
441
|
}
|
|
427
442
|
}
|
|
@@ -205,7 +205,7 @@ depends on config.
|
|
|
205
205
|
first pending); `/todos` keeps the submitted order, and the transcript result
|
|
206
206
|
orders by status via `compareByActivity` so active work is never hidden. The
|
|
207
207
|
transcript result draws the same indented glyph rail as the goal result
|
|
208
|
-
(`BODY_INDENT` / `GLYPH_GAP` from [`lib/
|
|
208
|
+
(`BODY_INDENT` / `GLYPH_GAP` from [`lib/tui/glyphs.ts`](../../lib/tui/glyphs.ts)), wrapping
|
|
209
209
|
long items with continuation rows aligned under the text. Progress lives on
|
|
210
210
|
the call line, so the rail carries no footer.
|
|
211
211
|
- **The transcript reuses its components.** `renderCall` reuses the slot's
|
|
@@ -223,7 +223,7 @@ depends on config.
|
|
|
223
223
|
| `state.ts` | Branch reconstruction and status queries (pure). |
|
|
224
224
|
| `nudge.ts` | Lag detection: mutating-tool classification, due check, reminder text (pure). |
|
|
225
225
|
| `format.ts` | Model-facing and transcript text (pure). |
|
|
226
|
-
| `tui.ts` | Widget and scrollable `/todos` components. |
|
|
226
|
+
| `tui.ts` | Widget and scrollable `/todos` components, drawn from the `lib/tui/` kit (`titleRule`, `dockListFrame`, `listRow`, `emptyState`). |
|
|
227
227
|
| `config.ts` | Widget config load/validation (the `todo` section of `bytefly.json`). |
|
|
228
228
|
| `runtime.ts` | Session-scoped state, lag tracking, and widget synchronization. |
|
|
229
229
|
| `tools.ts` | `todo` tool registration and rendering. |
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { sliceByColumn, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
7
|
-
import { BODY_INDENT, expandHint, GLYPH_GAP, GLYPHS } from "../../lib/
|
|
7
|
+
import { BODY_INDENT, expandHint, GLYPH_GAP, GLYPHS } from "../../lib/tui/glyphs.ts";
|
|
8
|
+
import { paint, type TuiRole } from "../../lib/tui/tokens.ts";
|
|
8
9
|
import { completedCount, currentTodo } from "./state.ts";
|
|
9
10
|
import type { Todo, TodoStatus } from "./types.ts";
|
|
10
11
|
|
|
@@ -26,23 +27,43 @@ const STATUS_GLYPH: Record<TodoStatus, string> = {
|
|
|
26
27
|
completed: GLYPHS.check,
|
|
27
28
|
};
|
|
28
29
|
|
|
29
|
-
/**
|
|
30
|
-
export function
|
|
30
|
+
/** The design role a status glyph is drawn in. */
|
|
31
|
+
export function todoGlyphRole(todo: Todo): TuiRole {
|
|
31
32
|
switch (todo.status) {
|
|
32
33
|
case "completed":
|
|
33
|
-
return
|
|
34
|
+
return "ok";
|
|
34
35
|
case "in_progress":
|
|
35
|
-
return
|
|
36
|
+
return "accent";
|
|
36
37
|
default:
|
|
37
|
-
return
|
|
38
|
+
return "faint";
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
/** The status glyph, from the shared vocabulary. */
|
|
43
|
+
export function todoGlyphChar(todo: Todo): string {
|
|
44
|
+
return STATUS_GLYPH[todo.status];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The design role an item label is drawn in. */
|
|
48
|
+
export function todoLabelRole(todo: Todo): TuiRole {
|
|
49
|
+
if (todo.status === "completed") return "faint";
|
|
50
|
+
if (todo.status === "in_progress") return "label";
|
|
51
|
+
return "meta";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The item text: the active form while in progress, the content otherwise. */
|
|
55
|
+
export function todoLabelText(todo: Todo): string {
|
|
56
|
+
return todo.status === "in_progress" ? (todo.activeForm ?? todo.content) : todo.content;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Themed status glyph, shared by the widget and the transcript renderer. */
|
|
60
|
+
export function todoGlyph(todo: Todo, theme: Theme): string {
|
|
61
|
+
return paint(theme, todoGlyphRole(todo), todoGlyphChar(todo));
|
|
62
|
+
}
|
|
63
|
+
|
|
41
64
|
/** Themed item text: completed dim, in-progress accented, pending muted. */
|
|
42
65
|
export function todoLabel(todo: Todo, theme: Theme): string {
|
|
43
|
-
|
|
44
|
-
if (todo.status === "in_progress") return theme.fg("text", todo.activeForm ?? todo.content);
|
|
45
|
-
return theme.fg("muted", todo.content);
|
|
66
|
+
return paint(theme, todoLabelRole(todo), todoLabelText(todo));
|
|
46
67
|
}
|
|
47
68
|
|
|
48
69
|
/** "2/5" — completed over total. */
|
|
@@ -136,7 +157,7 @@ export function todoRailLines(input: TodoRailInput, theme: Theme, width: number)
|
|
|
136
157
|
});
|
|
137
158
|
}
|
|
138
159
|
if (input.more && input.more > 0) {
|
|
139
|
-
lines.push(`${continuation}${theme
|
|
160
|
+
lines.push(`${continuation}${paint(theme, "faint", `… ${input.more} more`)} ${expandHint(theme)}`);
|
|
140
161
|
}
|
|
141
162
|
return lines.map((line) => truncateToWidth(line, w, "…"));
|
|
142
163
|
}
|
package/extensions/todo/tools.ts
CHANGED
|
@@ -14,7 +14,8 @@ import type { JsonValue } from "@earendil-works/pi-ai";
|
|
|
14
14
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
15
15
|
import { sanitizeLine } from "../../lib/format.ts";
|
|
16
16
|
import { TODO_TOOL } from "../../lib/tool-names.ts";
|
|
17
|
-
import { reuseText, toolDetails } from "../../lib/tui.ts";
|
|
17
|
+
import { reuseText, toolDetails } from "../../lib/tui/chrome.ts";
|
|
18
|
+
import { paint } from "../../lib/tui/tokens.ts";
|
|
18
19
|
import { compareByActivity, formatCallText, formatTodoText, type TodoRailInput } from "./format.ts";
|
|
19
20
|
import type { TodoRuntime } from "./runtime.ts";
|
|
20
21
|
import { normalizeTodos, TodoParams, TodoResult, type TodoArgs } from "./schema.ts";
|
|
@@ -97,7 +98,7 @@ export function registerTools(pi: ExtensionAPI, runtime: TodoRuntime): void {
|
|
|
97
98
|
const text = reuseText(context.lastComponent);
|
|
98
99
|
text.setText(
|
|
99
100
|
theme.fg("toolTitle", theme.bold(`${TOOL_NAME} `)) +
|
|
100
|
-
theme
|
|
101
|
+
paint(theme, "meta", formatCallText(args.todos as Todo[] | undefined, context.argsComplete)),
|
|
101
102
|
);
|
|
102
103
|
return text;
|
|
103
104
|
},
|
|
@@ -106,7 +107,7 @@ export function registerTools(pi: ExtensionAPI, runtime: TodoRuntime): void {
|
|
|
106
107
|
const text = reuseText(context.lastComponent);
|
|
107
108
|
const details = toolDetails<TodoDetails>(result.details);
|
|
108
109
|
if (details?.error) {
|
|
109
|
-
text.setText(theme
|
|
110
|
+
text.setText(paint(theme, "bad", `Error: ${sanitizeLine(details.error)}`));
|
|
110
111
|
return text;
|
|
111
112
|
}
|
|
112
113
|
// A call rejected before `execute` (argument validation or a permission
|
|
@@ -117,7 +118,7 @@ export function registerTools(pi: ExtensionAPI, runtime: TodoRuntime): void {
|
|
|
117
118
|
return text;
|
|
118
119
|
}
|
|
119
120
|
if (details.todos.length === 0) {
|
|
120
|
-
text.setText(theme
|
|
121
|
+
text.setText(paint(theme, "ok", "✓ ") + paint(theme, "meta", "Cleared the todo list"));
|
|
121
122
|
return text;
|
|
122
123
|
}
|
|
123
124
|
|
package/extensions/todo/tui.ts
CHANGED
|
@@ -8,10 +8,23 @@
|
|
|
8
8
|
|
|
9
9
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import { truncateToWidth, type Component, type TuiMouseEvent, type TuiMouseEventResult } from "@earendil-works/pi-tui";
|
|
11
|
-
import { clampScroll, fitRows, formatRange, isCloseKey, navigateScroll, wheelDelta } from "../../lib/
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
11
|
+
import { clampScroll, fitRows, formatRange, isCloseKey, navigateScroll, wheelDelta } from "../../lib/tui/cursor.ts";
|
|
12
|
+
import { dockListFrame, screenHint, type ViewportRowsSource } from "../../lib/tui/chrome.ts";
|
|
13
|
+
import { clip } from "../../lib/tui/columns.ts";
|
|
14
|
+
import { emptyState, listRow } from "../../lib/tui/rows.ts";
|
|
15
|
+
import { titleRule } from "../../lib/tui/rules.ts";
|
|
16
|
+
import { paint } from "../../lib/tui/tokens.ts";
|
|
17
|
+
import {
|
|
18
|
+
progressCount,
|
|
19
|
+
progressSummary,
|
|
20
|
+
todoGlyphChar,
|
|
21
|
+
todoGlyphRole,
|
|
22
|
+
todoLabel,
|
|
23
|
+
todoLabelRole,
|
|
24
|
+
todoLabelText,
|
|
25
|
+
todoRailLines,
|
|
26
|
+
type TodoRailInput,
|
|
27
|
+
} from "./format.ts";
|
|
15
28
|
import { currentTodo } from "./state.ts";
|
|
16
29
|
import type { Todo } from "./types.ts";
|
|
17
30
|
|
|
@@ -23,8 +36,12 @@ const SCREEN_DEFAULT_ITEMS = 12;
|
|
|
23
36
|
|
|
24
37
|
/** One indented item line, clipped to `width`. */
|
|
25
38
|
function todoRow(todo: Todo, theme: Theme, width: number): string {
|
|
26
|
-
|
|
27
|
-
|
|
39
|
+
return listRow(theme, width, {
|
|
40
|
+
glyph: todoGlyphChar(todo),
|
|
41
|
+
glyphRole: todoGlyphRole(todo),
|
|
42
|
+
label: todoLabelText(todo),
|
|
43
|
+
labelRole: todoLabelRole(todo),
|
|
44
|
+
});
|
|
28
45
|
}
|
|
29
46
|
|
|
30
47
|
/**
|
|
@@ -33,10 +50,10 @@ function todoRow(todo: Todo, theme: Theme, width: number): string {
|
|
|
33
50
|
* `jobs` widgets. Callers clip it to the available width.
|
|
34
51
|
*/
|
|
35
52
|
function todoLine(todos: Todo[], theme: Theme): string {
|
|
36
|
-
const head = `${theme
|
|
53
|
+
const head = `${paint(theme, "accent", "Todos")} ${paint(theme, "faint", "·")} ${paint(theme, "faint", progressCount(todos))}`;
|
|
37
54
|
const current = currentTodo(todos);
|
|
38
|
-
if (!current) return `${head} ${theme
|
|
39
|
-
return `${head} ${theme
|
|
55
|
+
if (!current) return `${head} ${paint(theme, "faint", "completed")}`;
|
|
56
|
+
return `${head} ${paint(theme, "faint", "·")} ${todoLabel(current, theme)}`;
|
|
40
57
|
}
|
|
41
58
|
|
|
42
59
|
/** Transcript result block: the indented glyph rail, wrapping at the render width. */
|
|
@@ -139,32 +156,34 @@ export class TodoListComponent implements Component {
|
|
|
139
156
|
|
|
140
157
|
render(width: number): string[] {
|
|
141
158
|
const w = Math.max(1, width);
|
|
142
|
-
const
|
|
159
|
+
const header = titleRule(this.theme, w, "Todos");
|
|
143
160
|
|
|
144
161
|
if (this.todos.length === 0) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
162
|
+
return dockListFrame({
|
|
163
|
+
header,
|
|
164
|
+
body: emptyState(this.theme, w, { label: "No todos yet. Ask the agent to plan some work." }),
|
|
165
|
+
hint: screenHint(this.theme, w, ["Esc close"]),
|
|
166
|
+
});
|
|
150
167
|
}
|
|
151
168
|
|
|
152
|
-
lines.push(truncateToWidth(` ${this.theme.fg("muted", progressSummary(this.todos))}`, w));
|
|
153
|
-
lines.push("");
|
|
154
169
|
// header, summary, blank, blank-after-list, hint, blank
|
|
155
170
|
const chrome = 6;
|
|
156
171
|
this.visible = fitRows(this.viewportRowsSource, this.todos.length, chrome, SCREEN_DEFAULT_ITEMS);
|
|
157
172
|
this.scrollTop = clampScroll(this.scrollTop, this.todos.length, this.visible);
|
|
158
173
|
|
|
159
174
|
const end = Math.min(this.todos.length, this.scrollTop + this.visible);
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
175
|
+
const body: string[] = [];
|
|
176
|
+
for (let i = this.scrollTop; i < end; i++) body.push(todoRow(this.todos[i], this.theme, w));
|
|
177
|
+
|
|
178
|
+
return dockListFrame({
|
|
179
|
+
header,
|
|
180
|
+
summary: clip(` ${paint(this.theme, "meta", progressSummary(this.todos))}`, w),
|
|
181
|
+
body,
|
|
182
|
+
range:
|
|
183
|
+
this.scrollTop > 0 || end < this.todos.length
|
|
184
|
+
? formatRange(this.theme, w, { start: this.scrollTop, end, total: this.todos.length })
|
|
185
|
+
: undefined,
|
|
186
|
+
hint: screenHint(this.theme, w, ["Esc close", "↑/↓ or k/j scroll", "PgUp/PgDn", "g/G or Home/End"]),
|
|
187
|
+
});
|
|
169
188
|
}
|
|
170
189
|
}
|
|
@@ -200,8 +200,8 @@ turn — so a match is never dropped when the wake window is busy.
|
|
|
200
200
|
| `runtime.ts` | The session-scoped watcher table, matcher, poll tick, and shutdown. |
|
|
201
201
|
| `process.ts` | Injectable spawn and process-tree kill. |
|
|
202
202
|
| `format.ts` | Pure sanitization and text formatting. |
|
|
203
|
-
| `ui.ts` | Status chips and the match alert. |
|
|
204
|
-
| `tui.ts` | `WatchResult` transcript block and the `/watch` dock screen. |
|
|
203
|
+
| `ui.ts` | Status chips and the match alert, over the shared `lib/tui/status-controller.ts`. |
|
|
204
|
+
| `tui.ts` | `WatchResult` transcript block and the `/watch` dock screen, drawn from the `lib/tui/` kit (`titleRule`, `listRow`, `emptyState`, `IdCursor`, `LinePane`). |
|
|
205
205
|
| `commands.ts` | The `/watch` command and its completions. |
|
|
206
206
|
| `config.ts` | The `watch` config section load and normalization. |
|
|
207
207
|
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
13
13
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
14
14
|
import { sanitizeLine } from "../../lib/format.ts";
|
|
15
|
-
import { GLYPHS } from "../../lib/
|
|
15
|
+
import { GLYPHS } from "../../lib/tui/glyphs.ts";
|
|
16
|
+
import { paint, type TuiRole } from "../../lib/tui/tokens.ts";
|
|
16
17
|
import type { PendingMatch, WatchRecord, WatchStatus } from "./types.ts";
|
|
17
18
|
|
|
18
19
|
/** Longest label shown in the transcript or `/watch` screen. */
|
|
@@ -63,20 +64,39 @@ export function shortLabel(watch: WatchRecord): string {
|
|
|
63
64
|
return clipLabel(watch.label || sourceLabel(watch));
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
/**
|
|
67
|
-
export function
|
|
67
|
+
/** The design role a watcher status is drawn in. */
|
|
68
|
+
export function statusRole(status: WatchStatus): TuiRole {
|
|
69
|
+
switch (status) {
|
|
70
|
+
case "exited":
|
|
71
|
+
return "ok";
|
|
72
|
+
case "error":
|
|
73
|
+
return "bad";
|
|
74
|
+
case "stopped":
|
|
75
|
+
return "meta";
|
|
76
|
+
default:
|
|
77
|
+
return "accent";
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Width-1 glyph for a watcher status, from the shared vocabulary. */
|
|
82
|
+
export function statusGlyphChar(status: WatchStatus): string {
|
|
68
83
|
switch (status) {
|
|
69
84
|
case "exited":
|
|
70
|
-
return
|
|
85
|
+
return GLYPHS.check;
|
|
71
86
|
case "error":
|
|
72
|
-
return
|
|
87
|
+
return GLYPHS.warn;
|
|
73
88
|
case "stopped":
|
|
74
|
-
return
|
|
89
|
+
return GLYPHS.stopped;
|
|
75
90
|
default:
|
|
76
|
-
return
|
|
91
|
+
return GLYPHS.target;
|
|
77
92
|
}
|
|
78
93
|
}
|
|
79
94
|
|
|
95
|
+
/** Width-1 themed glyph for a watcher status. */
|
|
96
|
+
export function statusGlyph(status: WatchStatus, theme: Theme): string {
|
|
97
|
+
return paint(theme, statusRole(status), statusGlyphChar(status));
|
|
98
|
+
}
|
|
99
|
+
|
|
80
100
|
/** Short status word, including an exit code or error when present. */
|
|
81
101
|
export function statusWord(watch: WatchRecord): string {
|
|
82
102
|
if (watch.status === "error" && watch.error) return `error (${watch.error})`;
|
|
@@ -175,22 +195,22 @@ export function formatCallText(action: string, args: Record<string, unknown>, ar
|
|
|
175
195
|
|
|
176
196
|
/** One-line outcome for a watcher, themed for the transcript result. */
|
|
177
197
|
export function formatWatchOutcomeLine(watch: WatchRecord, theme: Theme, now = Date.now()): string {
|
|
178
|
-
const sep = theme
|
|
198
|
+
const sep = paint(theme, "faint", " · ");
|
|
179
199
|
const age = Math.max(0, (watch.endedAt ?? now) - watch.startedAt);
|
|
180
200
|
const outcome =
|
|
181
201
|
watch.status === "error"
|
|
182
|
-
? theme
|
|
183
|
-
: theme
|
|
202
|
+
? paint(theme, "bad", statusWord(watch))
|
|
203
|
+
: paint(theme, "faint", `${watch.matchCount} match${watch.matchCount === 1 ? "" : "es"}`);
|
|
184
204
|
return (
|
|
185
205
|
statusGlyph(watch.status, theme) +
|
|
186
206
|
" " +
|
|
187
|
-
theme
|
|
207
|
+
paint(theme, "accent", watch.id) +
|
|
188
208
|
" " +
|
|
189
|
-
theme
|
|
209
|
+
paint(theme, "meta", shortLabel(watch)) +
|
|
190
210
|
sep +
|
|
191
211
|
outcome +
|
|
192
212
|
sep +
|
|
193
|
-
theme
|
|
213
|
+
paint(theme, "faint", `${Math.round(age / 1000)}s`)
|
|
194
214
|
);
|
|
195
215
|
}
|
|
196
216
|
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
14
14
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
15
15
|
import { isExtensionEnabled } from "../../lib/env.ts";
|
|
16
|
-
import { STATUS_KEYS } from "../../lib/
|
|
16
|
+
import { STATUS_KEYS } from "../../lib/tui/glyphs.ts";
|
|
17
17
|
import { registerCommands } from "./commands.ts";
|
|
18
18
|
import { formatPendingMatch } from "./format.ts";
|
|
19
19
|
import { createWatchRuntime, type WatchRuntime } from "./runtime.ts";
|