@gavin-hu/my-pi-agent 0.12.0 → 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 (153) hide show
  1. package/README.md +28 -21
  2. package/README.zh-CN.md +30 -22
  3. package/extensions/bytefly/README.md +139 -67
  4. package/extensions/bytefly/chrome.ts +7 -2
  5. package/extensions/bytefly/doctor/register.ts +6 -0
  6. package/extensions/bytefly/doctor/runtime.ts +36 -6
  7. package/extensions/bytefly/footer/collect/snapshot.ts +12 -2
  8. package/extensions/bytefly/footer/config.ts +7 -1
  9. package/extensions/bytefly/footer/delegation.ts +100 -0
  10. package/extensions/bytefly/footer/register.ts +13 -2
  11. package/extensions/bytefly/footer/render/lines.ts +2 -0
  12. package/extensions/bytefly/footer/render/segments.ts +27 -0
  13. package/extensions/bytefly/footer/types.ts +11 -0
  14. package/extensions/bytefly/index.ts +4 -3
  15. package/extensions/bytefly/notify/config.ts +2 -0
  16. package/extensions/bytefly/notify/format.ts +19 -1
  17. package/extensions/bytefly/notify/register.ts +37 -13
  18. package/extensions/bytefly/notify/types.ts +2 -0
  19. package/extensions/bytefly/panel/commands.ts +34 -13
  20. package/extensions/bytefly/panel/settings.ts +98 -16
  21. package/extensions/bytefly/update/cache.ts +73 -0
  22. package/extensions/bytefly/update/format.ts +65 -10
  23. package/extensions/bytefly/update/register.ts +99 -13
  24. package/extensions/bytefly/update/state.ts +13 -1
  25. package/extensions/document/README.md +22 -12
  26. package/extensions/document/config.ts +4 -0
  27. package/extensions/document/deadline.ts +61 -0
  28. package/extensions/document/tool.ts +13 -2
  29. package/extensions/file-browse/README.md +12 -8
  30. package/extensions/file-browse/client.ts +6 -7
  31. package/extensions/file-browse/files.ts +0 -4
  32. package/extensions/file-browse/git.ts +9 -10
  33. package/extensions/file-browse/html.ts +17 -7
  34. package/extensions/file-browse/router.ts +24 -15
  35. package/extensions/goal/README.md +7 -1
  36. package/extensions/goal/schema.ts +6 -2
  37. package/extensions/goal/types.ts +6 -2
  38. package/extensions/job/README.md +16 -12
  39. package/extensions/job/runtime.ts +23 -6
  40. package/extensions/job/status.ts +4 -2
  41. package/extensions/memory/README.md +55 -13
  42. package/extensions/memory/commands.ts +20 -2
  43. package/extensions/memory/doctor.ts +137 -0
  44. package/extensions/memory/format.ts +24 -7
  45. package/extensions/memory/runtime.ts +49 -0
  46. package/extensions/memory/store.ts +31 -0
  47. package/extensions/memory/tui.ts +136 -12
  48. package/extensions/plan/README.md +60 -29
  49. package/extensions/plan/commands.ts +67 -8
  50. package/extensions/plan/index.ts +35 -11
  51. package/extensions/plan/plan-index.ts +234 -0
  52. package/extensions/plan/plan-text.ts +95 -0
  53. package/extensions/plan/plans.ts +90 -1
  54. package/extensions/plan/review.ts +12 -9
  55. package/extensions/plan/runtime.ts +72 -27
  56. package/extensions/plan/steps.ts +35 -25
  57. package/extensions/plan/todos.ts +24 -2
  58. package/extensions/plan/tools.ts +124 -24
  59. package/extensions/plan/tui.ts +7 -28
  60. package/extensions/plan/types.ts +9 -0
  61. package/extensions/plan/web-page.ts +2 -2
  62. package/extensions/plan/web.ts +12 -11
  63. package/extensions/rewind/README.md +28 -8
  64. package/extensions/rewind/commands.ts +1 -1
  65. package/extensions/rewind/format.ts +4 -4
  66. package/extensions/rewind/git.ts +56 -18
  67. package/extensions/rewind/index.ts +3 -5
  68. package/extensions/rewind/path-picker.ts +198 -0
  69. package/extensions/rewind/paths.ts +128 -0
  70. package/extensions/rewind/restore.ts +69 -28
  71. package/extensions/rewind/rewind.ts +80 -3
  72. package/extensions/rewind/runtime.ts +13 -5
  73. package/extensions/rewind/snapshot-run.ts +5 -2
  74. package/extensions/rewind/types.ts +2 -0
  75. package/extensions/subagent/README.md +7 -1
  76. package/extensions/subagent/accounting.ts +34 -0
  77. package/extensions/subagent/index.ts +23 -1
  78. package/extensions/subagent/stream.ts +2 -0
  79. package/extensions/subagent/types.ts +2 -0
  80. package/extensions/watch/README.md +8 -3
  81. package/extensions/watch/index.ts +19 -0
  82. package/extensions/web-access/README.md +43 -13
  83. package/extensions/web-access/charset.ts +58 -0
  84. package/extensions/web-access/fetch/find.ts +11 -7
  85. package/extensions/web-access/fetch/format.ts +64 -18
  86. package/extensions/web-access/fetch/page.ts +5 -1
  87. package/extensions/web-access/fetch/tool.ts +19 -18
  88. package/extensions/web-access/http.ts +49 -8
  89. package/extensions/web-access/search/facts.ts +38 -0
  90. package/extensions/web-access/search/tool.ts +13 -0
  91. package/extensions/web-access/text.ts +50 -11
  92. package/extensions/worktree/README.md +74 -15
  93. package/extensions/worktree/config.ts +11 -0
  94. package/extensions/worktree/exit.ts +35 -3
  95. package/extensions/worktree/gaps.ts +74 -0
  96. package/extensions/worktree/guard.ts +50 -4
  97. package/extensions/worktree/handoff.ts +55 -0
  98. package/extensions/worktree/index.ts +12 -2
  99. package/extensions/worktree/runtime.ts +20 -0
  100. package/extensions/worktree/status.ts +15 -2
  101. package/extensions/worktree/tools.ts +3 -2
  102. package/lib/README.md +10 -8
  103. package/lib/events.ts +86 -1
  104. package/lib/goal-state.ts +95 -0
  105. package/lib/requirements.ts +282 -62
  106. package/lib/tui/README.md +91 -0
  107. package/lib/tui/glyphs.ts +1 -0
  108. package/lib/web/layout.ts +12 -2
  109. package/package.json +5 -3
  110. package/themes/bytefly-dark.json +1 -1
  111. package/assets/chrome.png +0 -0
  112. package/assets/file-browse.png +0 -0
  113. package/assets/job.png +0 -0
  114. package/assets/plan-review.png +0 -0
  115. package/assets/question.png +0 -0
  116. package/assets/rewind.png +0 -0
  117. package/assets/todo.png +0 -0
  118. package/assets/tool-renderings.png +0 -0
  119. package/assets/tools/ask_user_question-cancelled.png +0 -0
  120. package/assets/tools/ask_user_question.png +0 -0
  121. package/assets/tools/bash.png +0 -0
  122. package/assets/tools/edit.png +0 -0
  123. package/assets/tools/enter_plan_mode.png +0 -0
  124. package/assets/tools/enter_worktree.png +0 -0
  125. package/assets/tools/exit_plan_mode.png +0 -0
  126. package/assets/tools/exit_worktree.png +0 -0
  127. package/assets/tools/find.png +0 -0
  128. package/assets/tools/goal.png +0 -0
  129. package/assets/tools/grep.png +0 -0
  130. package/assets/tools/job.png +0 -0
  131. package/assets/tools/list_worktrees.png +0 -0
  132. package/assets/tools/ls.png +0 -0
  133. package/assets/tools/memory-long-subject.png +0 -0
  134. package/assets/tools/memory.png +0 -0
  135. package/assets/tools/notice.png +0 -0
  136. package/assets/tools/open_path.png +0 -0
  137. package/assets/tools/prune_worktrees.png +0 -0
  138. package/assets/tools/read.png +0 -0
  139. package/assets/tools/read_doc-running.png +0 -0
  140. package/assets/tools/read_doc.png +0 -0
  141. package/assets/tools/run_doctor.png +0 -0
  142. package/assets/tools/send_wechat.png +0 -0
  143. package/assets/tools/set_plan_steps.png +0 -0
  144. package/assets/tools/subagent.png +0 -0
  145. package/assets/tools/todo.png +0 -0
  146. package/assets/tools/turn-rule.png +0 -0
  147. package/assets/tools/user-message.png +0 -0
  148. package/assets/tools/watch.png +0 -0
  149. package/assets/tools/web_fetch-rejected.png +0 -0
  150. package/assets/tools/web_fetch.png +0 -0
  151. package/assets/tools/web_search.png +0 -0
  152. package/assets/tools/write.png +0 -0
  153. package/assets/tools/write_plan.png +0 -0
@@ -69,13 +69,19 @@ in [`header/config.ts`](./header/config.ts).
69
69
  - **Line 1 — identity:** `pwd` and the session name on the left; git state
70
70
  (`⎇ branch · ⑂ worktree`) and the `◉` browse chip on the right.
71
71
  - **Line 2 — resources:** the context gauge and usage meters on the left, the
72
- mode/alert slot (`⋮ plan`) trailing after a `╎` when present; the model and
73
- 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
74
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).
75
81
 
76
82
  ```
77
- ~/repo/my-pi-agent ⎇ main · ⑂ smoke
78
- ▰▰▰▰▰▰▱▱▱▱ 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
79
85
  ```
80
86
 
81
87
  (The examples show the default `¥` symbol. A model that neither `rates` nor the
@@ -266,21 +272,25 @@ underneath, and `run_doctor` returns the same report as text and typed data.
266
272
  Only a missing
267
273
  *hard* requirement posts a notice at session start; everything else waits in the
268
274
  report.
269
- See [The bytefly screen](#the-bytefly-screen).
275
+ See [The control center](#the-control-center).
270
276
 
271
277
  **Notify.** Off by default, one `notify` section in `bytefly.json` turns on OS
272
278
  notifications for unattended runs: when a full agent run settles, when a
273
- 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
274
281
  method follows the terminal (OSC 777, OSC 99, a Windows toast) or falls back to
275
282
  the platform notifier (`osascript`, `notify-send`); see
276
283
  [Notifications](#notifications).
277
284
 
278
- **Update check.** Once per interactive session, in the background, the loaded
285
+ **Update check.** In the background at an interactive session's start, the loaded
279
286
  version is compared with the published `@gavin-hu/my-pi-agent` on npm. A newer
280
287
  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).
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).
284
294
 
285
295
  ## Commands
286
296
 
@@ -289,9 +299,10 @@ is set, makes no request outside `tui`, and a failed request is silent; see
289
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. |
290
300
  | `/bytefly settings` | Open the control center on Settings. Outside `tui` it prints the grouped settings listing with each row's value. |
291
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. |
292
303
 
293
304
  Tab completes the subcommands, extension names after `doctor`, and `refresh`;
294
- `/bytefly settings` takes no name.
305
+ `/bytefly settings` and `/bytefly update` take no name.
295
306
 
296
307
  ## Behaviour by mode
297
308
 
@@ -328,7 +339,7 @@ separators, icons, status keys), and the turn separator in
328
339
  colors).
329
340
 
330
341
  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
342
+ [configuration reference](../../docs/references/bytefly-config.md); this section
332
343
  covers what that table cannot — when each section is read, and what the cost
333
344
  does when no rate matches.
334
345
 
@@ -392,7 +403,7 @@ named by `doctor`; a wrong-typed value falls back to its default, and a
392
403
  wrong-typed timezone drops the peak schedule rather than the session.
393
404
 
394
405
  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
406
+ needed for an unattended run, and the four trigger flags then narrow what is
396
407
  announced (see [Notifications](#notifications)).
397
408
 
398
409
  The proactive doctor check is part of the bytefly startup TUI, so it posts only
@@ -574,20 +585,25 @@ installs or fetches a package.
574
585
  without leaving the session.
575
586
 
576
587
  **Settings.** The second section uses the same frame and the same `Tab` scope
577
- switch, with its own cursor. It lists ten preferences in six groups:
588
+ switch, with its own cursor. It lists fifteen preferences in six groups:
578
589
 
579
590
  ```
580
591
  bytefly ──────────────────────────────────────────────────────────────
581
- Extensions · Settings Global · 10 settings
592
+ Extensions · Settings Global · 15 settings
582
593
 
583
594
  Startup (1)
584
595
  ❯ ◻ Bytefly startup TUI Off
585
- Notifications (3)
596
+ Notifications (7)
586
597
  ◻ Desktop notifications Off
598
+ ◼ Turn finished On
599
+ ◼ Needs input On
600
+ ◼ Job finished On
601
+ ◼ Watcher matched On
587
602
  Delivery method Auto
588
603
  Notification title Pi
589
- Diagnostics (1)
604
+ Diagnostics (2)
590
605
  ◼ Proactive health check On
606
+ Probe timeout 2000
591
607
  Pricing (2)
592
608
  Currency CNY (¥)
593
609
  Cost decimals 2
@@ -607,12 +623,17 @@ bytefly ────────────────────────
607
623
  |---|---|---|---|
608
624
  | Startup | Bytefly startup TUI | Pi `settings.json` (`quietStartup`) | Off, On |
609
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 |
610
630
  | Notifications | Delivery method | `notify.method` | Auto, OSC 777, OSC 99, Windows toast, macOS, Linux |
611
631
  | Notifications | Notification title | `notify.title` | Text, up to 40 characters |
612
632
  | Diagnostics | Proactive health check | `doctor.proactive` | Off, On |
633
+ | Diagnostics | Probe timeout | `doctor.timeoutMs` | Number, 100–30000 ms |
613
634
  | Pricing | Currency | `price.currency.code` | CNY, USD |
614
635
  | Pricing | Cost decimals | `price.currency.decimals` | 0, 1, 2, 3, 4 |
615
- | Transcript | Rail glyph | `chrome.rail` | `╎`, `│`, `┃`, `▏` |
636
+ | Transcript | Rail glyph | `chrome.rail` | Text, exactly one column |
616
637
  | Transcript | Output padding | Pi `settings.json` | 0, 1 |
617
638
  | Terminal | Hardware cursor | Pi `settings.json` | Off, On |
618
639
 
@@ -620,7 +641,8 @@ Choosing **Currency** also writes that currency's default precision. Both rows
620
641
  stay editable afterwards, so the footer follows the explicit values.
621
642
 
622
643
  `Space` (or `Enter`) changes the focused row: a boolean toggles, an enum cycles
623
- forward, and a text row opens an inline editor on its value. 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
624
646
  where the value is written — `Global · ~/.pi/agent/settings.json`, `Project ·
625
647
  .pi/bytefly.json`, or `Inherited from Global · .pi/bytefly.json`. A successful
626
648
  change marks the screen `Saved · reload to apply` — even when the terminal is too
@@ -635,7 +657,7 @@ with `true` or `false`.
635
657
 
636
658
  App rows are **Global only**: Pi's setters for them write the personal
637
659
  `settings.json`, and only resource paths have public project setters. Project
638
- scope therefore lists the six `bytefly.json` rows and a
660
+ scope therefore lists the twelve `bytefly.json` rows and a
639
661
  `App settings are Global only.` note rather than promising a write that cannot
640
662
  happen. A `bytefly.json` row writes one leaf in the selected scope (a depth-1
641
663
  key, or a nested one such as `price.currency.decimals`) through a per-key
@@ -645,14 +667,19 @@ malformed document is refused with a notice instead of overwritten.
645
667
  The non-`tui` form prints the headline, the groups, and `Label: Value` rows:
646
668
 
647
669
  ```
648
- bytefly: Settings · Global · 9 settings
670
+ bytefly: Settings · Global · 15 settings
649
671
 
650
672
  Startup (1)
651
673
  Bytefly startup TUI: Off
652
674
 
653
- Notifications (2)
675
+ Notifications (7)
654
676
  Desktop notifications: Off
677
+ Turn finished: On
678
+ Needs input: On
679
+ Job finished: On
680
+ Watcher matched: On
655
681
  Delivery method: Auto
682
+ Notification title: Pi
656
683
  ...
657
684
  ```
658
685
 
@@ -664,6 +691,14 @@ means reduced capability — an optional package that is absent, a config file
664
691
  that fails to parse, a login that has not happened. Doctor never changes
665
692
  anything and never installs anything.
666
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
+
667
702
  `/bytefly <extension>` narrows the same screen to that extension; `Enter`
668
703
  expands its findings, including the checks that passed, into the free height.
669
704
  `/bytefly refresh` ignores the findings cached earlier in the session. Every
@@ -718,7 +753,7 @@ the same entry in `lib/requirements.ts`, so the two cannot drift.
718
753
 
719
754
  The notify capability exists for unattended runs: start a long turn, leave the
720
755
  terminal, and get a desktop notification when Pi needs you — or when it is done.
721
- 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
722
757
  of `bytefly.json` (see [Configuration](#configuration)):
723
758
 
724
759
  - **A run settled** (`agent_settled`). Pi finished the turn and waits for the
@@ -731,6 +766,12 @@ of `bytefly.json` (see [Configuration](#configuration)):
731
766
  - **A background job finished.** The [job](../job/) extension announces each
732
767
  terminal transition on the shared `pi.events` bus
733
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.
734
775
 
735
776
  ### Delivery method
736
777
 
@@ -777,6 +818,7 @@ reports it once, in the two places it already owns:
777
818
  bytefly · 0.12.0 is available
778
819
  ╎ ◌ current · v0.11.4
779
820
  ╎ ◌ update · pi update --extensions
821
+ ╎ ◌ release · https://github.com/gavin-hu/my-pi-agent/releases/tag/v0.12.0
780
822
  ```
781
823
 
782
824
  The marker is the shared `↑` glyph and the newest version — the arrow is one cell
@@ -796,10 +838,27 @@ fired without awaiting at `session_start` (Pi awaits handlers, and a five-second
796
838
  registry call must not hold the first prompt) with a five-second abort. Only a
797
839
  strictly newer, parseable semver announces anything: a non-ok status, a body
798
840
  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.
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.
803
862
 
804
863
  ## Limitations
805
864
 
@@ -814,15 +873,16 @@ switch) turns the check off; `PI_SKIP_VERSION_CHECK` only governs Pi's
814
873
  - **Project overrides of app settings are not shown.** App rows read the
815
874
  personal file; a `.pi/settings.json` override still wins at runtime, and
816
875
  `pi config` is the place to manage it.
817
- - **The rail glyph picker is curated.** Four one-column glyphs are offered; a
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.
823
- - **`doctor` is a reserved first argument.** `/bytefly doctor` always selects the
824
- report, so an extension literally named `doctor` cannot be narrowed to by
825
- 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.
826
886
  - **The picker manages only the `extensions` resource type.** Skills, prompts,
827
887
  and themes are handled by Pi's own `pi config`.
828
888
  - **Built-in extensions are not listed** (`builtin:mcp`, `builtin:codemode`, …):
@@ -833,17 +893,19 @@ switch) turns the check off; `PI_SKIP_VERSION_CHECK` only governs Pi's
833
893
  published under another tag, or a checkout ahead of npm reports nothing, and
834
894
  there is no channel or prerelease preference: `0.11.4` is behind `0.12.0`
835
895
  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.
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`.
839
900
  - **The marker needs the bytefly startup TUI.** It lives on the custom header,
840
901
  so with `quietStartup` off the notice is the whole signal.
841
902
  - **Pi may report the same update.** An npm-installed copy can also show Pi's
842
903
  own `Package Updates Available` block; the two checks are independent and
843
904
  `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.
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.
847
909
  - **Project toggles are two-state per press** (`enable`/`disable`). `x` clears
848
910
  the override, which is the third state; a resource with no project entry in
849
911
  `pi config` is still managed there.
@@ -915,8 +977,8 @@ switch) turns the check off; `PI_SKIP_VERSION_CHECK` only governs Pi's
915
977
  - No status chip for the doctor report: it is on demand plus one notice, not a
916
978
  permanent footer segment.
917
979
  - No notification for individual tool calls, message turns, or subagent steps:
918
- the three triggers — a settled run, a prompt mid-run, a finished job — are the
919
- whole surface.
980
+ the four triggers — a settled run, a prompt mid-run, a finished job, a watcher
981
+ match — are the whole surface.
920
982
  - No sound, action buttons, notification history, or de-duplication; a
921
983
  notification is a fire-and-forget message, not an inbox.
922
984
  - No frozen per-run working duration: the counter is live and is handed back to
@@ -947,10 +1009,10 @@ rather than showing a wrong value.
947
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. |
948
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. |
949
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. |
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. |
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. |
952
1014
  | Data sources | `footerData.getGitBranch()`, `footerData.getAvailableProviderCount()`, `ctx.getContextUsage()`, `ctx.sessionManager.getEntries()`, `ctx.sessionManager.getSessionName()`, `ctx.model`; subscribes to `footerData.onBranchChange` to repaint. |
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. |
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. |
954
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. |
955
1017
 
956
1018
  ## Design notes
@@ -1041,6 +1103,10 @@ rather than showing a wrong value.
1041
1103
  - **Two lines, four zones.** Line 1 is identity (`pwd` left, branch + worktree
1042
1104
  right); line 2 is resources (meters left, model + level right). This keeps
1043
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.
1044
1110
  - **The gauge is anchored at column 0.** A leading mode slot would shift the
1045
1111
  context percentage left and right as plan mode came and went; trailing it
1046
1112
  after a `╎` keeps the percentage in a fixed place while the mode stays
@@ -1137,16 +1203,18 @@ rather than showing a wrong value.
1137
1203
  - **The install hint has one home.** `document` and `web-access` format their
1138
1204
  tool errors from the same `REQUIREMENTS` entries `doctor` prints, so a user
1139
1205
  reads the same command whether a tool failed or the report named it.
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.
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.
1150
1218
  - **The doctor is quiet unless the bytefly startup TUI is on.** Only `missing`
1151
1219
  findings post a notice, only for enabled extensions, only with a UI, only once
1152
1220
  per session, and only when Pi's `quietStartup` is `true` — the same state that
@@ -1190,7 +1258,7 @@ elapsed counter on Pi's working row), `panel/` (the
1190
1258
  unified screen and its merged model),
1191
1259
  `picker/` (discovery and the settings store), `doctor/` (the `run_doctor` tool
1192
1260
  and its probe runtime), `notify/`, and `update/` (the published-version check
1193
- behind the header marker). The chrome and the screen are drawn from
1261
+ behind the header marker, its disk cache, and the on-demand re-check). The chrome and the screen are drawn from
1194
1262
  the package's [`lib/tui/`](../../lib/tui/) design system (tokens, rules, rows,
1195
1263
  columns, gauge), so the look follows the active theme.
1196
1264
 
@@ -1257,13 +1325,14 @@ columns, gauge), so the look follows the active theme.
1257
1325
  | `notify/types.ts` | `NotifyConfig`, `NotifyMethod`, and the method list. |
1258
1326
  | `notify/config.ts` | The `notify` section of `bytefly.json`: defaults, validation, loader. |
1259
1327
  | `notify/dispatch.ts` | Method selection and delivery: OSC 777/99, Windows toast, `osascript`, `notify-send`, over injected seams. |
1260
- | `notify/format.ts` | Pure notification text: session label and the turn / input / job bodies. |
1261
- | `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. |
1262
1330
  | `update/check.ts` | The one registry read: the `latest` URL, the abort timeout, the `FetchLike` seam, and the failure-to-`undefined` mapping. |
1263
1331
  | `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. |
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. |
1267
1336
 
1268
1337
  ## Testing
1269
1338
 
@@ -1303,13 +1372,16 @@ formatting (`doctor/format`), its config loader (`doctor/config`), and
1303
1372
  registration, the proactive notice, the session cache, and `run_doctor`
1304
1373
  through the factory (`doctor/register`). The notify capability is covered by its section loader
1305
1374
  (`notify/config`), the pure text builders (`notify/format`), method selection and
1306
- delivery over recording seams (`notify/dispatch`), and its three triggers,
1375
+ delivery over recording seams (`notify/dispatch`), and its four triggers,
1307
1376
  reload, and shutdown through `registerNotify` (`notify/register`). The update
1308
1377
  check is covered by the version-ordering table (`update/version`), the transport
1309
1378
  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`).
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`).
1313
1385
  The user-message nameplate is covered by its pure transform
1314
1386
  (`message/format`, including a render through pi-tui's real `Markdown`) and its
1315
1387
  registration (`message/register`), plus the factory check in `extension`.
@@ -50,8 +50,13 @@ export const DEFAULT_RAIL_GLYPH: string = GLYPHS.divider;
50
50
 
51
51
  export const DEFAULT_CHROME_CONFIG: ChromeConfig = { rail: DEFAULT_RAIL_GLYPH };
52
52
 
53
- /** Whether `value` is a single, unstyled terminal column. */
54
- function isRailGlyph(value: string): boolean {
53
+ /**
54
+ * Whether `value` is a single, unstyled terminal column.
55
+ *
56
+ * The Settings row validates with this same function, so the editor cannot save
57
+ * a glyph the next read would replace.
58
+ */
59
+ export function isRailGlyph(value: string): boolean {
55
60
  // Control characters first: a bare ESC confuses a width measurement.
56
61
  if (/[\u0000-\u001f\u007f]/.test(value)) return false;
57
62
  return [...value].length === 1 && visibleWidth(value) === 1;
@@ -16,6 +16,7 @@
16
16
  */
17
17
 
18
18
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
19
+ import { WEB_SEARCH_PROVIDER_CHANNEL } from "../../../lib/events.ts";
19
20
  import { missingFindings } from "../../../lib/requirements.ts";
20
21
  import { loadConfig } from "./config.ts";
21
22
  import { warningNotice } from "./format.ts";
@@ -39,6 +40,11 @@ export function registerDoctor(pi: ExtensionAPI, deps: DoctorDeps = {}): DoctorR
39
40
 
40
41
  registerTools(pi, runtime);
41
42
 
43
+ // `web-access` publishes what it resolved at session start; the doctor reports
44
+ // the provider, its key variable, and (on a refresh) its endpoint from that,
45
+ // without importing the extension.
46
+ pi.events.on(WEB_SEARCH_PROVIDER_CHANNEL, (facts) => runtime.noteSearchFacts(facts));
47
+
42
48
  pi.on("session_start", async (_event, ctx) => {
43
49
  const config = loadConfig(ctx.cwd);
44
50
  runtime.configure({ cwd: ctx.cwd, config });
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  import { CONFIG_DIR_NAME, getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
12
+ import type { WebSearchProviderEvent } from "../../../lib/events.ts";
12
13
  import { readJsonResult } from "../../../lib/config.ts";
13
14
  import { isExtensionEnabled } from "../../../lib/env.ts";
14
15
  import {
@@ -47,10 +48,28 @@ export interface DoctorRuntime {
47
48
  probeAll(options?: { refresh?: boolean }): Promise<ExtensionReport[]>;
48
49
  /** The cached reports from the last probe, if any. */
49
50
  peek(): ExtensionReport[] | undefined;
50
- /** Drop the cached reports; the configured cwd is kept. */
51
+ /** Remember the search provider `web-access` published for this session. */
52
+ noteSearchFacts(facts: unknown): void;
53
+ /** Drop the cached reports and the published facts; the configured cwd is kept. */
51
54
  reset(): void;
52
55
  }
53
56
 
57
+ /** How long a reachability probe may take; a doctor refresh is not a fetch. */
58
+ const REACH_TIMEOUT_MS = 3_000;
59
+
60
+ /**
61
+ * Probe a URL's reachability. Any HTTP answer proves the host is there; a 5xx is
62
+ * reported because it usually means the server is up but broken.
63
+ */
64
+ async function probeReach(url: string): Promise<{ ok: boolean; error?: string }> {
65
+ try {
66
+ const response = await fetch(url, { method: "GET", signal: AbortSignal.timeout(REACH_TIMEOUT_MS) });
67
+ return response.status < 500 ? { ok: true } : { ok: false, error: `HTTP ${response.status}` };
68
+ } catch (error) {
69
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
70
+ }
71
+ }
72
+
54
73
  /** Build a runtime over the host's `exec`, with `deps.seams` for tests. */
55
74
  export function createDoctorRuntime(
56
75
  pi: Pick<ExtensionAPI, "exec">,
@@ -59,9 +78,11 @@ export function createDoctorRuntime(
59
78
  let cwd = process.cwd();
60
79
  let config: DoctorConfig = DEFAULT_CONFIG;
61
80
  let cached: ExtensionReport[] | undefined;
81
+ /** What `web-access` published at session start; absent when it did not load. */
82
+ let searchProvider: WebSearchProviderEvent | undefined;
62
83
 
63
- /** The full seam set for the current session. */
64
- const seams = (): ProbeSeams => ({
84
+ /** The full seam set for the current session; `refresh` is the caller's. */
85
+ const seams = (refresh: boolean): ProbeSeams => ({
65
86
  exec: async (command, args) => {
66
87
  const result = await pi.exec(command, args, { timeout: config.timeoutMs });
67
88
  return { code: result.code };
@@ -72,6 +93,9 @@ export function createDoctorRuntime(
72
93
  isEnabled: (extension) => isExtensionEnabled(extension) && !config.skip.includes(extension),
73
94
  notify: notifySeam(cwd),
74
95
  isTTY: process.stdout.isTTY === true,
96
+ searchProvider: () => searchProvider,
97
+ reach: probeReach,
98
+ refresh,
75
99
  ...deps.seams,
76
100
  });
77
101
 
@@ -84,20 +108,26 @@ export function createDoctorRuntime(
84
108
 
85
109
  async probeRequired() {
86
110
  // Concurrent: a probe is a short-lived process, and session start must
87
- // not wait on the sum of every timeout.
88
- return Promise.all(hardRequirementExtensions().map((name) => probeExtension(name, seams())));
111
+ // not wait on the sum of every timeout. Never a refresh: this runs before
112
+ // anyone asked, so no probe may touch the network.
113
+ return Promise.all(hardRequirementExtensions().map((name) => probeExtension(name, seams(false))));
89
114
  },
90
115
 
91
116
  async probeAll(options = {}) {
92
117
  if (cached && !options.refresh) return cached;
93
- cached = await probeDeclared(seams(), Object.keys(REQUIREMENTS));
118
+ cached = await probeDeclared(seams(options.refresh === true), Object.keys(REQUIREMENTS));
94
119
  return cached;
95
120
  },
96
121
 
97
122
  peek: () => cached,
98
123
 
124
+ noteSearchFacts(facts) {
125
+ searchProvider = facts && typeof facts === "object" ? (facts as WebSearchProviderEvent) : undefined;
126
+ },
127
+
99
128
  reset() {
100
129
  cached = undefined;
130
+ searchProvider = undefined;
101
131
  },
102
132
  };
103
133
  }
@@ -7,9 +7,14 @@
7
7
  * hold thousands of entries and the footer renders on every frame. The config is
8
8
  * captured when the reader is built, so re-reading the file means building a new
9
9
  * reader (which is what installing the footer does).
10
+ *
11
+ * The child-run ledger's version joins the memo key: a finished subagent invalidates
12
+ * the same way an appended entry does, so the roll-up appears on the next frame
13
+ * without re-pricing every frame.
10
14
  */
11
15
 
12
16
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
17
+ import type { DelegationLedger } from "../delegation.ts";
13
18
  import { createPricer } from "../price/price.ts";
14
19
  import type { ByteflyConfig } from "../price/types.ts";
15
20
  import type { ContextInfo, FooterData, StatusSnapshot } from "../types.ts";
@@ -23,22 +28,26 @@ import { emptyUsage, usageRecords } from "./entries.ts";
23
28
  * are stale; this avoids copying the full entry list and rebuilding the session
24
29
  * projection per frame. Pi's own footer memoizes its stats on the same idea.
25
30
  */
26
- export function createSnapshotReader(config: ByteflyConfig) {
31
+ export function createSnapshotReader(config: ByteflyConfig, delegation: DelegationLedger) {
27
32
  const price = createPricer(config);
28
33
  let key: string | null = null;
29
34
  let usage = emptyUsage();
35
+ let delegationTotals = { runs: 0, usage: emptyUsage() };
30
36
  let cacheHitRate: number | null = null;
31
37
  let context: ContextInfo = { tokens: null, contextWindow: 0, percent: null };
32
38
 
33
39
  return (ctx: ExtensionContext, footerData: FooterData): StatusSnapshot => {
34
40
  const session = ctx.sessionManager;
35
41
  const current = ctx.model;
36
- const nextKey = `${session.getSessionId()}:${session.getLeafId() ?? ""}:${current?.provider ?? ""}/${current?.id ?? ""}`;
42
+ const nextKey = `${session.getSessionId()}:${session.getLeafId() ?? ""}:${current?.provider ?? ""}/${current?.id ?? ""}:${delegation.version()}`;
37
43
  if (nextKey !== key) {
38
44
  const active = current ? { provider: current.provider, model: current.id } : undefined;
39
45
  const scanned = usageRecords(session.getEntries(), active);
40
46
  usage = price(scanned.records);
41
47
  cacheHitRate = scanned.cacheHitRate;
48
+ // The child runs are priced by the same pricer, so a currency or rate
49
+ // change moves both numbers together — and neither is folded into the other.
50
+ delegationTotals = { runs: delegation.runs(), usage: price(delegation.records()) };
42
51
 
43
52
  const usageInfo = ctx.getContextUsage();
44
53
  context = {
@@ -65,6 +74,7 @@ export function createSnapshotReader(config: ByteflyConfig) {
65
74
  currency: config.currency,
66
75
  cacheHitRate,
67
76
  providerCount: footerData.getAvailableProviderCount(),
77
+ delegation: delegationTotals,
68
78
  };
69
79
  };
70
80
  }
@@ -16,7 +16,13 @@ export const CONFIG = {
16
16
  gauge: { full: GLYPHS.gaugeFull, empty: GLYPHS.gaugeEmpty, widths: [10, 5, 3, 0] as const },
17
17
  /** Separators printed before grouped and other segments. */
18
18
  separators: { group: SEPARATORS.group, item: SEPARATORS.item },
19
- icons: { branch: GLYPHS.branch, detached: GLYPHS.detached, worktree: GLYPHS.worktree, browse: GLYPHS.browse },
19
+ icons: {
20
+ branch: GLYPHS.branch,
21
+ detached: GLYPHS.detached,
22
+ worktree: GLYPHS.worktree,
23
+ browse: GLYPHS.browse,
24
+ delegation: GLYPHS.delegation,
25
+ },
20
26
  /** Status key the worktree extension uses; routed to line 1's right zone. */
21
27
  worktreeStatusKey: STATUS_KEYS.worktree,
22
28
  /** Status key the file-browse extension uses; routed to line 1's right zone. */