@gavin-hu/my-pi-agent 0.11.4 → 0.13.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 +38 -26
- package/README.zh-CN.md +33 -23
- package/extensions/bytefly/README.md +381 -225
- package/extensions/bytefly/chrome.ts +7 -2
- package/extensions/bytefly/doctor/register.ts +6 -0
- package/extensions/bytefly/doctor/runtime.ts +52 -6
- package/extensions/bytefly/doctor/tools.ts +1 -3
- package/extensions/bytefly/footer/collect/snapshot.ts +12 -2
- package/extensions/bytefly/footer/config.ts +7 -1
- package/extensions/bytefly/footer/delegation.ts +100 -0
- package/extensions/bytefly/footer/price/config.ts +54 -15
- package/extensions/bytefly/footer/price/price.ts +20 -7
- package/extensions/bytefly/footer/price/types.ts +13 -7
- package/extensions/bytefly/footer/register.ts +13 -2
- package/extensions/bytefly/footer/render/format.ts +3 -2
- package/extensions/bytefly/footer/render/lines.ts +2 -0
- package/extensions/bytefly/footer/render/segments.ts +30 -2
- package/extensions/bytefly/footer/types.ts +12 -0
- package/extensions/bytefly/header/component.ts +21 -6
- package/extensions/bytefly/header/config.ts +3 -0
- package/extensions/bytefly/header/register.ts +34 -3
- package/extensions/bytefly/index.ts +15 -4
- package/extensions/bytefly/notify/config.ts +2 -0
- package/extensions/bytefly/notify/format.ts +19 -1
- package/extensions/bytefly/notify/register.ts +37 -13
- package/extensions/bytefly/notify/types.ts +2 -0
- package/extensions/bytefly/panel/commands.ts +34 -13
- package/extensions/bytefly/panel/settings-runtime.ts +71 -28
- package/extensions/bytefly/panel/settings.ts +293 -48
- package/extensions/bytefly/panel/tui/entries.ts +58 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +86 -0
- package/extensions/bytefly/panel/tui/frame.ts +71 -0
- package/extensions/bytefly/panel/tui/nav.ts +75 -0
- package/extensions/bytefly/panel/tui/settings-view.ts +115 -0
- package/extensions/bytefly/panel/tui.ts +205 -319
- package/extensions/bytefly/picker/runtime.ts +61 -2
- package/extensions/bytefly/picker/toggle.ts +56 -0
- package/extensions/bytefly/update/cache.ts +73 -0
- package/extensions/bytefly/update/check.ts +59 -0
- package/extensions/bytefly/update/format.ts +87 -0
- package/extensions/bytefly/update/register.ts +158 -0
- package/extensions/bytefly/update/state.ts +40 -0
- package/extensions/bytefly/update/version.ts +83 -0
- package/extensions/document/README.md +24 -14
- package/extensions/document/config.ts +4 -0
- package/extensions/document/deadline.ts +61 -0
- package/extensions/document/tool.ts +14 -5
- package/extensions/file-browse/README.md +12 -8
- package/extensions/file-browse/client.ts +6 -7
- package/extensions/file-browse/files.ts +0 -4
- package/extensions/file-browse/git.ts +9 -10
- package/extensions/file-browse/html.ts +17 -7
- package/extensions/file-browse/router.ts +24 -15
- package/extensions/file-open/README.md +30 -15
- package/extensions/file-open/index.ts +4 -3
- package/extensions/file-open/open.ts +52 -21
- package/extensions/goal/README.md +45 -13
- package/extensions/goal/clarify-prompt.ts +5 -36
- package/extensions/goal/clarify.ts +55 -86
- package/extensions/goal/commands.ts +41 -35
- package/extensions/goal/format.ts +12 -4
- package/extensions/goal/schema.ts +6 -2
- package/extensions/goal/tools.ts +28 -8
- package/extensions/goal/tui.ts +1 -1
- package/extensions/goal/types.ts +6 -2
- package/extensions/job/README.md +16 -12
- package/extensions/job/runtime.ts +23 -6
- package/extensions/job/status.ts +4 -2
- package/extensions/memory/README.md +56 -14
- package/extensions/memory/commands.ts +20 -2
- package/extensions/memory/doctor.ts +137 -0
- package/extensions/memory/format.ts +24 -7
- package/extensions/memory/runtime.ts +49 -0
- package/extensions/memory/store.ts +31 -0
- package/extensions/memory/tui.ts +136 -12
- package/extensions/plan/README.md +83 -36
- package/extensions/plan/blocks.ts +1 -35
- package/extensions/plan/commands.ts +84 -19
- package/extensions/plan/index.ts +35 -11
- package/extensions/plan/plan-index.ts +234 -0
- package/extensions/plan/plan-text.ts +95 -0
- package/extensions/plan/plans.ts +91 -2
- package/extensions/plan/policy.ts +4 -3
- package/extensions/plan/review.ts +246 -0
- package/extensions/plan/runtime.ts +73 -24
- package/extensions/plan/steps.ts +60 -39
- package/extensions/plan/todos.ts +29 -60
- package/extensions/plan/tools.ts +188 -244
- package/extensions/plan/tui.ts +18 -31
- package/extensions/plan/types.ts +12 -0
- package/extensions/plan/web-client.ts +51 -18
- package/extensions/plan/web-page.ts +7 -2
- package/extensions/plan/web-router.ts +11 -3
- package/extensions/plan/web.ts +106 -41
- package/extensions/question/README.md +1 -1
- package/extensions/question/tools.ts +1 -1
- package/extensions/rewind/README.md +28 -8
- package/extensions/rewind/commands.ts +1 -1
- package/extensions/rewind/format.ts +4 -4
- package/extensions/rewind/git.ts +56 -18
- package/extensions/rewind/index.ts +3 -5
- package/extensions/rewind/path-picker.ts +198 -0
- package/extensions/rewind/paths.ts +128 -0
- package/extensions/rewind/restore.ts +69 -28
- package/extensions/rewind/rewind.ts +80 -3
- package/extensions/rewind/runtime.ts +13 -5
- package/extensions/rewind/snapshot-run.ts +5 -2
- package/extensions/rewind/types.ts +2 -0
- package/extensions/subagent/README.md +7 -1
- package/extensions/subagent/accounting.ts +34 -0
- package/extensions/subagent/index.ts +23 -3
- package/extensions/subagent/stream.ts +2 -0
- package/extensions/subagent/types.ts +2 -0
- package/extensions/todo/README.md +19 -11
- package/extensions/todo/index.ts +5 -3
- package/extensions/todo/nudge.ts +58 -9
- package/extensions/todo/runtime.ts +28 -17
- package/extensions/watch/README.md +8 -3
- package/extensions/watch/index.ts +19 -0
- package/extensions/web-access/README.md +46 -16
- package/extensions/web-access/charset.ts +58 -0
- package/extensions/web-access/fetch/find.ts +11 -7
- package/extensions/web-access/fetch/format.ts +64 -18
- package/extensions/web-access/fetch/page.ts +5 -1
- package/extensions/web-access/fetch/tool.ts +20 -21
- package/extensions/web-access/http.ts +49 -8
- package/extensions/web-access/search/facts.ts +38 -0
- package/extensions/web-access/search/tool.ts +14 -3
- package/extensions/web-access/text.ts +50 -11
- package/extensions/wechat/README.md +5 -4
- package/extensions/wechat/tools.ts +0 -2
- package/extensions/worktree/README.md +74 -15
- package/extensions/worktree/config.ts +11 -0
- package/extensions/worktree/exit.ts +35 -3
- package/extensions/worktree/gaps.ts +74 -0
- package/extensions/worktree/guard.ts +50 -4
- package/extensions/worktree/handoff.ts +55 -0
- package/extensions/worktree/index.ts +12 -2
- package/extensions/worktree/runtime.ts +20 -0
- package/extensions/worktree/status.ts +15 -2
- package/extensions/worktree/tools.ts +3 -2
- package/lib/README.md +12 -9
- package/lib/config.ts +43 -1
- package/lib/env.ts +12 -0
- package/lib/events.ts +86 -1
- package/lib/goal-state.ts +95 -0
- package/lib/requirements.ts +340 -46
- package/lib/todo-state.ts +62 -0
- package/lib/tui/README.md +91 -0
- package/lib/tui/glyphs.ts +2 -0
- package/lib/web/layout.ts +12 -2
- package/package.json +5 -3
- package/themes/bytefly-dark.json +1 -1
- package/assets/bytefly.schema.json +0 -785
- 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-cancelled.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-long-subject.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-running.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-rejected.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
|
@@ -171,13 +171,41 @@ export function windowSegment(snapshot: StatusSnapshot, theme: Theme): Segment |
|
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
export function costSegment(snapshot: StatusSnapshot, theme: Theme): Segment | null {
|
|
174
|
-
|
|
174
|
+
const { cost } = snapshot.usage;
|
|
175
|
+
if (cost === 0) return null;
|
|
175
176
|
return {
|
|
176
177
|
id: "cost",
|
|
177
178
|
weight: 2,
|
|
178
179
|
droppable: true,
|
|
179
180
|
separator: faint(theme, CONFIG.separators.item),
|
|
180
|
-
forms: [paint(theme, "meta", formatCost(
|
|
181
|
+
forms: [paint(theme, "meta", formatCost(cost, snapshot.currency))],
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Child-run spend, next to the session's own cost and never inside it: a
|
|
187
|
+
* subagent runs in its own process, so its tokens are not the session's. The
|
|
188
|
+
* roll-up covers the ledger's retained window (the last 500 runs).
|
|
189
|
+
*/
|
|
190
|
+
export function delegationSegment(snapshot: StatusSnapshot, theme: Theme): Segment | null {
|
|
191
|
+
const { runs, usage } = snapshot.delegation;
|
|
192
|
+
if (runs === 0) return null;
|
|
193
|
+
const icon = CONFIG.icons.delegation;
|
|
194
|
+
const cost = formatCost(usage.cost, snapshot.currency);
|
|
195
|
+
// Detailed first; the run count survives longer than the price, because "some
|
|
196
|
+
// delegation happened" is the part a narrow bar can still say.
|
|
197
|
+
const forms = [
|
|
198
|
+
`${paint(theme, "meta", `${icon} ${runs} · ${cost}`)}`,
|
|
199
|
+
`${paint(theme, "meta", `${icon} ${cost}`)}`,
|
|
200
|
+
paint(theme, "meta", `${icon} ${runs}`),
|
|
201
|
+
paint(theme, "meta", icon),
|
|
202
|
+
];
|
|
203
|
+
return {
|
|
204
|
+
id: "delegation",
|
|
205
|
+
weight: 4,
|
|
206
|
+
droppable: true,
|
|
207
|
+
separator: faint(theme, CONFIG.separators.item),
|
|
208
|
+
forms,
|
|
181
209
|
};
|
|
182
210
|
}
|
|
183
211
|
|
|
@@ -32,6 +32,7 @@ export interface UsageTotals {
|
|
|
32
32
|
output: number;
|
|
33
33
|
cacheRead: number;
|
|
34
34
|
cacheWrite: number;
|
|
35
|
+
/** Cost in the configured currency: rated in CNY mode, Pi's recorded amount in USD mode. */
|
|
35
36
|
cost: number;
|
|
36
37
|
}
|
|
37
38
|
|
|
@@ -69,6 +70,17 @@ export interface StatusSnapshot {
|
|
|
69
70
|
/** Latest cache hit rate as a percentage, or `null` when unknown. */
|
|
70
71
|
cacheHitRate: number | null;
|
|
71
72
|
providerCount: number;
|
|
73
|
+
/**
|
|
74
|
+
* What this session's delegated child runs spent, priced like `usage` but kept
|
|
75
|
+
* apart from it: the session total never includes child spend.
|
|
76
|
+
*/
|
|
77
|
+
delegation: DelegationTotals;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Child-run totals: how many runs finished, and what the retained window spent. */
|
|
81
|
+
export interface DelegationTotals {
|
|
82
|
+
runs: number;
|
|
83
|
+
usage: UsageTotals;
|
|
72
84
|
}
|
|
73
85
|
|
|
74
86
|
/** One renderable piece of a line. */
|
|
@@ -17,6 +17,7 @@ import { keyHintText } from "../../../lib/tui/glyphs.ts";
|
|
|
17
17
|
import { BODY_INDENT } from "../../../lib/tui/indent.ts";
|
|
18
18
|
import { RULE_GLYPH } from "../../../lib/tui/rules.ts";
|
|
19
19
|
import { paint } from "../../../lib/tui/tokens.ts";
|
|
20
|
+
import { updateMarker } from "../update/format.ts";
|
|
20
21
|
import { HEADER_CONFIG, type HelpEntry } from "./config.ts";
|
|
21
22
|
|
|
22
23
|
/** The header's shape, resolved once per apply so `render` stays cheap. */
|
|
@@ -27,6 +28,8 @@ export interface HeaderView {
|
|
|
27
28
|
halfBlocks: boolean;
|
|
28
29
|
/** Columns of left margin: Pi's output pad, so the nameplate sits on the transcript margin. */
|
|
29
30
|
pad: number;
|
|
31
|
+
/** A newer published version of this package, when one is known. */
|
|
32
|
+
latest?: string;
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
/** The mark block is four columns on both rows, so the identity column aligns. */
|
|
@@ -78,7 +81,7 @@ export class ByteflyHeader implements Component {
|
|
|
78
81
|
const w = Math.max(1, width);
|
|
79
82
|
const theme = this.getTheme();
|
|
80
83
|
const view = this.view();
|
|
81
|
-
const viewKey = `${view.showVersion}:${view.halfBlocks}:${view.pad}`;
|
|
84
|
+
const viewKey = `${view.showVersion}:${view.halfBlocks}:${view.pad}:${view.latest ?? ""}`;
|
|
82
85
|
const keys = this.keySignature();
|
|
83
86
|
if (
|
|
84
87
|
this.cachedWidth === w &&
|
|
@@ -106,7 +109,8 @@ export class ByteflyHeader implements Component {
|
|
|
106
109
|
}
|
|
107
110
|
|
|
108
111
|
private build(w: number, theme: Theme, view: HeaderView): string[] {
|
|
109
|
-
const { nameRole, chromeRole, detailRole, chromeTagline, chromeName, chromeVersion, help } =
|
|
112
|
+
const { nameRole, chromeRole, detailRole, updateRole, chromeTagline, chromeName, chromeVersion, help } =
|
|
113
|
+
HEADER_CONFIG;
|
|
110
114
|
const padWidth = Math.max(0, Math.min(Math.floor(view.pad), Math.max(0, w - 1)));
|
|
111
115
|
const pad = " ".repeat(padWidth);
|
|
112
116
|
const [markTop, markBottom] = markLines(theme, view.halfBlocks);
|
|
@@ -120,11 +124,18 @@ export class ByteflyHeader implements Component {
|
|
|
120
124
|
: theme.bold(paint(theme, nameRole, HEADER_CONFIG.appName))
|
|
121
125
|
: version;
|
|
122
126
|
const chrome = `${paint(theme, detailRole, chromeTagline)} ${paint(theme, chromeRole, chromeName)}`;
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
127
|
+
const chromeWithVersion = chromeVersion ? `${chrome} ${paint(theme, detailRole, `v${chromeVersion}`)}` : chrome;
|
|
128
|
+
// The published version follows the loaded one: `v0.11.4 ↑0.12.0`.
|
|
129
|
+
const chromeWithMarker =
|
|
130
|
+
chromeVersion && view.latest
|
|
131
|
+
? `${chromeWithVersion} ${paint(theme, updateRole, updateMarker(view.latest))}`
|
|
132
|
+
: chromeWithVersion;
|
|
133
|
+
|
|
134
|
+
// The identity rows shed as one block: the update marker first, then the
|
|
135
|
+
// chrome version, the chrome line, and finally the agent.
|
|
126
136
|
const states: Array<readonly [string, string]> = [
|
|
127
|
-
[agent,
|
|
137
|
+
[agent, chromeWithMarker],
|
|
138
|
+
[agent, chromeWithVersion],
|
|
128
139
|
[agent, chrome],
|
|
129
140
|
[agent, ""],
|
|
130
141
|
["", ""],
|
|
@@ -132,7 +143,11 @@ export class ByteflyHeader implements Component {
|
|
|
132
143
|
const lineWidth = (cell: string): number => pad.length + MARK_WIDTH + (cell ? 1 + visibleWidth(cell) : 0);
|
|
133
144
|
const [agentCell, chromeCell] = states.find(([a, c]) => Math.max(lineWidth(a), lineWidth(c)) <= w) ?? ["", ""];
|
|
134
145
|
|
|
146
|
+
// The header owns its leading blank: Pi mounts the built-in header inside
|
|
147
|
+
// `Spacer(1)`s, but the `quietStartup: true` branch that hands this header the
|
|
148
|
+
// screen adds none, and `setHeader` swaps only the header child.
|
|
135
149
|
return [
|
|
150
|
+
"",
|
|
136
151
|
this.identityLine(pad, markTop, agentCell, w),
|
|
137
152
|
this.identityLine(pad, markBottom, chromeCell, w),
|
|
138
153
|
this.rule(theme, w, pad),
|
|
@@ -35,6 +35,8 @@ export interface HeaderConfig {
|
|
|
35
35
|
readonly chromeRole: TuiRole;
|
|
36
36
|
/** Role for the versions, the tagline, and the help descriptions. */
|
|
37
37
|
readonly detailRole: TuiRole;
|
|
38
|
+
/** Role for the update marker: an action, so the accent rather than a warning colour. */
|
|
39
|
+
readonly updateRole: TuiRole;
|
|
38
40
|
/** Role for the rule's accent lead under the mark. */
|
|
39
41
|
readonly ruleAccentRole: TuiRole;
|
|
40
42
|
/** Role for the rest of the rule. */
|
|
@@ -57,6 +59,7 @@ export const HEADER_CONFIG: HeaderConfig = {
|
|
|
57
59
|
nameRole: "label",
|
|
58
60
|
chromeRole: "meta",
|
|
59
61
|
detailRole: "faint",
|
|
62
|
+
updateRole: "accent",
|
|
60
63
|
ruleAccentRole: "accent",
|
|
61
64
|
ruleRole: "rule",
|
|
62
65
|
chromeTagline: "enhanced by",
|
|
@@ -9,21 +9,40 @@
|
|
|
9
9
|
* `pi config` remains the way to turn all of bytefly's chrome off. The header
|
|
10
10
|
* is installed only in interactive (`tui`) sessions; every other mode leaves
|
|
11
11
|
* Pi's built-in header in place, which does not exist there.
|
|
12
|
+
*
|
|
13
|
+
* The update check resolves after this install, so the header reads the newest
|
|
14
|
+
* known version through `UpdateSource` and `refresh()` re-applies the last
|
|
15
|
+
* context's header once one arrives. That is the only reason the header is ever
|
|
16
|
+
* installed twice.
|
|
12
17
|
*/
|
|
13
18
|
|
|
14
19
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
15
20
|
import { isAppleTerminalSession } from "@earendil-works/pi-tui";
|
|
16
21
|
import { DEFAULT_HOST_PAD } from "../pad.ts";
|
|
22
|
+
import { createUpdateState, type UpdateSource } from "../update/state.ts";
|
|
17
23
|
import { createHeader, type HeaderView } from "./component.ts";
|
|
18
24
|
import { HEADER_CONFIG } from "./config.ts";
|
|
19
25
|
|
|
20
|
-
/**
|
|
21
|
-
export
|
|
26
|
+
/** The installed header, and the way to re-install it after a change. */
|
|
27
|
+
export interface HeaderHandle {
|
|
28
|
+
/** Re-apply the last session's header, picking up the current update marker. */
|
|
29
|
+
refresh(): void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Register the install/restore lifecycle for the custom header.
|
|
34
|
+
*
|
|
35
|
+
* `updates` is the store the update check writes, so the marker appears when a
|
|
36
|
+
* newer version is known. A test that does not exercise the marker may omit it.
|
|
37
|
+
*/
|
|
38
|
+
export function registerHeader(pi: ExtensionAPI, updates: UpdateSource = createUpdateState()): HeaderHandle {
|
|
22
39
|
// Half blocks misalign in Apple Terminal, so the mark falls back to a wordmark
|
|
23
40
|
// there, mirroring Pi's own built-in header.
|
|
24
41
|
const halfBlocks = !isAppleTerminalSession();
|
|
42
|
+
let last: ExtensionContext | undefined;
|
|
25
43
|
|
|
26
44
|
const apply = (ctx: ExtensionContext): void => {
|
|
45
|
+
last = ctx;
|
|
27
46
|
if (ctx.mode !== "tui") return;
|
|
28
47
|
// Bytefly's startup TUI is on only when Pi's `quietStartup` is true, which
|
|
29
48
|
// hides Pi's built-in startup header. Every other value (`false`,
|
|
@@ -36,14 +55,26 @@ export function registerHeader(pi: ExtensionAPI): void {
|
|
|
36
55
|
// value the tool frame and the notice inset by. An older host without
|
|
37
56
|
// `getSettings` keeps the default margin.
|
|
38
57
|
const pad = pi.getSettings?.().outputPad ?? DEFAULT_HOST_PAD;
|
|
39
|
-
const view = (): HeaderView => ({
|
|
58
|
+
const view = (): HeaderView => ({
|
|
59
|
+
showVersion: HEADER_CONFIG.showVersion,
|
|
60
|
+
halfBlocks,
|
|
61
|
+
pad,
|
|
62
|
+
latest: updates.latest(),
|
|
63
|
+
});
|
|
40
64
|
ctx.ui.setHeader(createHeader(() => ctx.ui.theme, view));
|
|
41
65
|
};
|
|
42
66
|
|
|
43
67
|
pi.on("session_start", (_event, ctx) => apply(ctx));
|
|
44
68
|
pi.on("session_tree", (_event, ctx) => apply(ctx));
|
|
45
69
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
70
|
+
last = undefined;
|
|
46
71
|
if (ctx.mode !== "tui") return;
|
|
47
72
|
ctx.ui.setHeader(undefined);
|
|
48
73
|
});
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
refresh() {
|
|
77
|
+
if (last) apply(last);
|
|
78
|
+
},
|
|
79
|
+
};
|
|
49
80
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* bytefly — the package's terminal chrome and diagnostics, composed from
|
|
2
|
+
* bytefly — the package's terminal chrome and diagnostics, composed from eleven
|
|
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
|
* `message/` (the user-message nameplate), `notice/` (the shared transcript
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* transcript turn rule), `working/` (the live elapsed counter on Pi's working
|
|
8
8
|
* row), `panel/` (the unified `/bytefly` screen and its
|
|
9
9
|
* `doctor` report), `doctor/` (the `run_doctor` tool and its session-scoped
|
|
10
|
-
* probe runtime),
|
|
10
|
+
* probe runtime), `notify/` (OS notifications for unattended runs), and
|
|
11
|
+
* `update/` (the published-version check behind the header's update marker).
|
|
11
12
|
*
|
|
12
13
|
* The header, footer, turn rule, and working timer are installed only in
|
|
13
14
|
* interactive (`tui`) sessions, and are always on; the user-message marker,
|
|
@@ -34,6 +35,8 @@ import { type NotifyDeps, registerNotify } from "./notify/register.ts";
|
|
|
34
35
|
import { createPadStore } from "./pad.ts";
|
|
35
36
|
import { type PanelCommandDeps, registerPanel } from "./panel/commands.ts";
|
|
36
37
|
import { registerTurnSeparator } from "./separator/register.ts";
|
|
38
|
+
import { registerUpdate, type UpdateDeps } from "./update/register.ts";
|
|
39
|
+
import { createUpdateState } from "./update/state.ts";
|
|
37
40
|
import { registerWorkingTimer } from "./working/register.ts";
|
|
38
41
|
|
|
39
42
|
/** Host seams, injected by tests. */
|
|
@@ -44,13 +47,18 @@ export interface ByteflyDeps {
|
|
|
44
47
|
doctor?: DoctorDeps;
|
|
45
48
|
/** Seams for the notify capability. */
|
|
46
49
|
notify?: NotifyDeps;
|
|
50
|
+
/** Seams for the published-version check. */
|
|
51
|
+
update?: UpdateDeps;
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
/** Register every bytefly surface; the factory only registers. */
|
|
50
55
|
export default function bytefly(pi: ExtensionAPI, deps: ByteflyDeps = {}): void {
|
|
51
56
|
if (!isExtensionEnabled("bytefly")) return;
|
|
52
57
|
|
|
53
|
-
|
|
58
|
+
// One store per load: the update check writes the newest published version and
|
|
59
|
+
// the header reads it, so the marker and every re-install agree.
|
|
60
|
+
const updates = createUpdateState();
|
|
61
|
+
const header = registerHeader(pi, updates);
|
|
54
62
|
registerFooter(pi);
|
|
55
63
|
registerUserMessage(pi);
|
|
56
64
|
// One store per load: the tool frame and the notice record Pi's output pad and
|
|
@@ -64,6 +72,9 @@ export default function bytefly(pi: ExtensionAPI, deps: ByteflyDeps = {}): void
|
|
|
64
72
|
registerTurnSeparator(pi, pad);
|
|
65
73
|
registerWorkingTimer(pi);
|
|
66
74
|
const doctor = registerDoctor(pi, deps.doctor);
|
|
67
|
-
registerPanel(pi, { doctor, discover: deps.extensions?.discover });
|
|
68
75
|
registerNotify(pi, deps.notify);
|
|
76
|
+
// The returned handle is what `/bytefly update` re-runs, so the update surface is
|
|
77
|
+
// registered before the panel that offers the command.
|
|
78
|
+
const update = registerUpdate(pi, updates, { ...deps.update, onUpdate: () => header.refresh() });
|
|
79
|
+
registerPanel(pi, { doctor, update, discover: deps.extensions?.discover });
|
|
69
80
|
}
|
|
@@ -25,6 +25,7 @@ export const DEFAULT_NOTIFY_CONFIG: NotifyConfig = {
|
|
|
25
25
|
turnFinished: true,
|
|
26
26
|
needsInput: true,
|
|
27
27
|
jobFinished: true,
|
|
28
|
+
matchFound: true,
|
|
28
29
|
method: "auto",
|
|
29
30
|
title: "Pi",
|
|
30
31
|
};
|
|
@@ -49,6 +50,7 @@ export function normalizeNotifyConfig(raw: Record<string, unknown> | undefined,
|
|
|
49
50
|
turnFinished: bool(raw.turnFinished, base.turnFinished),
|
|
50
51
|
needsInput: bool(raw.needsInput, base.needsInput),
|
|
51
52
|
jobFinished: bool(raw.jobFinished, base.jobFinished),
|
|
53
|
+
matchFound: bool(raw.matchFound, base.matchFound),
|
|
52
54
|
method: method(raw.method, base.method),
|
|
53
55
|
title: cleanString(raw.title, base.title).slice(0, MAX_TITLE_LENGTH),
|
|
54
56
|
};
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { basename } from "node:path";
|
|
11
|
-
import type { JobFinishedEvent } from "../../../lib/events.ts";
|
|
11
|
+
import type { JobFinishedEvent, WatchMatchEvent } from "../../../lib/events.ts";
|
|
12
12
|
import { sanitizeLine } from "../../../lib/format.ts";
|
|
13
13
|
|
|
14
14
|
/** A short session label, so concurrent unattended runs can be told apart. */
|
|
@@ -37,3 +37,21 @@ export function formatJobBody(job: JobFinishedEvent): string {
|
|
|
37
37
|
const label = sanitizeLine(job.label);
|
|
38
38
|
return `Job ${job.id} ${verb}${code}${label ? ` · ${label}` : ""}`;
|
|
39
39
|
}
|
|
40
|
+
|
|
41
|
+
/** How much of a matched line an OS notification carries. */
|
|
42
|
+
export const MAX_WATCH_LINE = 80;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Body for a `watch` wake match: the newest line that matched, and how many did.
|
|
46
|
+
*
|
|
47
|
+
* The line is watched output rather than something this package wrote, so it is
|
|
48
|
+
* sanitized and clipped here — the one place a notification's text is shaped.
|
|
49
|
+
*/
|
|
50
|
+
export function formatWatchBody(event: WatchMatchEvent): string {
|
|
51
|
+
const count = Math.max(1, event.count);
|
|
52
|
+
const label = sanitizeLine(event.label);
|
|
53
|
+
const line = sanitizeLine(event.line).slice(0, MAX_WATCH_LINE).trimEnd();
|
|
54
|
+
const head = `${count} watched line${count === 1 ? "" : "s"} matched`;
|
|
55
|
+
const source = `${sanitizeLine(event.watcherId)}${label ? ` ${label}` : ""}`;
|
|
56
|
+
return line ? `${head} · ${source}: ${line}` : `${head} · ${source}`;
|
|
57
|
+
}
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
* asking for input. A prompt while idle is a command the user typed, so it
|
|
10
10
|
* does not fire;
|
|
11
11
|
* - a finished background job, announced by the `job` extension on
|
|
12
|
-
* {@link JOB_FINISHED_CHANNEL}
|
|
13
|
-
*
|
|
12
|
+
* {@link JOB_FINISHED_CHANNEL}; and a `watch` wake match, announced by the
|
|
13
|
+
* `watch` extension on {@link WATCH_MATCH_CHANNEL}. The three extensions never
|
|
14
|
+
* import each other; the bus and its payload are the whole contract.
|
|
14
15
|
*
|
|
15
16
|
* The config is read at `session_start` and the bus subscription is a
|
|
16
17
|
* long-lived resource, so it is created there too and released in an
|
|
@@ -18,10 +19,15 @@
|
|
|
18
19
|
*/
|
|
19
20
|
|
|
20
21
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
21
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
JOB_FINISHED_CHANNEL,
|
|
24
|
+
type JobFinishedEvent,
|
|
25
|
+
WATCH_MATCH_CHANNEL,
|
|
26
|
+
type WatchMatchEvent,
|
|
27
|
+
} from "../../../lib/events.ts";
|
|
22
28
|
import { DEFAULT_NOTIFY_CONFIG, loadNotifyConfig } from "./config.ts";
|
|
23
29
|
import { defaultSeams, type NotifySeams, sendNotification } from "./dispatch.ts";
|
|
24
|
-
import { formatInputBody, formatJobBody, formatTurnBody, sessionLabel } from "./format.ts";
|
|
30
|
+
import { formatInputBody, formatJobBody, formatTurnBody, formatWatchBody, sessionLabel } from "./format.ts";
|
|
25
31
|
import type { NotifyConfig } from "./types.ts";
|
|
26
32
|
|
|
27
33
|
/** Host seams, injected by tests. */
|
|
@@ -37,12 +43,25 @@ function isJobFinished(value: unknown): value is JobFinishedEvent {
|
|
|
37
43
|
return typeof event.id === "string" && typeof event.label === "string" && typeof event.status === "string";
|
|
38
44
|
}
|
|
39
45
|
|
|
40
|
-
/**
|
|
46
|
+
/** Narrow an untrusted bus payload to a wake match. */
|
|
47
|
+
function isWatchMatch(value: unknown): value is WatchMatchEvent {
|
|
48
|
+
if (!value || typeof value !== "object") return false;
|
|
49
|
+
const event = value as Partial<WatchMatchEvent>;
|
|
50
|
+
return (
|
|
51
|
+
typeof event.count === "number" &&
|
|
52
|
+
typeof event.watcherId === "string" &&
|
|
53
|
+
typeof event.label === "string" &&
|
|
54
|
+
typeof event.line === "string"
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Register the notification triggers. */
|
|
41
59
|
export function registerNotify(pi: ExtensionAPI, deps: NotifyDeps = {}): void {
|
|
42
60
|
const seams = deps.seams ?? defaultSeams();
|
|
43
61
|
let config: NotifyConfig = DEFAULT_NOTIFY_CONFIG;
|
|
44
62
|
let busy = false;
|
|
45
|
-
|
|
63
|
+
/** Bus subscriptions: one per emitting extension, all created at session start. */
|
|
64
|
+
const unsubscribes: Array<() => void> = [];
|
|
46
65
|
|
|
47
66
|
const fire = (body: string): void => {
|
|
48
67
|
sendNotification(config, config.title, body, seams);
|
|
@@ -59,11 +78,17 @@ export function registerNotify(pi: ExtensionAPI, deps: NotifyDeps = {}): void {
|
|
|
59
78
|
pi.on("session_start", (_event, ctx) => {
|
|
60
79
|
config = loadNotifyConfig(ctx.cwd);
|
|
61
80
|
busy = false;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
81
|
+
while (unsubscribes.length > 0) unsubscribes.pop()?.();
|
|
82
|
+
unsubscribes.push(
|
|
83
|
+
pi.events.on(JOB_FINISHED_CHANNEL, (data) => {
|
|
84
|
+
if (!config.jobFinished || !isJobFinished(data)) return;
|
|
85
|
+
fire(formatJobBody(data));
|
|
86
|
+
}),
|
|
87
|
+
pi.events.on(WATCH_MATCH_CHANNEL, (data) => {
|
|
88
|
+
if (!config.matchFound || !isWatchMatch(data)) return;
|
|
89
|
+
fire(formatWatchBody(data));
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
67
92
|
});
|
|
68
93
|
|
|
69
94
|
pi.on("agent_settled", (event, ctx) => {
|
|
@@ -78,8 +103,7 @@ export function registerNotify(pi: ExtensionAPI, deps: NotifyDeps = {}): void {
|
|
|
78
103
|
});
|
|
79
104
|
|
|
80
105
|
pi.on("session_shutdown", () => {
|
|
81
|
-
|
|
82
|
-
unsubscribe = undefined;
|
|
106
|
+
while (unsubscribes.length > 0) unsubscribes.pop()?.();
|
|
83
107
|
busy = false;
|
|
84
108
|
});
|
|
85
109
|
}
|
|
@@ -21,6 +21,8 @@ export interface NotifyConfig {
|
|
|
21
21
|
needsInput: boolean;
|
|
22
22
|
/** Notify when a background `job` finishes. */
|
|
23
23
|
jobFinished: boolean;
|
|
24
|
+
/** Notify when a `watch` wake match fires while the agent is idle. */
|
|
25
|
+
matchFound: boolean;
|
|
24
26
|
/** Delivery method; `auto` chooses by terminal and platform. */
|
|
25
27
|
method: NotifyMethod;
|
|
26
28
|
/** Notification title, e.g. `Pi`. */
|
|
@@ -1,23 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The `/bytefly` surface: one command, `/bytefly [doctor] [settings] [extension] [refresh]`.
|
|
2
|
+
* The `/bytefly` surface: one command, `/bytefly [doctor] [settings] [update] [extension] [refresh]`.
|
|
3
3
|
*
|
|
4
4
|
* Without `doctor` it opens the extension screen: the dock in `tui`, the
|
|
5
5
|
* grouped listing elsewhere. `settings` opens the Settings section, or prints
|
|
6
|
-
* its grouped listing outside `tui` without resolving packages.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* the
|
|
6
|
+
* its grouped listing outside `tui` without resolving packages. `update`
|
|
7
|
+
* re-runs the published-version check and reports what it found, which is the
|
|
8
|
+
* one place a request is made on demand. With `doctor` it produces the doctor
|
|
9
|
+
* report — the same text `run_doctor` returns — for the whole set or one named
|
|
10
|
+
* extension; in `tui` the report is the dock screen. Discovery, the doctor
|
|
11
|
+
* probe, and the dirty-to-reload confirm all live here. The doctor runtime and
|
|
12
|
+
* the update handle are injected by the composition root, so `run_doctor` and
|
|
13
|
+
* the screen keep reading one cache, and the command paints the marker it finds.
|
|
12
14
|
*/
|
|
13
15
|
|
|
14
16
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
15
17
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
18
|
+
import pkg from "../../../package.json";
|
|
16
19
|
import { sanitizeLine } from "../../../lib/format.ts";
|
|
17
20
|
import { withRailsSuppressed } from "../../../lib/rails.ts";
|
|
18
21
|
import { REQUIREMENTS, type ExtensionReport } from "../../../lib/requirements.ts";
|
|
19
22
|
import { formatExtension, formatReport, hasProblems } from "../doctor/format.ts";
|
|
20
23
|
import type { DoctorRuntime } from "../doctor/runtime.ts";
|
|
24
|
+
import { updateOutcomeText } from "../update/format.ts";
|
|
25
|
+
import type { UpdateHandle } from "../update/register.ts";
|
|
21
26
|
import { createSettingsStores, discoverExtensions, type DiscoveryDeps } from "../picker/discovery.ts";
|
|
22
27
|
import { createExtensionsPickerRuntime, type ExtensionsPickerRuntime } from "../picker/runtime.ts";
|
|
23
28
|
import type { Discovery } from "../picker/types.ts";
|
|
@@ -31,6 +36,8 @@ import type { PanelSection } from "./types.ts";
|
|
|
31
36
|
export interface PanelCommandDeps {
|
|
32
37
|
/** The doctor runtime created by `registerDoctor`; shared with `run_doctor`. */
|
|
33
38
|
doctor: DoctorRuntime;
|
|
39
|
+
/** The update handle `registerUpdate` returns, for `/bytefly update`. */
|
|
40
|
+
update: UpdateHandle;
|
|
34
41
|
/** The picker's discovery seam. */
|
|
35
42
|
discover?: (deps: DiscoveryDeps) => Promise<Discovery>;
|
|
36
43
|
/** The Settings runtime seam. */
|
|
@@ -49,13 +56,14 @@ function selectableNames(discovery: Discovery): Set<string> {
|
|
|
49
56
|
}
|
|
50
57
|
|
|
51
58
|
/** The values a bare `/bytefly <tab>` offers, in screen order. */
|
|
52
|
-
const FIRST_TOKENS = ["doctor", "settings", ...Object.keys(REQUIREMENTS), "refresh"] as const;
|
|
59
|
+
const FIRST_TOKENS = ["doctor", "settings", "update", ...Object.keys(REQUIREMENTS), "refresh"] as const;
|
|
53
60
|
|
|
54
61
|
/**
|
|
55
62
|
* Argument completions. The host replaces the whole argument text with the
|
|
56
63
|
* returned `value`, so a second token returns the full `doctor <name>` or
|
|
57
|
-
* `<name> refresh` argument rather than just the word. `/bytefly settings`
|
|
58
|
-
* no name, and `refresh` is never followed by another
|
|
64
|
+
* `<name> refresh` argument rather than just the word. `/bytefly settings` and
|
|
65
|
+
* `/bytefly update` take no name, and `refresh` is never followed by another
|
|
66
|
+
* token.
|
|
59
67
|
*/
|
|
60
68
|
function completions(prefix: string): Array<{ value: string; label: string }> | null {
|
|
61
69
|
const [first = "", second, ...rest] = prefix.trimStart().split(/\s+/);
|
|
@@ -64,7 +72,7 @@ function completions(prefix: string): Array<{ value: string; label: string }> |
|
|
|
64
72
|
const options = FIRST_TOKENS.filter((option) => option.startsWith(first));
|
|
65
73
|
return options.length > 0 ? options.map((option) => ({ value: `${option} `, label: option })) : null;
|
|
66
74
|
}
|
|
67
|
-
if (first === "settings" || first === "refresh") return null;
|
|
75
|
+
if (first === "settings" || first === "refresh" || first === "update") return null;
|
|
68
76
|
const options = (first === "doctor" ? [...Object.keys(REQUIREMENTS), "refresh"] : ["refresh"]).filter((option) =>
|
|
69
77
|
option.startsWith(second),
|
|
70
78
|
);
|
|
@@ -72,7 +80,7 @@ function completions(prefix: string): Array<{ value: string; label: string }> |
|
|
|
72
80
|
}
|
|
73
81
|
|
|
74
82
|
/** Which bytefly surface the arguments select. */
|
|
75
|
-
type PanelMode = "screen" | "doctor";
|
|
83
|
+
type PanelMode = "screen" | "doctor" | "update";
|
|
76
84
|
|
|
77
85
|
/** `/bytefly [doctor] [settings] [extension] [refresh]`, parsed. */
|
|
78
86
|
interface PanelArgs {
|
|
@@ -85,12 +93,15 @@ interface PanelArgs {
|
|
|
85
93
|
refresh: boolean;
|
|
86
94
|
}
|
|
87
95
|
|
|
88
|
-
/** Parse `/bytefly [doctor] [settings] [extension] [refresh]`; extra tokens are ignored. */
|
|
96
|
+
/** Parse `/bytefly [doctor] [settings] [update] [extension] [refresh]`; extra tokens are ignored. */
|
|
89
97
|
function parseArgs(args: string): PanelArgs {
|
|
90
98
|
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
91
99
|
const refresh = parts.includes("refresh");
|
|
92
100
|
const rest = parts.filter((part) => part !== "refresh");
|
|
93
101
|
if (rest[0] === "doctor") return { mode: "doctor", section: "extensions", name: rest[1], refresh };
|
|
102
|
+
// `update` is already a refresh of the published version, so a stray `refresh`
|
|
103
|
+
// beside it is accepted and ignored rather than refused.
|
|
104
|
+
if (rest[0] === "update") return { mode: "update", section: "extensions", refresh };
|
|
94
105
|
if (rest[0] === "settings") return { mode: "screen", section: "settings", name: rest[1], refresh };
|
|
95
106
|
return { mode: "screen", section: "extensions", name: rest[0], refresh };
|
|
96
107
|
}
|
|
@@ -146,6 +157,16 @@ export function registerPanel(pi: ExtensionAPI, deps: PanelCommandDeps): void {
|
|
|
146
157
|
};
|
|
147
158
|
const settingsNotice = (message: string): void => surfaceNotice(message, "warning");
|
|
148
159
|
|
|
160
|
+
// `/bytefly update` answers one question and resolves nothing: no package
|
|
161
|
+
// discovery, no doctor probe, no screen. It is the one path a version check
|
|
162
|
+
// takes on demand, so the request happens here and nowhere else.
|
|
163
|
+
if (mode === "update") {
|
|
164
|
+
const outcome = await deps.update.recheck();
|
|
165
|
+
const { text, severity } = updateOutcomeText(outcome, pkg.version);
|
|
166
|
+
ctx.ui.notify(text, severity);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
149
170
|
// Settings outside `tui` prints its grouped listing without resolving
|
|
150
171
|
// packages or probing the doctor.
|
|
151
172
|
if (section === "settings" && ctx.mode !== "tui") {
|