@gavin-hu/my-pi-agent 0.8.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -7
- package/README.zh-CN.md +7 -7
- package/assets/file-browse.png +0 -0
- package/assets/plan-review.png +0 -0
- package/extensions/bytefly/README.md +49 -36
- package/extensions/bytefly/doctor/format.ts +36 -10
- package/extensions/bytefly/doctor/tools.ts +6 -5
- package/extensions/bytefly/footer/config.ts +1 -1
- package/extensions/bytefly/footer/render/format.ts +5 -18
- package/extensions/bytefly/footer/render/layout.ts +2 -1
- package/extensions/bytefly/footer/render/segments.ts +51 -78
- package/extensions/bytefly/header/component.ts +33 -16
- package/extensions/bytefly/header/config.ts +28 -18
- package/extensions/bytefly/index.ts +1 -1
- package/extensions/bytefly/panel/commands.ts +69 -35
- package/extensions/bytefly/panel/format.ts +8 -8
- package/extensions/bytefly/panel/tui.ts +45 -70
- package/extensions/bytefly/separator/config.ts +16 -8
- package/extensions/bytefly/separator/format.ts +13 -40
- package/extensions/bytefly/separator/types.ts +0 -10
- package/extensions/document/README.md +1 -1
- package/extensions/document/render.ts +11 -10
- package/extensions/document/tool.ts +1 -1
- package/extensions/file-browse/README.md +51 -9
- package/extensions/file-browse/client.ts +103 -2
- package/extensions/file-browse/config.ts +12 -0
- package/extensions/file-browse/diff.ts +187 -0
- package/extensions/file-browse/git.ts +176 -1
- package/extensions/file-browse/html.ts +385 -19
- package/extensions/file-browse/index.ts +15 -4
- package/extensions/file-browse/markdown.ts +33 -0
- package/extensions/file-browse/router.ts +94 -2
- package/extensions/file-browse/server.ts +4 -1
- package/extensions/goal/format.ts +11 -10
- package/extensions/goal/tools.ts +6 -5
- package/extensions/goal/tui.ts +1 -1
- package/extensions/job/README.md +2 -2
- package/extensions/job/format.ts +36 -13
- package/extensions/job/index.ts +1 -1
- package/extensions/job/tools.ts +11 -10
- package/extensions/job/tui.ts +141 -220
- package/extensions/job/ui.ts +24 -49
- package/extensions/memory/README.md +1 -1
- package/extensions/memory/tools.ts +4 -3
- package/extensions/memory/tui.ts +32 -36
- package/extensions/plan/README.md +64 -34
- package/extensions/plan/blocks.ts +44 -2
- package/extensions/plan/feedback.ts +20 -5
- package/extensions/plan/index.ts +1 -1
- package/extensions/plan/runtime.ts +3 -2
- package/extensions/plan/tools.ts +23 -22
- package/extensions/plan/tui.ts +21 -27
- package/extensions/plan/web-client.ts +335 -30
- package/extensions/plan/web-page.ts +110 -30
- package/extensions/plan/web-router.ts +6 -4
- package/extensions/plan/web.ts +41 -10
- package/extensions/question/README.md +1 -1
- package/extensions/question/tools.ts +12 -11
- package/extensions/question/tui.ts +132 -76
- package/extensions/rewind/README.md +1 -1
- package/extensions/rewind/format.ts +29 -1
- package/extensions/rewind/rewind-tui.ts +66 -62
- package/extensions/rewind/status.ts +3 -2
- package/extensions/subagent/render.ts +62 -47
- package/extensions/todo/README.md +2 -2
- package/extensions/todo/format.ts +31 -10
- package/extensions/todo/tools.ts +5 -4
- package/extensions/todo/tui.ts +45 -26
- package/extensions/watch/README.md +2 -2
- package/extensions/watch/format.ts +33 -13
- package/extensions/watch/index.ts +1 -1
- package/extensions/watch/tools.ts +12 -7
- package/extensions/watch/tui.ts +135 -194
- package/extensions/watch/ui.ts +24 -46
- package/extensions/web-access/fetch/tool.ts +12 -11
- package/extensions/web-access/search/tool.ts +16 -13
- package/extensions/wechat/README.md +2 -2
- package/extensions/wechat/index.ts +4 -3
- package/extensions/wechat/login-screen.ts +8 -5
- package/extensions/wechat/status-screen.ts +41 -22
- package/extensions/wechat/tools.ts +6 -5
- package/extensions/worktree/enter.ts +1 -1
- package/extensions/worktree/index.ts +1 -1
- package/extensions/worktree/runtime.ts +1 -1
- package/lib/README.md +10 -7
- package/lib/requirements.ts +2 -2
- package/lib/{tui.ts → tui/chrome.ts} +9 -30
- package/lib/tui/columns.ts +96 -0
- package/lib/{list-cursor.ts → tui/cursor.ts} +56 -3
- package/lib/tui/gauge.ts +78 -0
- package/lib/tui/index.ts +25 -0
- package/lib/tui/line-pane.ts +173 -0
- package/lib/tui/rows.ts +109 -0
- package/lib/tui/rules.ts +88 -0
- package/lib/tui/status-controller.ts +107 -0
- package/lib/tui/tokens.ts +58 -0
- package/package.json +1 -1
- /package/lib/{ui.ts → tui/glyphs.ts} +0 -0
|
@@ -3,26 +3,23 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Each segment carries progressively shorter `forms` and a `weight`; the layout
|
|
5
5
|
* engine advances forms and drops segments from the highest weight down, so the
|
|
6
|
-
* bar degrades predictably instead of clipping arbitrary text.
|
|
6
|
+
* bar degrades predictably instead of clipping arbitrary text. Colors come from
|
|
7
|
+
* the design-system roles, so the bar follows the active theme.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
10
11
|
import { stripTerminalSequences } from "@earendil-works/pi-tui";
|
|
11
12
|
import { formatTokens, sanitize, sanitizeLine } from "../../../../lib/format.ts";
|
|
13
|
+
import { gaugeBar } from "../../../../lib/tui/gauge.ts";
|
|
14
|
+
import { chip } from "../../../../lib/tui/rows.ts";
|
|
15
|
+
import { paint } from "../../../../lib/tui/tokens.ts";
|
|
12
16
|
import { CONFIG } from "../config.ts";
|
|
13
|
-
import {
|
|
14
|
-
computeGauge,
|
|
15
|
-
contextColor,
|
|
16
|
-
formatCost,
|
|
17
|
-
formatCwd,
|
|
18
|
-
formatPercent,
|
|
19
|
-
thinkingColor,
|
|
20
|
-
truncateLabel,
|
|
21
|
-
} from "./format.ts";
|
|
17
|
+
import { contextRole, formatCost, formatCwd, formatPercent, thinkingColor, truncateLabel } from "./format.ts";
|
|
22
18
|
import { compactStatus, dropStatusDetail } from "./status.ts";
|
|
23
19
|
import type { Segment, StatusSnapshot } from "../types.ts";
|
|
24
20
|
|
|
25
|
-
|
|
21
|
+
/** Tertiary text: paths and de-emphasized meters. */
|
|
22
|
+
const faint = (theme: Theme, text: string): string => paint(theme, "faint", text);
|
|
26
23
|
|
|
27
24
|
/**
|
|
28
25
|
* The label part of a themed status chip: strip its escape sequences, sanitize
|
|
@@ -41,9 +38,9 @@ export function pwdSegment(snapshot: StatusSnapshot, theme: Theme, home: string
|
|
|
41
38
|
droppable: false,
|
|
42
39
|
separator: "",
|
|
43
40
|
forms: [
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
41
|
+
faint(theme, formatCwd(snapshot.cwd, home, 0)),
|
|
42
|
+
faint(theme, formatCwd(snapshot.cwd, home, 1)),
|
|
43
|
+
faint(theme, formatCwd(snapshot.cwd, home, 2)),
|
|
47
44
|
],
|
|
48
45
|
};
|
|
49
46
|
}
|
|
@@ -54,13 +51,13 @@ export function sessionSegment(snapshot: StatusSnapshot, theme: Theme): Segment
|
|
|
54
51
|
const short = truncateLabel(name, CONFIG.sessionLabelMax);
|
|
55
52
|
// Brackets and a brighter token keep the session name from reading as a
|
|
56
53
|
// trailing path segment.
|
|
57
|
-
const forms = [theme
|
|
58
|
-
if (short !== name) forms.push(theme
|
|
54
|
+
const forms = [paint(theme, "meta", `[${name}]`)];
|
|
55
|
+
if (short !== name) forms.push(paint(theme, "meta", `[${short}]`));
|
|
59
56
|
return {
|
|
60
57
|
id: "session",
|
|
61
58
|
weight: 3,
|
|
62
59
|
droppable: true,
|
|
63
|
-
separator:
|
|
60
|
+
separator: faint(theme, CONFIG.separators.item),
|
|
64
61
|
forms,
|
|
65
62
|
};
|
|
66
63
|
}
|
|
@@ -68,14 +65,13 @@ export function sessionSegment(snapshot: StatusSnapshot, theme: Theme): Segment
|
|
|
68
65
|
export function branchSegment(snapshot: StatusSnapshot, theme: Theme): Segment | null {
|
|
69
66
|
if (!snapshot.branch) return null;
|
|
70
67
|
const detached = snapshot.branch === CONFIG.labels.detached;
|
|
71
|
-
const color = detached ? "warning" : "muted";
|
|
72
68
|
const icon = detached ? CONFIG.icons.detached : CONFIG.icons.branch;
|
|
73
69
|
return {
|
|
74
70
|
id: "branch",
|
|
75
71
|
weight: 1,
|
|
76
72
|
droppable: true,
|
|
77
|
-
separator:
|
|
78
|
-
forms: [theme
|
|
73
|
+
separator: faint(theme, CONFIG.separators.item),
|
|
74
|
+
forms: [chip(theme, icon, snapshot.branch, detached ? "warn" : "meta")],
|
|
79
75
|
};
|
|
80
76
|
}
|
|
81
77
|
|
|
@@ -83,27 +79,16 @@ export function worktreeSegment(snapshot: StatusSnapshot, theme: Theme): Segment
|
|
|
83
79
|
const raw = snapshot.statuses.get(CONFIG.worktreeStatusKey);
|
|
84
80
|
if (!raw) return null;
|
|
85
81
|
const icon = CONFIG.icons.worktree;
|
|
82
|
+
const separator = faint(theme, CONFIG.separators.item);
|
|
86
83
|
const label = chipLabel(raw);
|
|
87
84
|
if (!label) {
|
|
88
|
-
return {
|
|
89
|
-
id: "worktree",
|
|
90
|
-
weight: 3,
|
|
91
|
-
droppable: true,
|
|
92
|
-
separator: dim(theme, CONFIG.separators.item),
|
|
93
|
-
forms: [theme.fg("success", icon)],
|
|
94
|
-
};
|
|
85
|
+
return { id: "worktree", weight: 3, droppable: true, separator, forms: [paint(theme, "ok", icon)] };
|
|
95
86
|
}
|
|
96
87
|
const short = truncateLabel(label, CONFIG.worktreeLabelMax);
|
|
97
|
-
const forms = [theme
|
|
98
|
-
if (short !== label) forms.push(theme
|
|
99
|
-
forms.push(theme
|
|
100
|
-
return {
|
|
101
|
-
id: "worktree",
|
|
102
|
-
weight: 3,
|
|
103
|
-
droppable: true,
|
|
104
|
-
separator: dim(theme, CONFIG.separators.item),
|
|
105
|
-
forms,
|
|
106
|
-
};
|
|
88
|
+
const forms = [chip(theme, icon, label, "ok")];
|
|
89
|
+
if (short !== label) forms.push(chip(theme, icon, short, "ok"));
|
|
90
|
+
forms.push(paint(theme, "ok", icon));
|
|
91
|
+
return { id: "worktree", weight: 3, droppable: true, separator, forms };
|
|
107
92
|
}
|
|
108
93
|
|
|
109
94
|
export function browseSegment(snapshot: StatusSnapshot, theme: Theme): Segment | null {
|
|
@@ -111,25 +96,20 @@ export function browseSegment(snapshot: StatusSnapshot, theme: Theme): Segment |
|
|
|
111
96
|
if (!raw) return null;
|
|
112
97
|
const icon = CONFIG.icons.browse;
|
|
113
98
|
const label = chipLabel(raw);
|
|
114
|
-
const separator =
|
|
99
|
+
const separator = faint(theme, CONFIG.separators.item);
|
|
115
100
|
if (!label) {
|
|
116
|
-
return {
|
|
117
|
-
id: "browse",
|
|
118
|
-
weight: 3,
|
|
119
|
-
droppable: true,
|
|
120
|
-
separator,
|
|
121
|
-
forms: [theme.fg("success", icon)],
|
|
122
|
-
};
|
|
101
|
+
return { id: "browse", weight: 3, droppable: true, separator, forms: [paint(theme, "ok", icon)] };
|
|
123
102
|
}
|
|
103
|
+
// A two-tone chip: the `◉` glyph stays green while the port reads as accent.
|
|
124
104
|
return {
|
|
125
105
|
id: "browse",
|
|
126
106
|
weight: 3,
|
|
127
107
|
droppable: true,
|
|
128
108
|
separator,
|
|
129
109
|
forms: [
|
|
130
|
-
`${theme
|
|
131
|
-
`${theme
|
|
132
|
-
theme
|
|
110
|
+
`${paint(theme, "ok", icon)} ${paint(theme, "accent", label)}`,
|
|
111
|
+
`${paint(theme, "ok", icon)}${paint(theme, "accent", label)}`,
|
|
112
|
+
paint(theme, "ok", icon),
|
|
133
113
|
],
|
|
134
114
|
};
|
|
135
115
|
}
|
|
@@ -150,7 +130,7 @@ export function modesSegment(snapshot: StatusSnapshot, theme: Theme): Segment |
|
|
|
150
130
|
})
|
|
151
131
|
.filter(Boolean);
|
|
152
132
|
if (others.length === 0) return null;
|
|
153
|
-
const full = others.join(
|
|
133
|
+
const full = others.join(faint(theme, CONFIG.separators.item));
|
|
154
134
|
// The compact form keeps each status's own colour (see `compactStatus`); a
|
|
155
135
|
// two-token badge keeps its count (`↺ 2` -> `↺2`) instead of collapsing to a
|
|
156
136
|
// bare icon.
|
|
@@ -161,31 +141,22 @@ export function modesSegment(snapshot: StatusSnapshot, theme: Theme): Segment |
|
|
|
161
141
|
// than truncating the model, which sits at the end of the line.
|
|
162
142
|
weight: 1,
|
|
163
143
|
droppable: true,
|
|
164
|
-
separator:
|
|
144
|
+
separator: faint(theme, CONFIG.separators.group),
|
|
165
145
|
forms: [full, icons],
|
|
166
146
|
};
|
|
167
147
|
}
|
|
168
148
|
|
|
169
149
|
export function contextSegment(snapshot: StatusSnapshot, theme: Theme): Segment {
|
|
170
150
|
const percent = snapshot.context.percent;
|
|
171
|
-
const
|
|
172
|
-
const percentText = theme
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
return `${gauge}${percentText}`;
|
|
181
|
-
});
|
|
182
|
-
return {
|
|
183
|
-
id: "context",
|
|
184
|
-
weight: 1,
|
|
185
|
-
droppable: false,
|
|
186
|
-
separator: "",
|
|
187
|
-
forms,
|
|
188
|
-
};
|
|
151
|
+
const role = contextRole(percent);
|
|
152
|
+
const percentText = paint(theme, role, formatPercent(percent));
|
|
153
|
+
// Unknown usage draws no track, so `?` sits where the gauge would be rather
|
|
154
|
+
// than a full row of empty blocks claiming a known zero.
|
|
155
|
+
const widths: readonly number[] = percent === null ? [0] : CONFIG.gauge.widths;
|
|
156
|
+
const forms = widths.map((blocks) =>
|
|
157
|
+
blocks > 0 ? `${gaugeBar(theme, { percent, blocks, role, emptyRole: "faint" })} ${percentText}` : percentText,
|
|
158
|
+
);
|
|
159
|
+
return { id: "context", weight: 1, droppable: false, separator: "", forms };
|
|
189
160
|
}
|
|
190
161
|
|
|
191
162
|
export function windowSegment(snapshot: StatusSnapshot, theme: Theme): Segment | null {
|
|
@@ -195,7 +166,7 @@ export function windowSegment(snapshot: StatusSnapshot, theme: Theme): Segment |
|
|
|
195
166
|
weight: 3,
|
|
196
167
|
droppable: true,
|
|
197
168
|
separator: "",
|
|
198
|
-
forms: [
|
|
169
|
+
forms: [faint(theme, `/${formatTokens(snapshot.context.contextWindow)}`)],
|
|
199
170
|
};
|
|
200
171
|
}
|
|
201
172
|
|
|
@@ -205,8 +176,8 @@ export function costSegment(snapshot: StatusSnapshot, theme: Theme): Segment | n
|
|
|
205
176
|
id: "cost",
|
|
206
177
|
weight: 2,
|
|
207
178
|
droppable: true,
|
|
208
|
-
separator:
|
|
209
|
-
forms: [theme
|
|
179
|
+
separator: faint(theme, CONFIG.separators.item),
|
|
180
|
+
forms: [paint(theme, "meta", formatCost(snapshot.usage.cost, snapshot.currency))],
|
|
210
181
|
};
|
|
211
182
|
}
|
|
212
183
|
|
|
@@ -220,8 +191,8 @@ export function tokensSegment(snapshot: StatusSnapshot, theme: Theme): Segment |
|
|
|
220
191
|
id: "tokens",
|
|
221
192
|
weight: 4,
|
|
222
193
|
droppable: true,
|
|
223
|
-
separator:
|
|
224
|
-
forms: [
|
|
194
|
+
separator: faint(theme, CONFIG.separators.item),
|
|
195
|
+
forms: [faint(theme, parts.join(" "))],
|
|
225
196
|
};
|
|
226
197
|
}
|
|
227
198
|
|
|
@@ -240,8 +211,8 @@ export function cacheSegment(snapshot: StatusSnapshot, theme: Theme): Segment |
|
|
|
240
211
|
id: "cache",
|
|
241
212
|
weight: 5,
|
|
242
213
|
droppable: true,
|
|
243
|
-
separator:
|
|
244
|
-
forms: [
|
|
214
|
+
separator: faint(theme, CONFIG.separators.item),
|
|
215
|
+
forms: [faint(theme, parts.join(" "))],
|
|
245
216
|
};
|
|
246
217
|
}
|
|
247
218
|
|
|
@@ -254,7 +225,7 @@ export function modelSegment(snapshot: StatusSnapshot, theme: Theme): Segment {
|
|
|
254
225
|
weight: 1,
|
|
255
226
|
droppable: false,
|
|
256
227
|
separator: "",
|
|
257
|
-
forms: [theme
|
|
228
|
+
forms: [paint(theme, "meta", CONFIG.labels.noModel)],
|
|
258
229
|
};
|
|
259
230
|
}
|
|
260
231
|
const provider = snapshot.providerCount > 1 ? `(${model.provider}) ` : "";
|
|
@@ -263,19 +234,21 @@ export function modelSegment(snapshot: StatusSnapshot, theme: Theme): Segment {
|
|
|
263
234
|
weight: 1,
|
|
264
235
|
droppable: false,
|
|
265
236
|
separator: "",
|
|
266
|
-
forms: [theme
|
|
237
|
+
forms: [paint(theme, "accent", `${provider}${model.id}`), paint(theme, "accent", model.id)],
|
|
267
238
|
};
|
|
268
239
|
}
|
|
269
240
|
|
|
270
241
|
export function thinkingSegment(snapshot: StatusSnapshot, theme: Theme): Segment | null {
|
|
271
242
|
if (!snapshot.model?.reasoning) return null;
|
|
272
243
|
const level = snapshot.thinkingLevel ?? "off";
|
|
244
|
+
// The reasoning scale is Pi's own seven-token ramp, not a design role: a
|
|
245
|
+
// level colors itself from `thinkingColor`.
|
|
273
246
|
const color = thinkingColor(level);
|
|
274
247
|
return {
|
|
275
248
|
id: "thinking",
|
|
276
249
|
weight: 2,
|
|
277
250
|
droppable: true,
|
|
278
|
-
separator:
|
|
251
|
+
separator: faint(theme, CONFIG.separators.item),
|
|
279
252
|
forms: [theme.fg(color, level)],
|
|
280
253
|
};
|
|
281
254
|
}
|
|
@@ -3,16 +3,18 @@
|
|
|
3
3
|
*
|
|
4
4
|
* It draws bytefly's nameplate: a two-tone mark beside the agent identity (Pi
|
|
5
5
|
* and its version) and the chrome identity (the tagline, bytefly, and its
|
|
6
|
-
* version), a
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* version), a signature rule whose accent lead sits under the mark, and a
|
|
7
|
+
* grouped help block. The mark and text are theme-colored through the shared
|
|
8
|
+
* design-system roles, the mark falls back to a wordmark where half blocks do
|
|
9
|
+
* not render, the help keys are resolved live from Pi's keybindings, and every
|
|
10
|
+
* line is truncated to the width it is given.
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
import { keyText, type Theme, VERSION } from "@earendil-works/pi-coding-agent";
|
|
13
14
|
import { type Component, type TUI, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
14
|
-
import { screenHint } from "../../../lib/tui.ts";
|
|
15
|
-
import { keyHintText } from "../../../lib/
|
|
15
|
+
import { screenHint } from "../../../lib/tui/chrome.ts";
|
|
16
|
+
import { keyHintText } from "../../../lib/tui/glyphs.ts";
|
|
17
|
+
import { paint } from "../../../lib/tui/tokens.ts";
|
|
16
18
|
import { HEADER_CONFIG, type HelpEntry } from "./config.ts";
|
|
17
19
|
|
|
18
20
|
/** The header's shape, resolved once per apply so `render` stays cheap. */
|
|
@@ -33,9 +35,9 @@ function markBlock(line: string): string {
|
|
|
33
35
|
|
|
34
36
|
/** The two mark lines: the half-block logo, or the wordmark where it will not render. */
|
|
35
37
|
function markLines(theme: Theme, halfBlocks: boolean): [string, string] {
|
|
36
|
-
const {
|
|
37
|
-
if (halfBlocks) return [theme
|
|
38
|
-
return [theme
|
|
38
|
+
const { markRole, markAccentRole } = HEADER_CONFIG;
|
|
39
|
+
if (halfBlocks) return [paint(theme, markRole, "█▀█"), paint(theme, markAccentRole, "█▀ █")];
|
|
40
|
+
return [paint(theme, markRole, "P") + paint(theme, markAccentRole, "i"), ""];
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
/** One help entry split into the text to print and its description. */
|
|
@@ -61,20 +63,20 @@ export class ByteflyHeader implements Component {
|
|
|
61
63
|
const w = Math.max(1, width);
|
|
62
64
|
const theme = this.getTheme();
|
|
63
65
|
const view = this.view();
|
|
64
|
-
const { padX,
|
|
66
|
+
const { padX, nameRole, chromeRole, detailRole, chromeTagline, chromeName, chromeVersion, help } = HEADER_CONFIG;
|
|
65
67
|
const pad = " ".repeat(Math.min(padX, Math.max(0, w - 1)));
|
|
66
68
|
const [markTop, markBottom] = markLines(theme, view.halfBlocks);
|
|
67
69
|
|
|
68
|
-
const version = view.showVersion && this.version ? theme
|
|
70
|
+
const version = view.showVersion && this.version ? paint(theme, detailRole, `v${this.version}`) : "";
|
|
69
71
|
// The wordmark mark spells the app name, so the agent cell carries only the
|
|
70
72
|
// version there; the half-block mark does not, so the name stays beside it.
|
|
71
73
|
const agent = view.halfBlocks
|
|
72
74
|
? version
|
|
73
|
-
? `${theme.
|
|
74
|
-
: theme.
|
|
75
|
+
? `${theme.bold(paint(theme, nameRole, HEADER_CONFIG.appName))} ${version}`
|
|
76
|
+
: theme.bold(paint(theme, nameRole, HEADER_CONFIG.appName))
|
|
75
77
|
: version;
|
|
76
|
-
const chrome = `${theme
|
|
77
|
-
const chromeVersioned = chromeVersion ? `${chrome} ${theme
|
|
78
|
+
const chrome = `${paint(theme, detailRole, chromeTagline)} ${paint(theme, chromeRole, chromeName)}`;
|
|
79
|
+
const chromeVersioned = chromeVersion ? `${chrome} ${paint(theme, detailRole, `v${chromeVersion}`)}` : chrome;
|
|
78
80
|
|
|
79
81
|
// The identity rows shed as one block: chrome version, chrome line, agent.
|
|
80
82
|
const states: Array<readonly [string, string]> = [
|
|
@@ -89,11 +91,26 @@ export class ByteflyHeader implements Component {
|
|
|
89
91
|
return [
|
|
90
92
|
this.identityLine(pad, markTop, agentCell, w),
|
|
91
93
|
this.identityLine(pad, markBottom, chromeCell, w),
|
|
92
|
-
|
|
94
|
+
this.rule(theme, w, pad),
|
|
93
95
|
...help.map((row) => this.helpLine(theme, w, row)),
|
|
94
96
|
];
|
|
95
97
|
}
|
|
96
98
|
|
|
99
|
+
/**
|
|
100
|
+
* The nameplate's signature rule: an accent lead as wide as the mark, so the
|
|
101
|
+
* rule is anchored to the mark, then a muted fill to the edge.
|
|
102
|
+
*/
|
|
103
|
+
private rule(theme: Theme, width: number, pad: string): string {
|
|
104
|
+
const remaining = Math.max(0, width - pad.length);
|
|
105
|
+
const lead = Math.min(MARK_WIDTH, remaining);
|
|
106
|
+
const fill = remaining - lead;
|
|
107
|
+
return (
|
|
108
|
+
pad +
|
|
109
|
+
paint(theme, HEADER_CONFIG.ruleAccentRole, "─".repeat(lead)) +
|
|
110
|
+
paint(theme, HEADER_CONFIG.ruleRole, "─".repeat(fill))
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
97
114
|
/** A nameplate row: the padded mark, a gutter, then the identity cell if it survived. */
|
|
98
115
|
private identityLine(pad: string, mark: string, cell: string, width: number): string {
|
|
99
116
|
const line = cell ? `${pad}${markBlock(mark)} ${cell}` : `${pad}${markBlock(mark)}`;
|
|
@@ -3,14 +3,16 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The header is a nameplate: a two-tone mark beside the agent identity (Pi and
|
|
5
5
|
* its version) and the chrome identity (the tagline, bytefly, and its version),
|
|
6
|
-
* a
|
|
7
|
-
*
|
|
8
|
-
* keybindings, so a rebound key is
|
|
6
|
+
* a signature rule whose accent lead sits under the mark, and a grouped help
|
|
7
|
+
* block. Every color is a design-system role resolved by the active theme, and
|
|
8
|
+
* the help keys are resolved live from Pi's keybindings, so a rebound key is
|
|
9
|
+
* respected.
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
|
-
import type { AppKeybinding
|
|
12
|
+
import type { AppKeybinding } from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import { EXPAND_KEYBINDING } from "../../../lib/tui/glyphs.ts";
|
|
14
|
+
import type { TuiRole } from "../../../lib/tui/tokens.ts";
|
|
12
15
|
import pkg from "../../../package.json";
|
|
13
|
-
import { EXPAND_KEYBINDING } from "../../../lib/ui.ts";
|
|
14
16
|
|
|
15
17
|
/** One help entry: a Pi keybinding resolved live, or a raw key or command prefix. */
|
|
16
18
|
export type HelpEntry =
|
|
@@ -23,14 +25,20 @@ export interface HeaderConfig {
|
|
|
23
25
|
readonly appName: string;
|
|
24
26
|
/** Print Pi's version on the agent line. */
|
|
25
27
|
readonly showVersion: boolean;
|
|
26
|
-
/**
|
|
27
|
-
readonly
|
|
28
|
-
/**
|
|
29
|
-
readonly
|
|
30
|
-
/**
|
|
31
|
-
readonly
|
|
32
|
-
/**
|
|
33
|
-
readonly
|
|
28
|
+
/** Role for the mark's top line. */
|
|
29
|
+
readonly markRole: TuiRole;
|
|
30
|
+
/** Role for the mark's bottom line. */
|
|
31
|
+
readonly markAccentRole: TuiRole;
|
|
32
|
+
/** Role for the agent name. */
|
|
33
|
+
readonly nameRole: TuiRole;
|
|
34
|
+
/** Role for the chrome name. */
|
|
35
|
+
readonly chromeRole: TuiRole;
|
|
36
|
+
/** Role for the versions, the tagline, and the help descriptions. */
|
|
37
|
+
readonly detailRole: TuiRole;
|
|
38
|
+
/** Role for the rule's accent lead under the mark. */
|
|
39
|
+
readonly ruleAccentRole: TuiRole;
|
|
40
|
+
/** Role for the rest of the rule. */
|
|
41
|
+
readonly ruleRole: TuiRole;
|
|
34
42
|
/** Columns of left padding, matching Pi's built-in header indent. */
|
|
35
43
|
readonly padX: number;
|
|
36
44
|
/** The tagline before the chrome name, so the relationship reads in words. */
|
|
@@ -46,10 +54,13 @@ export interface HeaderConfig {
|
|
|
46
54
|
export const HEADER_CONFIG: HeaderConfig = {
|
|
47
55
|
appName: "Pi",
|
|
48
56
|
showVersion: true,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
57
|
+
markRole: "accent",
|
|
58
|
+
markAccentRole: "warn",
|
|
59
|
+
nameRole: "label",
|
|
60
|
+
chromeRole: "meta",
|
|
61
|
+
detailRole: "faint",
|
|
62
|
+
ruleAccentRole: "accent",
|
|
63
|
+
ruleRole: "rule",
|
|
53
64
|
padX: 1,
|
|
54
65
|
chromeTagline: "enhanced by",
|
|
55
66
|
chromeName: "bytefly",
|
|
@@ -74,7 +85,6 @@ export const HEADER_CONFIG: HeaderConfig = {
|
|
|
74
85
|
{ raw: "!", description: "bash" },
|
|
75
86
|
{ raw: "!!", description: "bash (no context)" },
|
|
76
87
|
{ raw: "/bytefly", description: "extensions" },
|
|
77
|
-
{ raw: "/doctor", description: "checks" },
|
|
78
88
|
{ binding: "app.message.followUp", description: "to queue follow-up" },
|
|
79
89
|
{ binding: "app.clipboard.pasteImage", description: "paste files" },
|
|
80
90
|
],
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* feature folders: `header/` (the themed startup mark and version), `footer/`
|
|
4
4
|
* (the two-line, width-adaptive status bar with its price calculation),
|
|
5
5
|
* `separator/` (the transcript turn rule), `panel/` (the unified `/bytefly`
|
|
6
|
-
* screen
|
|
6
|
+
* screen and its `doctor` report), `doctor/` (the
|
|
7
7
|
* `run_doctor` tool and its session-scoped probe runtime), and `notify/` (OS
|
|
8
8
|
* notifications for unattended runs).
|
|
9
9
|
*
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The `/bytefly` surface: one
|
|
3
|
-
* `/extensions` and `/doctor` registered against the same handler.
|
|
2
|
+
* The `/bytefly` surface: one command, `/bytefly [doctor] [extension] [refresh]`.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* Without `doctor` it opens the extension screen: the dock in `tui`, the
|
|
5
|
+
* grouped listing elsewhere. With `doctor` it produces the doctor report — the
|
|
6
|
+
* same text `run_doctor` returns — for the whole set or one named extension; in
|
|
7
|
+
* `tui` the report is the dock screen. Discovery, the doctor probe, and the
|
|
8
|
+
* dirty-to-reload confirm all live here. The doctor runtime is injected by the
|
|
9
|
+
* composition root, so `run_doctor` and the screen keep reading one cache.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
@@ -14,10 +14,10 @@ import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
|
14
14
|
import { sanitizeLine } from "../../../lib/format.ts";
|
|
15
15
|
import { withRailsSuppressed } from "../../../lib/rails.ts";
|
|
16
16
|
import { REQUIREMENTS, type ExtensionReport } from "../../../lib/requirements.ts";
|
|
17
|
-
import { formatExtension, hasProblems } from "../doctor/format.ts";
|
|
17
|
+
import { formatExtension, formatReport, hasProblems } from "../doctor/format.ts";
|
|
18
18
|
import type { DoctorRuntime } from "../doctor/runtime.ts";
|
|
19
19
|
import { discoverExtensions, type DiscoveryDeps } from "../picker/discovery.ts";
|
|
20
|
-
import { createExtensionsPickerRuntime } from "../picker/runtime.ts";
|
|
20
|
+
import { createExtensionsPickerRuntime, type ExtensionsPickerRuntime } from "../picker/runtime.ts";
|
|
21
21
|
import type { Discovery } from "../picker/types.ts";
|
|
22
22
|
import { formatListing } from "./format.ts";
|
|
23
23
|
import { buildPanelModel, extensionName } from "./join.ts";
|
|
@@ -42,20 +42,63 @@ function selectableNames(discovery: Discovery): Set<string> {
|
|
|
42
42
|
return names;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
/**
|
|
45
|
+
/** Argument completions: the `doctor` subcommand, every declared extension, then `refresh`. */
|
|
46
46
|
function completions(prefix: string): Array<{ value: string; label: string }> | null {
|
|
47
|
-
const options = [...Object.keys(REQUIREMENTS), "refresh"].filter((option) => option.startsWith(prefix));
|
|
47
|
+
const options = ["doctor", ...Object.keys(REQUIREMENTS), "refresh"].filter((option) => option.startsWith(prefix));
|
|
48
48
|
return options.length > 0 ? options.map((value) => ({ value, label: value })) : null;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
/**
|
|
51
|
+
/** Which bytefly surface the arguments select. */
|
|
52
|
+
type PanelMode = "screen" | "doctor";
|
|
53
|
+
|
|
54
|
+
/** `/bytefly [doctor] [extension] [refresh]`, parsed. */
|
|
55
|
+
interface PanelArgs {
|
|
56
|
+
mode: PanelMode;
|
|
57
|
+
/** The extension to narrow to, when one is named. */
|
|
58
|
+
name?: string;
|
|
59
|
+
/** Re-probe the doctor instead of reusing the session cache. */
|
|
60
|
+
refresh: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Parse `/bytefly [doctor] [extension] [refresh]`; extra tokens are ignored. */
|
|
64
|
+
function parseArgs(args: string): PanelArgs {
|
|
65
|
+
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
66
|
+
const refresh = parts.includes("refresh");
|
|
67
|
+
const rest = parts.filter((part) => part !== "refresh");
|
|
68
|
+
const mode: PanelMode = rest[0] === "doctor" ? "doctor" : "screen";
|
|
69
|
+
return { mode, name: rest[mode === "doctor" ? 1 : 0], refresh };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Print one extension's full findings, or say why it has none. The screen's
|
|
74
|
+
* named form and the `doctor` subcommand share this text path.
|
|
75
|
+
*/
|
|
76
|
+
function notifyExtension(
|
|
77
|
+
ctx: ExtensionCommandContext,
|
|
78
|
+
name: string,
|
|
79
|
+
reports: readonly ExtensionReport[],
|
|
80
|
+
runtime: ExtensionsPickerRuntime,
|
|
81
|
+
): void {
|
|
82
|
+
const report = reports.find((entry) => entry.extension === name);
|
|
83
|
+
if (report) {
|
|
84
|
+
ctx.ui.notify(formatExtension(report), hasProblems([report]) ? "warning" : "info");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const item = runtime.items().find((entry) => extensionName(entry.path) === name);
|
|
88
|
+
ctx.ui.notify(
|
|
89
|
+
item
|
|
90
|
+
? `bytefly: ${sanitizeLine(item.path)} — no doctor report`
|
|
91
|
+
: `bytefly: no extension named "${sanitizeLine(name)}"`,
|
|
92
|
+
"info",
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Register the single `/bytefly` command. */
|
|
52
97
|
export function registerPanel(pi: ExtensionAPI, deps: PanelCommandDeps): void {
|
|
53
98
|
const discover = deps.discover ?? discoverExtensions;
|
|
54
99
|
|
|
55
100
|
const handler = async (args: string, ctx: ExtensionCommandContext): Promise<void> => {
|
|
56
|
-
const
|
|
57
|
-
const refresh = parts.includes("refresh");
|
|
58
|
-
const name = parts.find((part) => part !== "refresh");
|
|
101
|
+
const { mode, name, refresh } = parseArgs(args);
|
|
59
102
|
|
|
60
103
|
const cwd = ctx.cwd ?? process.cwd();
|
|
61
104
|
const agentDir = getAgentDir();
|
|
@@ -88,18 +131,11 @@ export function registerPanel(pi: ExtensionAPI, deps: PanelCommandDeps): void {
|
|
|
88
131
|
|
|
89
132
|
if (ctx.mode !== "tui") {
|
|
90
133
|
if (name !== undefined) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const item = runtime.items().find((entry) => extensionName(entry.path) === name);
|
|
97
|
-
ctx.ui.notify(
|
|
98
|
-
item
|
|
99
|
-
? `bytefly: ${sanitizeLine(item.path)} — no doctor report`
|
|
100
|
-
: `bytefly: no extension named "${sanitizeLine(name)}"`,
|
|
101
|
-
"info",
|
|
102
|
-
);
|
|
134
|
+
notifyExtension(ctx, name, reports, runtime);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (mode === "doctor") {
|
|
138
|
+
ctx.ui.notify(formatReport(reports), hasProblems(reports) ? "warning" : "info");
|
|
103
139
|
return;
|
|
104
140
|
}
|
|
105
141
|
const model = buildPanelModel({ items: runtime.items(), reports, agentDir });
|
|
@@ -136,12 +172,10 @@ export function registerPanel(pi: ExtensionAPI, deps: PanelCommandDeps): void {
|
|
|
136
172
|
if (reload) await ctx.reload();
|
|
137
173
|
};
|
|
138
174
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
pi.registerCommand(command, { description, getArgumentCompletions: completions, handler });
|
|
146
|
-
}
|
|
175
|
+
pi.registerCommand("bytefly", {
|
|
176
|
+
description:
|
|
177
|
+
"Open the bytefly extension screen: /bytefly [extension] [refresh]; /bytefly doctor [extension] [refresh] prints the doctor report",
|
|
178
|
+
getArgumentCompletions: completions,
|
|
179
|
+
handler,
|
|
180
|
+
});
|
|
147
181
|
}
|
|
@@ -8,18 +8,18 @@
|
|
|
8
8
|
* came from a probe is sanitized by `doctor/format.ts`.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import type {
|
|
12
|
-
import { GLYPHS } from "../../../lib/
|
|
11
|
+
import type { TuiRole } from "../../../lib/tui/tokens.ts";
|
|
12
|
+
import { GLYPHS } from "../../../lib/tui/glyphs.ts";
|
|
13
13
|
import { reportSeverity, reportSummary } from "../doctor/format.ts";
|
|
14
14
|
import type { PanelModel, PanelRow } from "./types.ts";
|
|
15
15
|
|
|
16
16
|
/** The settings scope the screen shows. */
|
|
17
17
|
export type PanelScope = "global" | "project";
|
|
18
18
|
|
|
19
|
-
/** One segment of the headline, with its
|
|
19
|
+
/** One segment of the headline, with its design role for the TUI. */
|
|
20
20
|
export interface SummarySegment {
|
|
21
21
|
text: string;
|
|
22
|
-
color:
|
|
22
|
+
color: TuiRole;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
/** Row and problem counts over a model. */
|
|
@@ -67,19 +67,19 @@ export function summarySegments(model: PanelModel, scope: PanelScope, options: S
|
|
|
67
67
|
const counts = panelCounts(model);
|
|
68
68
|
const segments: SummarySegment[] = [{ text: scope === "global" ? "Global" : "Project", color: "accent" }];
|
|
69
69
|
if (options.filter !== undefined) {
|
|
70
|
-
segments.push({ text: `${options.filter} only`, color: "
|
|
70
|
+
segments.push({ text: `${options.filter} only`, color: "meta" });
|
|
71
71
|
} else {
|
|
72
72
|
const noun = options.compact ? "ext" : counts.extensions === 1 ? "extension" : "extensions";
|
|
73
|
-
segments.push({ text: `${counts.extensions} ${noun}`, color: "
|
|
73
|
+
segments.push({ text: `${counts.extensions} ${noun}`, color: "meta" });
|
|
74
74
|
if (counts.disabled > 0) {
|
|
75
|
-
segments.push({ text: `${counts.disabled} ${options.compact ? "off" : "disabled"}`, color: "
|
|
75
|
+
segments.push({ text: `${counts.disabled} ${options.compact ? "off" : "disabled"}`, color: "meta" });
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
const problems: string[] = [];
|
|
79
79
|
if (counts.missing > 0) problems.push(`${counts.missing} missing`);
|
|
80
80
|
if (counts.degraded > 0) problems.push(`${counts.degraded} degraded`);
|
|
81
81
|
if (problems.length > 0) {
|
|
82
|
-
segments.push({ text: problems.join(", "), color: counts.missing > 0 ? "
|
|
82
|
+
segments.push({ text: problems.join(", "), color: counts.missing > 0 ? "bad" : "warn" });
|
|
83
83
|
}
|
|
84
84
|
return segments;
|
|
85
85
|
}
|