@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
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { truncateToWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
7
|
+
import { 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
|
|
|
@@ -16,9 +17,6 @@ export function compareByActivity(a: Todo, b: Todo): number {
|
|
|
16
17
|
return STATUS_ORDER[a.status] - STATUS_ORDER[b.status];
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
/** Longest first-item preview shown in the transcript call line. */
|
|
20
|
-
const CALL_PREVIEW_WIDTH = 40;
|
|
21
|
-
|
|
22
20
|
/** Plain status glyph per status; `todoGlyph` adds the theme color. */
|
|
23
21
|
const STATUS_GLYPH: Record<TodoStatus, string> = {
|
|
24
22
|
pending: GLYPHS.pending,
|
|
@@ -26,23 +24,43 @@ const STATUS_GLYPH: Record<TodoStatus, string> = {
|
|
|
26
24
|
completed: GLYPHS.check,
|
|
27
25
|
};
|
|
28
26
|
|
|
29
|
-
/**
|
|
30
|
-
export function
|
|
27
|
+
/** The design role a status glyph is drawn in. */
|
|
28
|
+
export function todoGlyphRole(todo: Todo): TuiRole {
|
|
31
29
|
switch (todo.status) {
|
|
32
30
|
case "completed":
|
|
33
|
-
return
|
|
31
|
+
return "ok";
|
|
34
32
|
case "in_progress":
|
|
35
|
-
return
|
|
33
|
+
return "accent";
|
|
36
34
|
default:
|
|
37
|
-
return
|
|
35
|
+
return "faint";
|
|
38
36
|
}
|
|
39
37
|
}
|
|
40
38
|
|
|
39
|
+
/** The status glyph, from the shared vocabulary. */
|
|
40
|
+
export function todoGlyphChar(todo: Todo): string {
|
|
41
|
+
return STATUS_GLYPH[todo.status];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The design role an item label is drawn in. */
|
|
45
|
+
export function todoLabelRole(todo: Todo): TuiRole {
|
|
46
|
+
if (todo.status === "completed") return "faint";
|
|
47
|
+
if (todo.status === "in_progress") return "label";
|
|
48
|
+
return "meta";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The item text: the active form while in progress, the content otherwise. */
|
|
52
|
+
export function todoLabelText(todo: Todo): string {
|
|
53
|
+
return todo.status === "in_progress" ? (todo.activeForm ?? todo.content) : todo.content;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Themed status glyph, shared by the widget and the transcript renderer. */
|
|
57
|
+
export function todoGlyph(todo: Todo, theme: Theme): string {
|
|
58
|
+
return paint(theme, todoGlyphRole(todo), todoGlyphChar(todo));
|
|
59
|
+
}
|
|
60
|
+
|
|
41
61
|
/** Themed item text: completed dim, in-progress accented, pending muted. */
|
|
42
62
|
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);
|
|
63
|
+
return paint(theme, todoLabelRole(todo), todoLabelText(todo));
|
|
46
64
|
}
|
|
47
65
|
|
|
48
66
|
/** "2/5" — completed over total. */
|
|
@@ -86,22 +104,16 @@ export function formatTodoText(todos: Todo[]): string {
|
|
|
86
104
|
* One-line body for the transcript call renderer.
|
|
87
105
|
*
|
|
88
106
|
* The renderer owns the styled `todo ` title, so this returns only the body.
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
* the
|
|
107
|
+
* Progress is the summary; the result rail lists the items (active work first),
|
|
108
|
+
* so the call line does not repeat the current item. `todos` is `undefined`
|
|
109
|
+
* while the call's arguments are still streaming, which is distinct from an
|
|
110
|
+
* empty list (a real clear). `argsComplete` disambiguates the tail end of the
|
|
111
|
+
* stream.
|
|
92
112
|
*/
|
|
93
113
|
export function formatCallText(todos: Todo[] | undefined, argsComplete = true): string {
|
|
94
114
|
if (todos === undefined) return argsComplete ? "→ clear list" : "→ …";
|
|
95
115
|
if (todos.length === 0) return "→ clear list";
|
|
96
|
-
|
|
97
|
-
const current = currentTodo(todos);
|
|
98
|
-
const label = current ? (current.activeForm ?? current.content).trim() : "completed";
|
|
99
|
-
if (!label) return argsComplete ? `→ ${progressCount(todos)} · …` : "→ …";
|
|
100
|
-
// `strict` drops a wide grapheme that would cross the boundary, so the slice
|
|
101
|
-
// plus ellipsis never exceeds `CALL_PREVIEW_WIDTH` columns.
|
|
102
|
-
const preview =
|
|
103
|
-
visibleWidth(label) > CALL_PREVIEW_WIDTH ? `${sliceByColumn(label, 0, CALL_PREVIEW_WIDTH - 1, true)}…` : label;
|
|
104
|
-
return `→ ${progressCount(todos)} · ${preview}`;
|
|
116
|
+
return `→ ${progressCount(todos)}`;
|
|
105
117
|
}
|
|
106
118
|
|
|
107
119
|
/** Rows and footer for the transcript result rail. */
|
|
@@ -113,18 +125,19 @@ export interface TodoRailInput {
|
|
|
113
125
|
}
|
|
114
126
|
|
|
115
127
|
/**
|
|
116
|
-
* Transcript result rail: each row is
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
128
|
+
* Transcript result rail: each row is led by its status glyph, and its label
|
|
129
|
+
* wrapped with continuation rows aligned under the text. A trailing `… N more`
|
|
130
|
+
* follows when rows were capped. Progress lives on the call line, so the rail
|
|
131
|
+
* carries no footer. Every line is clipped to `width`.
|
|
132
|
+
*
|
|
133
|
+
* The rail draws at its own left edge; the tool-block frame supplies
|
|
134
|
+
* `BODY_INDENT`, so the rail must not add it again.
|
|
121
135
|
*/
|
|
122
136
|
export function todoRailLines(input: TodoRailInput, theme: Theme, width: number): string[] {
|
|
123
137
|
const w = Math.max(1, width);
|
|
124
138
|
// All three status glyphs are single-column, so the text column is fixed.
|
|
125
|
-
const textColumn =
|
|
139
|
+
const textColumn = 1 + GLYPH_GAP;
|
|
126
140
|
const inner = Math.max(1, w - textColumn);
|
|
127
|
-
const pad = " ".repeat(BODY_INDENT);
|
|
128
141
|
const gap = " ".repeat(GLYPH_GAP);
|
|
129
142
|
const continuation = " ".repeat(textColumn);
|
|
130
143
|
const lines: string[] = [];
|
|
@@ -132,11 +145,11 @@ export function todoRailLines(input: TodoRailInput, theme: Theme, width: number)
|
|
|
132
145
|
const glyph = todoGlyph(todo, theme);
|
|
133
146
|
const wrapped = wrapTextWithAnsi(todoLabel(todo, theme), inner);
|
|
134
147
|
wrapped.forEach((line, index) => {
|
|
135
|
-
lines.push(index === 0 ? `${
|
|
148
|
+
lines.push(index === 0 ? `${glyph}${gap}${line}` : `${continuation}${line}`);
|
|
136
149
|
});
|
|
137
150
|
}
|
|
138
151
|
if (input.more && input.more > 0) {
|
|
139
|
-
lines.push(`${continuation}${theme
|
|
152
|
+
lines.push(`${continuation}${paint(theme, "faint", `… ${input.more} more`)} ${expandHint(theme)}`);
|
|
140
153
|
}
|
|
141
154
|
return lines.map((line) => truncateToWidth(line, w, "…"));
|
|
142
155
|
}
|
package/extensions/todo/tools.ts
CHANGED
|
@@ -14,7 +14,9 @@ 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 {
|
|
17
|
+
import { GLYPHS } from "../../lib/tui/glyphs.ts";
|
|
18
|
+
import { reuseText, toolDetails } from "../../lib/tui/chrome.ts";
|
|
19
|
+
import { paint } from "../../lib/tui/tokens.ts";
|
|
18
20
|
import { compareByActivity, formatCallText, formatTodoText, type TodoRailInput } from "./format.ts";
|
|
19
21
|
import type { TodoRuntime } from "./runtime.ts";
|
|
20
22
|
import { normalizeTodos, TodoParams, TodoResult, type TodoArgs } from "./schema.ts";
|
|
@@ -97,7 +99,7 @@ export function registerTools(pi: ExtensionAPI, runtime: TodoRuntime): void {
|
|
|
97
99
|
const text = reuseText(context.lastComponent);
|
|
98
100
|
text.setText(
|
|
99
101
|
theme.fg("toolTitle", theme.bold(`${TOOL_NAME} `)) +
|
|
100
|
-
theme
|
|
102
|
+
paint(theme, "meta", formatCallText(args.todos as Todo[] | undefined, context.argsComplete)),
|
|
101
103
|
);
|
|
102
104
|
return text;
|
|
103
105
|
},
|
|
@@ -106,7 +108,7 @@ export function registerTools(pi: ExtensionAPI, runtime: TodoRuntime): void {
|
|
|
106
108
|
const text = reuseText(context.lastComponent);
|
|
107
109
|
const details = toolDetails<TodoDetails>(result.details);
|
|
108
110
|
if (details?.error) {
|
|
109
|
-
text.setText(theme
|
|
111
|
+
text.setText(paint(theme, "bad", `Error: ${sanitizeLine(details.error)}`));
|
|
110
112
|
return text;
|
|
111
113
|
}
|
|
112
114
|
// A call rejected before `execute` (argument validation or a permission
|
|
@@ -117,7 +119,7 @@ export function registerTools(pi: ExtensionAPI, runtime: TodoRuntime): void {
|
|
|
117
119
|
return text;
|
|
118
120
|
}
|
|
119
121
|
if (details.todos.length === 0) {
|
|
120
|
-
text.setText(theme
|
|
122
|
+
text.setText(paint(theme, "ok", `${GLYPHS.check} `) + paint(theme, "meta", "Cleared the todo list"));
|
|
121
123
|
return text;
|
|
122
124
|
}
|
|
123
125
|
|
package/extensions/todo/tui.ts
CHANGED
|
@@ -8,10 +8,24 @@
|
|
|
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 { SEPARATORS } from "../../lib/tui/glyphs.ts";
|
|
16
|
+
import { titleRule } from "../../lib/tui/rules.ts";
|
|
17
|
+
import { paint } from "../../lib/tui/tokens.ts";
|
|
18
|
+
import {
|
|
19
|
+
progressCount,
|
|
20
|
+
progressSummary,
|
|
21
|
+
todoGlyphChar,
|
|
22
|
+
todoGlyphRole,
|
|
23
|
+
todoLabel,
|
|
24
|
+
todoLabelRole,
|
|
25
|
+
todoLabelText,
|
|
26
|
+
todoRailLines,
|
|
27
|
+
type TodoRailInput,
|
|
28
|
+
} from "./format.ts";
|
|
15
29
|
import { currentTodo } from "./state.ts";
|
|
16
30
|
import type { Todo } from "./types.ts";
|
|
17
31
|
|
|
@@ -23,8 +37,12 @@ const SCREEN_DEFAULT_ITEMS = 12;
|
|
|
23
37
|
|
|
24
38
|
/** One indented item line, clipped to `width`. */
|
|
25
39
|
function todoRow(todo: Todo, theme: Theme, width: number): string {
|
|
26
|
-
|
|
27
|
-
|
|
40
|
+
return listRow(theme, width, {
|
|
41
|
+
glyph: todoGlyphChar(todo),
|
|
42
|
+
glyphRole: todoGlyphRole(todo),
|
|
43
|
+
label: todoLabelText(todo),
|
|
44
|
+
labelRole: todoLabelRole(todo),
|
|
45
|
+
});
|
|
28
46
|
}
|
|
29
47
|
|
|
30
48
|
/**
|
|
@@ -33,10 +51,10 @@ function todoRow(todo: Todo, theme: Theme, width: number): string {
|
|
|
33
51
|
* `jobs` widgets. Callers clip it to the available width.
|
|
34
52
|
*/
|
|
35
53
|
function todoLine(todos: Todo[], theme: Theme): string {
|
|
36
|
-
const head = `${theme
|
|
54
|
+
const head = `${paint(theme, "accent", "Todos")}${paint(theme, "faint", SEPARATORS.item)}${paint(theme, "faint", progressCount(todos))}`;
|
|
37
55
|
const current = currentTodo(todos);
|
|
38
|
-
if (!current) return `${head} ${theme
|
|
39
|
-
return `${head}
|
|
56
|
+
if (!current) return `${head} ${paint(theme, "faint", "completed")}`;
|
|
57
|
+
return `${head}${paint(theme, "faint", SEPARATORS.item)}${todoLabel(current, theme)}`;
|
|
40
58
|
}
|
|
41
59
|
|
|
42
60
|
/** Transcript result block: the indented glyph rail, wrapping at the render width. */
|
|
@@ -139,32 +157,34 @@ export class TodoListComponent implements Component {
|
|
|
139
157
|
|
|
140
158
|
render(width: number): string[] {
|
|
141
159
|
const w = Math.max(1, width);
|
|
142
|
-
const
|
|
160
|
+
const header = titleRule(this.theme, w, "Todos");
|
|
143
161
|
|
|
144
162
|
if (this.todos.length === 0) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
163
|
+
return dockListFrame({
|
|
164
|
+
header,
|
|
165
|
+
body: emptyState(this.theme, w, { label: "No todos yet. Ask the agent to plan some work." }),
|
|
166
|
+
hint: screenHint(this.theme, w, ["Esc close"]),
|
|
167
|
+
});
|
|
150
168
|
}
|
|
151
169
|
|
|
152
|
-
lines.push(truncateToWidth(` ${this.theme.fg("muted", progressSummary(this.todos))}`, w));
|
|
153
|
-
lines.push("");
|
|
154
170
|
// header, summary, blank, blank-after-list, hint, blank
|
|
155
171
|
const chrome = 6;
|
|
156
172
|
this.visible = fitRows(this.viewportRowsSource, this.todos.length, chrome, SCREEN_DEFAULT_ITEMS);
|
|
157
173
|
this.scrollTop = clampScroll(this.scrollTop, this.todos.length, this.visible);
|
|
158
174
|
|
|
159
175
|
const end = Math.min(this.todos.length, this.scrollTop + this.visible);
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
176
|
+
const body: string[] = [];
|
|
177
|
+
for (let i = this.scrollTop; i < end; i++) body.push(todoRow(this.todos[i], this.theme, w));
|
|
178
|
+
|
|
179
|
+
return dockListFrame({
|
|
180
|
+
header,
|
|
181
|
+
summary: clip(` ${paint(this.theme, "meta", progressSummary(this.todos))}`, w),
|
|
182
|
+
body,
|
|
183
|
+
range:
|
|
184
|
+
this.scrollTop > 0 || end < this.todos.length
|
|
185
|
+
? formatRange(this.theme, w, { start: this.scrollTop, end, total: this.todos.length })
|
|
186
|
+
: undefined,
|
|
187
|
+
hint: screenHint(this.theme, w, ["Esc close", "↑/↓ or k/j scroll", "PgUp/PgDn", "g/G or Home/End"]),
|
|
188
|
+
});
|
|
169
189
|
}
|
|
170
190
|
}
|
|
@@ -11,6 +11,12 @@ pi -e . # load the whole @gavin-hu/my-pi-agent pack
|
|
|
11
11
|
pi install ./ # install the package
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
+
## Tool rendering
|
|
15
|
+
|
|
16
|
+
How the `watch` call and its result look in the transcript — the call line as a headline and the body indented under it:
|
|
17
|
+
|
|
18
|
+
<img src="../../assets/tools/watch.png" alt="The watch tool's result: the call line and an indented body with a running file watcher and an exited command watcher" width="100%">
|
|
19
|
+
|
|
14
20
|
## What it does
|
|
15
21
|
|
|
16
22
|
- Registers one model-callable tool, `watch`, with actions `add`, `list`,
|
|
@@ -85,9 +91,11 @@ and the command's `pid`/`exitCode`/`signal` when applicable.
|
|
|
85
91
|
|
|
86
92
|
### Transcript
|
|
87
93
|
|
|
88
|
-
`add` and `stop` render one outcome line; `list` renders
|
|
89
|
-
capped at eight rows
|
|
90
|
-
|
|
94
|
+
`add` and `stop` render one outcome line; `list` renders the `/watch` rows
|
|
95
|
+
capped at eight rows, minus the selection-marker column (`listRow`'s
|
|
96
|
+
`marker: false`) that only the interactive screen needs; `matches` and `logs`
|
|
97
|
+
render the tail of the sanitized lines, with the hidden count noted above and
|
|
98
|
+
`(ctrl+o to expand)`.
|
|
91
99
|
|
|
92
100
|
### Command: /watch
|
|
93
101
|
|
|
@@ -172,8 +180,8 @@ other.
|
|
|
172
180
|
| Annotations | `readOnlyHint: false`, `destructiveHint: true`, `idempotentHint: false`, `openWorldHint: true`. |
|
|
173
181
|
| `outputSchema` | None; the result carries `details` only (no `structuredContent` mirror). |
|
|
174
182
|
| State storage | In-memory runtime closure; no registry, no `getBranch()` rebuild. |
|
|
175
|
-
| Injected context | A
|
|
176
|
-
| Lifecycle | `session_start` loads config and re-arms `onMatch`; `session_tree` repaints; `agent_start`/`agent_settled` track busy; `before_agent_start`
|
|
183
|
+
| Injected context | A shared transcript notice (`my-pi-agent/notice`), rendered by bytefly; pending matches are read from the runtime, never from the transcript. |
|
|
184
|
+
| Lifecycle | `session_start` loads config and re-arms `onMatch`; `session_tree` repaints; `agent_start`/`agent_settled` track busy; `before_agent_start` posts pending matches as a notice; `context` prunes stale notices of its own source; `session_shutdown` kills commands and clears chips. |
|
|
177
185
|
|
|
178
186
|
`watch` is gated by `PI_DISABLED_EXTENSIONS` like every extension.
|
|
179
187
|
|
|
@@ -193,15 +201,15 @@ turn — so a match is never dropped when the wake window is busy.
|
|
|
193
201
|
|
|
194
202
|
| Module | Responsibility |
|
|
195
203
|
|---|---|
|
|
196
|
-
| `index.ts` | Extension factory: registers the tool and command, wires the lifecycle and
|
|
204
|
+
| `index.ts` | Extension factory: registers the tool and command, wires the lifecycle and the shared notices. |
|
|
197
205
|
| `tools.ts` | The `watch` tool definition, execution, and transcript rendering. |
|
|
198
206
|
| `schema.ts` | TypeBox parameters and pure `normalizeCall` validation. |
|
|
199
207
|
| `types.ts` | `WatchAction`/`WatchStatus`, `WatchRecord`, `WatchDetails`, `PendingMatch`. |
|
|
200
208
|
| `runtime.ts` | The session-scoped watcher table, matcher, poll tick, and shutdown. |
|
|
201
209
|
| `process.ts` | Injectable spawn and process-tree kill. |
|
|
202
210
|
| `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. |
|
|
211
|
+
| `ui.ts` | Status chips and the match alert, over the shared `lib/tui/status-controller.ts`. |
|
|
212
|
+
| `tui.ts` | `WatchResult` transcript block and the `/watch` dock screen, drawn from the `lib/tui/` kit (`titleRule`, `listRow`, `emptyState`, `IdCursor`, `LinePane`). |
|
|
205
213
|
| `commands.ts` | The `/watch` command and its completions. |
|
|
206
214
|
| `config.ts` | The `watch` config section load and normalization. |
|
|
207
215
|
|
|
@@ -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, SEPARATORS } 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", SEPARATORS.item);
|
|
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
|
|
|
@@ -3,19 +3,20 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Registers the `watch` tool and `/watch` command. Watchers read a file or a
|
|
5
5
|
* command's merged output, match each new line against a regular expression,
|
|
6
|
-
* and alert the user; matching lines are reported to the model
|
|
7
|
-
* turn, or wake a turn immediately when the
|
|
8
|
-
* are session-only and stopped on shutdown.
|
|
6
|
+
* and alert the user; matching lines are reported to the model — and shown as a
|
|
7
|
+
* bytefly notice — at the next turn, or wake a turn immediately when the
|
|
8
|
+
* watcher opted into `wake`. Watchers are session-only and stopped on shutdown.
|
|
9
9
|
*
|
|
10
10
|
* Load with: pi --extension ./extensions/watch
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
14
13
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
15
14
|
import { isExtensionEnabled } from "../../lib/env.ts";
|
|
16
|
-
import {
|
|
15
|
+
import { sanitizeLine } from "../../lib/format.ts";
|
|
16
|
+
import { noticeMessage, type NoticeMessage, pruneStaleNotices } from "../../lib/notice.ts";
|
|
17
|
+
import { STATUS_KEYS } from "../../lib/tui/glyphs.ts";
|
|
17
18
|
import { registerCommands } from "./commands.ts";
|
|
18
|
-
import { formatPendingMatch } from "./format.ts";
|
|
19
|
+
import { clipLabel, formatPendingMatch } from "./format.ts";
|
|
19
20
|
import { createWatchRuntime, type WatchRuntime } from "./runtime.ts";
|
|
20
21
|
import { registerTools } from "./tools.ts";
|
|
21
22
|
import type { PendingMatch } from "./types.ts";
|
|
@@ -23,13 +24,10 @@ import type { PendingMatch } from "./types.ts";
|
|
|
23
24
|
/** Marker embedded in the injected match context. */
|
|
24
25
|
const WATCH_CONTEXT_MARKER = "[WATCH MATCHES]";
|
|
25
26
|
|
|
26
|
-
/** Custom-entry type used for the injected match message. */
|
|
27
|
-
export const WATCH_CONTEXT_TYPE = "watch-context";
|
|
28
|
-
|
|
29
27
|
/** At most this many match lines are injected in one message. */
|
|
30
28
|
const MAX_CONTEXT_MATCHES = 20;
|
|
31
29
|
|
|
32
|
-
/** Build the
|
|
30
|
+
/** Build the model-facing text for the pending-match notice. */
|
|
33
31
|
export function buildWatchContext(matches: PendingMatch[]): string {
|
|
34
32
|
const shown = matches.slice(0, MAX_CONTEXT_MATCHES);
|
|
35
33
|
const lines = shown.map(formatPendingMatch);
|
|
@@ -38,8 +36,21 @@ export function buildWatchContext(matches: PendingMatch[]): string {
|
|
|
38
36
|
return `${WATCH_CONTEXT_MARKER}\n${matches.length} watched line${matches.length === 1 ? "" : "s"} matched:\n${lines.join("\n")}${suffix}\nRead more with \`watch matches <id>\`.`;
|
|
39
37
|
}
|
|
40
38
|
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
/** The shared notice for pending matches: model text plus renderer details. */
|
|
40
|
+
function watchNotice(matches: PendingMatch[]): NoticeMessage {
|
|
41
|
+
const count = matches.length;
|
|
42
|
+
return noticeMessage({
|
|
43
|
+
content: buildWatchContext(matches),
|
|
44
|
+
details: {
|
|
45
|
+
source: "watch",
|
|
46
|
+
title: `${count} watched line${count === 1 ? "" : "s"} matched`,
|
|
47
|
+
items: matches.slice(0, MAX_CONTEXT_MATCHES).map((match) => ({
|
|
48
|
+
label: `${match.watcherId} ${clipLabel(match.label)}`,
|
|
49
|
+
detail: sanitizeLine(match.line),
|
|
50
|
+
severity: "warn",
|
|
51
|
+
})),
|
|
52
|
+
},
|
|
53
|
+
});
|
|
43
54
|
}
|
|
44
55
|
|
|
45
56
|
export interface WatchDeps {
|
|
@@ -64,10 +75,7 @@ export default function watch(pi: ExtensionAPI, deps: WatchDeps = {}): void {
|
|
|
64
75
|
const pending = runtime.pendingMatches();
|
|
65
76
|
if (pending.length === 0) return;
|
|
66
77
|
try {
|
|
67
|
-
pi.sendMessage(
|
|
68
|
-
{ customType: WATCH_CONTEXT_TYPE, content: buildWatchContext(pending), display: false },
|
|
69
|
-
{ triggerTurn: true },
|
|
70
|
-
);
|
|
78
|
+
pi.sendMessage(watchNotice(pending), { triggerTurn: true });
|
|
71
79
|
runtime.markReported(pending);
|
|
72
80
|
waking = true;
|
|
73
81
|
} catch {
|
|
@@ -103,17 +111,14 @@ export default function watch(pi: ExtensionAPI, deps: WatchDeps = {}): void {
|
|
|
103
111
|
if (pending.length === 0) return undefined;
|
|
104
112
|
runtime.markReported(pending);
|
|
105
113
|
runtime.setStatus(ctx);
|
|
106
|
-
return {
|
|
107
|
-
message: { customType: WATCH_CONTEXT_TYPE, content: buildWatchContext(pending), display: false },
|
|
108
|
-
};
|
|
114
|
+
return { message: watchNotice(pending) };
|
|
109
115
|
});
|
|
110
116
|
|
|
111
|
-
// Keep stale match
|
|
117
|
+
// Keep stale match notices out of later turns. Only this extension's notices
|
|
118
|
+
// are pruned; another source's are left for its own handler.
|
|
112
119
|
pi.on("context", (event) => {
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
const last = contexts[contexts.length - 1];
|
|
116
|
-
return { messages: event.messages.filter((message) => !isWatchContext(message) || message === last) };
|
|
120
|
+
const pruned = pruneStaleNotices(event.messages, "watch");
|
|
121
|
+
return pruned ? { messages: pruned } : undefined;
|
|
117
122
|
});
|
|
118
123
|
|
|
119
124
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
|
|
10
10
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import { sanitizeLine } from "../../lib/format.ts";
|
|
12
|
-
import { reuseText, toolDetails } from "../../lib/tui.ts";
|
|
12
|
+
import { reuseText, toolDetails } from "../../lib/tui/chrome.ts";
|
|
13
|
+
import { SEPARATORS } from "../../lib/tui/glyphs.ts";
|
|
14
|
+
import { paint } from "../../lib/tui/tokens.ts";
|
|
13
15
|
import {
|
|
14
16
|
compareWatches,
|
|
15
17
|
formatCallText,
|
|
@@ -192,8 +194,9 @@ export function registerTools(pi: ExtensionAPI, runtime: WatchRuntime): void {
|
|
|
192
194
|
const text = reuseText(context.lastComponent);
|
|
193
195
|
text.setText(
|
|
194
196
|
theme.fg("toolTitle", theme.bold(`${TOOL_NAME} `)) +
|
|
195
|
-
|
|
196
|
-
|
|
197
|
+
paint(
|
|
198
|
+
theme,
|
|
199
|
+
"meta",
|
|
197
200
|
sanitizeLine(formatCallText(action, args as Record<string, unknown>, context.argsComplete)),
|
|
198
201
|
),
|
|
199
202
|
);
|
|
@@ -209,13 +212,13 @@ export function registerTools(pi: ExtensionAPI, runtime: WatchRuntime): void {
|
|
|
209
212
|
return text;
|
|
210
213
|
}
|
|
211
214
|
if (details.error) {
|
|
212
|
-
text.setText(theme
|
|
215
|
+
text.setText(paint(theme, "bad", `Error: ${sanitizeLine(details.error)}`));
|
|
213
216
|
return text;
|
|
214
217
|
}
|
|
215
218
|
|
|
216
219
|
if (details.action === "list" && details.watches) {
|
|
217
220
|
if (details.watches.length === 0) {
|
|
218
|
-
text.setText(theme
|
|
221
|
+
text.setText(paint(theme, "meta", "No watchers."));
|
|
219
222
|
return text;
|
|
220
223
|
}
|
|
221
224
|
const ordered = [...details.watches].sort(compareWatches);
|
|
@@ -232,7 +235,7 @@ export function registerTools(pi: ExtensionAPI, runtime: WatchRuntime): void {
|
|
|
232
235
|
const all = raw === "" ? [] : raw.split("\n");
|
|
233
236
|
const label = details.action === "matches" ? "matches" : "output";
|
|
234
237
|
if (all.length === 0) {
|
|
235
|
-
text.setText(theme
|
|
238
|
+
text.setText(paint(theme, "meta", `No ${label} yet.`));
|
|
236
239
|
return text;
|
|
237
240
|
}
|
|
238
241
|
const shownLines = options.expanded ? all : all.slice(Math.max(0, all.length - RESULT_ROWS));
|
|
@@ -251,7 +254,11 @@ export function registerTools(pi: ExtensionAPI, runtime: WatchRuntime): void {
|
|
|
251
254
|
if (details.action === "clear") {
|
|
252
255
|
const count = details.cleared ?? 0;
|
|
253
256
|
text.setText(
|
|
254
|
-
|
|
257
|
+
paint(
|
|
258
|
+
theme,
|
|
259
|
+
"meta",
|
|
260
|
+
count === 0 ? "Nothing to clear." : `Cleared ${count} watcher${count === 1 ? "" : "s"}.`,
|
|
261
|
+
),
|
|
255
262
|
);
|
|
256
263
|
return text;
|
|
257
264
|
}
|
|
@@ -259,12 +266,12 @@ export function registerTools(pi: ExtensionAPI, runtime: WatchRuntime): void {
|
|
|
259
266
|
if (details.watch) {
|
|
260
267
|
let line = formatWatchOutcomeLine(details.watch, theme);
|
|
261
268
|
if (details.action === "stop") {
|
|
262
|
-
line += theme
|
|
269
|
+
line += paint(theme, "warn", details.stopped ? " · stopped" : " · already stopped");
|
|
263
270
|
} else if (details.action === "add") {
|
|
264
271
|
const badges: string[] = [];
|
|
265
272
|
if (details.watch.wake) badges.push("wake");
|
|
266
273
|
if (details.watch.fromStart) badges.push("from start");
|
|
267
|
-
if (badges.length > 0) line += theme
|
|
274
|
+
if (badges.length > 0) line += paint(theme, "faint", `${SEPARATORS.item}${badges.join(SEPARATORS.item)}`);
|
|
268
275
|
}
|
|
269
276
|
text.setText(line);
|
|
270
277
|
return text;
|