@gavin-hu/my-pi-agent 0.11.3 → 0.12.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 (86) hide show
  1. package/README.md +13 -8
  2. package/README.zh-CN.md +5 -3
  3. package/assets/chrome.png +0 -0
  4. package/assets/plan-review.png +0 -0
  5. package/extensions/bytefly/README.md +291 -207
  6. package/extensions/bytefly/doctor/runtime.ts +16 -0
  7. package/extensions/bytefly/doctor/tools.ts +1 -3
  8. package/extensions/bytefly/footer/price/config.ts +54 -15
  9. package/extensions/bytefly/footer/price/price.ts +20 -7
  10. package/extensions/bytefly/footer/price/types.ts +13 -7
  11. package/extensions/bytefly/footer/render/format.ts +3 -2
  12. package/extensions/bytefly/footer/render/segments.ts +3 -2
  13. package/extensions/bytefly/footer/types.ts +1 -0
  14. package/extensions/bytefly/header/component.ts +21 -6
  15. package/extensions/bytefly/header/config.ts +3 -0
  16. package/extensions/bytefly/header/register.ts +34 -3
  17. package/extensions/bytefly/index.ts +13 -3
  18. package/extensions/bytefly/panel/settings-runtime.ts +71 -28
  19. package/extensions/bytefly/panel/settings.ts +201 -38
  20. package/extensions/bytefly/panel/tui/entries.ts +58 -0
  21. package/extensions/bytefly/panel/tui/extensions-view.ts +86 -0
  22. package/extensions/bytefly/panel/tui/frame.ts +71 -0
  23. package/extensions/bytefly/panel/tui/nav.ts +75 -0
  24. package/extensions/bytefly/panel/tui/settings-view.ts +115 -0
  25. package/extensions/bytefly/panel/tui.ts +205 -319
  26. package/extensions/bytefly/picker/runtime.ts +61 -2
  27. package/extensions/bytefly/picker/toggle.ts +56 -0
  28. package/extensions/bytefly/update/check.ts +59 -0
  29. package/extensions/bytefly/update/format.ts +32 -0
  30. package/extensions/bytefly/update/register.ts +72 -0
  31. package/extensions/bytefly/update/state.ts +28 -0
  32. package/extensions/bytefly/update/version.ts +83 -0
  33. package/extensions/document/README.md +2 -2
  34. package/extensions/document/tool.ts +1 -3
  35. package/extensions/file-open/README.md +30 -15
  36. package/extensions/file-open/index.ts +4 -3
  37. package/extensions/file-open/open.ts +52 -21
  38. package/extensions/goal/README.md +39 -13
  39. package/extensions/goal/clarify-prompt.ts +5 -36
  40. package/extensions/goal/clarify.ts +55 -86
  41. package/extensions/goal/commands.ts +41 -35
  42. package/extensions/goal/format.ts +12 -4
  43. package/extensions/goal/tools.ts +28 -8
  44. package/extensions/goal/tui.ts +1 -1
  45. package/extensions/memory/README.md +1 -1
  46. package/extensions/plan/README.md +24 -8
  47. package/extensions/plan/blocks.ts +1 -35
  48. package/extensions/plan/commands.ts +18 -12
  49. package/extensions/plan/plans.ts +1 -1
  50. package/extensions/plan/policy.ts +4 -3
  51. package/extensions/plan/review.ts +243 -0
  52. package/extensions/plan/runtime.ts +4 -0
  53. package/extensions/plan/steps.ts +34 -23
  54. package/extensions/plan/todos.ts +5 -58
  55. package/extensions/plan/tools.ts +66 -222
  56. package/extensions/plan/tui.ts +17 -9
  57. package/extensions/plan/types.ts +3 -0
  58. package/extensions/plan/web-client.ts +51 -18
  59. package/extensions/plan/web-page.ts +5 -0
  60. package/extensions/plan/web-router.ts +11 -3
  61. package/extensions/plan/web.ts +100 -36
  62. package/extensions/question/README.md +1 -1
  63. package/extensions/question/tools.ts +1 -1
  64. package/extensions/subagent/index.ts +0 -2
  65. package/extensions/todo/README.md +19 -11
  66. package/extensions/todo/index.ts +5 -3
  67. package/extensions/todo/nudge.ts +58 -9
  68. package/extensions/todo/runtime.ts +28 -17
  69. package/extensions/web-access/README.md +3 -3
  70. package/extensions/web-access/fetch/tool.ts +1 -3
  71. package/extensions/web-access/search/tool.ts +1 -3
  72. package/extensions/wechat/README.md +22 -11
  73. package/extensions/wechat/bridge.ts +90 -22
  74. package/extensions/wechat/commands.ts +45 -1
  75. package/extensions/wechat/doctor.ts +1 -0
  76. package/extensions/wechat/index.ts +3 -1
  77. package/extensions/wechat/status-screen.ts +32 -2
  78. package/extensions/wechat/tools.ts +0 -2
  79. package/lib/README.md +2 -1
  80. package/lib/config.ts +43 -1
  81. package/lib/env.ts +12 -0
  82. package/lib/requirements.ts +76 -2
  83. package/lib/todo-state.ts +62 -0
  84. package/lib/tui/glyphs.ts +1 -0
  85. package/package.json +1 -1
  86. package/assets/bytefly.schema.json +0 -785
@@ -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).
@@ -73,9 +78,10 @@ in [`header/config.ts`](./header/config.ts).
73
78
  ▰▰▰▰▰▰▱▱▱▱ 62%/200k · ¥0.31 · ↑42k ↓8.0k · R96k CH 87% ╎ ⋮ plan opus-4.5 · high
74
79
  ```
75
80
 
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).)
81
+ (The examples show the default `¥` symbol. A model that neither `rates` nor the
82
+ built-in table covers contributes nothing in `CNY` mode, so the bar shows no
83
+ figure rather than a guessed one; `USD` mode adds Pi's recorded dollars for that
84
+ record instead — see the currency note under [Configuration](#configuration).)
79
85
 
80
86
  With no plan mode there is no mode slot, so the `╎` is omitted:
81
87
 
@@ -113,8 +119,9 @@ rounding to a misleading `¥0.3` — and a positive amount below one minor unit
113
119
  shows as `¥<0.01` rather than `¥0.00`.
114
120
 
115
121
  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:
122
+ rates and peak tariff built into [`footer/price/config.ts`](./footer/price/config.ts).
123
+ A `bytefly.json` can switch to the published dollar table, add models of your own,
124
+ or replace a built-in entry:
118
125
 
119
126
  ```
120
127
  ~/repo/project ⎇ main
@@ -268,6 +275,13 @@ method follows the terminal (OSC 777, OSC 99, a Windows toast) or falls back to
268
275
  the platform notifier (`osascript`, `notify-send`); see
269
276
  [Notifications](#notifications).
270
277
 
278
+ **Update check.** Once per interactive session, in the background, the loaded
279
+ version is compared with the published `@gavin-hu/my-pi-agent` on npm. A newer
280
+ one puts `↑0.12.0` after the header's chrome version and posts one transcript
281
+ notice naming `pi update --extensions`. The check is skipped when `PI_OFFLINE`
282
+ is set, makes no request outside `tui`, and a failed request is silent; see
283
+ [Update check](#update-check).
284
+
271
285
  ## Commands
272
286
 
273
287
  | Command | What it does |
@@ -303,45 +317,36 @@ print/JSON/RPC run gets an OS-level notification or none.
303
317
 
304
318
  ## Configuration
305
319
 
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.
320
+ Two things are JSON: the transcript rail's glyph (`chrome.rail`, default `╎`,
321
+ read at `session_start`/`session_tree`) and this extension's three config
322
+ sections — `price`, `notify`, and `doctor`. Everything else about the look is
323
+ tuned in code: the header in [`header/config.ts`](./header/config.ts) (mark and
324
+ identity tokens, tagline, grouped help), the footer in
325
+ [`footer/config.ts`](./footer/config.ts) (thresholds, gauge glyphs and widths,
326
+ separators, icons, status keys), and the turn separator in
327
+ [`separator/config.ts`](./separator/config.ts) (glyph, label prefix, padding,
328
+ colors).
329
+
330
+ Every key of every section is listed with its type, default, and bounds in the
331
+ [configuration reference](../../docs/references/configuration-reference.md); this section
332
+ covers what that table cannot — when each section is read, and what the cost
333
+ does when no rate matches.
334
+
335
+ `bytefly.json` is one shared document with a top-level section per extension (see
336
+ [Configuration](../../docs/configuration.md)). It is read from the global
337
+ document, then `<cwd>/.pi/bytefly.json`, project winning. The old per-extension
338
+ files (`document.json`, `jobs.json`, …) are no longer read; `bun run config:migrate`
339
+ moves them into the shared document.
340
+
341
+ Neither document is needed for DeepSeek. The built-in table for the selected
342
+ currency is the base layer for every session: `CNY` prices DeepSeek in yuan and
343
+ `USD` in dollars, both at the published peak and off-peak rates. A document can
344
+ change the currency, replace a built-in rate, or add rates of your own.
339
345
 
340
346
  ```json
341
347
  {
342
348
  "price": {
343
- "currency": { "symbol": "¥", "decimals": 2 },
344
- "fallbackToRecordedCost": true,
349
+ "currency": { "code": "CNY", "decimals": 2 },
345
350
  "rates": {
346
351
  "deepseek/deepseek-flash": { "input": 2, "output": 8, "cacheRead": 0.04, "cacheWrite": 0, "offPeakMultiplier": 0.5 },
347
352
  "deepseek/deepseek-v4-pro": { "input": 9, "output": 27, "cacheRead": 0.3, "cacheWrite": 0, "offPeakMultiplier": 0.5 }
@@ -357,101 +362,45 @@ drops the schedule rather than the session.
357
362
  }
358
363
  ```
359
364
 
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. |
365
+ That file restates the built-in defaults, so on its own it changes nothing; it is
366
+ shown to document every key at once.
373
367
 
374
368
  Rates merge key by key across the two documents, so a project document can
375
369
  override one model without restating the rest; a key present in both is replaced,
376
370
  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.
371
+ `offPeakMultiplier: 0.5` and no schedule stays flat.
398
372
 
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
- ```
373
+ **Only configured models get a price.** A record priced from `rates` or the
374
+ built-in table contributes to the total. A model with no matching rate differs by
375
+ currency: `CNY` contributes nothing, so the bar shows no cost rather than a
376
+ mixed-currency figure, while `USD` adds Pi's recorded dollar amount, which is
377
+ already in the right unit. To price the model yourself, add a rate entry (a `"*"`
378
+ catch-all prices everything).
411
379
 
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. |
380
+ ### When each section is read
420
381
 
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
425
-
426
- The doctor surface reads the same shared `bytefly.json`, under its own top-level
427
- `doctor` key:
428
-
429
- ```json
430
- {
431
- "doctor": {
432
- "proactive": true,
433
- "timeoutMs": 2000,
434
- "skip": []
435
- }
436
- }
437
- ```
438
-
439
- | Key | Default | Meaning |
382
+ | Section | Read at | Applies to |
440
383
  |---|---|---|
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.
384
+ | `chrome` | `session_start`, `session_tree` | the rail glyph |
385
+ | `price` | the footer install (`session_start`, `session_tree`) | the cost total |
386
+ | `notify` | `session_start` | which events are announced, and how |
387
+ | `doctor` | `session_start` | the proactive check |
388
+
389
+ Nothing watches a file, so an edit applies at the next read — a reload, a new
390
+ session, or a tree change. A malformed document is ignored by every loader and
391
+ named by `doctor`; a wrong-typed value falls back to its default, and a
392
+ wrong-typed timezone drops the peak schedule rather than the session.
393
+
394
+ The notify capability is **off by default**: `enabled: true` is the whole setup
395
+ needed for an unattended run, and the three trigger flags then narrow what is
396
+ announced (see [Notifications](#notifications)).
397
+
398
+ The proactive doctor check is part of the bytefly startup TUI, so it posts only
399
+ when Pi starts with `"quietStartup": true`, the same setting that hides Pi's own
400
+ header and loaded-resource listing. `false`, `"header"`, and unset leave Pi's
401
+ startup surface in charge, so the notice stays silent. When it does post, the
402
+ notice enters the message flow once per session, so it stays in scrollback and
403
+ the model reads the same sentence.
455
404
 
456
405
  ## Security
457
406
 
@@ -464,11 +413,12 @@ session, so it stays in scrollback and the model reads the same sentence.
464
413
  ## Cost calculation
465
414
 
466
415
  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:
416
+ never returns a price. The footer can total that catalog directly, or replace
417
+ it with its own rates for the models they match:
469
418
 
470
419
  - **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
420
+ yuan prices for `CNY` and its published dollar prices for `USD`, per model, with
421
+ the peak schedule that doubles the price during peak hours
472
422
  ([DeepSeek's pricing page](https://api-docs.deepseek.com/quick_start/pricing)).
473
423
  They are ordinary rate entries, so a `bytefly.json` key replaces one of
474
424
  them, and the schedule is the default `peak` every rate is judged against.
@@ -482,9 +432,16 @@ they match:
482
432
  applied at the local time the tokens were actually spent: an off-peak turn
483
433
  stays off-peak when peak starts. A record with no usable timestamp is priced
484
434
  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`.
435
+ - **Which algorithm.** `price.currency.code` chooses `CNY` (the default) or
436
+ `USD`. Both price matched records from `rates` and the same `peak` tariff; the
437
+ code selects which built-in table `rates` starts from — DeepSeek's yuan prices
438
+ or its dollar prices — and the two are separate methods, not an exchange rate.
439
+ Neither mode converts one currency into the other. `currency.decimals` only
440
+ changes how the total is printed; the symbol follows the code.
441
+ - **Which source.** Both currencies price a matched record from `rates`; only
442
+ the unmatched case differs. `CNY` contributes nothing for a model it cannot
443
+ price, while `USD` adds Pi's recorded dollar amount for that record, because
444
+ that number is already dollars.
488
445
 
489
446
  For example, DeepSeek flash at `¥2` in and `¥8` out per million, with the
490
447
  published peak windows and `offPeakMultiplier: 0.5`, prices a 1M-in + 1M-out
@@ -598,6 +555,12 @@ installs or fetches a package.
598
555
  into `extensions` (and package filter objects) in the active scope. A row in
599
556
  the trailing `Not resolved` group has no pattern to write, so `Space` says so
600
557
  instead of doing nothing.
558
+ - Clears the focused extension's project override with `x` in Project scope: the
559
+ `+`/`-` entry the toggle wrote is removed (with the plain path entry beside it
560
+ for an inherited personal resource, and a project package entry the toggle
561
+ created), and the row returns to its inherited state. A row with no override
562
+ says so instead of writing, and Global scope does not offer the key — the
563
+ personal file is the base layer.
601
564
  - Switches the write scope with `Tab`: **Global** (personal `settings.json`) or
602
565
  **Project** (`.pi/settings.json`, only when the project is trusted; an
603
566
  untrusted project refuses with a notice).
@@ -611,37 +574,53 @@ installs or fetches a package.
611
574
  without leaving the session.
612
575
 
613
576
  **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:
577
+ switch, with its own cursor. It lists ten preferences in six groups:
615
578
 
616
579
  ```
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
-
580
+ bytefly ──────────────────────────────────────────────────────────────
581
+ Extensions · Settings Global · 10 settings
582
+
583
+ Startup (1)
584
+ ❯ ◻ Bytefly startup TUI Off
585
+ Notifications (3)
586
+ ◻ Desktop notifications Off
587
+ Delivery method Auto
588
+ Notification title Pi
589
+ Diagnostics (1)
590
+ ◼ Proactive health check On
591
+ Pricing (2)
592
+ Currency CNY (¥)
593
+ Cost decimals 2
594
+ Transcript (2)
595
+ Rail glyph ╎
596
+ Output padding 1
597
+ Terminal (1)
598
+ ◻ Hardware cursor Off
630
599
  Bytefly startup TUI — Pi setting
631
600
  Use bytefly's startup header in place of Pi's startup TUI.
632
601
  Default · ~/.pi/agent/settings.json
602
+
603
+ Esc close · ←/→ · Space change · Tab scope · ↑/↓ select
633
604
  ```
634
605
 
635
606
  | Group | Row | Source | Values |
636
607
  |---|---|---|---|
637
608
  | Startup | Bytefly startup TUI | Pi `settings.json` (`quietStartup`) | Off, On |
638
- | Notifications | Desktop notifications | `bytefly.json` | Off, On |
639
- | Transcript | Rail glyph | `bytefly.json` | `╎`, `│`, `┃`, `▏` |
609
+ | Notifications | Desktop notifications | `notify.enabled` | Off, On |
610
+ | Notifications | Delivery method | `notify.method` | Auto, OSC 777, OSC 99, Windows toast, macOS, Linux |
611
+ | Notifications | Notification title | `notify.title` | Text, up to 40 characters |
612
+ | Diagnostics | Proactive health check | `doctor.proactive` | Off, On |
613
+ | Pricing | Currency | `price.currency.code` | CNY, USD |
614
+ | Pricing | Cost decimals | `price.currency.decimals` | 0, 1, 2, 3, 4 |
615
+ | Transcript | Rail glyph | `chrome.rail` | `╎`, `│`, `┃`, `▏` |
640
616
  | Transcript | Output padding | Pi `settings.json` | 0, 1 |
641
617
  | Terminal | Hardware cursor | Pi `settings.json` | Off, On |
642
618
 
619
+ Choosing **Currency** also writes that currency's default precision. Both rows
620
+ stay editable afterwards, so the footer follows the explicit values.
621
+
643
622
  `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
623
+ forward, and a text row opens an inline editor on its value. The detail pane names the source, describes the preference, and shows
645
624
  where the value is written — `Global · ~/.pi/agent/settings.json`, `Project ·
646
625
  .pi/bytefly.json`, or `Inherited from Global · .pi/bytefly.json`. A successful
647
626
  change marks the screen `Saved · reload to apply` — even when the terminal is too
@@ -656,21 +635,24 @@ with `true` or `false`.
656
635
 
657
636
  App rows are **Global only**: Pi's setters for them write the personal
658
637
  `settings.json`, and only resource paths have public project setters. Project
659
- scope therefore lists the two `bytefly.json` rows and a
638
+ scope therefore lists the six `bytefly.json` rows and a
660
639
  `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.
640
+ happen. A `bytefly.json` row writes one leaf in the selected scope (a depth-1
641
+ key, or a nested one such as `price.currency.decimals`) through a per-key
642
+ writer, so every sibling key, section, `$schema`, and `version` is preserved. A
643
+ malformed document is refused with a notice instead of overwritten.
664
644
 
665
645
  The non-`tui` form prints the headline, the groups, and `Label: Value` rows:
666
646
 
667
647
  ```
668
- bytefly: Settings · Global · 5 settings
648
+ bytefly: Settings · Global · 9 settings
669
649
 
670
650
  Startup (1)
671
651
  Bytefly startup TUI: Off
672
- Notifications (1)
652
+
653
+ Notifications (2)
673
654
  Desktop notifications: Off
655
+ Delivery method: Auto
674
656
  ...
675
657
  ```
676
658
 
@@ -693,8 +675,12 @@ headline; a named extension prints its full findings instead.
693
675
  | Key | Effect |
694
676
  |---|---|
695
677
  | `←` / `→` | 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). |
678
+ | `Space` | Toggle the focused extension (Extensions); change the focused setting (Settings), or open the editor on a text row. |
679
+ | `x` | Clear the focused extension's project override (Extensions, Project scope). |
680
+ | `Enter` | Expand or collapse the focused extension's findings (Extensions); change the focused setting, or open the editor on a text row (Settings). |
681
+ | `←` / `→`, `Home` / `End` (`Ctrl+A` / `Ctrl+E`) | Move the editor's caret while a text row is being edited. |
682
+ | `Backspace` / `Delete`, `Ctrl+U` | Edit the buffer: delete a character, or clear it. |
683
+ | `Enter` / `Esc` (editing) | Save the buffer, or cancel and restore the stored value. |
698
684
  | `Tab` | Switch between Global and Project scope, in either section. |
699
685
  | `r` | Re-probe the doctor (Extensions). |
700
686
  | `↑`/`↓`, `j`/`k` | Move the selection, in either section. |
@@ -720,7 +706,7 @@ has a keyboard path.
720
706
  | wechat | — | `wechat/credentials.json` (not logged in) |
721
707
  | job | — | Windows has no `EXIT` trap; `ps` is POSIX-only |
722
708
  | 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` |
709
+ | 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
710
  | question | — | needs an interactive UI |
725
711
 
726
712
  Every extension that reads a config file also gets the malformed-JSON check:
@@ -775,6 +761,46 @@ adds its id, status, exit code, and label. Every piece that can carry model- or
775
761
  user-authored text — a prompt title, a job label — is sanitized to one
776
762
  control-free line before it reaches an escape sequence or a shell argument.
777
763
 
764
+ ## Update check
765
+
766
+ A session can be days behind npm without anyone noticing, and Pi's own startup
767
+ check only covers package sources it manages — a `pi -e ./`, a linked checkout,
768
+ or a git install gets nothing. So bytefly reads its own published version and
769
+ reports it once, in the two places it already owns:
770
+
771
+ ```
772
+ █▀█ Pi v1.3.0
773
+ █▀ █ enhanced by bytefly v0.11.4 ↑0.12.0
774
+ ```
775
+
776
+ ```
777
+ bytefly · 0.12.0 is available
778
+ ╎ ◌ current · v0.11.4
779
+ ╎ ◌ update · pi update --extensions
780
+ ```
781
+
782
+ The marker is the shared `↑` glyph and the newest version — the arrow is one cell
783
+ wide, so the header's column math is unchanged — painted with the header's
784
+ `accent` role: an action rather than a warning, so the muted version beside it
785
+ stays quiet and the transcript is not turned yellow for a normal state. It is
786
+ part of the chrome cell, so the two identity cells still line up, and it is the
787
+ first thing the identity sheds when the terminal narrows.
788
+
789
+ The notice is the shared transcript block: the `bytefly · title` headline at the
790
+ margin, then the loaded version and the fixing command behind the rail, all at
791
+ `info` severity because nothing is broken. It stays in scrollback and the model
792
+ reads the same sentence in plain text.
793
+
794
+ The request is `GET https://registry.npmjs.org/@gavin-hu%2Fmy-pi-agent/latest`,
795
+ fired without awaiting at `session_start` (Pi awaits handlers, and a five-second
796
+ registry call must not hold the first prompt) with a five-second abort. Only a
797
+ strictly newer, parseable semver announces anything: a non-ok status, a body
798
+ that is not JSON, a missing version, a network error, or an older or equal
799
+ version is silent. Nothing is cached to disk and nothing is retried, so a
800
+ session that starts offline simply sees no marker. `PI_OFFLINE` (Pi's own
801
+ switch) turns the check off; `PI_SKIP_VERSION_CHECK` only governs Pi's
802
+ `pi.dev` request.
803
+
778
804
  ## Limitations
779
805
 
780
806
  - **Bytefly startup TUI is the startup surface only.** It installs or removes
@@ -790,6 +816,10 @@ control-free line before it reaches an escape sequence or a shell argument.
790
816
  `pi config` is the place to manage it.
791
817
  - **The rail glyph picker is curated.** Four one-column glyphs are offered; a
792
818
  custom glyph is still a `bytefly.json` edit.
819
+ - **Settings cover scalars only.** A row is a boolean, a short enum, or a short
820
+ string. The per-trigger notify flags (`notify.turnFinished`, `needsInput`,
821
+ `jobFinished`), arrays (`doctor.skip`), a numeric range (`doctor.timeoutMs`),
822
+ the `rates` table, and `price.peak` are still `bytefly.json` edits.
793
823
  - **`doctor` is a reserved first argument.** `/bytefly doctor` always selects the
794
824
  report, so an extension literally named `doctor` cannot be narrowed to by
795
825
  name. `settings` is reserved the same way.
@@ -799,8 +829,24 @@ control-free line before it reaches an escape sequence or a shell argument.
799
829
  their authoritative names are not exposed to extensions. Use `pi config`.
800
830
  - **Packages are not installed, removed, or updated here**; use `pi install`,
801
831
  `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`.
832
+ - **The update check reads npm's `latest` only.** A private registry, a version
833
+ published under another tag, or a checkout ahead of npm reports nothing, and
834
+ there is no channel or prerelease preference: `0.11.4` is behind `0.12.0`
835
+ whether or not a `0.12.0-beta.1` exists.
836
+ - **One check per session, with no refresh.** It runs at `session_start` in
837
+ `tui` mode; `/bytefly` and `doctor` do not report it, nothing is cached to
838
+ disk, and a failed or slow request is simply not retried that session.
839
+ - **The marker needs the bytefly startup TUI.** It lives on the custom header,
840
+ so with `quietStartup` off the notice is the whole signal.
841
+ - **Pi may report the same update.** An npm-installed copy can also show Pi's
842
+ own `Package Updates Available` block; the two checks are independent and
843
+ `pi update --extensions` satisfies both.
844
+ - **No auto-update and no release notes.** The notice names a command instead
845
+ of running one, and the notice renderer has no link support, so there is no
846
+ changelog URL.
847
+ - **Project toggles are two-state per press** (`enable`/`disable`). `x` clears
848
+ the override, which is the third state; a resource with no project entry in
849
+ `pi config` is still managed there.
804
850
  - **One built-in provider.** DeepSeek is the only provider with a built-in
805
851
  table; another provider's currency and tariff have to be stated in
806
852
  `bytefly.json`.
@@ -810,10 +856,17 @@ control-free line before it reaches an escape sequence or a shell argument.
810
856
  with an `offPeakMultiplier` below 1 is affected by it, so two providers with
811
857
  different time-of-day tariffs cannot both be described.
812
858
  - **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.
859
+ Anthropic's 2× one-hour cache write are not modelled; switch to `USD` mode
860
+ to use Pi's recorded cost for those models.
861
+ - **No FX conversion.** The two modes are separate methods; neither converts
862
+ one currency into the other. A model is priced only by the selected method.
863
+ - **Two built-in footer indicators cannot be reproduced.** Pi's `(auto)` marks
864
+ that auto-compaction is on, and its `(sub)` marks a subscription-backed
865
+ provider. Auto-compaction is session state on Pi's own session object, and
866
+ `ReadonlyFooterDataProvider` exposes only the branch, the extension statuses,
867
+ and the provider count; `isUsingSubscription` lives on the internal model
868
+ runtime, not on the `ModelRegistry` an extension receives. A subscription
869
+ session therefore shows no cost segment at all, where Pi's footer said `(sub)`.
817
870
  - **The displayed number is the total, rounded once** — not the sum of rounded
818
871
  records.
819
872
  - **`doctor` can only check what is declared.** A new extension with no
@@ -835,8 +888,9 @@ control-free line before it reaches an escape sequence or a shell argument.
835
888
  prompt raised during a run, not a process waiting on stdin: jobs are
836
889
  fire-and-observe, so that state does not exist.
837
890
  - **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.
891
+ host lacks (or an OSC method off a TTY) and the notification is dropped; only
892
+ `auto` inspects the environment. `doctor` now reports the mismatch as a
893
+ `degraded` finding, which says what to change but does not fix it for you.
840
894
  - **The working counter is one global row.** `setWorkingMessage` drives the
841
895
  single working row, so bytefly and any other extension setting it race — last
842
896
  writer wins — and the message is reset to Pi's default at `agent_end`. If the
@@ -878,7 +932,7 @@ rather than showing a wrong value.
878
932
 
879
933
  | Integration point | Details |
880
934
  |---|---|
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. |
935
+ | 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
936
  | Footer | `ctx.ui.setFooter` with a custom component, replacing Pi's built-in footer; installed only when `ctx.mode === "tui"`. |
883
937
  | 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
938
  | 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 +943,15 @@ rather than showing a wrong value.
889
943
  | 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
944
  | Picker discovery | `SettingsManager.create(...)` plus `DefaultPackageManager.resolve({ onMissing: "skip" })`, both public exports; both views drop `bytefly`'s own entrypoint. |
891
945
  | 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. |
946
+ | 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. |
947
+ | 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
948
  | 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
949
  | 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. |
950
+ | Update check | `session_start` in `tui` mode, when `PI_OFFLINE` is off, fires `fetchLatestVersion()` without awaiting, so the handler never holds the prompt. 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. A generation counter drops a reply that lands after `session_shutdown`, and a per-load flag keeps it to one announcement. |
951
+ | 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. |
897
952
  | 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. |
953
+ | 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 nothing to the session or a file. |
954
+ | 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
955
 
901
956
  ## Design notes
902
957
 
@@ -991,14 +1046,12 @@ rather than showing a wrong value.
991
1046
  after a `╎` keeps the percentage in a fixed place while the mode stays
992
1047
  visible as the meters drop.
993
1048
  - **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.
1049
+ `setStatus` value on its own line, so the bar sorts them by meaning instead:
1050
+ `worktree` and `browse` are location, so the `◉ <port>` chip sits beside `⎇`
1051
+ branch and `⑂` worktree and the left zone stays the filesystem identity; a
1052
+ plan or a failed job is mode or alert, so it takes the slot after the gauge.
1053
+ The [wechat](../wechat/) extension publishes `✆ wechat` while its bridge runs,
1054
+ which is why the bar has to render `getExtensionStatuses()` at all.
1002
1055
  - **Segments degrade; they do not clip.** Each `Segment` carries progressively
1003
1056
  shorter `forms` and a `weight`; the layout advances the lowest-priority form
1004
1057
  or drops the lowest-priority segment, and only truncates once everything is
@@ -1043,10 +1096,10 @@ rather than showing a wrong value.
1043
1096
  usage records (`collect/`), records are priced into totals (`price/`), and the
1044
1097
  snapshot is drawn (`render/`). Pricing is a pure function of records and rates
1045
1098
  — 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.
1099
+ DeepSeek example be a unit test. In `CNY` mode a record with no matching rate
1100
+ simply contributes nothing; `USD` mode is the opt-in path to Pi's own number.
1101
+ The built-in DeepSeek table is only the default value of `rates`, so nothing
1102
+ in the pricer is provider-specific.
1050
1103
  - **DeepSeek is built in, not bundled as a catalog.** Its published price is in
1051
1104
  yuan at two tariffs, while Pi's catalog holds one USD number per model, so an
1052
1105
  unconfigured DeepSeek session showed a bill that was neither the right
@@ -1084,12 +1137,16 @@ rather than showing a wrong value.
1084
1137
  - **The install hint has one home.** `document` and `web-access` format their
1085
1138
  tool errors from the same `REQUIREMENTS` entries `doctor` prints, so a user
1086
1139
  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.
1140
+ - **Settings are a curated table, not a schema walker.** Eleven rows prove the
1141
+ row types — a boolean, an enum, and a short string with its own rule — both
1142
+ scopes, both write paths, and one nested leaf (`price.currency.decimals`). A row states its section and the path to its
1143
+ leaf, and one reader per section resolves it project-over-global-over-defaults
1144
+ through the same normalizer the extension loads with, so the screen and the
1145
+ runtime cannot disagree about a value. A generic walker would expose keys the
1146
+ screen cannot edit safely yet — arrays, nested tariffs, free text,
1147
+ credentials — so the catalogue grows one proven family at a time. App rows are
1148
+ Global-only because Pi's setters write the personal file; Project scope lists
1149
+ only the `bytefly.json` rows it can actually write.
1093
1150
  - **The doctor is quiet unless the bytefly startup TUI is on.** Only `missing`
1094
1151
  findings post a notice, only for enabled extensions, only with a UI, only once
1095
1152
  per session, and only when Pi's `quietStartup` is `true` — the same state that
@@ -1108,6 +1165,21 @@ rather than showing a wrong value.
1108
1165
  - **The prerequisite table is data.** No I/O, no host access, no module state:
1109
1166
  every probe dependency arrives as a seam, so `probeExtension` is testable with
1110
1167
  no process and `lib/` stays value-only.
1168
+ - **The update check is a self-report, not an update mechanism.** It answers one
1169
+ question — is the version in this checkout current? — and answers it in the two
1170
+ surfaces bytefly already owns, the header identity and one notice. It never
1171
+ installs, never edits settings, and never fails a session: an unreachable
1172
+ registry and an up-to-date one are indistinguishable to the user, which is the
1173
+ only failure mode a background check is allowed to have.
1174
+ - **The marker is an action, the notice is information.** The same fact is drawn
1175
+ in the accent on the header, where it sits beside a control the user can act
1176
+ on, and at `info` severity in the transcript, where a `warn` rail would say
1177
+ something is broken. Neither surface needs per-frame polling: the value arrives
1178
+ once, the memo key includes it, and one `setHeader` re-install repaints.
1179
+ - **The transport stays local.** `web-access`'s runner does redirects, SSRF
1180
+ checks, HTML extraction, and caching — none of it applies to one fixed JSON
1181
+ endpoint, and an extension may not import another's modules, so the ~20-line
1182
+ read lives beside the check with the `fetch` and timeout as seams.
1111
1183
 
1112
1184
  ## Files
1113
1185
 
@@ -1117,7 +1189,8 @@ calculation), `message/` (the user-message marker), `notice/` (the shared notice
1117
1189
  elapsed counter on Pi's working row), `panel/` (the
1118
1190
  unified screen and its merged model),
1119
1191
  `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
1192
+ and its probe runtime), `notify/`, and `update/` (the published-version check
1193
+ behind the header marker). The chrome and the screen are drawn from
1121
1194
  the package's [`lib/tui/`](../../lib/tui/) design system (tokens, rules, rows,
1122
1195
  columns, gauge), so the look follows the active theme.
1123
1196
 
@@ -1135,10 +1208,10 @@ columns, gauge), so the look follows the active theme.
1135
1208
  | `footer/collect/entries.ts` | Session entries → usage records, each resolved to the model in effect. |
1136
1209
  | `footer/collect/snapshot.ts` | Snapshot assembly; memoized pricing scan per branch leaf. |
1137
1210
  | `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. |
1211
+ | `footer/price/config.ts` | The `price` section of `bytefly.json`: schema, defaults, built-in DeepSeek rates/tariff, currency defaults, normalizer, loader. |
1139
1212
  | `footer/price/rates.ts` | Model-key matching: exact and wildcard, most specific first. |
1140
1213
  | `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. |
1214
+ | `footer/price/price.ts` | Records → totals: rated cost in both modes, Pi's recorded cost as USD's fallback, and the peak multiplier. |
1142
1215
  | `footer/render/format.ts` | Generic formatters (tokens, cost in the configured currency, cwd, the context role, truncation). |
1143
1216
  | `footer/render/status.ts` | Status-chip transforms: `compactStatus` and `dropStatusDetail`. |
1144
1217
  | `footer/render/segments.ts` | The segment builders: forms, weights, separators, roles. |
@@ -1166,8 +1239,8 @@ columns, gauge), so the look follows the active theme.
1166
1239
  | `panel/types.ts` | `PanelSection` (`extensions` | `settings`), `PanelRow`, and `PanelModel`, the merged row model. |
1167
1240
  | `panel/join.ts` | The item/report union with the unambiguous-name health rule and unique row keys, groups, and ordering. |
1168
1241
  | `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. |
1242
+ | `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. |
1243
+ | `panel/settings-runtime.ts` | The Settings runtime: snapshot cache, scope guards, app setters, per-leaf `bytefly.json` writes, dirty state, and change listeners. |
1171
1244
  | `panel/tui.ts` | `PanelComponent`, the two-section dock screen (Extensions list and Settings list), drawn from the `lib/tui/` kit. |
1172
1245
  | `panel/commands.ts` | `/bytefly`: one command for the screen sections, the `doctor` report, the settings listing, and their text modes. |
1173
1246
  | `picker/types.ts` | Picker host seams (`SettingsStore`, `ExtensionResolver`, `Discovery`), the app-settings setters, and row types. |
@@ -1186,6 +1259,11 @@ columns, gauge), so the look follows the active theme.
1186
1259
  | `notify/dispatch.ts` | Method selection and delivery: OSC 777/99, Windows toast, `osascript`, `notify-send`, over injected seams. |
1187
1260
  | `notify/format.ts` | Pure notification text: session label and the turn / input / job bodies. |
1188
1261
  | `notify/register.ts` | Registers the three triggers, loads the config at `session_start`, and owns the job-bus subscription. |
1262
+ | `update/check.ts` | The one registry read: the `latest` URL, the abort timeout, the `FetchLike` seam, and the failure-to-`undefined` mapping. |
1263
+ | `update/version.ts` | Pure semver parsing and `isNewerVersion`, including prerelease precedence. |
1264
+ | `update/state.ts` | `UpdateSource` and `createUpdateState`: the newest known version, one record per load. |
1265
+ | `update/format.ts` | `updateMarker` and `updateNotice`: the header's `↑0.12.0` and the two-row transcript notice. |
1266
+ | `update/register.ts` | The session-start check: the async read, the `PI_OFFLINE` and `tui` gates, the once-per-load announcement, and the shutdown generation guard. |
1189
1267
 
1190
1268
  ## Testing
1191
1269
 
@@ -1216,7 +1294,8 @@ transforms (`picker/toggle`), discovery with an injected factory
1216
1294
  (`picker/discovery`), and the runtime state machine (`picker/runtime`). The
1217
1295
  unified screen is covered by its pure model merge (`panel/join`), its text
1218
1296
  formatter (`panel/format`), the settings catalogue and its cycling
1219
- (`panel/settings`), the scope-aware settings runtime (`panel/settings-runtime`),
1297
+ (`panel/settings`), the scope-aware settings runtime (`panel/settings-runtime`,
1298
+ including a nested leaf write that keeps its siblings),
1220
1299
  the screen's rendering and key handling (`panel/tui`), and the command through
1221
1300
  the bytefly factory (`panel/commands`).
1222
1301
  The doctor surface is covered by its pure report
@@ -1225,7 +1304,12 @@ registration, the proactive notice, the session cache, and `run_doctor`
1225
1304
  through the factory (`doctor/register`). The notify capability is covered by its section loader
1226
1305
  (`notify/config`), the pure text builders (`notify/format`), method selection and
1227
1306
  delivery over recording seams (`notify/dispatch`), and its three triggers,
1228
- reload, and shutdown through `registerNotify` (`notify/register`).
1307
+ reload, and shutdown through `registerNotify` (`notify/register`). The update
1308
+ check is covered by the version-ordering table (`update/version`), the transport
1309
+ with an injected fetch (`update/check`), the per-load store (`update/state`), the
1310
+ marker and notice wording (`update/format`), and its lifecycle — once per load,
1311
+ silent on failure or an older version, skipped offline and outside `tui`, and
1312
+ immune to a reply that lands after shutdown (`update/register`).
1229
1313
  The user-message nameplate is covered by its pure transform
1230
1314
  (`message/format`, including a render through pi-tui's real `Markdown`) and its
1231
1315
  registration (`message/register`), plus the factory check in `extension`.