@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
package/extensions/watch/tui.ts
CHANGED
|
@@ -3,48 +3,53 @@
|
|
|
3
3
|
*
|
|
4
4
|
* `WatchListComponent` is the dismissible `/watch` screen with a focused detail
|
|
5
5
|
* pane and a matches view. It reads live state through injected closures and
|
|
6
|
-
* tracks the selection by id so a live re-sort cannot move the cursor
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* tracks the selection by id so a live re-sort cannot move the cursor, through
|
|
7
|
+
* the shared `IdCursor` and `LinePane`.
|
|
8
|
+
*
|
|
9
|
+
* The screen is drawn from the design system: a label-first `titleRule`, one
|
|
10
|
+
* `listRow` grammar, and `emptyState`. Matched lines arrive already sanitized by
|
|
11
|
+
* the runtime, so no untrusted escape sequence reaches the terminal here.
|
|
9
12
|
*/
|
|
10
13
|
|
|
11
14
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
} from "
|
|
21
|
-
import {
|
|
22
|
-
clampScroll,
|
|
23
|
-
fitRows,
|
|
24
|
-
formatRange,
|
|
25
|
-
isCloseKey,
|
|
26
|
-
keepVisible,
|
|
27
|
-
navIntent,
|
|
28
|
-
navigateScroll,
|
|
29
|
-
selectionMarker,
|
|
30
|
-
wheelDelta,
|
|
31
|
-
} from "../../lib/list-cursor.ts";
|
|
32
|
-
import { screenHeader, screenHint, type ViewportRowsSource } from "../../lib/tui.ts";
|
|
33
|
-
import { expandHint } from "../../lib/ui.ts";
|
|
34
|
-
import { compareWatches, formatWatchDetail, shortLabel, statusGlyph } from "./format.ts";
|
|
15
|
+
import { Key, matchesKey, type Component, type TuiMouseEvent, type TuiMouseEventResult } from "@earendil-works/pi-tui";
|
|
16
|
+
import { dockListFrame, screenHint, type ViewportRowsSource } from "../../lib/tui/chrome.ts";
|
|
17
|
+
import { clip, fitCells } from "../../lib/tui/columns.ts";
|
|
18
|
+
import { fitRows, formatRange, IdCursor, isCloseKey, navIntent, wheelDelta } from "../../lib/tui/cursor.ts";
|
|
19
|
+
import { expandHint, SEPARATORS } from "../../lib/tui/glyphs.ts";
|
|
20
|
+
import { LinePane } from "../../lib/tui/line-pane.ts";
|
|
21
|
+
import { emptyState, statusRow, type StatusRow } from "../../lib/tui/rows.ts";
|
|
22
|
+
import { titleRule } from "../../lib/tui/rules.ts";
|
|
23
|
+
import { paint } from "../../lib/tui/tokens.ts";
|
|
24
|
+
import { compareWatches, formatWatchDetail, shortLabel, statusGlyphChar, statusRole } from "./format.ts";
|
|
35
25
|
import type { WatchRecord } from "./types.ts";
|
|
36
26
|
|
|
37
27
|
/** Rows the `/watch` screen body shows when the terminal height is unknown. */
|
|
38
28
|
const SCREEN_DEFAULT_ROWS = 14;
|
|
39
29
|
|
|
40
|
-
/**
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
30
|
+
/** Fixed rows around the list body: header, summary, blank, blank, hint, blank. */
|
|
31
|
+
const LIST_CHROME_ROWS = 7;
|
|
32
|
+
|
|
33
|
+
/** Fixed rows around the matches body: header, subtitle, blank, blank, hint, blank. */
|
|
34
|
+
const LINES_CHROME_ROWS = 6;
|
|
35
|
+
|
|
36
|
+
/** A watcher's stable identity, for the id-anchored cursor. */
|
|
37
|
+
const watchId = (watch: WatchRecord): string => watch.id;
|
|
38
|
+
|
|
39
|
+
/** One watcher row: marker, status glyph, id, label, and a right-aligned count. */
|
|
40
|
+
function watchRow(
|
|
41
|
+
watch: WatchRecord,
|
|
42
|
+
theme: Theme,
|
|
43
|
+
width: number,
|
|
44
|
+
options: Pick<StatusRow, "selected" | "marker"> = {},
|
|
45
|
+
): string {
|
|
46
|
+
return statusRow(theme, width, {
|
|
47
|
+
...options,
|
|
48
|
+
glyph: statusGlyphChar(watch.status),
|
|
49
|
+
glyphRole: statusRole(watch.status),
|
|
50
|
+
label: `${watch.id} ${shortLabel(watch)}`,
|
|
51
|
+
trailing: `${watch.matchCount}`,
|
|
52
|
+
});
|
|
48
53
|
}
|
|
49
54
|
|
|
50
55
|
/** Input for the transcript result: a themed watcher list or a matched-line tail. */
|
|
@@ -79,24 +84,18 @@ export class WatchResult implements Component {
|
|
|
79
84
|
// A leading blank line separates the result from the call header.
|
|
80
85
|
const lines: string[] = [""];
|
|
81
86
|
if (this.input.kind === "list") {
|
|
82
|
-
for (const watch of this.input.watches) lines.push(watchRow(watch, this.theme, w));
|
|
87
|
+
for (const watch of this.input.watches) lines.push(watchRow(watch, this.theme, w, { marker: false }));
|
|
83
88
|
if (this.input.more > 0) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
);
|
|
89
|
+
const note = `${paint(this.theme, "faint", `… ${this.input.more} more`)} ${expandHint(this.theme)}`;
|
|
90
|
+
lines.push(clip(` ${note}`, w));
|
|
87
91
|
}
|
|
88
92
|
} else {
|
|
89
93
|
if (this.input.earlier > 0) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
`${this.theme.fg("dim", `… ${this.input.earlier} earlier ${this.input.label}`)} ${expandHint(this.theme)}`,
|
|
93
|
-
w,
|
|
94
|
-
"…",
|
|
95
|
-
),
|
|
96
|
-
);
|
|
94
|
+
const note = `${paint(this.theme, "faint", `… ${this.input.earlier} earlier ${this.input.label}`)} ${expandHint(this.theme)}`;
|
|
95
|
+
lines.push(clip(note, w));
|
|
97
96
|
}
|
|
98
97
|
for (const line of this.input.lines) {
|
|
99
|
-
lines.push(this.theme.fg("toolOutput",
|
|
98
|
+
lines.push(this.theme.fg("toolOutput", clip(line, w)));
|
|
100
99
|
}
|
|
101
100
|
}
|
|
102
101
|
return lines;
|
|
@@ -116,19 +115,12 @@ type PendingConfirm = { kind: "stop"; id: string; label: string } | { kind: "cle
|
|
|
116
115
|
/** Dismissible `/watch` screen: a selectable list with a focus pane and a matches view. */
|
|
117
116
|
export class WatchListComponent implements Component {
|
|
118
117
|
private mode: "list" | "lines" = "list";
|
|
119
|
-
/**
|
|
120
|
-
private
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
private lineWatchId: string | undefined;
|
|
124
|
-
private lineScroll = 0;
|
|
125
|
-
private lineLines: string[] = [];
|
|
126
|
-
private lineTitle = "";
|
|
127
|
-
private lineMore = false;
|
|
128
|
-
private follow = true;
|
|
118
|
+
/** Focus anchored to a watcher id; the list is re-sorted from live state each render. */
|
|
119
|
+
private readonly cursor = new IdCursor();
|
|
120
|
+
/** The open matches tail. */
|
|
121
|
+
private readonly pane = new LinePane();
|
|
129
122
|
private confirm: PendingConfirm | undefined;
|
|
130
123
|
private notice: string | undefined;
|
|
131
|
-
private visible = SCREEN_DEFAULT_ROWS;
|
|
132
124
|
|
|
133
125
|
constructor(
|
|
134
126
|
private readonly watches: () => WatchRecord[],
|
|
@@ -143,81 +135,43 @@ export class WatchListComponent implements Component {
|
|
|
143
135
|
|
|
144
136
|
/** Replace the matches pane contents (called by the command's poll loop). */
|
|
145
137
|
setLines(title: string, lines: string[], meta: { more?: boolean } = {}): void {
|
|
146
|
-
|
|
147
|
-
const linesChanged = !this.sameLines(lines);
|
|
148
|
-
const moreChanged = meta.more !== undefined && meta.more !== this.lineMore;
|
|
149
|
-
this.lineTitle = title;
|
|
150
|
-
this.lineLines = lines;
|
|
151
|
-
if (meta.more !== undefined) this.lineMore = meta.more;
|
|
152
|
-
if (this.follow) this.lineScroll = this.maxLineScroll;
|
|
153
|
-
else this.lineScroll = Math.min(this.lineScroll, this.maxLineScroll);
|
|
154
|
-
if (titleChanged || linesChanged || moreChanged) this.requestRender();
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
private sameLines(next: string[]): boolean {
|
|
158
|
-
if (next.length !== this.lineLines.length) return false;
|
|
159
|
-
for (let i = 0; i < next.length; i++) if (next[i] !== this.lineLines[i]) return false;
|
|
160
|
-
return true;
|
|
138
|
+
if (this.pane.update({ title, lines, more: meta.more })) this.requestRender();
|
|
161
139
|
}
|
|
162
140
|
|
|
163
141
|
/** The watcher whose matches pane is open, or undefined in list mode. */
|
|
164
142
|
currentLineId(): string | undefined {
|
|
165
|
-
return this.mode === "lines" ? this.
|
|
143
|
+
return this.mode === "lines" && this.pane.id ? this.pane.id : undefined;
|
|
166
144
|
}
|
|
167
145
|
|
|
168
146
|
/** Re-read the open matches pane from the runtime. */
|
|
169
147
|
refreshLines(id: string): void {
|
|
170
148
|
const result = this.callbacks.lines(id);
|
|
171
|
-
if (result
|
|
149
|
+
if (result && this.pane.update({ lines: result.lines, more: result.more })) this.requestRender();
|
|
172
150
|
}
|
|
173
151
|
|
|
174
152
|
private get ordered(): WatchRecord[] {
|
|
175
153
|
return [...this.watches()].sort(compareWatches);
|
|
176
154
|
}
|
|
177
155
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const index = this.selectedId ? ordered.findIndex((watch) => watch.id === this.selectedId) : -1;
|
|
181
|
-
return index >= 0 ? index : 0;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
private setSelection(next: number): void {
|
|
156
|
+
/** Focus watcher `next` (clamped) and keep it visible. */
|
|
157
|
+
private select(next: number): void {
|
|
185
158
|
const ordered = this.ordered;
|
|
186
159
|
if (ordered.length === 0) return;
|
|
187
|
-
|
|
188
|
-
this.selectedId = ordered[index].id;
|
|
189
|
-
this.listScroll = Math.max(0, keepVisible(this.listScroll, index, this.visible));
|
|
160
|
+
this.cursor.select(ordered, watchId, next);
|
|
190
161
|
this.requestRender();
|
|
191
162
|
}
|
|
192
163
|
|
|
193
|
-
private clampSelection(): void {
|
|
194
|
-
const ordered = this.ordered;
|
|
195
|
-
if (ordered.length === 0) {
|
|
196
|
-
this.selectedId = undefined;
|
|
197
|
-
this.listScroll = 0;
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
this.selectedId = ordered[this.indexOfSelected(ordered)].id;
|
|
201
|
-
this.listScroll = clampScroll(this.listScroll, ordered.length, this.visible);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
private get maxLineScroll(): number {
|
|
205
|
-
return Math.max(0, this.lineLines.length - this.visible);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
164
|
private openLines(): void {
|
|
209
|
-
const watch = this.ordered[this.
|
|
165
|
+
const watch = this.ordered[this.cursor.indexOf(this.ordered, watchId)];
|
|
210
166
|
if (!watch) return;
|
|
211
167
|
this.mode = "lines";
|
|
212
|
-
this.lineWatchId = watch.id;
|
|
213
|
-
this.lineScroll = 0;
|
|
214
|
-
this.follow = true;
|
|
215
|
-
this.lineLines = [];
|
|
216
|
-
this.lineTitle = `${watch.id} ${shortLabel(watch)}`;
|
|
217
|
-
this.lineMore = false;
|
|
218
168
|
const result = this.callbacks.lines(watch.id);
|
|
219
|
-
|
|
220
|
-
|
|
169
|
+
this.pane.open(watch.id, {
|
|
170
|
+
title: `${watch.id} ${shortLabel(watch)}`,
|
|
171
|
+
lines: result?.lines ?? [],
|
|
172
|
+
more: result?.more ?? false,
|
|
173
|
+
});
|
|
174
|
+
this.requestRender();
|
|
221
175
|
}
|
|
222
176
|
|
|
223
177
|
handleInput(data: string): void {
|
|
@@ -233,8 +187,12 @@ export class WatchListComponent implements Component {
|
|
|
233
187
|
const delta = wheelDelta(event);
|
|
234
188
|
if (delta === undefined) return undefined;
|
|
235
189
|
if (this.confirm) return { handled: true };
|
|
236
|
-
if (this.mode === "list")
|
|
237
|
-
|
|
190
|
+
if (this.mode === "list") {
|
|
191
|
+
this.select(this.cursor.indexOf(this.ordered, watchId) + delta);
|
|
192
|
+
} else {
|
|
193
|
+
this.pane.setScroll(this.pane.scroll + delta);
|
|
194
|
+
this.requestRender();
|
|
195
|
+
}
|
|
238
196
|
return { handled: true };
|
|
239
197
|
}
|
|
240
198
|
|
|
@@ -248,25 +206,25 @@ export class WatchListComponent implements Component {
|
|
|
248
206
|
this.requestRender();
|
|
249
207
|
}
|
|
250
208
|
const ordered = this.ordered;
|
|
251
|
-
const index = this.
|
|
209
|
+
const index = this.cursor.indexOf(ordered, watchId);
|
|
252
210
|
switch (navIntent(data)) {
|
|
253
211
|
case "up":
|
|
254
|
-
this.
|
|
212
|
+
this.select(index - 1);
|
|
255
213
|
return;
|
|
256
214
|
case "down":
|
|
257
|
-
this.
|
|
215
|
+
this.select(index + 1);
|
|
258
216
|
return;
|
|
259
217
|
case "pageUp":
|
|
260
|
-
this.
|
|
218
|
+
this.select(index - this.cursor.visible);
|
|
261
219
|
return;
|
|
262
220
|
case "pageDown":
|
|
263
|
-
this.
|
|
221
|
+
this.select(index + this.cursor.visible);
|
|
264
222
|
return;
|
|
265
223
|
case "home":
|
|
266
|
-
this.
|
|
224
|
+
this.select(0);
|
|
267
225
|
return;
|
|
268
226
|
case "end":
|
|
269
|
-
this.
|
|
227
|
+
this.select(ordered.length - 1);
|
|
270
228
|
return;
|
|
271
229
|
}
|
|
272
230
|
if (matchesKey(data, Key.enter) || data === "l") {
|
|
@@ -295,7 +253,7 @@ export class WatchListComponent implements Component {
|
|
|
295
253
|
this.confirm = undefined;
|
|
296
254
|
if (pending.kind === "stop") this.callbacks.stop(pending.id);
|
|
297
255
|
else this.callbacks.clear();
|
|
298
|
-
this.
|
|
256
|
+
this.cursor.clamp(this.ordered, watchId);
|
|
299
257
|
this.requestRender();
|
|
300
258
|
return;
|
|
301
259
|
}
|
|
@@ -312,36 +270,24 @@ export class WatchListComponent implements Component {
|
|
|
312
270
|
return `Clear ${pending.count} stopped watcher${pending.count === 1 ? "" : "s"}? y/N`;
|
|
313
271
|
}
|
|
314
272
|
|
|
315
|
-
private setLineScroll(next: number): void {
|
|
316
|
-
this.lineScroll = clampScroll(next, this.lineLines.length, this.visible);
|
|
317
|
-
this.follow = this.lineScroll >= this.maxLineScroll;
|
|
318
|
-
this.requestRender();
|
|
319
|
-
}
|
|
320
|
-
|
|
321
273
|
private handleLinesInput(data: string): void {
|
|
322
274
|
if (matchesKey(data, Key.escape) || matchesKey(data, Key.backspace) || data === "q") {
|
|
323
275
|
this.mode = "list";
|
|
324
|
-
this.
|
|
276
|
+
this.pane.close();
|
|
325
277
|
this.requestRender();
|
|
326
278
|
return;
|
|
327
279
|
}
|
|
328
|
-
|
|
329
|
-
if (next !== undefined) {
|
|
330
|
-
this.setLineScroll(next);
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
if (data === "g") this.setLineScroll(0);
|
|
334
|
-
else if (data === "G") this.setLineScroll(this.maxLineScroll);
|
|
280
|
+
if (this.pane.handleKey(data)) this.requestRender();
|
|
335
281
|
}
|
|
336
282
|
|
|
337
283
|
private summary(watches: WatchRecord[], width: number): string {
|
|
338
|
-
const
|
|
339
|
-
let text = this.theme.fg("muted", `${watches.length} watcher${watches.length === 1 ? "" : "s"}`);
|
|
284
|
+
const cells = [paint(this.theme, "meta", `${watches.length} watcher${watches.length === 1 ? "" : "s"}`)];
|
|
340
285
|
const running = watches.filter((watch) => watch.status === "running").length;
|
|
341
286
|
const matched = watches.reduce((sum, watch) => sum + watch.matchCount, 0);
|
|
342
|
-
if (running > 0)
|
|
343
|
-
if (matched > 0)
|
|
344
|
-
|
|
287
|
+
if (running > 0) cells.push(paint(this.theme, "meta", `${running} running`));
|
|
288
|
+
if (matched > 0) cells.push(paint(this.theme, "accent", `${matched} matches`));
|
|
289
|
+
const separator = paint(this.theme, "meta", SEPARATORS.item);
|
|
290
|
+
return clip(` ${fitCells(cells, Math.max(1, width - 2), separator)}`, width);
|
|
345
291
|
}
|
|
346
292
|
|
|
347
293
|
render(width: number): string[] {
|
|
@@ -351,77 +297,77 @@ export class WatchListComponent implements Component {
|
|
|
351
297
|
|
|
352
298
|
private renderList(w: number): string[] {
|
|
353
299
|
const watches = this.ordered;
|
|
354
|
-
this.
|
|
355
|
-
const index = this.
|
|
300
|
+
this.cursor.clamp(watches, watchId);
|
|
301
|
+
const index = this.cursor.indexOf(watches, watchId);
|
|
356
302
|
const focused = watches[index];
|
|
357
303
|
const detail = focused ? formatWatchDetail(focused, Date.now()) : [];
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
304
|
+
this.cursor.visible = fitRows(
|
|
305
|
+
this.viewportRowsSource,
|
|
306
|
+
watches.length,
|
|
307
|
+
LIST_CHROME_ROWS + detail.length,
|
|
308
|
+
SCREEN_DEFAULT_ROWS,
|
|
309
|
+
);
|
|
310
|
+
this.cursor.clamp(watches, watchId);
|
|
362
311
|
|
|
363
|
-
const
|
|
312
|
+
const body: string[] = [];
|
|
313
|
+
let range: string | undefined;
|
|
364
314
|
if (watches.length === 0) {
|
|
365
|
-
|
|
315
|
+
body.push(...emptyState(this.theme, w, { label: "No watchers." }));
|
|
366
316
|
} else {
|
|
367
|
-
const end = Math.min(watches.length, this.
|
|
368
|
-
for (let i = this.
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
lines.push(formatRange(this.theme, w, { start: this.listScroll, end, total: watches.length }));
|
|
317
|
+
const end = Math.min(watches.length, this.cursor.scroll + this.cursor.visible);
|
|
318
|
+
for (let i = this.cursor.scroll; i < end; i++)
|
|
319
|
+
body.push(watchRow(watches[i], this.theme, w, { selected: i === index }));
|
|
320
|
+
if (this.cursor.scroll > 0 || end < watches.length) {
|
|
321
|
+
range = formatRange(this.theme, w, { start: this.cursor.scroll, end, total: watches.length });
|
|
373
322
|
}
|
|
374
323
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
324
|
+
|
|
325
|
+
const hint = this.confirm
|
|
326
|
+
? clip(` ${paint(this.theme, "warn", this.confirmPrompt())}`, w)
|
|
327
|
+
: this.notice
|
|
328
|
+
? clip(` ${paint(this.theme, "meta", this.notice)}`, w)
|
|
329
|
+
: screenHint(this.theme, w, [
|
|
330
|
+
"Esc close",
|
|
331
|
+
"↑/↓ or j/k select",
|
|
332
|
+
"Enter or l matches",
|
|
333
|
+
"x or K stop",
|
|
334
|
+
"d clear stopped",
|
|
335
|
+
"PgUp/PgDn",
|
|
336
|
+
]);
|
|
337
|
+
|
|
338
|
+
return dockListFrame({
|
|
339
|
+
header: titleRule(this.theme, w, "Watch"),
|
|
340
|
+
summary: this.summary(watches, w),
|
|
341
|
+
body,
|
|
342
|
+
range,
|
|
343
|
+
// The detail pane is separated from the list by a blank line.
|
|
344
|
+
detail: ["", ...detail.map((line) => clip(` ${paint(this.theme, "meta", line)}`, w))],
|
|
345
|
+
hint,
|
|
346
|
+
});
|
|
394
347
|
}
|
|
395
348
|
|
|
396
349
|
private renderLines(w: number): string[] {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
const parts: string[] = [];
|
|
405
|
-
if (this.lineMore && this.lineLines.length > 0) parts.push(`showing last ${this.lineLines.length} lines`);
|
|
406
|
-
if (this.lineLines.length > 0) parts.push(this.follow ? "following" : "paused");
|
|
407
|
-
const titleLine = truncateToWidth(` ${this.theme.fg("dim", parts.join(" · "))}`, w);
|
|
408
|
-
const lines: string[] = [screenHeader(this.theme, w, label), titleLine, ""];
|
|
409
|
-
if (this.lineLines.length === 0) {
|
|
410
|
-
lines.push(truncateToWidth(` ${this.theme.fg("dim", "No matches yet.")}`, w));
|
|
350
|
+
const visible = fitRows(this.viewportRowsSource, this.pane.count, LINES_CHROME_ROWS, SCREEN_DEFAULT_ROWS);
|
|
351
|
+
this.pane.fit(visible);
|
|
352
|
+
|
|
353
|
+
const body: string[] = [];
|
|
354
|
+
let range: string | undefined;
|
|
355
|
+
if (this.pane.count === 0) {
|
|
356
|
+
body.push(...emptyState(this.theme, w, { label: "No matches yet." }));
|
|
411
357
|
} else {
|
|
412
|
-
const end =
|
|
413
|
-
for (let i =
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
if (this.lineScroll > 0 || end < this.lineLines.length) {
|
|
417
|
-
lines.push(
|
|
418
|
-
formatRange(this.theme, w, { start: this.lineScroll, end, total: this.lineLines.length, label: "line" }),
|
|
419
|
-
);
|
|
358
|
+
const { start, end } = this.pane.window();
|
|
359
|
+
for (let i = start; i < end; i++) body.push(clip(` ${this.pane.lines[i]}`, w));
|
|
360
|
+
if (start > 0 || end < this.pane.count) {
|
|
361
|
+
range = formatRange(this.theme, w, { start, end, total: this.pane.count, label: "line" });
|
|
420
362
|
}
|
|
421
363
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
364
|
+
|
|
365
|
+
return dockListFrame({
|
|
366
|
+
header: titleRule(this.theme, w, this.pane.title("Watch Matches")),
|
|
367
|
+
summary: clip(` ${paint(this.theme, "faint", this.pane.subtitleLine())}`, w),
|
|
368
|
+
body,
|
|
369
|
+
range,
|
|
370
|
+
hint: screenHint(this.theme, w, ["Esc back", "↑/↓ scroll", "g/G ends", "PgUp/PgDn"]),
|
|
371
|
+
});
|
|
426
372
|
}
|
|
427
373
|
}
|
package/extensions/watch/ui.ts
CHANGED
|
@@ -3,26 +3,19 @@
|
|
|
3
3
|
* alert.
|
|
4
4
|
*
|
|
5
5
|
* Kept separate from the runtime so the runtime owns lifecycle (the watcher
|
|
6
|
-
* table, the tail clock) and this owns "how watchers are shown".
|
|
7
|
-
* the
|
|
8
|
-
*
|
|
6
|
+
* table, the tail clock) and this owns "how watchers are shown". The chips come
|
|
7
|
+
* from the shared `lib/tui/status-controller.ts`, so only the vocabulary — the
|
|
8
|
+
* keys, glyphs, and counts — is watch-specific.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
12
|
-
import { GLYPHS, STATUS_KEYS } from "../../lib/
|
|
12
|
+
import { GLYPHS, STATUS_KEYS } from "../../lib/tui/glyphs.ts";
|
|
13
|
+
import { createStatusController, type StatusChip, type StatusController } from "../../lib/tui/status-controller.ts";
|
|
13
14
|
import type { WatchConfig } from "./config.ts";
|
|
14
15
|
import { shortLabel } from "./format.ts";
|
|
15
16
|
import type { WatchRecord } from "./types.ts";
|
|
16
17
|
|
|
17
|
-
export interface UiController {
|
|
18
|
-
/** Remember the live context; later calls use it when no context is passed. */
|
|
19
|
-
attach(ctx: ExtensionContext): void;
|
|
20
|
-
/** Forget the context, for example on shutdown or reload. */
|
|
21
|
-
detach(): void;
|
|
22
|
-
/** Repaint the chips using the attached context. */
|
|
23
|
-
paint(): void;
|
|
24
|
-
/** Publish the footer chips (and attach `ctx` when given). */
|
|
25
|
-
setStatus(ctx?: ExtensionContext): void;
|
|
18
|
+
export interface UiController extends StatusController {
|
|
26
19
|
/** Alert the user to a new match; called once per unreported burst. */
|
|
27
20
|
notifyMatch(watch: WatchRecord, line: string): void;
|
|
28
21
|
}
|
|
@@ -35,41 +28,21 @@ export interface UiOptions {
|
|
|
35
28
|
|
|
36
29
|
export function createUiController(options: UiOptions): UiController {
|
|
37
30
|
const { getWatches, getUnseenMatches, getConfig } = options;
|
|
31
|
+
// The match alert also needs the context, so it is mirrored here.
|
|
38
32
|
let ctx: ExtensionContext | undefined;
|
|
39
33
|
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return text;
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const setStatus = (next?: ExtensionContext): void => {
|
|
49
|
-
if (next) ctx = next;
|
|
50
|
-
const target = ctx;
|
|
51
|
-
if (!target) return;
|
|
52
|
-
try {
|
|
53
|
-
if (!getConfig().showStatus) {
|
|
54
|
-
target.ui.setStatus(STATUS_KEYS.watch, undefined);
|
|
55
|
-
target.ui.setStatus(STATUS_KEYS.watchMatches, undefined);
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
34
|
+
const controller = createStatusController({
|
|
35
|
+
keys: [STATUS_KEYS.watch, STATUS_KEYS.watchMatches],
|
|
36
|
+
enabled: () => getConfig().showStatus,
|
|
37
|
+
chips: (): StatusChip[] => {
|
|
58
38
|
let running = 0;
|
|
59
39
|
for (const watch of getWatches()) if (watch.status === "running") running++;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
STATUS_KEYS.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
STATUS_KEYS.watchMatches,
|
|
67
|
-
unseen > 0 ? theme(target, "error", `${GLYPHS.warn} ${unseen}`) : undefined,
|
|
68
|
-
);
|
|
69
|
-
} catch {
|
|
70
|
-
// UI may be unavailable in non-interactive modes.
|
|
71
|
-
}
|
|
72
|
-
};
|
|
40
|
+
return [
|
|
41
|
+
{ key: STATUS_KEYS.watch, glyph: GLYPHS.target, count: running, role: "accent" },
|
|
42
|
+
{ key: STATUS_KEYS.watchMatches, glyph: GLYPHS.warn, count: getUnseenMatches(), role: "bad" },
|
|
43
|
+
];
|
|
44
|
+
},
|
|
45
|
+
});
|
|
73
46
|
|
|
74
47
|
const notifyMatch = (watch: WatchRecord, line: string): void => {
|
|
75
48
|
const target = ctx;
|
|
@@ -84,14 +57,19 @@ export function createUiController(options: UiOptions): UiController {
|
|
|
84
57
|
return {
|
|
85
58
|
attach: (next) => {
|
|
86
59
|
ctx = next;
|
|
60
|
+
controller.attach(next);
|
|
87
61
|
},
|
|
88
62
|
detach: () => {
|
|
89
63
|
ctx = undefined;
|
|
64
|
+
controller.detach();
|
|
90
65
|
},
|
|
91
66
|
paint: () => {
|
|
92
|
-
|
|
67
|
+
controller.paint();
|
|
68
|
+
},
|
|
69
|
+
setStatus: (next) => {
|
|
70
|
+
if (next) ctx = next;
|
|
71
|
+
controller.setStatus(next);
|
|
93
72
|
},
|
|
94
|
-
setStatus,
|
|
95
73
|
notifyMatch,
|
|
96
74
|
};
|
|
97
75
|
}
|
|
@@ -12,6 +12,14 @@ pi -e . # load the whole @gavin-hu/my-pi-agent
|
|
|
12
12
|
pi install ./ # or install the package
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
+
## Tool rendering
|
|
16
|
+
|
|
17
|
+
How the `web_search` and `web_fetch` calls and their results look in the transcript — the call line as a headline and the body indented under it:
|
|
18
|
+
|
|
19
|
+
<img src="../../assets/tools/web_search.png" alt="The web_search tool's call and result: the call line and an indented body with the query and the ranked results" width="100%">
|
|
20
|
+
|
|
21
|
+
<img src="../../assets/tools/web_fetch.png" alt="The web_fetch tool's call and result: the call line and an indented body with the URL, title, and fetched text preview" width="100%">
|
|
22
|
+
|
|
15
23
|
## What it does
|
|
16
24
|
|
|
17
25
|
- Registers `web_search`, general web search through a pluggable provider:
|
|
@@ -46,17 +54,18 @@ web_fetch({ url: "https://pi.dev/", find: ["documentation", "extensions"] })
|
|
|
46
54
|
|
|
47
55
|
### Transcript
|
|
48
56
|
|
|
49
|
-
The call line names the query or URL
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
result
|
|
57
|
+
The call line names the query or URL and states the requested limit
|
|
58
|
+
(`(max 8)`), so it never reads as the number of results returned. The result
|
|
59
|
+
separates the header from its body and sanitizes untrusted web text (answers,
|
|
60
|
+
titles, fetched titles and errors) to one line before it reaches the terminal.
|
|
61
|
+
A search result labels its answer and lists one URL per result; expanding
|
|
62
|
+
reveals snippets and the full result list.
|
|
54
63
|
|
|
55
64
|
```
|
|
56
|
-
web_search "pi coding agent" (8
|
|
65
|
+
web_search "pi coding agent" (max 8)
|
|
57
66
|
|
|
58
67
|
via searxng · 8 results
|
|
59
|
-
|
|
68
|
+
Answer: Pi is a coding agent harness …
|
|
60
69
|
1. Pi — a coding agent harness
|
|
61
70
|
https://pi.dev/
|
|
62
71
|
2. GitHub - earendil-works/pi
|