@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.
Files changed (197) hide show
  1. package/README.md +38 -26
  2. package/README.zh-CN.md +33 -23
  3. package/extensions/bytefly/README.md +381 -225
  4. package/extensions/bytefly/chrome.ts +7 -2
  5. package/extensions/bytefly/doctor/register.ts +6 -0
  6. package/extensions/bytefly/doctor/runtime.ts +52 -6
  7. package/extensions/bytefly/doctor/tools.ts +1 -3
  8. package/extensions/bytefly/footer/collect/snapshot.ts +12 -2
  9. package/extensions/bytefly/footer/config.ts +7 -1
  10. package/extensions/bytefly/footer/delegation.ts +100 -0
  11. package/extensions/bytefly/footer/price/config.ts +54 -15
  12. package/extensions/bytefly/footer/price/price.ts +20 -7
  13. package/extensions/bytefly/footer/price/types.ts +13 -7
  14. package/extensions/bytefly/footer/register.ts +13 -2
  15. package/extensions/bytefly/footer/render/format.ts +3 -2
  16. package/extensions/bytefly/footer/render/lines.ts +2 -0
  17. package/extensions/bytefly/footer/render/segments.ts +30 -2
  18. package/extensions/bytefly/footer/types.ts +12 -0
  19. package/extensions/bytefly/header/component.ts +21 -6
  20. package/extensions/bytefly/header/config.ts +3 -0
  21. package/extensions/bytefly/header/register.ts +34 -3
  22. package/extensions/bytefly/index.ts +15 -4
  23. package/extensions/bytefly/notify/config.ts +2 -0
  24. package/extensions/bytefly/notify/format.ts +19 -1
  25. package/extensions/bytefly/notify/register.ts +37 -13
  26. package/extensions/bytefly/notify/types.ts +2 -0
  27. package/extensions/bytefly/panel/commands.ts +34 -13
  28. package/extensions/bytefly/panel/settings-runtime.ts +71 -28
  29. package/extensions/bytefly/panel/settings.ts +293 -48
  30. package/extensions/bytefly/panel/tui/entries.ts +58 -0
  31. package/extensions/bytefly/panel/tui/extensions-view.ts +86 -0
  32. package/extensions/bytefly/panel/tui/frame.ts +71 -0
  33. package/extensions/bytefly/panel/tui/nav.ts +75 -0
  34. package/extensions/bytefly/panel/tui/settings-view.ts +115 -0
  35. package/extensions/bytefly/panel/tui.ts +205 -319
  36. package/extensions/bytefly/picker/runtime.ts +61 -2
  37. package/extensions/bytefly/picker/toggle.ts +56 -0
  38. package/extensions/bytefly/update/cache.ts +73 -0
  39. package/extensions/bytefly/update/check.ts +59 -0
  40. package/extensions/bytefly/update/format.ts +87 -0
  41. package/extensions/bytefly/update/register.ts +158 -0
  42. package/extensions/bytefly/update/state.ts +40 -0
  43. package/extensions/bytefly/update/version.ts +83 -0
  44. package/extensions/document/README.md +24 -14
  45. package/extensions/document/config.ts +4 -0
  46. package/extensions/document/deadline.ts +61 -0
  47. package/extensions/document/tool.ts +14 -5
  48. package/extensions/file-browse/README.md +12 -8
  49. package/extensions/file-browse/client.ts +6 -7
  50. package/extensions/file-browse/files.ts +0 -4
  51. package/extensions/file-browse/git.ts +9 -10
  52. package/extensions/file-browse/html.ts +17 -7
  53. package/extensions/file-browse/router.ts +24 -15
  54. package/extensions/file-open/README.md +30 -15
  55. package/extensions/file-open/index.ts +4 -3
  56. package/extensions/file-open/open.ts +52 -21
  57. package/extensions/goal/README.md +45 -13
  58. package/extensions/goal/clarify-prompt.ts +5 -36
  59. package/extensions/goal/clarify.ts +55 -86
  60. package/extensions/goal/commands.ts +41 -35
  61. package/extensions/goal/format.ts +12 -4
  62. package/extensions/goal/schema.ts +6 -2
  63. package/extensions/goal/tools.ts +28 -8
  64. package/extensions/goal/tui.ts +1 -1
  65. package/extensions/goal/types.ts +6 -2
  66. package/extensions/job/README.md +16 -12
  67. package/extensions/job/runtime.ts +23 -6
  68. package/extensions/job/status.ts +4 -2
  69. package/extensions/memory/README.md +56 -14
  70. package/extensions/memory/commands.ts +20 -2
  71. package/extensions/memory/doctor.ts +137 -0
  72. package/extensions/memory/format.ts +24 -7
  73. package/extensions/memory/runtime.ts +49 -0
  74. package/extensions/memory/store.ts +31 -0
  75. package/extensions/memory/tui.ts +136 -12
  76. package/extensions/plan/README.md +83 -36
  77. package/extensions/plan/blocks.ts +1 -35
  78. package/extensions/plan/commands.ts +84 -19
  79. package/extensions/plan/index.ts +35 -11
  80. package/extensions/plan/plan-index.ts +234 -0
  81. package/extensions/plan/plan-text.ts +95 -0
  82. package/extensions/plan/plans.ts +91 -2
  83. package/extensions/plan/policy.ts +4 -3
  84. package/extensions/plan/review.ts +246 -0
  85. package/extensions/plan/runtime.ts +73 -24
  86. package/extensions/plan/steps.ts +60 -39
  87. package/extensions/plan/todos.ts +29 -60
  88. package/extensions/plan/tools.ts +188 -244
  89. package/extensions/plan/tui.ts +18 -31
  90. package/extensions/plan/types.ts +12 -0
  91. package/extensions/plan/web-client.ts +51 -18
  92. package/extensions/plan/web-page.ts +7 -2
  93. package/extensions/plan/web-router.ts +11 -3
  94. package/extensions/plan/web.ts +106 -41
  95. package/extensions/question/README.md +1 -1
  96. package/extensions/question/tools.ts +1 -1
  97. package/extensions/rewind/README.md +28 -8
  98. package/extensions/rewind/commands.ts +1 -1
  99. package/extensions/rewind/format.ts +4 -4
  100. package/extensions/rewind/git.ts +56 -18
  101. package/extensions/rewind/index.ts +3 -5
  102. package/extensions/rewind/path-picker.ts +198 -0
  103. package/extensions/rewind/paths.ts +128 -0
  104. package/extensions/rewind/restore.ts +69 -28
  105. package/extensions/rewind/rewind.ts +80 -3
  106. package/extensions/rewind/runtime.ts +13 -5
  107. package/extensions/rewind/snapshot-run.ts +5 -2
  108. package/extensions/rewind/types.ts +2 -0
  109. package/extensions/subagent/README.md +7 -1
  110. package/extensions/subagent/accounting.ts +34 -0
  111. package/extensions/subagent/index.ts +23 -3
  112. package/extensions/subagent/stream.ts +2 -0
  113. package/extensions/subagent/types.ts +2 -0
  114. package/extensions/todo/README.md +19 -11
  115. package/extensions/todo/index.ts +5 -3
  116. package/extensions/todo/nudge.ts +58 -9
  117. package/extensions/todo/runtime.ts +28 -17
  118. package/extensions/watch/README.md +8 -3
  119. package/extensions/watch/index.ts +19 -0
  120. package/extensions/web-access/README.md +46 -16
  121. package/extensions/web-access/charset.ts +58 -0
  122. package/extensions/web-access/fetch/find.ts +11 -7
  123. package/extensions/web-access/fetch/format.ts +64 -18
  124. package/extensions/web-access/fetch/page.ts +5 -1
  125. package/extensions/web-access/fetch/tool.ts +20 -21
  126. package/extensions/web-access/http.ts +49 -8
  127. package/extensions/web-access/search/facts.ts +38 -0
  128. package/extensions/web-access/search/tool.ts +14 -3
  129. package/extensions/web-access/text.ts +50 -11
  130. package/extensions/wechat/README.md +5 -4
  131. package/extensions/wechat/tools.ts +0 -2
  132. package/extensions/worktree/README.md +74 -15
  133. package/extensions/worktree/config.ts +11 -0
  134. package/extensions/worktree/exit.ts +35 -3
  135. package/extensions/worktree/gaps.ts +74 -0
  136. package/extensions/worktree/guard.ts +50 -4
  137. package/extensions/worktree/handoff.ts +55 -0
  138. package/extensions/worktree/index.ts +12 -2
  139. package/extensions/worktree/runtime.ts +20 -0
  140. package/extensions/worktree/status.ts +15 -2
  141. package/extensions/worktree/tools.ts +3 -2
  142. package/lib/README.md +12 -9
  143. package/lib/config.ts +43 -1
  144. package/lib/env.ts +12 -0
  145. package/lib/events.ts +86 -1
  146. package/lib/goal-state.ts +95 -0
  147. package/lib/requirements.ts +340 -46
  148. package/lib/todo-state.ts +62 -0
  149. package/lib/tui/README.md +91 -0
  150. package/lib/tui/glyphs.ts +2 -0
  151. package/lib/web/layout.ts +12 -2
  152. package/package.json +5 -3
  153. package/themes/bytefly-dark.json +1 -1
  154. package/assets/bytefly.schema.json +0 -785
  155. package/assets/chrome.png +0 -0
  156. package/assets/file-browse.png +0 -0
  157. package/assets/job.png +0 -0
  158. package/assets/plan-review.png +0 -0
  159. package/assets/question.png +0 -0
  160. package/assets/rewind.png +0 -0
  161. package/assets/todo.png +0 -0
  162. package/assets/tool-renderings.png +0 -0
  163. package/assets/tools/ask_user_question-cancelled.png +0 -0
  164. package/assets/tools/ask_user_question.png +0 -0
  165. package/assets/tools/bash.png +0 -0
  166. package/assets/tools/edit.png +0 -0
  167. package/assets/tools/enter_plan_mode.png +0 -0
  168. package/assets/tools/enter_worktree.png +0 -0
  169. package/assets/tools/exit_plan_mode.png +0 -0
  170. package/assets/tools/exit_worktree.png +0 -0
  171. package/assets/tools/find.png +0 -0
  172. package/assets/tools/goal.png +0 -0
  173. package/assets/tools/grep.png +0 -0
  174. package/assets/tools/job.png +0 -0
  175. package/assets/tools/list_worktrees.png +0 -0
  176. package/assets/tools/ls.png +0 -0
  177. package/assets/tools/memory-long-subject.png +0 -0
  178. package/assets/tools/memory.png +0 -0
  179. package/assets/tools/notice.png +0 -0
  180. package/assets/tools/open_path.png +0 -0
  181. package/assets/tools/prune_worktrees.png +0 -0
  182. package/assets/tools/read.png +0 -0
  183. package/assets/tools/read_doc-running.png +0 -0
  184. package/assets/tools/read_doc.png +0 -0
  185. package/assets/tools/run_doctor.png +0 -0
  186. package/assets/tools/send_wechat.png +0 -0
  187. package/assets/tools/set_plan_steps.png +0 -0
  188. package/assets/tools/subagent.png +0 -0
  189. package/assets/tools/todo.png +0 -0
  190. package/assets/tools/turn-rule.png +0 -0
  191. package/assets/tools/user-message.png +0 -0
  192. package/assets/tools/watch.png +0 -0
  193. package/assets/tools/web_fetch-rejected.png +0 -0
  194. package/assets/tools/web_fetch.png +0 -0
  195. package/assets/tools/web_search.png +0 -0
  196. package/assets/tools/write.png +0 -0
  197. package/assets/tools/write_plan.png +0 -0
@@ -8,10 +8,14 @@ marks each completed turn in the transcript, one keyboard-driven screen with two
8
8
  sections — Extensions, every resolved Pi extension with its enable checkbox and
9
9
  its health, and Settings, the common app and `bytefly.json` preferences — and
10
10
  an opt-in notify capability that raises an OS notification when a run finishes,
11
- a job finishes, or the agent asks for input.
11
+ a job finishes, or the agent asks for input, and a published-version check that
12
+ marks the header and posts one notice when a newer bytefly is on npm.
12
13
  DeepSeek is priced from the provider's published yuan rates and peak tariff by
13
- default, and any other model can be priced from your own rates when Pi's catalog
14
- does not match what a provider actually charges.
14
+ default; set `price.currency.code` to `USD` to price it from the provider's
15
+ published dollar table instead, under the same peak windows, with Pi's recorded
16
+ dollars covering a model the table does not list. Any other model can be priced
17
+ from your own yuan rates when Pi's
18
+ catalog does not match what a provider actually charges.
15
19
 
16
20
  ```
17
21
  pi --extension ./extensions/bytefly # load just this extension
@@ -27,10 +31,10 @@ How the `run_doctor` call and its result look in the transcript — the call lin
27
31
 
28
32
  ## What it does
29
33
 
30
- The extension owns nine surfaces: the startup header, the footer, the
34
+ The extension owns ten surfaces: the startup header, the footer, the
31
35
  user-message marker, the rule between completed turns, the notice block for
32
36
  shared transcript notices, the tool-block frame, the live working counter, the
33
- two-section control center, and notifications.
37
+ two-section control center, notifications, and the published-version check.
34
38
 
35
39
  **Header.** A nameplate: a two-tone half-block mark from the active theme's
36
40
  `accent` / `warn` roles, the agent identity (`Pi`, bold, and its version), the
@@ -38,7 +42,8 @@ chrome identity (`enhanced by bytefly` and the package version), a signature
38
42
  rule whose `accent` lead sits under the mark, and a grouped help block. The help
39
43
  keys resolve live from Pi's keybindings, so a rebound key shows the user's
40
44
  chord; the identity and help text are `meta` / `faint` so the mark keeps the
41
- eye. It is installed only in interactive (`tui`) sessions when Pi's
45
+ eye. It opens with a blank row, so the nameplate clears the top edge. It is
46
+ installed only in interactive (`tui`) sessions when Pi's
42
47
  `quietStartup` is `true` — the Settings row **Bytefly startup TUI** — and
43
48
  `false`, `"header"`, and unset leave Pi's built-in header in place; on terminals
44
49
  that cannot render half blocks the mark falls back to a two-tone `Pi` wordmark.
@@ -52,9 +57,9 @@ that cannot render half blocks the mark falls back to a two-tone `Pi` wordmark.
52
57
  / commands · ! bash · !! bash (no context) · /bytefly extensions
53
58
  ```
54
59
 
55
- As the terminal narrows, the identity sheds the chrome version, then the chrome
56
- line, then the agent name, and the help rows drop whole keys, so nothing wraps or
57
- clips mid-word. The nameplate sits on the transcript margin, which follows Pi's
60
+ As the terminal narrows, the identity sheds the update marker, then the chrome
61
+ version, then the chrome line, then the agent name, and the help rows drop whole
62
+ keys, so nothing wraps or clips mid-word. The nameplate sits on the transcript margin, which follows Pi's
58
63
  `outputPad` (the value the tool frame and the notice use too), and the help hangs
59
64
  one body indent inside it. The mark, tokens, identity, and grouped help are tuned
60
65
  in [`header/config.ts`](./header/config.ts).
@@ -64,18 +69,25 @@ in [`header/config.ts`](./header/config.ts).
64
69
  - **Line 1 — identity:** `pwd` and the session name on the left; git state
65
70
  (`⎇ branch · ⑂ worktree`) and the `◉` browse chip on the right.
66
71
  - **Line 2 — resources:** the context gauge and usage meters on the left, the
67
- mode/alert slot (`⋮ plan`) trailing after a `╎` when present; the model and
68
- thinking level on the right. The gauge is anchored at column 0, so `%` is
72
+ child-run roll-up (`↳ 3 · ¥0.12`, present only once a subagent has finished),
73
+ the mode/alert slot (`⋮ plan`) trailing after a `╎` when present, and the model
74
+ and thinking level on the right. The gauge is anchored at column 0, so `%` is
69
75
  always in the same place.
76
+ The roll-up is its own number, never folded into the session's cost: a subagent
77
+ runs in its own process, and the footer prices those runs with the same rate
78
+ table and currency as the session so a currency change moves both. It covers
79
+ the last 500 child runs of the current process, and starts from zero after
80
+ `/resume` (the events are not session entries).
70
81
 
71
82
  ```
72
- ~/repo/my-pi-agent ⎇ main · ⑂ smoke
73
- ▰▰▰▰▰▰▱▱▱▱ 62%/200k · ¥0.31 · ↑42k ↓8.0k · R96k CH 87% ╎ ⋮ plan opus-4.5 · high
83
+ ~/repo/my-pi-agent ⎇ main · ⑂ smoke
84
+ ▰▰▰▰▰▰▱▱▱▱ 62%/200k · ¥0.31 · ↳ 2 · ¥0.06 · ↑42k ↓8.0k · R96k CH 87% ╎ ⋮ plan opus-4.5 · high
74
85
  ```
75
86
 
76
- (The examples show the default `¥` symbol. For a model the built-in table does
77
- not cover, the number stays Pi's recorded USD amount — see the currency note
78
- under [Configuration](#configuration).)
87
+ (The examples show the default `¥` symbol. A model that neither `rates` nor the
88
+ built-in table covers contributes nothing in `CNY` mode, so the bar shows no
89
+ figure rather than a guessed one; `USD` mode adds Pi's recorded dollars for that
90
+ record instead — see the currency note under [Configuration](#configuration).)
79
91
 
80
92
  With no plan mode there is no mode slot, so the `╎` is omitted:
81
93
 
@@ -113,8 +125,9 @@ rounding to a misleading `¥0.3` — and a positive amount below one minor unit
113
125
  shows as `¥<0.01` rather than `¥0.00`.
114
126
 
115
127
  DeepSeek needs no config file at all: the bar prices it from the published yuan
116
- rates and peak tariff built into [`footer/price/config.ts`](./footer/price/config.ts). A
117
- `bytefly.json` adds models of your own, or replaces a built-in entry:
128
+ rates and peak tariff built into [`footer/price/config.ts`](./footer/price/config.ts).
129
+ A `bytefly.json` can switch to the published dollar table, add models of your own,
130
+ or replace a built-in entry:
118
131
 
119
132
  ```
120
133
  ~/repo/project ⎇ main
@@ -259,15 +272,26 @@ underneath, and `run_doctor` returns the same report as text and typed data.
259
272
  Only a missing
260
273
  *hard* requirement posts a notice at session start; everything else waits in the
261
274
  report.
262
- See [The bytefly screen](#the-bytefly-screen).
275
+ See [The control center](#the-control-center).
263
276
 
264
277
  **Notify.** Off by default, one `notify` section in `bytefly.json` turns on OS
265
278
  notifications for unattended runs: when a full agent run settles, when a
266
- background job finishes, and when the agent opens a prompt asking for input. The
279
+ background job finishes, when the agent opens a prompt asking for input, and when
280
+ a `watch` wake match fires while the agent is idle. The
267
281
  method follows the terminal (OSC 777, OSC 99, a Windows toast) or falls back to
268
282
  the platform notifier (`osascript`, `notify-send`); see
269
283
  [Notifications](#notifications).
270
284
 
285
+ **Update check.** In the background at an interactive session's start, the loaded
286
+ version is compared with the published `@gavin-hu/my-pi-agent` on npm. A newer
287
+ one puts `↑0.12.0` after the header's chrome version and posts one transcript
288
+ notice naming `pi update --extensions` and its release URL. The answer is cached
289
+ in `<agentDir>/bytefly/update.json` with the time it was read, so a session
290
+ starting inside the 24-hour window reuses it instead of repeating the request,
291
+ and `/bytefly update` re-runs the check on demand. The check is skipped when
292
+ `PI_OFFLINE` is set, makes no request outside `tui`, and a failed background
293
+ request is silent; see [Update check](#update-check).
294
+
271
295
  ## Commands
272
296
 
273
297
  | Command | What it does |
@@ -275,9 +299,10 @@ the platform notifier (`osascript`, `notify-send`); see
275
299
  | `/bytefly [extension] [refresh]` | Open the control center on Extensions. In `tui` it mounts a docked list; every other mode prints the grouped listing, or one extension's full findings when named. |
276
300
  | `/bytefly settings` | Open the control center on Settings. Outside `tui` it prints the grouped settings listing with each row's value. |
277
301
  | `/bytefly doctor [extension] [refresh]` | Print the doctor report. Outside `tui` it prints `run_doctor`'s text for every extension, or one named extension's findings; in `tui` it mounts the same docked screen. |
302
+ | `/bytefly update` | Re-run the published-version check now, ignoring the cache window, and report what it found — the newest version, an available release with its URL, `PI_OFFLINE`, or an unreadable registry. It resolves no packages and probes no extension. |
278
303
 
279
304
  Tab completes the subcommands, extension names after `doctor`, and `refresh`;
280
- `/bytefly settings` takes no name.
305
+ `/bytefly settings` and `/bytefly update` take no name.
281
306
 
282
307
  ## Behaviour by mode
283
308
 
@@ -303,45 +328,36 @@ print/JSON/RPC run gets an OS-level notification or none.
303
328
 
304
329
  ## Configuration
305
330
 
306
- The header is tuned in [`header/config.ts`](./header/config.ts) — the mark and
307
- identity theme tokens, the tagline, and the grouped help entries (each naming a
308
- Pi keybinding resolved live or a raw command prefix). The footer's
309
- content and layout are tuned in [`footer/config.ts`](./footer/config.ts) —
310
- thresholds, gauge
311
- glyphs and widths, separators, icons, and status keys. The turn separator has no
312
- JSON either: its glyph, label prefix, padding, and colors are tuned in
313
- [`separator/config.ts`](./separator/config.ts).
314
-
315
- The transcript rail's glyph is the one chrome value that is JSON-configurable:
316
- the `chrome` section's `rail` key (default `╎`) picks a single-column rail glyph,
317
- read at `session_start`/`session_tree`. Everything else about the chrome is tuned
318
- in code.
319
-
320
- Everything JSON-configurable lives in `bytefly.json`, one shared document with a
321
- top-level section per extension (see [Configuration](../../docs/configuration.md)).
322
- It is read from the global document, then `<cwd>/.pi/bytefly.json`. The old
323
- per-extension files
324
- (`document.json`, `jobs.json`, …) are no longer read; run
325
- `bun run config:migrate` to move them into the shared document.
326
-
327
- The price
328
- calculation is configured by the `price` section of the shared `bytefly.json`
329
- (see [Configuration](../../docs/configuration.md)), read from
330
- `~/.pi/agent/bytefly.json`, then `<cwd>/.pi/bytefly.json` (project wins).
331
-
332
- Both documents are optional, and neither is needed for DeepSeek. The built-in
333
- table is the base layer for every session: DeepSeek is priced in yuan at the
334
- published peak and off-peak rates, and every other model keeps Pi's recorded
335
- cost, whose number comes from Pi's catalog. A document can change the currency,
336
- replace a built-in rate, or add rates of your own. A malformed document is
337
- ignored, a wrong-typed value falls back to its default, and an unknown timezone
338
- drops the schedule rather than the session.
331
+ Two things are JSON: the transcript rail's glyph (`chrome.rail`, default `╎`,
332
+ read at `session_start`/`session_tree`) and this extension's three config
333
+ sections — `price`, `notify`, and `doctor`. Everything else about the look is
334
+ tuned in code: the header in [`header/config.ts`](./header/config.ts) (mark and
335
+ identity tokens, tagline, grouped help), the footer in
336
+ [`footer/config.ts`](./footer/config.ts) (thresholds, gauge glyphs and widths,
337
+ separators, icons, status keys), and the turn separator in
338
+ [`separator/config.ts`](./separator/config.ts) (glyph, label prefix, padding,
339
+ colors).
340
+
341
+ Every key of every section is listed with its type, default, and bounds in the
342
+ [configuration reference](../../docs/references/bytefly-config.md); this section
343
+ covers what that table cannot — when each section is read, and what the cost
344
+ does when no rate matches.
345
+
346
+ `bytefly.json` is one shared document with a top-level section per extension (see
347
+ [Configuration](../../docs/configuration.md)). It is read from the global
348
+ document, then `<cwd>/.pi/bytefly.json`, project winning. The old per-extension
349
+ files (`document.json`, `jobs.json`, …) are no longer read; `bun run config:migrate`
350
+ moves them into the shared document.
351
+
352
+ Neither document is needed for DeepSeek. The built-in table for the selected
353
+ currency is the base layer for every session: `CNY` prices DeepSeek in yuan and
354
+ `USD` in dollars, both at the published peak and off-peak rates. A document can
355
+ change the currency, replace a built-in rate, or add rates of your own.
339
356
 
340
357
  ```json
341
358
  {
342
359
  "price": {
343
- "currency": { "symbol": "¥", "decimals": 2 },
344
- "fallbackToRecordedCost": true,
360
+ "currency": { "code": "CNY", "decimals": 2 },
345
361
  "rates": {
346
362
  "deepseek/deepseek-flash": { "input": 2, "output": 8, "cacheRead": 0.04, "cacheWrite": 0, "offPeakMultiplier": 0.5 },
347
363
  "deepseek/deepseek-v4-pro": { "input": 9, "output": 27, "cacheRead": 0.3, "cacheWrite": 0, "offPeakMultiplier": 0.5 }
@@ -357,101 +373,45 @@ drops the schedule rather than the session.
357
373
  }
358
374
  ```
359
375
 
360
- That file restates the built-in defaults, so on its own it changes nothing; it
361
- is shown to document every key at once.
362
-
363
- | Key | Meaning |
364
- |---|---|
365
- | `currency.symbol` | Printed before the number; at most four characters. Default `¥`. |
366
- | `currency.decimals` | Digits after the point, 0–4. Default `2`. |
367
- | `fallbackToRecordedCost` | Use Pi's recorded cost for a record no rate matches. Default `true`. |
368
- | `rates` | Per-million-token prices in `currency`, keyed by `provider/model`; the built-in DeepSeek table is the base, and a key stated here replaces its entry. |
369
- | `rates.<key>.input` · `output` · `cacheRead` · `cacheWrite` | Rate per token class; a class left out is free. |
370
- | `rates.<key>.offPeakMultiplier` | Multiplier outside every peak window, 0–1. Default `1`. |
371
- | `peak.timezone` | The IANA zone the windows are stated in. |
372
- | `peak.windows` | `days` (1 = Monday … 7 = Sunday; omitted means every day), `from` inclusive, `to` exclusive, `HH:MM` local; a `to` before `from` wraps midnight. An empty list means every moment is peak. |
376
+ That file restates the built-in defaults, so on its own it changes nothing; it is
377
+ shown to document every key at once.
373
378
 
374
379
  Rates merge key by key across the two documents, so a project document can
375
380
  override one model without restating the rest; a key present in both is replaced,
376
381
  not merged field by field. No windows means every moment is peak, so a rate with
377
- `offPeakMultiplier: 0.5` and no schedule stays flat. The `price` section is read
378
- once per install — at `session_start`, and again on a `session_tree` change — so
379
- a new session or tree change picks up an edit; there is no toggle, and nothing
380
- watches the file.
381
-
382
- **Mixing currencies is the sharp edge of the built-in default.** A record priced
383
- from `rates` or the built-in table is in `currency.symbol` units — yuan for
384
- DeepSeek — while a record that fell back carries Pi's USD number under that same
385
- symbol. A session mixing DeepSeek with another provider therefore sums two
386
- currencies into one number. Either cover every model you use (a `"*"` catch-all
387
- helps) and set `fallbackToRecordedCost: false`, or set the symbol to `"$"` and
388
- treat the rates purely as an off-peak correction.
389
-
390
- ### The `notify` section
391
-
392
- The notify capability reads the same shared `bytefly.json`, under its own
393
- top-level `notify` key.
394
- It is **off by default**; `enabled: true` is the whole setup needed for an
395
- unattended run, and the three trigger flags then narrow what is announced. The
396
- price section and the notify section are parsed independently, so a wrong-typed
397
- value in one never reaches the other.
398
-
399
- ```json
400
- {
401
- "notify": {
402
- "enabled": true,
403
- "turnFinished": true,
404
- "needsInput": true,
405
- "jobFinished": true,
406
- "method": "auto",
407
- "title": "Pi"
408
- }
409
- }
410
- ```
411
-
412
- | Key | Default | Meaning |
413
- |---|---|---|
414
- | `enabled` | `false` | Master switch. A notification on every turn is intrusive, so it is opt-in. |
415
- | `turnFinished` | `true` | Notify when a full agent run settles and Pi waits for the next message. An aborted run is never announced. |
416
- | `needsInput` | `true` | Notify when the agent opens a prompt mid-run (`select`, `confirm`, `input`, `editor`, `custom`). A prompt the user typed while idle does not fire. |
417
- | `jobFinished` | `true` | Notify when a background `job` reaches a terminal state. |
418
- | `method` | `auto` | `auto`, `osascript`, `notify-send`, `toast`, `osc777`, or `osc99`. An explicit method that the environment cannot deliver is a no-op. |
419
- | `title` | `Pi` | Notification title, clamped to 40 characters. |
420
-
421
- A `notify` section is read at `session_start`, so a new session or `/reload`
422
- picks up an edit.
423
-
424
- ### The `doctor` section
382
+ `offPeakMultiplier: 0.5` and no schedule stays flat.
425
383
 
426
- The doctor surface reads the same shared `bytefly.json`, under its own top-level
427
- `doctor` key:
384
+ **Only configured models get a price.** A record priced from `rates` or the
385
+ built-in table contributes to the total. A model with no matching rate differs by
386
+ currency: `CNY` contributes nothing, so the bar shows no cost rather than a
387
+ mixed-currency figure, while `USD` adds Pi's recorded dollar amount, which is
388
+ already in the right unit. To price the model yourself, add a rate entry (a `"*"`
389
+ catch-all prices everything).
428
390
 
429
- ```json
430
- {
431
- "doctor": {
432
- "proactive": true,
433
- "timeoutMs": 2000,
434
- "skip": []
435
- }
436
- }
437
- ```
391
+ ### When each section is read
438
392
 
439
- | Key | Default | Meaning |
393
+ | Section | Read at | Applies to |
440
394
  |---|---|---|
441
- | `proactive` | `true` | Probe the extensions with hard requirements at session start and post one notice when something is missing. |
442
- | `timeoutMs` | `2000` | Per-probe timeout for one spawned command, clamped to 100–30000. |
443
- | `skip` | `[]` | Extension names to leave out of every report. |
444
-
445
- A malformed `bytefly.json` is ignored like every other config file — and named
446
- by the report `doctor` produces. The section is read at `session_start`, so a
447
- new session or `/reload` picks up an edit.
448
-
449
- Wherever it runs, the proactive check is part of the bytefly startup TUI, so it
450
- posts only when Pi starts with `"quietStartup": true`, the same setting that
451
- hides Pi's own header and loaded-resource listing. `false`, `"header"`, and
452
- unset leave Pi's startup surface in charge, so the notice stays silent. When it
453
- does post, the notice enters the message flow once per
454
- session, so it stays in scrollback and the model reads the same sentence.
395
+ | `chrome` | `session_start`, `session_tree` | the rail glyph |
396
+ | `price` | the footer install (`session_start`, `session_tree`) | the cost total |
397
+ | `notify` | `session_start` | which events are announced, and how |
398
+ | `doctor` | `session_start` | the proactive check |
399
+
400
+ Nothing watches a file, so an edit applies at the next read — a reload, a new
401
+ session, or a tree change. A malformed document is ignored by every loader and
402
+ named by `doctor`; a wrong-typed value falls back to its default, and a
403
+ wrong-typed timezone drops the peak schedule rather than the session.
404
+
405
+ The notify capability is **off by default**: `enabled: true` is the whole setup
406
+ needed for an unattended run, and the four trigger flags then narrow what is
407
+ announced (see [Notifications](#notifications)).
408
+
409
+ The proactive doctor check is part of the bytefly startup TUI, so it posts only
410
+ when Pi starts with `"quietStartup": true`, the same setting that hides Pi's own
411
+ header and loaded-resource listing. `false`, `"header"`, and unset leave Pi's
412
+ startup surface in charge, so the notice stays silent. When it does post, the
413
+ notice enters the message flow once per session, so it stays in scrollback and
414
+ the model reads the same sentence.
455
415
 
456
416
  ## Security
457
417
 
@@ -464,11 +424,12 @@ session, so it stays in scrollback and the model reads the same sentence.
464
424
  ## Cost calculation
465
425
 
466
426
  Pi prices every model call from its bundled catalog in USD, and the provider API
467
- never returns a price. Built-in and configured rates replace that for the models
468
- they match:
427
+ never returns a price. The footer can total that catalog directly, or replace
428
+ it with its own rates for the models they match:
469
429
 
470
430
  - **Which built-in rates.** [`footer/price/config.ts`](./footer/price/config.ts) ships DeepSeek's published
471
- yuan prices, with the peak schedule that doubles the price during peak hours
431
+ yuan prices for `CNY` and its published dollar prices for `USD`, per model, with
432
+ the peak schedule that doubles the price during peak hours
472
433
  ([DeepSeek's pricing page](https://api-docs.deepseek.com/quick_start/pricing)).
473
434
  They are ordinary rate entries, so a `bytefly.json` key replaces one of
474
435
  them, and the schedule is the default `peak` every rate is judged against.
@@ -482,9 +443,16 @@ they match:
482
443
  applied at the local time the tokens were actually spent: an off-peak turn
483
444
  stays off-peak when peak starts. A record with no usable timestamp is priced
484
445
  at peak, because it cannot be dated.
485
- - **Which source.** A record with no matching rate — built-in or configured —
486
- keeps Pi's recorded cost, or contributes nothing when
487
- `fallbackToRecordedCost` is `false`.
446
+ - **Which algorithm.** `price.currency.code` chooses `CNY` (the default) or
447
+ `USD`. Both price matched records from `rates` and the same `peak` tariff; the
448
+ code selects which built-in table `rates` starts from — DeepSeek's yuan prices
449
+ or its dollar prices — and the two are separate methods, not an exchange rate.
450
+ Neither mode converts one currency into the other. `currency.decimals` only
451
+ changes how the total is printed; the symbol follows the code.
452
+ - **Which source.** Both currencies price a matched record from `rates`; only
453
+ the unmatched case differs. `CNY` contributes nothing for a model it cannot
454
+ price, while `USD` adds Pi's recorded dollar amount for that record, because
455
+ that number is already dollars.
488
456
 
489
457
  For example, DeepSeek flash at `¥2` in and `¥8` out per million, with the
490
458
  published peak windows and `offPeakMultiplier: 0.5`, prices a 1M-in + 1M-out
@@ -598,6 +566,12 @@ installs or fetches a package.
598
566
  into `extensions` (and package filter objects) in the active scope. A row in
599
567
  the trailing `Not resolved` group has no pattern to write, so `Space` says so
600
568
  instead of doing nothing.
569
+ - Clears the focused extension's project override with `x` in Project scope: the
570
+ `+`/`-` entry the toggle wrote is removed (with the plain path entry beside it
571
+ for an inherited personal resource, and a project package entry the toggle
572
+ created), and the row returns to its inherited state. A row with no override
573
+ says so instead of writing, and Global scope does not offer the key — the
574
+ personal file is the base layer.
601
575
  - Switches the write scope with `Tab`: **Global** (personal `settings.json`) or
602
576
  **Project** (`.pi/settings.json`, only when the project is trusted; an
603
577
  untrusted project refuses with a notice).
@@ -611,37 +585,64 @@ installs or fetches a package.
611
585
  without leaving the session.
612
586
 
613
587
  **Settings.** The second section uses the same frame and the same `Tab` scope
614
- switch, with its own cursor. It lists five preferences in four groups:
588
+ switch, with its own cursor. It lists fifteen preferences in six groups:
615
589
 
616
590
  ```
617
- bytefly ─────────────────────────────────────────────────────────────
618
- Extensions · Settings Global · 5 settings
619
-
620
- Startup
621
- ❯ ◻ Bytefly startup TUI Off
622
- Notifications
623
- ◼ Desktop notifications On
624
- Transcript
625
- Rail glyph ╎
626
- Output padding 1
627
- Terminal
628
- Hardware cursor Off
629
-
591
+ bytefly ──────────────────────────────────────────────────────────────
592
+ Extensions · Settings Global · 15 settings
593
+
594
+ Startup (1)
595
+ ❯ ◻ Bytefly startup TUI Off
596
+ Notifications (7)
597
+ ◻ Desktop notifications Off
598
+ ◼ Turn finished On
599
+ ◼ Needs input On
600
+ ◼ Job finished On
601
+ ◼ Watcher matched On
602
+ Delivery method Auto
603
+ Notification title Pi
604
+ Diagnostics (2)
605
+ ◼ Proactive health check On
606
+ Probe timeout 2000
607
+ Pricing (2)
608
+ Currency CNY (¥)
609
+ Cost decimals 2
610
+ Transcript (2)
611
+ Rail glyph ╎
612
+ Output padding 1
613
+ Terminal (1)
614
+ ◻ Hardware cursor Off
630
615
  Bytefly startup TUI — Pi setting
631
616
  Use bytefly's startup header in place of Pi's startup TUI.
632
617
  Default · ~/.pi/agent/settings.json
618
+
619
+ Esc close · ←/→ · Space change · Tab scope · ↑/↓ select
633
620
  ```
634
621
 
635
622
  | Group | Row | Source | Values |
636
623
  |---|---|---|---|
637
624
  | Startup | Bytefly startup TUI | Pi `settings.json` (`quietStartup`) | Off, On |
638
- | Notifications | Desktop notifications | `bytefly.json` | Off, On |
639
- | Transcript | Rail glyph | `bytefly.json` | `╎`, `│`, `┃`, `▏` |
625
+ | Notifications | Desktop notifications | `notify.enabled` | Off, On |
626
+ | Notifications | Turn finished | `notify.turnFinished` | Off, On |
627
+ | Notifications | Needs input | `notify.needsInput` | Off, On |
628
+ | Notifications | Job finished | `notify.jobFinished` | Off, On |
629
+ | Notifications | Watcher matched | `notify.matchFound` | Off, On |
630
+ | Notifications | Delivery method | `notify.method` | Auto, OSC 777, OSC 99, Windows toast, macOS, Linux |
631
+ | Notifications | Notification title | `notify.title` | Text, up to 40 characters |
632
+ | Diagnostics | Proactive health check | `doctor.proactive` | Off, On |
633
+ | Diagnostics | Probe timeout | `doctor.timeoutMs` | Number, 100–30000 ms |
634
+ | Pricing | Currency | `price.currency.code` | CNY, USD |
635
+ | Pricing | Cost decimals | `price.currency.decimals` | 0, 1, 2, 3, 4 |
636
+ | Transcript | Rail glyph | `chrome.rail` | Text, exactly one column |
640
637
  | Transcript | Output padding | Pi `settings.json` | 0, 1 |
641
638
  | Terminal | Hardware cursor | Pi `settings.json` | Off, On |
642
639
 
640
+ Choosing **Currency** also writes that currency's default precision. Both rows
641
+ stay editable afterwards, so the footer follows the explicit values.
642
+
643
643
  `Space` (or `Enter`) changes the focused row: a boolean toggles, an enum cycles
644
- forward. The detail pane names the source, describes the preference, and shows
644
+ forward, and a text row opens an inline editor on its value — the notification
645
+ title, the probe timeout, and the rail glyph. The detail pane names the source, describes the preference, and shows
645
646
  where the value is written — `Global · ~/.pi/agent/settings.json`, `Project ·
646
647
  .pi/bytefly.json`, or `Inherited from Global · .pi/bytefly.json`. A successful
647
648
  change marks the screen `Saved · reload to apply` — even when the terminal is too
@@ -656,21 +657,29 @@ with `true` or `false`.
656
657
 
657
658
  App rows are **Global only**: Pi's setters for them write the personal
658
659
  `settings.json`, and only resource paths have public project setters. Project
659
- scope therefore lists the two `bytefly.json` rows and a
660
+ scope therefore lists the twelve `bytefly.json` rows and a
660
661
  `App settings are Global only.` note rather than promising a write that cannot
661
- happen. A `bytefly.json` row writes one key in the selected scope through a
662
- per-key writer, so every other key, section, `$schema`, and `version` is
663
- preserved. A malformed document is refused with a notice instead of overwritten.
662
+ happen. A `bytefly.json` row writes one leaf in the selected scope (a depth-1
663
+ key, or a nested one such as `price.currency.decimals`) through a per-key
664
+ writer, so every sibling key, section, `$schema`, and `version` is preserved. A
665
+ malformed document is refused with a notice instead of overwritten.
664
666
 
665
667
  The non-`tui` form prints the headline, the groups, and `Label: Value` rows:
666
668
 
667
669
  ```
668
- bytefly: Settings · Global · 5 settings
670
+ bytefly: Settings · Global · 15 settings
669
671
 
670
672
  Startup (1)
671
673
  Bytefly startup TUI: Off
672
- Notifications (1)
674
+
675
+ Notifications (7)
673
676
  Desktop notifications: Off
677
+ Turn finished: On
678
+ Needs input: On
679
+ Job finished: On
680
+ Watcher matched: On
681
+ Delivery method: Auto
682
+ Notification title: Pi
674
683
  ...
675
684
  ```
676
685
 
@@ -682,6 +691,14 @@ means reduced capability — an optional package that is absent, a config file
682
691
  that fails to parse, a login that has not happened. Doctor never changes
683
692
  anything and never installs anything.
684
693
 
694
+ `web-access` publishes what it resolved at `session_start`, so the report names
695
+ the search provider and why it won (an explicit choice, the `auto` rule, or the
696
+ keyless default), and — when the provider takes a key and no literal `apiKey` is
697
+ set — its API-key variable and whether that variable is set. The key value is
698
+ never part of the report, only its variable name. Reachability is the one check
699
+ that touches the network, so it runs only on an explicit `/bytefly refresh`:
700
+ a session-start check stays offline.
701
+
685
702
  `/bytefly <extension>` narrows the same screen to that extension; `Enter`
686
703
  expands its findings, including the checks that passed, into the free height.
687
704
  `/bytefly refresh` ignores the findings cached earlier in the session. Every
@@ -693,8 +710,12 @@ headline; a named extension prints its full findings instead.
693
710
  | Key | Effect |
694
711
  |---|---|
695
712
  | `←` / `→` | Show the Extensions or Settings section. |
696
- | `Space` | Toggle the focused extension (Extensions); change the focused setting (Settings). |
697
- | `Enter` | Expand or collapse the focused extension's findings (Extensions); change the focused setting (Settings). |
713
+ | `Space` | Toggle the focused extension (Extensions); change the focused setting (Settings), or open the editor on a text row. |
714
+ | `x` | Clear the focused extension's project override (Extensions, Project scope). |
715
+ | `Enter` | Expand or collapse the focused extension's findings (Extensions); change the focused setting, or open the editor on a text row (Settings). |
716
+ | `←` / `→`, `Home` / `End` (`Ctrl+A` / `Ctrl+E`) | Move the editor's caret while a text row is being edited. |
717
+ | `Backspace` / `Delete`, `Ctrl+U` | Edit the buffer: delete a character, or clear it. |
718
+ | `Enter` / `Esc` (editing) | Save the buffer, or cancel and restore the stored value. |
698
719
  | `Tab` | Switch between Global and Project scope, in either section. |
699
720
  | `r` | Re-probe the doctor (Extensions). |
700
721
  | `↑`/`↓`, `j`/`k` | Move the selection, in either section. |
@@ -720,7 +741,7 @@ has a keyboard path.
720
741
  | wechat | — | `wechat/credentials.json` (not logged in) |
721
742
  | job | — | Windows has no `EXIT` trap; `ps` is POSIX-only |
722
743
  | todo, goal | — | their config files |
723
- | bytefly | — | TUI only for the header, footer, and turn rule; the screen's project scope needs a trusted project; `bytefly.json` |
744
+ | bytefly | — | TUI only for the header, footer, and turn rule; the screen's project scope needs a trusted project; an explicit `notify.method` this host cannot deliver; `bytefly.json` |
724
745
  | question | — | needs an interactive UI |
725
746
 
726
747
  Every extension that reads a config file also gets the malformed-JSON check:
@@ -732,7 +753,7 @@ the same entry in `lib/requirements.ts`, so the two cannot drift.
732
753
 
733
754
  The notify capability exists for unattended runs: start a long turn, leave the
734
755
  terminal, and get a desktop notification when Pi needs you — or when it is done.
735
- It fires on three events, each independently switchable in the `notify` section
756
+ It fires on four events, each independently switchable in the `notify` section
736
757
  of `bytefly.json` (see [Configuration](#configuration)):
737
758
 
738
759
  - **A run settled** (`agent_settled`). Pi finished the turn and waits for the
@@ -745,6 +766,12 @@ of `bytefly.json` (see [Configuration](#configuration)):
745
766
  - **A background job finished.** The [job](../job/) extension announces each
746
767
  terminal transition on the shared `pi.events` bus
747
768
  (`my-pi-agent/job-finished`); bytefly listens and never imports it.
769
+ - **A watcher matched.** A [watch](../watch/) whose `wake` is on matched while the
770
+ agent was idle, and announced it on `my-pi-agent/watch-match` with the cycle's
771
+ count, the watcher, and its newest line. The body names that line, sanitized and
772
+ clipped to 80 characters, because the line is the alert: a watch usually tails a
773
+ build or an error log. A match the agent was busy for does not notify — it is
774
+ collected for the next turn instead.
748
775
 
749
776
  ### Delivery method
750
777
 
@@ -775,6 +802,64 @@ adds its id, status, exit code, and label. Every piece that can carry model- or
775
802
  user-authored text — a prompt title, a job label — is sanitized to one
776
803
  control-free line before it reaches an escape sequence or a shell argument.
777
804
 
805
+ ## Update check
806
+
807
+ A session can be days behind npm without anyone noticing, and Pi's own startup
808
+ check only covers package sources it manages — a `pi -e ./`, a linked checkout,
809
+ or a git install gets nothing. So bytefly reads its own published version and
810
+ reports it once, in the two places it already owns:
811
+
812
+ ```
813
+ █▀█ Pi v1.3.0
814
+ █▀ █ enhanced by bytefly v0.11.4 ↑0.12.0
815
+ ```
816
+
817
+ ```
818
+ bytefly · 0.12.0 is available
819
+ ╎ ◌ current · v0.11.4
820
+ ╎ ◌ update · pi update --extensions
821
+ ╎ ◌ release · https://github.com/gavin-hu/my-pi-agent/releases/tag/v0.12.0
822
+ ```
823
+
824
+ The marker is the shared `↑` glyph and the newest version — the arrow is one cell
825
+ wide, so the header's column math is unchanged — painted with the header's
826
+ `accent` role: an action rather than a warning, so the muted version beside it
827
+ stays quiet and the transcript is not turned yellow for a normal state. It is
828
+ part of the chrome cell, so the two identity cells still line up, and it is the
829
+ first thing the identity sheds when the terminal narrows.
830
+
831
+ The notice is the shared transcript block: the `bytefly · title` headline at the
832
+ margin, then the loaded version and the fixing command behind the rail, all at
833
+ `info` severity because nothing is broken. It stays in scrollback and the model
834
+ reads the same sentence in plain text.
835
+
836
+ The request is `GET https://registry.npmjs.org/@gavin-hu%2Fmy-pi-agent/latest`,
837
+ fired without awaiting at `session_start` (Pi awaits handlers, and a five-second
838
+ registry call must not hold the first prompt) with a five-second abort. Only a
839
+ strictly newer, parseable semver announces anything: a non-ok status, a body
840
+ that is not JSON, a missing version, a network error, or an older or equal
841
+ version is silent. `PI_OFFLINE` (Pi's own switch) turns the check off;
842
+ `PI_SKIP_VERSION_CHECK` only governs Pi's `pi.dev` request.
843
+
844
+ The answer is cached in `<agentDir>/bytefly/update.json` as `latest` plus the
845
+ epoch milliseconds it was read at. A session that starts inside the 24-hour
846
+ window reuses that entry — painting the marker and posting the notice from it —
847
+ instead of repeating the request, so a working day of sessions costs one read.
848
+ Past the window the check runs again and rewrites the entry; a failed read writes
849
+ nothing, so the next session tries again. Offline, the cached entry is used with
850
+ no request and no write, because a stale version is still the best answer
851
+ available and the marker is advisory.
852
+
853
+ `/bytefly update` is the on-demand half: it ignores the window, reads the registry
854
+ now, records and paints the result, and prints one line — `up to date — v0.12.0
855
+ is the newest published version`, `v0.13.0 is available (current 0.12.0) —
856
+ https://…/releases/tag/v0.13.0`, `the update check was skipped (PI_OFFLINE is
857
+ set)`, or `could not read the published version`. It resolves no packages, probes
858
+ no extension, and posts no second notice, so a session open for days can see a
859
+ release without a restart. Opening the control center never checks: the screen has
860
+ no update surface, and the only request paths are `session_start` and this
861
+ command.
862
+
778
863
  ## Limitations
779
864
 
780
865
  - **Bytefly startup TUI is the startup surface only.** It installs or removes
@@ -788,19 +873,42 @@ control-free line before it reaches an escape sequence or a shell argument.
788
873
  - **Project overrides of app settings are not shown.** App rows read the
789
874
  personal file; a `.pi/settings.json` override still wins at runtime, and
790
875
  `pi config` is the place to manage it.
791
- - **The rail glyph picker is curated.** Four one-column glyphs are offered; a
792
- custom glyph is still a `bytefly.json` edit.
793
- - **`doctor` is a reserved first argument.** `/bytefly doctor` always selects the
794
- report, so an extension literally named `doctor` cannot be narrowed to by
795
- name. `settings` is reserved the same way.
876
+ - **Arrays and tables stay JSON.** A row is one boolean, enum, string, or number.
877
+ `doctor.skip` (an array), the `rates` table, and `price.peak` (a nested object)
878
+ are still `bytefly.json` edits; every scalar preference has a row.
879
+ - **A custom rail glyph must be one column.** The editor refuses anything else,
880
+ because a two-column glyph would push every railed body one cell right; the
881
+ former four-glyph menu is now the description's example list.
882
+ - **`doctor`, `settings`, and `update` are reserved first arguments.**
883
+ `/bytefly doctor` always selects the report, so an extension literally named
884
+ `doctor` cannot be narrowed to by name; `settings` and `update` are reserved
885
+ the same way.
796
886
  - **The picker manages only the `extensions` resource type.** Skills, prompts,
797
887
  and themes are handled by Pi's own `pi config`.
798
888
  - **Built-in extensions are not listed** (`builtin:mcp`, `builtin:codemode`, …):
799
889
  their authoritative names are not exposed to extensions. Use `pi config`.
800
890
  - **Packages are not installed, removed, or updated here**; use `pi install`,
801
891
  `pi remove`, and `pi update`.
802
- - **Project toggles are two-state** (`enable`/`disable`). Resetting an override
803
- back to "inherit" is done in `pi config`.
892
+ - **The update check reads npm's `latest` only.** A private registry, a version
893
+ published under another tag, or a checkout ahead of npm reports nothing, and
894
+ there is no channel or prerelease preference: `0.11.4` is behind `0.12.0`
895
+ whether or not a `0.12.0-beta.1` exists.
896
+ - **The cache window is fixed at 24 hours.** It runs at `session_start` in `tui`
897
+ mode and reuses an entry younger than that; there is no setting for it, and
898
+ `/bytefly` and `doctor` do not report the version. A failed or slow background
899
+ request is retried by the next session, or immediately by `/bytefly update`.
900
+ - **The marker needs the bytefly startup TUI.** It lives on the custom header,
901
+ so with `quietStartup` off the notice is the whole signal.
902
+ - **Pi may report the same update.** An npm-installed copy can also show Pi's
903
+ own `Package Updates Available` block; the two checks are independent and
904
+ `pi update --extensions` satisfies both.
905
+ - **No auto-update, and the release URL is text only.** The notice names a
906
+ command instead of running one, and the notice renderer cannot open a link, so
907
+ the release row prints the URL and nothing more — no changelog body, no way to
908
+ click through from the transcript.
909
+ - **Project toggles are two-state per press** (`enable`/`disable`). `x` clears
910
+ the override, which is the third state; a resource with no project entry in
911
+ `pi config` is still managed there.
804
912
  - **One built-in provider.** DeepSeek is the only provider with a built-in
805
913
  table; another provider's currency and tariff have to be stated in
806
914
  `bytefly.json`.
@@ -810,10 +918,17 @@ control-free line before it reaches an escape sequence or a shell argument.
810
918
  with an `offPeakMultiplier` below 1 is affected by it, so two providers with
811
919
  different time-of-day tariffs cannot both be described.
812
920
  - **No tiers, no special cache writes.** Per-request input tiers and
813
- Anthropic's 2× one-hour cache write are not modelled; Pi's recorded cost
814
- covers those models when no rate is configured.
815
- - **No FX conversion.** A fallback record is USD and is never converted into
816
- the configured currency.
921
+ Anthropic's 2× one-hour cache write are not modelled; switch to `USD` mode
922
+ to use Pi's recorded cost for those models.
923
+ - **No FX conversion.** The two modes are separate methods; neither converts
924
+ one currency into the other. A model is priced only by the selected method.
925
+ - **Two built-in footer indicators cannot be reproduced.** Pi's `(auto)` marks
926
+ that auto-compaction is on, and its `(sub)` marks a subscription-backed
927
+ provider. Auto-compaction is session state on Pi's own session object, and
928
+ `ReadonlyFooterDataProvider` exposes only the branch, the extension statuses,
929
+ and the provider count; `isUsingSubscription` lives on the internal model
930
+ runtime, not on the `ModelRegistry` an extension receives. A subscription
931
+ session therefore shows no cost segment at all, where Pi's footer said `(sub)`.
817
932
  - **The displayed number is the total, rounded once** — not the sum of rounded
818
933
  records.
819
934
  - **`doctor` can only check what is declared.** A new extension with no
@@ -835,8 +950,9 @@ control-free line before it reaches an escape sequence or a shell argument.
835
950
  prompt raised during a run, not a process waiting on stdin: jobs are
836
951
  fire-and-observe, so that state does not exist.
837
952
  - **An explicit `method` is honored blindly.** Point `method` at a notifier the
838
- host lacks (or an OSC method off a TTY) and the notification is silently
839
- dropped; only `auto` inspects the environment.
953
+ host lacks (or an OSC method off a TTY) and the notification is dropped; only
954
+ `auto` inspects the environment. `doctor` now reports the mismatch as a
955
+ `degraded` finding, which says what to change but does not fix it for you.
840
956
  - **The working counter is one global row.** `setWorkingMessage` drives the
841
957
  single working row, so bytefly and any other extension setting it race — last
842
958
  writer wins — and the message is reset to Pi's default at `agent_end`. If the
@@ -861,8 +977,8 @@ control-free line before it reaches an escape sequence or a shell argument.
861
977
  - No status chip for the doctor report: it is on demand plus one notice, not a
862
978
  permanent footer segment.
863
979
  - No notification for individual tool calls, message turns, or subagent steps:
864
- the three triggers — a settled run, a prompt mid-run, a finished job — are the
865
- whole surface.
980
+ the four triggers — a settled run, a prompt mid-run, a finished job, a watcher
981
+ match — are the whole surface.
866
982
  - No sound, action buttons, notification history, or de-duplication; a
867
983
  notification is a fire-and-forget message, not an inbox.
868
984
  - No frozen per-run working duration: the counter is live and is handed back to
@@ -878,7 +994,7 @@ rather than showing a wrong value.
878
994
 
879
995
  | Integration point | Details |
880
996
  |---|---|
881
- | Header | `ctx.ui.setHeader` with a custom component, replacing Pi's built-in header; installed only when `ctx.mode === "tui"` and `pi.getSettings().quietStartup === true` (the **Bytefly startup TUI** row). `false`, `"header"`, and unset call `setHeader(undefined)`, leaving Pi's built-in header in place. The nameplate insets by Pi's `outputPad` (read through `pi.getSettings()`), so it sits on the transcript margin, and the help hangs one body indent inside it. The mark and text are drawn from the active theme's tokens, the help keys resolve live through `keyText`, and the mark falls back to a wordmark on Apple Terminal (`isAppleTerminalSession`), mirroring Pi's built-in. |
997
+ | Header | `ctx.ui.setHeader` with a custom component, replacing Pi's built-in header; installed only when `ctx.mode === "tui"` and `pi.getSettings().quietStartup === true` (the **Bytefly startup TUI** row). `false`, `"header"`, and unset call `setHeader(undefined)`, leaving Pi's built-in header in place. The nameplate insets by Pi's `outputPad` (read through `pi.getSettings()`), so it sits on the transcript margin, and the help hangs one body indent inside it. The mark and text are drawn from the active theme's tokens, the help keys resolve live through `keyText`, and the mark falls back to a wordmark on Apple Terminal (`isAppleTerminalSession`), mirroring Pi's built-in. The chrome version gains `↑<latest>` from the `UpdateSource`, and `registerHeader` returns a `refresh()` handle that re-installs the last context's header when that value arrives. |
882
998
  | Footer | `ctx.ui.setFooter` with a custom component, replacing Pi's built-in footer; installed only when `ctx.mode === "tui"`. |
883
999
  | User message | `pi.registerMarkdownTransformer` puts a two-row `You` nameplate (the label over a full-width rule) above each user message; one padded level-2 heading carries the accent through `mdHeading` for both rows, and the shipped themes set `userMessageBg` to the terminal default, so the block has no fill. Pi renders the result as Markdown, so the surface adds no component or color of its own. |
884
1000
  | Notice | `registerMessageRenderer(NOTICE_TYPE)` draws every shared transcript notice at the margin from its `details`; the headline stays at the margin and its rows hang from the transcript rail, painted by the notice's worst severity, whose glyph comes from the shared `RailSource`. `job` and `watch` post one when their work settles, and `doctor` posts one at `session_start` for a missing hard requirement. The type and shape come from `lib/notice.ts`. A message without `details` falls back to Pi's default custom-message box. |
@@ -889,14 +1005,15 @@ rather than showing a wrong value.
889
1005
  | Notifications | `agent_settled` (skipped when aborted), `ui_prompt_start` only while `agent_start`…`agent_settled` marks a run active, and `my-pi-agent/job-finished` on `pi.events`; the `notify` section of `bytefly.json` gates each. Delivery writes an OSC 777/99 sequence to stdout or spawns `osascript`, `notify-send`, or `powershell.exe`. The bus subscription is created in `session_start` and released in `session_shutdown`, and the `job` extension never imports bytefly. |
890
1006
  | Picker discovery | `SettingsManager.create(...)` plus `DefaultPackageManager.resolve({ onMissing: "skip" })`, both public exports; both views drop `bytefly`'s own entrypoint. |
891
1007
  | Picker writes | `SettingsManager` setters (`setExtensionPaths`, `setPackages`, `setProjectExtensionPaths`, `setProjectPackages`) then `flush()`; `ctx.reload()` after the screen closes when changes are pending. |
892
- | Settings writes | App rows call `SettingsManager` setters (`setQuietStartup`, `setOutputPad`, `setShowHardwareCursor`) on the global store and `flush()`, then surface `drainErrors()`; `bytefly.json` rows call `writeConfigValue` on the selected scope's document, one key at a time. A failed or refused write shows a notice in the screen's hint row and leaves the displayed value unchanged; a successful one marks the screen dirty. |
893
- | Doctor tool | `pi.registerTool("run_doctor")`, read-only (`readOnlyHint: true`), `exposure: "direct"`, active by default, with a typed `outputSchema` mirroring `structuredContent`. Its `readOnlyHint` is what the shared default-deny policy in `lib/policy.ts` reads to allow it in plan mode. |
1008
+ | Settings writes | App rows call `SettingsManager` setters (`setQuietStartup`, `setOutputPad`, `setShowHardwareCursor`) on the global store and `flush()`, then surface `drainErrors()`; `bytefly.json` rows call `writeConfigValueAt` on the selected scope's document with the row's leaf path, so a nested key (`price.currency.decimals`) and a flat one (`notify.method`) share one writer. A failed or refused write shows a notice in the screen's hint row and leaves the displayed value unchanged; a successful one marks the screen dirty. |
1009
+ | Doctor tool | `pi.registerTool("run_doctor")`, read-only (`readOnlyHint: true`, `openWorldHint: false`), direct exposure and active on registration (the defaults, so not declared), with a typed `outputSchema` mirroring `structuredContent`. Its `readOnlyHint` is what the shared default-deny policy in `lib/policy.ts` reads to allow it in plan mode. |
894
1010
  | Doctor probes | `run_doctor` and the screen share the session cache created by `registerDoctor`; the screen mounts on that cache and probes on the first Extensions view (the command's `refresh` forces it), while the text modes probe before printing, and `/bytefly refresh` and `refresh: true` re-probe. Probes spawn `<binary> --version`, lazily import optional packages, and read config files through `lib/requirements.ts`; nothing is ever written. |
895
1011
  | Status routing | Reads `footerData.getExtensionStatuses()` (a custom footer replaces the built-in one entirely); `worktree` routes to line 1, plan/alert statuses to line 2's trailing slot. |
896
- | Config | `bytefly.json` from the agent directory and `<cwd>/.pi`; the `price` section is read once per footer install (a new session or a session-tree change), the `notify` and `doctor` sections at `session_start`, and the `chrome` section's rail glyph at `session_start`/`session_tree`. Nothing watches the document. |
1012
+ | Update check | `session_start` in `tui` mode reads `<agentDir>/bytefly/update.json` and, when the entry is inside `UPDATE_CACHE_TTL_MS` (24 h), records it without a request; otherwise it fires `fetchLatestVersion()` without awaiting, so the handler never holds the prompt, and writes the answer back. The registry read is a fixed `GET` of npm's `latest` document over an injected `fetch`, bounded by an abort timeout; a strictly newer, parseable semver is recorded in the per-load `UpdateSource`, `onUpdate` re-installs the header, and `pi.sendMessage(updateNotice(...))` posts the one notice with its release URL. `PI_OFFLINE` uses the cache with no request and no write. `/bytefly update` calls the returned `UpdateHandle.recheck()`, which bypasses the window, records, paints, and returns an `UpdateOutcome`; a generation counter drops a background reply that lands after `session_shutdown`, and a per-load flag keeps it to one announcement. |
1013
+ | Config | `bytefly.json` from the agent directory and `<cwd>/.pi`; the `price` section is read once per footer install (a new session or a session-tree change), the `notify` and `doctor` sections at `session_start`, and the `chrome` section's rail glyph at `session_start`/`session_tree`. Nothing watches the document. The update check has no section: it is on whenever `PI_OFFLINE` is off, and its 24 h window is not configurable. |
897
1014
  | Data sources | `footerData.getGitBranch()`, `footerData.getAvailableProviderCount()`, `ctx.getContextUsage()`, `ctx.sessionManager.getEntries()`, `ctx.sessionManager.getSessionName()`, `ctx.model`; subscribes to `footerData.onBranchChange` to repaint. |
898
- | State storage | None besides the inert separator entries. The footer reads session entries and the price config (a memoized scan) and writes no tool-result `details`; it appends `bytefly-separator` entries excluded from LLM context. The screen derives its view from the picker runtime and the current reports, and persists extension toggles through `settings.json`. The Settings section derives from a snapshot of the personal `settings.json` and both `bytefly.json` documents, and persists through the same `SettingsManager` setters or the per-key writer. Doctor's findings are session-scoped in memory only — cached for the session, never written to the session or a file. Notify holds only the config and the run-active flag in its closure. |
899
- | Lifecycle | `session_start` installs the header when Pi's `quietStartup` is `true` and the footer, loads the chrome rail glyph, points the doctor runtime at the session cwd, probes the extensions with hard requirements when `bytefly.json`'s `doctor.proactive` is on and `quietStartup` is `true`, and loads the notify config and subscribes to the job bus; `session_tree` reinstalls the header and footer and reloads the rail; `session_shutdown` restores Pi's built-in header and footer, disposes the branch subscription, drops doctor's cached findings, and drops the notify bus subscription. |
1015
+ | State storage | None besides the inert separator entries. The footer reads session entries and the price config (a memoized scan) and writes no tool-result `details`; it appends `bytefly-separator` entries excluded from LLM context. The screen derives its view from the picker runtime and the current reports, and persists extension toggles through `settings.json`. The Settings section derives from a snapshot of the personal `settings.json` and both `bytefly.json` documents, and persists through the same `SettingsManager` setters or the per-leaf `bytefly.json` writer. Doctor's findings are session-scoped in memory only — cached for the session, never written to the session or a file. Notify holds only the config and the run-active flag in its closure. The update check holds one version in the per-load `UpdateSource` and writes no session entry; its one file is `<agentDir>/bytefly/update.json`, the cached `latest` plus `checkedAt`, written atomically and read back on the next session. |
1016
+ | Lifecycle | `session_start` installs the header when Pi's `quietStartup` is `true` and the footer, loads the chrome rail glyph, points the doctor runtime at the session cwd, probes the extensions with hard requirements when `bytefly.json`'s `doctor.proactive` is on and `quietStartup` is `true`, loads the notify config and subscribes to the job bus, and starts the published-version check; `session_tree` reinstalls the header and footer and reloads the rail; `session_shutdown` restores Pi's built-in header and footer, disposes the branch subscription, drops doctor's cached findings, drops the notify bus subscription, and clears the recorded version. |
900
1017
 
901
1018
  ## Design notes
902
1019
 
@@ -986,19 +1103,21 @@ rather than showing a wrong value.
986
1103
  - **Two lines, four zones.** Line 1 is identity (`pwd` left, branch + worktree
987
1104
  right); line 2 is resources (meters left, model + level right). This keeps
988
1105
  "where am I / what state" separate from "how much am I burning".
1106
+ - **Child spend is a second figure.** The footer never adds a subagent's tokens to
1107
+ the session's; it prices the `my-pi-agent/subagent-finished` events with the
1108
+ same pricer and renders them as `↳`, so "this session" keeps meaning the
1109
+ dispatching process and the delegated work stays visible beside it.
989
1110
  - **The gauge is anchored at column 0.** A leading mode slot would shift the
990
1111
  context percentage left and right as plan mode came and went; trailing it
991
1112
  after a `╎` keeps the percentage in a fixed place while the mode stays
992
1113
  visible as the meters drop.
993
1114
  - **Statuses are routed, not repeated.** The built-in footer renders every
994
- `setStatus` value on its own line; the bar splits them by meaning (`worktree`
995
- and `browse` are location, plan/alert is mode). The [wechat](../wechat/)
996
- extension publishes `✆ wechat` while its bridge runs, joining the mode/alert
997
- slot; this is why the bar must render `getExtensionStatuses()` at all.
998
- - **Browse joins the git state.** The `◉ <port>` chip is routed beside `⎇` branch
999
- and `⑂` worktree rather than after `pwd`, so the left zone stays the
1000
- filesystem identity (path + session name) and every "where am I / what is
1001
- running" fact sits on line 1 right.
1115
+ `setStatus` value on its own line, so the bar sorts them by meaning instead:
1116
+ `worktree` and `browse` are location, so the `◉ <port>` chip sits beside `⎇`
1117
+ branch and `⑂` worktree and the left zone stays the filesystem identity; a
1118
+ plan or a failed job is mode or alert, so it takes the slot after the gauge.
1119
+ The [wechat](../wechat/) extension publishes `✆ wechat` while its bridge runs,
1120
+ which is why the bar has to render `getExtensionStatuses()` at all.
1002
1121
  - **Segments degrade; they do not clip.** Each `Segment` carries progressively
1003
1122
  shorter `forms` and a `weight`; the layout advances the lowest-priority form
1004
1123
  or drops the lowest-priority segment, and only truncates once everything is
@@ -1043,10 +1162,10 @@ rather than showing a wrong value.
1043
1162
  usage records (`collect/`), records are priced into totals (`price/`), and the
1044
1163
  snapshot is drawn (`render/`). Pricing is a pure function of records and rates
1045
1164
  — no session, terminal, or theme to stub — which is what lets the published
1046
- DeepSeek example be a unit test. The recorded-cost fallback keeps the stage
1047
- opt-in per model: a record with no matching rate is exactly Pi's own number,
1048
- in Pi's catalog units. The built-in DeepSeek table is only the default value
1049
- of `rates`, so nothing in the pricer is provider-specific.
1165
+ DeepSeek example be a unit test. In `CNY` mode a record with no matching rate
1166
+ simply contributes nothing; `USD` mode is the opt-in path to Pi's own number.
1167
+ The built-in DeepSeek table is only the default value of `rates`, so nothing
1168
+ in the pricer is provider-specific.
1050
1169
  - **DeepSeek is built in, not bundled as a catalog.** Its published price is in
1051
1170
  yuan at two tariffs, while Pi's catalog holds one USD number per model, so an
1052
1171
  unconfigured DeepSeek session showed a bill that was neither the right
@@ -1084,12 +1203,18 @@ rather than showing a wrong value.
1084
1203
  - **The install hint has one home.** `document` and `web-access` format their
1085
1204
  tool errors from the same `REQUIREMENTS` entries `doctor` prints, so a user
1086
1205
  reads the same command whether a tool failed or the report named it.
1087
- - **Settings are a curated table, not a schema walker.** Five rows prove the
1088
- row types, both scopes, and both write paths. A generic walker would expose
1089
- keys the screen cannot edit safely yet — arrays, nested tariffs, credentials —
1090
- so the catalogue grows one proven family at a time. App rows are Global-only
1091
- because Pi's setters write the personal file; Project scope lists only the
1092
- `bytefly.json` rows it can actually write.
1206
+ - **Settings are a curated table, not a schema walker.** The rows cover every
1207
+ scalar preference across four kinds — a boolean, an enum, a string with
1208
+ its own rule, and a number — both scopes, both write paths, and one nested leaf
1209
+ (`price.currency.decimals`). A row states its section and the path to its leaf,
1210
+ and one reader per section resolves it project-over-global-over-defaults through
1211
+ the same normalizer the extension loads with, so the screen and the runtime
1212
+ cannot disagree about a value; a text row's rule calls the same predicate the
1213
+ loader uses, so an accepted value is never replaced on the next read. A generic
1214
+ walker would expose keys the screen cannot edit safely — arrays, nested
1215
+ tariffs, credentials — so the catalogue grows one proven family at a time. App
1216
+ rows are Global-only because Pi's setters write the personal file; Project
1217
+ scope lists only the `bytefly.json` rows it can actually write.
1093
1218
  - **The doctor is quiet unless the bytefly startup TUI is on.** Only `missing`
1094
1219
  findings post a notice, only for enabled extensions, only with a UI, only once
1095
1220
  per session, and only when Pi's `quietStartup` is `true` — the same state that
@@ -1108,6 +1233,21 @@ rather than showing a wrong value.
1108
1233
  - **The prerequisite table is data.** No I/O, no host access, no module state:
1109
1234
  every probe dependency arrives as a seam, so `probeExtension` is testable with
1110
1235
  no process and `lib/` stays value-only.
1236
+ - **The update check is a self-report, not an update mechanism.** It answers one
1237
+ question — is the version in this checkout current? — and answers it in the two
1238
+ surfaces bytefly already owns, the header identity and one notice. It never
1239
+ installs, never edits settings, and never fails a session: an unreachable
1240
+ registry and an up-to-date one are indistinguishable to the user, which is the
1241
+ only failure mode a background check is allowed to have.
1242
+ - **The marker is an action, the notice is information.** The same fact is drawn
1243
+ in the accent on the header, where it sits beside a control the user can act
1244
+ on, and at `info` severity in the transcript, where a `warn` rail would say
1245
+ something is broken. Neither surface needs per-frame polling: the value arrives
1246
+ once, the memo key includes it, and one `setHeader` re-install repaints.
1247
+ - **The transport stays local.** `web-access`'s runner does redirects, SSRF
1248
+ checks, HTML extraction, and caching — none of it applies to one fixed JSON
1249
+ endpoint, and an extension may not import another's modules, so the ~20-line
1250
+ read lives beside the check with the `fetch` and timeout as seams.
1111
1251
 
1112
1252
  ## Files
1113
1253
 
@@ -1117,7 +1257,8 @@ calculation), `message/` (the user-message marker), `notice/` (the shared notice
1117
1257
  elapsed counter on Pi's working row), `panel/` (the
1118
1258
  unified screen and its merged model),
1119
1259
  `picker/` (discovery and the settings store), `doctor/` (the `run_doctor` tool
1120
- and its probe runtime), and `notify/`. The chrome and the screen are drawn from
1260
+ and its probe runtime), `notify/`, and `update/` (the published-version check
1261
+ behind the header marker, its disk cache, and the on-demand re-check). The chrome and the screen are drawn from
1121
1262
  the package's [`lib/tui/`](../../lib/tui/) design system (tokens, rules, rows,
1122
1263
  columns, gauge), so the look follows the active theme.
1123
1264
 
@@ -1135,10 +1276,10 @@ columns, gauge), so the look follows the active theme.
1135
1276
  | `footer/collect/entries.ts` | Session entries → usage records, each resolved to the model in effect. |
1136
1277
  | `footer/collect/snapshot.ts` | Snapshot assembly; memoized pricing scan per branch leaf. |
1137
1278
  | `footer/price/types.ts` | `Currency`, `ModelRates`, `PeakWindow`/`PeakSchedule`, `ByteflyConfig`. |
1138
- | `footer/price/config.ts` | The `price` section of `bytefly.json`: schema, defaults, built-in DeepSeek rates/tariff, normalizer, loader. |
1279
+ | `footer/price/config.ts` | The `price` section of `bytefly.json`: schema, defaults, built-in DeepSeek rates/tariff, currency defaults, normalizer, loader. |
1139
1280
  | `footer/price/rates.ts` | Model-key matching: exact and wildcard, most specific first. |
1140
1281
  | `footer/price/schedule.ts` | Peak-window parsing and evaluation in a configured zone. |
1141
- | `footer/price/price.ts` | Records → totals: rates, peak multiplier, recorded-cost fallback. |
1282
+ | `footer/price/price.ts` | Records → totals: rated cost in both modes, Pi's recorded cost as USD's fallback, and the peak multiplier. |
1142
1283
  | `footer/render/format.ts` | Generic formatters (tokens, cost in the configured currency, cwd, the context role, truncation). |
1143
1284
  | `footer/render/status.ts` | Status-chip transforms: `compactStatus` and `dropStatusDetail`. |
1144
1285
  | `footer/render/segments.ts` | The segment builders: forms, weights, separators, roles. |
@@ -1166,8 +1307,8 @@ columns, gauge), so the look follows the active theme.
1166
1307
  | `panel/types.ts` | `PanelSection` (`extensions` | `settings`), `PanelRow`, and `PanelModel`, the merged row model. |
1167
1308
  | `panel/join.ts` | The item/report union with the unambiguous-name health rule and unique row keys, groups, and ordering. |
1168
1309
  | `panel/format.ts` | Non-TUI text: the extension headline and grouped listing with a per-row health suffix, plus the section segments, the Settings counts/headline, and the grouped Settings listing. |
1169
- | `panel/settings.ts` | The curated Settings catalogue: descriptors, snapshot views and origins, value cycling, stored-value mapping, validation, and grouping. |
1170
- | `panel/settings-runtime.ts` | The Settings runtime: snapshot cache, scope guards, app setters, per-key `bytefly.json` writes, dirty state, and change listeners. |
1310
+ | `panel/settings.ts` | The curated Settings catalogue: descriptors, snapshot views and origins, value cycling, stored-value mapping, validation, and grouping, plus the per-section readers that resolve a row's leaf over the global and project documents. |
1311
+ | `panel/settings-runtime.ts` | The Settings runtime: snapshot cache, scope guards, app setters, per-leaf `bytefly.json` writes, dirty state, and change listeners. |
1171
1312
  | `panel/tui.ts` | `PanelComponent`, the two-section dock screen (Extensions list and Settings list), drawn from the `lib/tui/` kit. |
1172
1313
  | `panel/commands.ts` | `/bytefly`: one command for the screen sections, the `doctor` report, the settings listing, and their text modes. |
1173
1314
  | `picker/types.ts` | Picker host seams (`SettingsStore`, `ExtensionResolver`, `Discovery`), the app-settings setters, and row types. |
@@ -1184,8 +1325,14 @@ columns, gauge), so the look follows the active theme.
1184
1325
  | `notify/types.ts` | `NotifyConfig`, `NotifyMethod`, and the method list. |
1185
1326
  | `notify/config.ts` | The `notify` section of `bytefly.json`: defaults, validation, loader. |
1186
1327
  | `notify/dispatch.ts` | Method selection and delivery: OSC 777/99, Windows toast, `osascript`, `notify-send`, over injected seams. |
1187
- | `notify/format.ts` | Pure notification text: session label and the turn / input / job bodies. |
1188
- | `notify/register.ts` | Registers the three triggers, loads the config at `session_start`, and owns the job-bus subscription. |
1328
+ | `notify/format.ts` | Pure notification text: session label and the turn / input / job / watch bodies. |
1329
+ | `notify/register.ts` | Registers the four triggers, loads the config at `session_start`, and owns the job- and watch-bus subscriptions. |
1330
+ | `update/check.ts` | The one registry read: the `latest` URL, the abort timeout, the `FetchLike` seam, and the failure-to-`undefined` mapping. |
1331
+ | `update/version.ts` | Pure semver parsing and `isNewerVersion`, including prerelease precedence. |
1332
+ | `update/state.ts` | `UpdateSource` and `createUpdateState` (the newest known version, one record per load), plus `UpdateOutcome`, the four answers an explicit re-check can give. |
1333
+ | `update/cache.ts` | The disk cache: `<agentDir>/bytefly/update.json`, the 24 h window in `isFresh`, a validated read that degrades to a miss, and the atomic write. |
1334
+ | `update/format.ts` | `updateMarker`, `updateNotice`, `releaseUrl`, and `updateOutcomeText`: the header's `↑0.12.0`, the transcript notice with its release row, and the one line `/bytefly update` prints. |
1335
+ | `update/register.ts` | The lifecycle: the cache-first session-start read, the async registry call, the `PI_OFFLINE` and `tui` gates, the once-per-load announcement, the shutdown generation guard, and the `recheck()` handle `/bytefly update` calls. |
1189
1336
 
1190
1337
  ## Testing
1191
1338
 
@@ -1216,7 +1363,8 @@ transforms (`picker/toggle`), discovery with an injected factory
1216
1363
  (`picker/discovery`), and the runtime state machine (`picker/runtime`). The
1217
1364
  unified screen is covered by its pure model merge (`panel/join`), its text
1218
1365
  formatter (`panel/format`), the settings catalogue and its cycling
1219
- (`panel/settings`), the scope-aware settings runtime (`panel/settings-runtime`),
1366
+ (`panel/settings`), the scope-aware settings runtime (`panel/settings-runtime`,
1367
+ including a nested leaf write that keeps its siblings),
1220
1368
  the screen's rendering and key handling (`panel/tui`), and the command through
1221
1369
  the bytefly factory (`panel/commands`).
1222
1370
  The doctor surface is covered by its pure report
@@ -1224,8 +1372,16 @@ formatting (`doctor/format`), its config loader (`doctor/config`), and
1224
1372
  registration, the proactive notice, the session cache, and `run_doctor`
1225
1373
  through the factory (`doctor/register`). The notify capability is covered by its section loader
1226
1374
  (`notify/config`), the pure text builders (`notify/format`), method selection and
1227
- delivery over recording seams (`notify/dispatch`), and its three triggers,
1228
- reload, and shutdown through `registerNotify` (`notify/register`).
1375
+ delivery over recording seams (`notify/dispatch`), and its four triggers,
1376
+ reload, and shutdown through `registerNotify` (`notify/register`). The update
1377
+ check is covered by the version-ordering table (`update/version`), the transport
1378
+ with an injected fetch (`update/check`), the per-load store (`update/state`), the
1379
+ marker, notice, and release-URL wording (`update/format`), the cache read, write,
1380
+ and window (`update/cache`), and its lifecycle — a fresh entry reused without a
1381
+ request, a stale one re-read and rewritten, silent on failure or an older version,
1382
+ cache-only offline, skipped outside `tui`, an explicit `recheck()` that ignores
1383
+ the window, and immunity to a reply that lands after shutdown
1384
+ (`update/register`).
1229
1385
  The user-message nameplate is covered by its pure transform
1230
1386
  (`message/format`, including a render through pi-tui's real `Markdown`) and its
1231
1387
  registration (`message/register`), plus the factory check in `extension`.