@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
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A scrollable text pane: the log tail a job screen opens, the match list a
|
|
3
|
+
* watcher screen opens.
|
|
4
|
+
*
|
|
5
|
+
* Both screens embed the same pane — an open row's id, a buffer of already
|
|
6
|
+
* sanitized lines, a scroll offset that follows the tail until the user scrolls
|
|
7
|
+
* away from it, a "showing last N" subtitle, and one dim range row — so the
|
|
8
|
+
* state machine lives here once. The screen owns rendering, the visible row
|
|
9
|
+
* budget, and `requestRender`; the pane owns only the state and the keys that
|
|
10
|
+
* move it.
|
|
11
|
+
*
|
|
12
|
+
* Value-only: a class with instance state, no module-level state.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { clampScroll, navigateScroll } from "./cursor.ts";
|
|
16
|
+
|
|
17
|
+
/** A patch for the pane's contents; each field given replaces that field. */
|
|
18
|
+
export interface PaneSource {
|
|
19
|
+
/** Title shown after the pane's prefix, e.g. `j1 build`. */
|
|
20
|
+
title?: string;
|
|
21
|
+
/** The lines, newest last. */
|
|
22
|
+
lines?: string[];
|
|
23
|
+
/** The dim subtitle line, e.g. the log file path. Already sanitized. */
|
|
24
|
+
subtitle?: string;
|
|
25
|
+
/** Whether earlier lines exist beyond the loaded window. */
|
|
26
|
+
more?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Whether two line buffers are identical. */
|
|
30
|
+
function sameLines(a: readonly string[], b: readonly string[]): boolean {
|
|
31
|
+
if (a.length !== b.length) return false;
|
|
32
|
+
for (let index = 0; index < a.length; index++) {
|
|
33
|
+
if (a[index] !== b[index]) return false;
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The scrollable text pane both the job and watch screens embed. */
|
|
39
|
+
export class LinePane {
|
|
40
|
+
/** Id of the row whose pane is open; `""` when the pane is closed. */
|
|
41
|
+
id = "";
|
|
42
|
+
/** First visible line. */
|
|
43
|
+
scroll = 0;
|
|
44
|
+
/** Rows shown at once; the screen sets this from `fitRows` each render. */
|
|
45
|
+
visible = 1;
|
|
46
|
+
/** Whether the view is parked at the tail; set false once the user scrolls up. */
|
|
47
|
+
follow = true;
|
|
48
|
+
|
|
49
|
+
private buffer: string[] = [];
|
|
50
|
+
private label = "";
|
|
51
|
+
private subtitle: string | undefined;
|
|
52
|
+
private more = false;
|
|
53
|
+
|
|
54
|
+
/** Lines loaded into the pane, oldest first. */
|
|
55
|
+
get lines(): readonly string[] {
|
|
56
|
+
return this.buffer;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Loaded line count. */
|
|
60
|
+
get count(): number {
|
|
61
|
+
return this.buffer.length;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The furthest the view can scroll while keeping a full window. */
|
|
65
|
+
get maxScroll(): number {
|
|
66
|
+
return Math.max(0, this.buffer.length - this.visible);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Open the pane on a row, reset to the tail. */
|
|
70
|
+
open(id: string, source: PaneSource): void {
|
|
71
|
+
this.id = id;
|
|
72
|
+
this.buffer = [];
|
|
73
|
+
this.label = "";
|
|
74
|
+
this.subtitle = undefined;
|
|
75
|
+
this.more = false;
|
|
76
|
+
this.scroll = 0;
|
|
77
|
+
this.follow = true;
|
|
78
|
+
this.update(source);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Close the pane and drop its buffer. */
|
|
82
|
+
close(): void {
|
|
83
|
+
this.id = "";
|
|
84
|
+
this.buffer = [];
|
|
85
|
+
this.label = "";
|
|
86
|
+
this.subtitle = undefined;
|
|
87
|
+
this.more = false;
|
|
88
|
+
this.scroll = 0;
|
|
89
|
+
this.follow = true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Replace the given fields.
|
|
94
|
+
*
|
|
95
|
+
* Returns whether anything visible changed, so a caller polling the pane
|
|
96
|
+
* every 500ms can skip a repaint — and leave the user's scroll alone — when
|
|
97
|
+
* the buffer is byte-identical.
|
|
98
|
+
*/
|
|
99
|
+
update(source: PaneSource): boolean {
|
|
100
|
+
const titleChanged = source.title !== undefined && source.title !== this.label;
|
|
101
|
+
const linesChanged = source.lines !== undefined && !sameLines(source.lines, this.buffer);
|
|
102
|
+
const moreChanged = source.more !== undefined && source.more !== this.more;
|
|
103
|
+
const subtitleChanged = source.subtitle !== undefined && source.subtitle !== this.subtitle;
|
|
104
|
+
|
|
105
|
+
if (source.title !== undefined) this.label = source.title;
|
|
106
|
+
if (source.lines !== undefined) this.buffer = source.lines;
|
|
107
|
+
if (source.more !== undefined) this.more = source.more;
|
|
108
|
+
if (source.subtitle !== undefined) this.subtitle = source.subtitle;
|
|
109
|
+
|
|
110
|
+
// Follow the tail; a paused view keeps its place but never past the end.
|
|
111
|
+
if (this.follow) this.scroll = this.maxScroll;
|
|
112
|
+
else this.scroll = Math.min(this.scroll, this.maxScroll);
|
|
113
|
+
|
|
114
|
+
return titleChanged || linesChanged || moreChanged || subtitleChanged;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The header label: the pane prefix, and the row's title when it has one. */
|
|
118
|
+
title(prefix: string): string {
|
|
119
|
+
return this.label ? `${prefix} · ${this.label}` : prefix;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The dim subtitle: the source (a log path), the loaded-window note, and
|
|
124
|
+
* whether the view is following. Empty when there is nothing to say.
|
|
125
|
+
*/
|
|
126
|
+
subtitleLine(): string {
|
|
127
|
+
const parts: string[] = [];
|
|
128
|
+
if (this.subtitle) parts.push(this.subtitle);
|
|
129
|
+
if (this.more && this.buffer.length > 0) parts.push(`showing last ${this.buffer.length} lines`);
|
|
130
|
+
if (this.buffer.length > 0) parts.push(this.follow ? "following" : "paused");
|
|
131
|
+
return parts.join(" · ");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The visible half-open line range, clamped to the buffer. */
|
|
135
|
+
window(): { start: number; end: number } {
|
|
136
|
+
const start = Math.min(Math.max(0, this.scroll), this.maxScroll);
|
|
137
|
+
return { start, end: Math.min(this.buffer.length, start + this.visible) };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Scroll to `next`, clamped; leaving the tail clears `follow`. */
|
|
141
|
+
setScroll(next: number): void {
|
|
142
|
+
this.scroll = clampScroll(next, this.buffer.length, this.visible);
|
|
143
|
+
this.follow = this.scroll >= this.maxScroll;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Adopt a new visible height for the coming render: clamp the scroll into
|
|
148
|
+
* range, and re-park a following view at the tail. Does not touch `follow`.
|
|
149
|
+
*/
|
|
150
|
+
fit(visible: number): void {
|
|
151
|
+
this.visible = visible;
|
|
152
|
+
this.scroll = clampScroll(this.scroll, this.buffer.length, this.visible);
|
|
153
|
+
if (this.follow) this.scroll = this.maxScroll;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Apply a scroll key; `false` when the key is not one the pane consumes. */
|
|
157
|
+
handleKey(data: string): boolean {
|
|
158
|
+
const next = navigateScroll(data, this.scroll, this.buffer.length, this.visible);
|
|
159
|
+
if (next !== undefined) {
|
|
160
|
+
this.setScroll(next);
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
if (data === "g") {
|
|
164
|
+
this.setScroll(0);
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
if (data === "G") {
|
|
168
|
+
this.setScroll(this.maxScroll);
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
}
|
package/lib/tui/rows.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The row grammar: a list row, a checkbox, a status chip, a key/value row, and
|
|
3
|
+
* an empty state.
|
|
4
|
+
*
|
|
5
|
+
* Every row is one line (an empty state is its two), built from the shared
|
|
6
|
+
* token roles and the shared selection marker, and clipped with `columns.ts` so
|
|
7
|
+
* it fits the width it was given. The point is that a list row looks the same
|
|
8
|
+
* whether it is the extension screen, the job list, or the rewind timeline:
|
|
9
|
+
*
|
|
10
|
+
* ❯ ◼ extensions 7 enabled
|
|
11
|
+
*
|
|
12
|
+
* Value-only: pure functions, theme passed in.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import { clip, padTo, splitRow } from "./columns.ts";
|
|
17
|
+
import { selectionMarker } from "./cursor.ts";
|
|
18
|
+
import { GLYPHS } from "./glyphs.ts";
|
|
19
|
+
import { BODY_INDENT } from "./indent.ts";
|
|
20
|
+
import { paint, type TuiRole } from "./tokens.ts";
|
|
21
|
+
|
|
22
|
+
/** One selectable list row. */
|
|
23
|
+
export interface ListRow {
|
|
24
|
+
/** Draw the `❯` marker in the accent role. A blank marker column is reserved either way. */
|
|
25
|
+
selected?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Reserve the `❯ ` marker column. Default true, so a column of rows stays
|
|
28
|
+
* aligned; a non-interactive row (a transcript result) sets false to drop the
|
|
29
|
+
* columns it can never use.
|
|
30
|
+
*/
|
|
31
|
+
marker?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* A pre-themed cell between the marker and the glyph, e.g. a `checkbox`.
|
|
34
|
+
* Reserved by the caller (pass a blank when the row has none) so a column of
|
|
35
|
+
* rows stays aligned.
|
|
36
|
+
*/
|
|
37
|
+
leading?: string;
|
|
38
|
+
/** A leading status or type glyph, painted in `glyphRole`. */
|
|
39
|
+
glyph?: string;
|
|
40
|
+
glyphRole?: TuiRole;
|
|
41
|
+
/** The row's text. */
|
|
42
|
+
label: string;
|
|
43
|
+
labelRole?: TuiRole;
|
|
44
|
+
/** Right-aligned meta (a count, a time, a state). */
|
|
45
|
+
trailing?: string;
|
|
46
|
+
trailingRole?: TuiRole;
|
|
47
|
+
/** Extra columns before the marker, for a nested row. */
|
|
48
|
+
indent?: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* One list row: an optional indent, the selection marker, an optional glyph,
|
|
53
|
+
* the label, and right-aligned trailing meta.
|
|
54
|
+
*
|
|
55
|
+
* The marker column is reserved by default, so selected and unselected rows
|
|
56
|
+
* align; `marker: false` drops it for a non-interactive row. The trailing meta
|
|
57
|
+
* is the anchor when the terminal is narrow (see `splitRow`), so a value at the
|
|
58
|
+
* end of the row survives.
|
|
59
|
+
*/
|
|
60
|
+
export function listRow(theme: Theme, width: number, row: ListRow): string {
|
|
61
|
+
const indent = " ".repeat(Math.max(0, Math.floor(row.indent ?? 0)));
|
|
62
|
+
const marker = row.marker === false ? "" : selectionMarker(theme, row.selected === true);
|
|
63
|
+
const leading = row.leading ? `${row.leading} ` : "";
|
|
64
|
+
const glyph = row.glyph ? `${paint(theme, row.glyphRole ?? "info", row.glyph)} ` : "";
|
|
65
|
+
const label = paint(theme, row.labelRole ?? "label", row.label);
|
|
66
|
+
const trailing = row.trailing ? paint(theme, row.trailingRole ?? "meta", row.trailing) : "";
|
|
67
|
+
return splitRow(`${indent}${marker}${leading}${glyph}${label}`, trailing, width);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** A status row (a job, a watcher): the marker, a status glyph, a label, and right-aligned meta. */
|
|
71
|
+
export interface StatusRow {
|
|
72
|
+
selected?: boolean;
|
|
73
|
+
/** Reserve the marker column. Default true. */
|
|
74
|
+
marker?: boolean;
|
|
75
|
+
glyph: string;
|
|
76
|
+
glyphRole: TuiRole;
|
|
77
|
+
label: string;
|
|
78
|
+
trailing: string;
|
|
79
|
+
/** Role for the trailing meta. Default `faint`. */
|
|
80
|
+
trailingRole?: TuiRole;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** One row of a live status list, on the shared `listRow` grammar. */
|
|
84
|
+
export function statusRow(theme: Theme, width: number, row: StatusRow): string {
|
|
85
|
+
return listRow(theme, width, {
|
|
86
|
+
selected: row.selected === true,
|
|
87
|
+
marker: row.marker !== false,
|
|
88
|
+
glyph: row.glyph,
|
|
89
|
+
glyphRole: row.glyphRole,
|
|
90
|
+
label: row.label,
|
|
91
|
+
trailing: row.trailing,
|
|
92
|
+
trailingRole: row.trailingRole ?? "faint",
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The on/off checkbox glyph, in one role per state. */
|
|
97
|
+
export function checkbox(theme: Theme, on: boolean, roles: { on?: TuiRole; off?: TuiRole } = {}): string {
|
|
98
|
+
return on ? paint(theme, roles.on ?? "ok", GLYPHS.boxOn) : paint(theme, roles.off ?? "faint", GLYPHS.boxOff);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** A glyph and a short label in one color: the vocabulary of a status chip. */
|
|
102
|
+
export function chip(theme: Theme, glyph: string, label: string, role: TuiRole = "meta"): string {
|
|
103
|
+
const head = paint(theme, role, glyph);
|
|
104
|
+
return label ? `${head} ${paint(theme, role, label)}` : head;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** A fixed-width label column followed by its value. */
|
|
108
|
+
export interface KeyValueRow {
|
|
109
|
+
label: string;
|
|
110
|
+
value: string;
|
|
111
|
+
/** Columns the label column occupies. Default 12. */
|
|
112
|
+
labelWidth?: number;
|
|
113
|
+
labelRole?: TuiRole;
|
|
114
|
+
valueRole?: TuiRole;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* One body-indented `label value` row, the label padded to a fixed column.
|
|
119
|
+
* The `BODY_INDENT` prefix matches `emptyState`, so a screen's body rows and its
|
|
120
|
+
* empty state share one indent.
|
|
121
|
+
*/
|
|
122
|
+
export function keyValueRow(theme: Theme, width: number, row: KeyValueRow): string {
|
|
123
|
+
const labelWidth = Math.max(1, Math.floor(row.labelWidth ?? 12));
|
|
124
|
+
const label = paint(theme, row.labelRole ?? "meta", padTo(row.label, labelWidth));
|
|
125
|
+
const value = paint(theme, row.valueRole ?? "label", row.value);
|
|
126
|
+
return clip(`${" ".repeat(BODY_INDENT)}${label}${value}`, width);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** A screen's empty state: what is missing, and the next action. */
|
|
130
|
+
export interface EmptyState {
|
|
131
|
+
/** One line naming what is absent, e.g. `No notes yet.`. */
|
|
132
|
+
label: string;
|
|
133
|
+
/** An optional line naming the next action. */
|
|
134
|
+
hint?: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** The empty-state lines: a body-indented label, then its hint. */
|
|
138
|
+
export function emptyState(theme: Theme, width: number, state: EmptyState): string[] {
|
|
139
|
+
const indent = " ".repeat(BODY_INDENT);
|
|
140
|
+
const lines = [clip(`${indent}${paint(theme, "faint", state.label)}`, width)];
|
|
141
|
+
if (state.hint) lines.push(clip(`${indent}${paint(theme, "faint", state.hint)}`, width));
|
|
142
|
+
return lines;
|
|
143
|
+
}
|
package/lib/tui/rules.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rule grammar: how a surface draws a horizontal divider.
|
|
3
|
+
*
|
|
4
|
+
* Two shapes, built from one idea — a run of a single-column glyph, with an
|
|
5
|
+
* optional label — so a screen title and a centered rule read as one family:
|
|
6
|
+
*
|
|
7
|
+
* title Extensions ────────────────── (label first, accent)
|
|
8
|
+
* centered ╌╌╌╌╌╌ turn 3 ╌╌╌╌╌╌ (label centered)
|
|
9
|
+
*
|
|
10
|
+
* A rule degrades to a plain fill rather than truncating a label into an
|
|
11
|
+
* ellipsis, matching Pi's built-in header.
|
|
12
|
+
*
|
|
13
|
+
* Value-only: pure functions, theme passed in.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
17
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
18
|
+
import { paint, type TuiRole } from "./tokens.ts";
|
|
19
|
+
|
|
20
|
+
/** The glyph and colors a rule is drawn with. */
|
|
21
|
+
export interface RuleStyle {
|
|
22
|
+
/** Single-column fill glyph. Default `─` (a centered rule defaults to `─` too). */
|
|
23
|
+
glyph?: string;
|
|
24
|
+
/** The label's role. */
|
|
25
|
+
labelRole?: TuiRole;
|
|
26
|
+
/** The fill's role. */
|
|
27
|
+
fillRole?: TuiRole;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A centered rule's style adds the padding around the label. */
|
|
31
|
+
export interface CenteredRuleStyle extends RuleStyle {
|
|
32
|
+
/** Spaces on each side of the label. Default 1. */
|
|
33
|
+
pad?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The solid rule fill: the glyph a divider repeats. One constant names it, so
|
|
38
|
+
* the header rule, a panel title, and the user-message nameplate cannot drift.
|
|
39
|
+
*/
|
|
40
|
+
export const RULE_GLYPH = "─";
|
|
41
|
+
|
|
42
|
+
/** A usable origin: terminal arithmetic starts from one column, never zero. */
|
|
43
|
+
function widthOf(width: number): number {
|
|
44
|
+
return Math.max(1, Math.floor(width));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A full-width divider in one color. */
|
|
48
|
+
export function plainRule(theme: Theme, width: number, style: RuleStyle = {}): string {
|
|
49
|
+
const w = widthOf(width);
|
|
50
|
+
return paint(theme, style.fillRole ?? "rule", (style.glyph ?? RULE_GLYPH).repeat(w));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The label-first shape a screen or panel title uses. */
|
|
54
|
+
export function titleRule(theme: Theme, width: number, label: string, style: RuleStyle = {}): string {
|
|
55
|
+
return labelRule(theme, width, label, "title", "rule", style);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** A label-first rule: the label at the margin, a fill to the edge. */
|
|
59
|
+
function labelRule(
|
|
60
|
+
theme: Theme,
|
|
61
|
+
width: number,
|
|
62
|
+
label: string,
|
|
63
|
+
labelRole: TuiRole,
|
|
64
|
+
fillRole: TuiRole,
|
|
65
|
+
style: RuleStyle,
|
|
66
|
+
): string {
|
|
67
|
+
const w = widthOf(width);
|
|
68
|
+
const text = label.trim();
|
|
69
|
+
if (!text) return plainRule(theme, w, style);
|
|
70
|
+
const labelWidth = visibleWidth(text);
|
|
71
|
+
if (labelWidth + 1 >= w) return plainRule(theme, w, style);
|
|
72
|
+
const fill = (style.glyph ?? RULE_GLYPH).repeat(w - labelWidth - 1);
|
|
73
|
+
return paint(theme, style.labelRole ?? labelRole, text) + " " + paint(theme, style.fillRole ?? fillRole, fill);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** A centered rule: fill, label, and the rest of the fill. */
|
|
77
|
+
export function centeredRule(theme: Theme, width: number, label: string, style: CenteredRuleStyle = {}): string {
|
|
78
|
+
const w = widthOf(width);
|
|
79
|
+
const glyph = style.glyph ?? RULE_GLYPH;
|
|
80
|
+
const fillRole = style.fillRole ?? "ruleStrong";
|
|
81
|
+
if (!label.trim()) return paint(theme, fillRole, glyph.repeat(w));
|
|
82
|
+
const pad = " ".repeat(Math.max(0, Math.floor(style.pad ?? 1)));
|
|
83
|
+
const text = `${pad}${label}${pad}`;
|
|
84
|
+
const labelWidth = visibleWidth(text);
|
|
85
|
+
if (labelWidth >= w) return paint(theme, fillRole, glyph.repeat(w));
|
|
86
|
+
const remaining = w - labelWidth;
|
|
87
|
+
const left = Math.floor(remaining / 2);
|
|
88
|
+
const right = remaining - left;
|
|
89
|
+
return (
|
|
90
|
+
paint(theme, fillRole, glyph.repeat(left)) +
|
|
91
|
+
paint(theme, style.labelRole ?? "meta", text) +
|
|
92
|
+
paint(theme, fillRole, glyph.repeat(right))
|
|
93
|
+
);
|
|
94
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The footer status chips two extensions publish the same way.
|
|
3
|
+
*
|
|
4
|
+
* A controller remembers the last context it was attached to, repaints the keys
|
|
5
|
+
* it owns, clears them when its feature is disabled, and never throws: a
|
|
6
|
+
* non-interactive mode has no UI, so every method no-ops or falls back to plain
|
|
7
|
+
* text instead.
|
|
8
|
+
*
|
|
9
|
+
* The chips themselves are the caller's: it supplies the keys, the counts, and
|
|
10
|
+
* the roles, so `job` and `watch` share the controller but not their vocabulary.
|
|
11
|
+
* Value-only: the per-caller state lives in this closure, never in the module.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import { paint, type TuiRole } from "./tokens.ts";
|
|
16
|
+
|
|
17
|
+
/** One chip a controller publishes. */
|
|
18
|
+
export interface StatusChip {
|
|
19
|
+
/** The `ctx.ui.setStatus` key. */
|
|
20
|
+
key: string;
|
|
21
|
+
/** Glyph shown before the count. */
|
|
22
|
+
glyph: string;
|
|
23
|
+
/** Count; `0` clears the key. */
|
|
24
|
+
count: number;
|
|
25
|
+
/** The design role the chip is drawn in. */
|
|
26
|
+
role: TuiRole;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** What a controller publishes, read fresh on every paint. */
|
|
30
|
+
export interface StatusControllerOptions {
|
|
31
|
+
/** The keys this controller owns, cleared together when it is disabled. */
|
|
32
|
+
keys: readonly string[];
|
|
33
|
+
/** Whether the chips should be published at all. */
|
|
34
|
+
enabled(): boolean;
|
|
35
|
+
/** The chips for the current state. */
|
|
36
|
+
chips(): readonly StatusChip[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** An attach/paint/clear surface for one extension's footer chips. */
|
|
40
|
+
export interface StatusController {
|
|
41
|
+
/** Remember the live context; later calls use it when none is passed. */
|
|
42
|
+
attach(ctx: ExtensionContext): void;
|
|
43
|
+
/** Forget the context, for example on shutdown or reload. */
|
|
44
|
+
detach(): void;
|
|
45
|
+
/** Republish using the attached context. */
|
|
46
|
+
paint(): void;
|
|
47
|
+
/** Publish the chips (and attach `ctx` when given). */
|
|
48
|
+
setStatus(ctx?: ExtensionContext): void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A context's theme, or `undefined` when the UI is unavailable. */
|
|
52
|
+
function themeOf(target: ExtensionContext): Theme | undefined {
|
|
53
|
+
try {
|
|
54
|
+
return target.ui.theme;
|
|
55
|
+
} catch {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Paint in role, falling back to plain text when the theme cannot. */
|
|
61
|
+
function safePaint(theme: Theme | undefined, role: TuiRole, text: string): string {
|
|
62
|
+
if (!theme) return text;
|
|
63
|
+
try {
|
|
64
|
+
return paint(theme, role, text);
|
|
65
|
+
} catch {
|
|
66
|
+
return text;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Build a controller for one extension's pair of chips. */
|
|
71
|
+
export function createStatusController(options: StatusControllerOptions): StatusController {
|
|
72
|
+
let ctx: ExtensionContext | undefined;
|
|
73
|
+
|
|
74
|
+
const setStatus = (next?: ExtensionContext): void => {
|
|
75
|
+
if (next) ctx = next;
|
|
76
|
+
const target = ctx;
|
|
77
|
+
if (!target) return;
|
|
78
|
+
try {
|
|
79
|
+
if (!options.enabled()) {
|
|
80
|
+
for (const key of options.keys) target.ui.setStatus(key, undefined);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const theme = themeOf(target);
|
|
84
|
+
for (const chip of options.chips()) {
|
|
85
|
+
// A space before the count keeps the compact chrome form (`▸2`),
|
|
86
|
+
// while the two chips stay separate segments in the full form.
|
|
87
|
+
const text = `${chip.glyph} ${chip.count}`;
|
|
88
|
+
target.ui.setStatus(chip.key, chip.count > 0 ? safePaint(theme, chip.role, text) : undefined);
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
// UI may be unavailable in non-interactive modes.
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
attach: (next) => {
|
|
97
|
+
ctx = next;
|
|
98
|
+
},
|
|
99
|
+
detach: () => {
|
|
100
|
+
ctx = undefined;
|
|
101
|
+
},
|
|
102
|
+
paint: () => {
|
|
103
|
+
setStatus();
|
|
104
|
+
},
|
|
105
|
+
setStatus,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Bytefly terminal design tokens: semantic roles mapped onto Pi's theme
|
|
3
|
+
* colors.
|
|
4
|
+
*
|
|
5
|
+
* A component names a role (`title`, `meta`, `bad`) and the active theme
|
|
6
|
+
* supplies the color, so switching themes re-skins every surface without a
|
|
7
|
+
* component changing. The mapping is deliberately indirect: Pi's token set is
|
|
8
|
+
* closed, so the kit only ever references it, never a literal color.
|
|
9
|
+
*
|
|
10
|
+
* Value-only: one frozen lookup table and pure helpers.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
14
|
+
|
|
15
|
+
/** A semantic role a surface can paint. */
|
|
16
|
+
export type TuiRole =
|
|
17
|
+
/** The brand highlight: selection, focus, a model name. */
|
|
18
|
+
| "accent"
|
|
19
|
+
/** A screen, section, or panel title. */
|
|
20
|
+
| "title"
|
|
21
|
+
/** Primary body text. */
|
|
22
|
+
| "label"
|
|
23
|
+
/** Secondary text: counts, paths, summaries. */
|
|
24
|
+
| "meta"
|
|
25
|
+
/** Tertiary text and de-emphasis. */
|
|
26
|
+
| "faint"
|
|
27
|
+
/** A divider line. */
|
|
28
|
+
| "rule"
|
|
29
|
+
/** A heavier divider. */
|
|
30
|
+
| "ruleStrong"
|
|
31
|
+
/** Success. */
|
|
32
|
+
| "ok"
|
|
33
|
+
/** Warning. */
|
|
34
|
+
| "warn"
|
|
35
|
+
/** Error. */
|
|
36
|
+
| "bad"
|
|
37
|
+
/** Neutral notice. */
|
|
38
|
+
| "info";
|
|
39
|
+
|
|
40
|
+
/** Role -> theme color. The single place the kit names a Pi token. */
|
|
41
|
+
export const TUI_TOKENS: Readonly<Record<TuiRole, ThemeColor>> = {
|
|
42
|
+
accent: "accent",
|
|
43
|
+
title: "accent",
|
|
44
|
+
label: "text",
|
|
45
|
+
meta: "muted",
|
|
46
|
+
faint: "dim",
|
|
47
|
+
rule: "borderMuted",
|
|
48
|
+
ruleStrong: "border",
|
|
49
|
+
ok: "success",
|
|
50
|
+
warn: "warning",
|
|
51
|
+
bad: "error",
|
|
52
|
+
info: "muted",
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** Paint `text` in a role's color. */
|
|
56
|
+
export function paint(theme: Theme, role: TuiRole, text: string): string {
|
|
57
|
+
return theme.fg(TUI_TOKENS[role], text);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The opening sequence for a role, without its reset. A frame that must repaint
|
|
62
|
+
* a run in place — mid-line, where the surrounding text already carries the
|
|
63
|
+
* closing reset — opens with this instead of wrapping a whole string.
|
|
64
|
+
*
|
|
65
|
+
* Derived from `paint` with a sentinel, so it tracks the theme's own escape
|
|
66
|
+
* shape rather than assuming one.
|
|
67
|
+
*/
|
|
68
|
+
export function paintPrefix(theme: Theme, role: TuiRole): string {
|
|
69
|
+
const marker = "\u0000";
|
|
70
|
+
const wrapped = paint(theme, role, marker);
|
|
71
|
+
return wrapped.slice(0, wrapped.indexOf(marker));
|
|
72
|
+
}
|
package/lib/web/base.ts
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
// Every focusable element gets the same ring; components must not re-declare
|
|
10
10
|
// their own, so the keyboard affordance cannot disappear surface by surface.
|
|
11
|
-
export const WEB_BASE_CSS =
|
|
11
|
+
export const WEB_BASE_CSS = `html,body{margin:0}
|
|
12
|
+
*{box-sizing:border-box}
|
|
12
13
|
[hidden]{display:none!important}
|
|
13
14
|
a{color:var(--accent);text-decoration:none}
|
|
14
15
|
a:hover{text-decoration:underline}
|
package/lib/web/components.ts
CHANGED
|
@@ -117,7 +117,9 @@ th[aria-sort="descending"] .bf-sort::after{content:" \\25BC";font-size:9px;color
|
|
|
117
117
|
.bf-md .md-check{color:var(--success)}
|
|
118
118
|
.bf-md table{margin:0;border-collapse:collapse;table-layout:auto;width:auto;font-size:var(--fs-sm)}
|
|
119
119
|
.bf-md .md-table-wrap{overflow-x:auto;margin:0 0 1em}
|
|
120
|
-
.bf-md th,.bf-md td{border:1px solid var(--border);padding:6px 10px;text-align:left;vertical-align:top}
|
|
120
|
+
.bf-md th,.bf-md td{border:1px solid var(--border);padding:6px 10px;text-align:left;vertical-align:top;overflow-wrap:break-word}
|
|
121
|
+
.bf-md th{white-space:nowrap}
|
|
122
|
+
.bf-md th code,.bf-md td code{white-space:nowrap}
|
|
121
123
|
.bf-md thead th{background:var(--surface);font-weight:var(--fw-medium)}
|
|
122
124
|
.bf-md .md-left{text-align:left}
|
|
123
125
|
.bf-md .md-center{text-align:center}
|
package/lib/web/layout.ts
CHANGED
|
@@ -11,15 +11,19 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Only the shared 860px collapse lives here; the plan-specific rules stay in
|
|
13
13
|
* `extensions/plan/web-page.ts`.
|
|
14
|
+
*
|
|
15
|
+
* The shell is a definite full-height column so the panes scroll inside it; the
|
|
16
|
+
* 860px breakpoint drops that back to normal page scroll for narrow screens.
|
|
14
17
|
*/
|
|
15
18
|
|
|
16
|
-
export const WEB_LAYOUT_CSS = `.bf-app{display:flex;flex-direction:column;min-height:
|
|
19
|
+
export const WEB_LAYOUT_CSS = `.bf-app{display:flex;flex-direction:column;height:100vh;min-height:0;overflow:hidden;background:var(--canvas);color:var(--text);font:var(--fs-base)/var(--lh-normal) var(--font-ui)}
|
|
17
20
|
.bf-app__header{position:sticky;top:0;z-index:2;flex:0 0 auto;background:var(--surface);border-bottom:1px solid var(--border)}
|
|
18
21
|
.bf-app__body{flex:1 1 auto;min-height:0;display:grid;grid-template-columns:260px minmax(0,1fr)}
|
|
19
22
|
.bf-app__sidebar{display:flex;flex-direction:column;min-width:0;min-height:0;overflow:hidden;background:var(--surface);border-right:1px solid var(--border)}
|
|
20
23
|
.bf-app__main{min-width:0;min-height:0;display:flex;flex-direction:column;overflow:auto;padding:var(--space-5) var(--space-6)}
|
|
21
24
|
.bf-app__footer{flex:0 0 auto;border-top:1px solid var(--border);color:var(--muted);font-size:var(--fs-sm);padding:var(--space-3) var(--space-6)}
|
|
22
25
|
@media (max-width:860px){
|
|
26
|
+
.bf-app{height:auto;min-height:100vh;overflow:visible}
|
|
23
27
|
.bf-app__body{flex:none;grid-template-columns:1fr}
|
|
24
28
|
.bf-app__sidebar{display:block;position:static;max-height:none;overflow:visible;border-right:0;border-bottom:1px solid var(--border)}
|
|
25
29
|
.bf-app__main{overflow:visible}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gavin-hu/my-pi-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Personal Pi customizations, packaged as a Pi package: worktree isolation and rewind snapshots, question, todo, goal, plan, subagent, job, watch, web-access, file-browse, file-open, document, memory, wechat, and bytefly.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
"themes": [
|
|
91
91
|
"./themes/*.json"
|
|
92
92
|
],
|
|
93
|
-
"image": "https://raw.githubusercontent.com/gavin-hu/my-pi-agent/main/assets/
|
|
93
|
+
"image": "https://raw.githubusercontent.com/gavin-hu/my-pi-agent/main/assets/chrome.png"
|
|
94
94
|
},
|
|
95
95
|
"scripts": {
|
|
96
96
|
"test": "bun test --parallel=4 --timeout 30000",
|
|
@@ -102,7 +102,11 @@
|
|
|
102
102
|
"check": "bun run format:check && bun run typecheck && bun run test",
|
|
103
103
|
"check:serial": "bun run format:check && bun run typecheck && bun run test:serial",
|
|
104
104
|
"release:notes": "bun run scripts/release-notes.ts",
|
|
105
|
-
"config:migrate": "bun run scripts/migrate-config.ts"
|
|
105
|
+
"config:migrate": "bun run scripts/migrate-config.ts",
|
|
106
|
+
"screenshot:plan": "bun run scripts/plan-review-shot.ts",
|
|
107
|
+
"screenshot:tools": "bun run scripts/tool-renderings.ts gallery assets/tool-renderings.png",
|
|
108
|
+
"screenshot:tool-pngs": "bun run scripts/tool-renderings.ts tools assets/tools",
|
|
109
|
+
"screenshots": "bun run scripts/screenshots.ts"
|
|
106
110
|
},
|
|
107
111
|
"dependencies": {
|
|
108
112
|
"exceljs": "^4.4.0",
|