@gavin-hu/my-pi-agent 0.12.0 → 0.13.1

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 (167) hide show
  1. package/README.md +28 -21
  2. package/README.zh-CN.md +30 -22
  3. package/extensions/bytefly/README.md +162 -70
  4. package/extensions/bytefly/chrome.ts +7 -2
  5. package/extensions/bytefly/doctor/register.ts +6 -0
  6. package/extensions/bytefly/doctor/runtime.ts +38 -6
  7. package/extensions/bytefly/footer/budget.ts +64 -0
  8. package/extensions/bytefly/footer/collect/snapshot.ts +33 -5
  9. package/extensions/bytefly/footer/config.ts +7 -1
  10. package/extensions/bytefly/footer/delegation.ts +100 -0
  11. package/extensions/bytefly/footer/price/config.ts +16 -0
  12. package/extensions/bytefly/footer/price/types.ts +5 -0
  13. package/extensions/bytefly/footer/register.ts +46 -4
  14. package/extensions/bytefly/footer/render/lines.ts +2 -0
  15. package/extensions/bytefly/footer/render/segments.ts +27 -0
  16. package/extensions/bytefly/footer/types.ts +11 -0
  17. package/extensions/bytefly/index.ts +8 -5
  18. package/extensions/bytefly/notify/config.ts +2 -0
  19. package/extensions/bytefly/notify/format.ts +19 -1
  20. package/extensions/bytefly/notify/register.ts +52 -14
  21. package/extensions/bytefly/notify/types.ts +2 -0
  22. package/extensions/bytefly/panel/commands.ts +34 -13
  23. package/extensions/bytefly/panel/settings.ts +129 -16
  24. package/extensions/bytefly/update/cache.ts +73 -0
  25. package/extensions/bytefly/update/format.ts +65 -10
  26. package/extensions/bytefly/update/register.ts +99 -13
  27. package/extensions/bytefly/update/state.ts +13 -1
  28. package/extensions/document/README.md +22 -12
  29. package/extensions/document/config.ts +4 -0
  30. package/extensions/document/deadline.ts +61 -0
  31. package/extensions/document/tool.ts +13 -2
  32. package/extensions/file-browse/README.md +12 -8
  33. package/extensions/file-browse/client.ts +6 -7
  34. package/extensions/file-browse/files.ts +0 -4
  35. package/extensions/file-browse/git.ts +9 -10
  36. package/extensions/file-browse/html.ts +17 -7
  37. package/extensions/file-browse/router.ts +24 -15
  38. package/extensions/goal/README.md +7 -1
  39. package/extensions/goal/schema.ts +6 -2
  40. package/extensions/goal/types.ts +6 -2
  41. package/extensions/job/README.md +16 -12
  42. package/extensions/job/format.ts +4 -7
  43. package/extensions/job/runtime.ts +23 -6
  44. package/extensions/job/status.ts +4 -2
  45. package/extensions/memory/README.md +55 -13
  46. package/extensions/memory/commands.ts +20 -2
  47. package/extensions/memory/doctor.ts +137 -0
  48. package/extensions/memory/format.ts +24 -7
  49. package/extensions/memory/runtime.ts +49 -0
  50. package/extensions/memory/store.ts +31 -0
  51. package/extensions/memory/tui.ts +136 -12
  52. package/extensions/plan/README.md +60 -29
  53. package/extensions/plan/commands.ts +67 -8
  54. package/extensions/plan/index.ts +35 -11
  55. package/extensions/plan/plan-index.ts +234 -0
  56. package/extensions/plan/plan-text.ts +95 -0
  57. package/extensions/plan/plans.ts +90 -1
  58. package/extensions/plan/review.ts +12 -9
  59. package/extensions/plan/runtime.ts +72 -27
  60. package/extensions/plan/steps.ts +35 -25
  61. package/extensions/plan/todos.ts +24 -2
  62. package/extensions/plan/tools.ts +124 -24
  63. package/extensions/plan/tui.ts +7 -28
  64. package/extensions/plan/types.ts +9 -0
  65. package/extensions/plan/web-page.ts +2 -2
  66. package/extensions/plan/web.ts +12 -11
  67. package/extensions/rewind/README.md +28 -8
  68. package/extensions/rewind/commands.ts +1 -1
  69. package/extensions/rewind/format.ts +4 -4
  70. package/extensions/rewind/git.ts +56 -18
  71. package/extensions/rewind/index.ts +3 -5
  72. package/extensions/rewind/path-picker.ts +198 -0
  73. package/extensions/rewind/paths.ts +128 -0
  74. package/extensions/rewind/restore.ts +69 -28
  75. package/extensions/rewind/rewind.ts +80 -3
  76. package/extensions/rewind/runtime.ts +13 -5
  77. package/extensions/rewind/snapshot-run.ts +5 -2
  78. package/extensions/rewind/types.ts +2 -0
  79. package/extensions/subagent/README.md +7 -1
  80. package/extensions/subagent/accounting.ts +34 -0
  81. package/extensions/subagent/index.ts +23 -1
  82. package/extensions/subagent/stream.ts +2 -0
  83. package/extensions/subagent/types.ts +2 -0
  84. package/extensions/todo/nudge.ts +5 -1
  85. package/extensions/watch/README.md +19 -8
  86. package/extensions/watch/commands.ts +4 -3
  87. package/extensions/watch/config.ts +1 -1
  88. package/extensions/watch/format.ts +4 -7
  89. package/extensions/watch/index.ts +19 -0
  90. package/extensions/watch/runtime.ts +83 -12
  91. package/extensions/web-access/README.md +43 -13
  92. package/extensions/web-access/charset.ts +58 -0
  93. package/extensions/web-access/fetch/find.ts +11 -7
  94. package/extensions/web-access/fetch/format.ts +64 -18
  95. package/extensions/web-access/fetch/page.ts +5 -1
  96. package/extensions/web-access/fetch/tool.ts +19 -18
  97. package/extensions/web-access/http.ts +49 -8
  98. package/extensions/web-access/search/facts.ts +38 -0
  99. package/extensions/web-access/search/tool.ts +13 -0
  100. package/extensions/web-access/text.ts +50 -11
  101. package/extensions/worktree/README.md +81 -16
  102. package/extensions/worktree/config.ts +11 -0
  103. package/extensions/worktree/enter.ts +10 -3
  104. package/extensions/worktree/exit.ts +55 -3
  105. package/extensions/worktree/gaps.ts +74 -0
  106. package/extensions/worktree/git.ts +28 -0
  107. package/extensions/worktree/guard.ts +50 -4
  108. package/extensions/worktree/handoff.ts +55 -0
  109. package/extensions/worktree/index.ts +14 -2
  110. package/extensions/worktree/root-tools.ts +1 -1
  111. package/extensions/worktree/runtime.ts +25 -0
  112. package/extensions/worktree/status.ts +15 -2
  113. package/extensions/worktree/tools.ts +3 -2
  114. package/lib/README.md +14 -10
  115. package/lib/events.ts +86 -1
  116. package/lib/goal-state.ts +95 -0
  117. package/lib/requirements.ts +361 -62
  118. package/lib/tui/README.md +91 -0
  119. package/lib/tui/columns.ts +17 -0
  120. package/lib/tui/glyphs.ts +1 -0
  121. package/lib/tui/tool.ts +6 -2
  122. package/lib/web/layout.ts +12 -2
  123. package/package.json +5 -3
  124. package/themes/bytefly-dark.json +1 -1
  125. package/assets/chrome.png +0 -0
  126. package/assets/file-browse.png +0 -0
  127. package/assets/job.png +0 -0
  128. package/assets/plan-review.png +0 -0
  129. package/assets/question.png +0 -0
  130. package/assets/rewind.png +0 -0
  131. package/assets/todo.png +0 -0
  132. package/assets/tool-renderings.png +0 -0
  133. package/assets/tools/ask_user_question-cancelled.png +0 -0
  134. package/assets/tools/ask_user_question.png +0 -0
  135. package/assets/tools/bash.png +0 -0
  136. package/assets/tools/edit.png +0 -0
  137. package/assets/tools/enter_plan_mode.png +0 -0
  138. package/assets/tools/enter_worktree.png +0 -0
  139. package/assets/tools/exit_plan_mode.png +0 -0
  140. package/assets/tools/exit_worktree.png +0 -0
  141. package/assets/tools/find.png +0 -0
  142. package/assets/tools/goal.png +0 -0
  143. package/assets/tools/grep.png +0 -0
  144. package/assets/tools/job.png +0 -0
  145. package/assets/tools/list_worktrees.png +0 -0
  146. package/assets/tools/ls.png +0 -0
  147. package/assets/tools/memory-long-subject.png +0 -0
  148. package/assets/tools/memory.png +0 -0
  149. package/assets/tools/notice.png +0 -0
  150. package/assets/tools/open_path.png +0 -0
  151. package/assets/tools/prune_worktrees.png +0 -0
  152. package/assets/tools/read.png +0 -0
  153. package/assets/tools/read_doc-running.png +0 -0
  154. package/assets/tools/read_doc.png +0 -0
  155. package/assets/tools/run_doctor.png +0 -0
  156. package/assets/tools/send_wechat.png +0 -0
  157. package/assets/tools/set_plan_steps.png +0 -0
  158. package/assets/tools/subagent.png +0 -0
  159. package/assets/tools/todo.png +0 -0
  160. package/assets/tools/turn-rule.png +0 -0
  161. package/assets/tools/user-message.png +0 -0
  162. package/assets/tools/watch.png +0 -0
  163. package/assets/tools/web_fetch-rejected.png +0 -0
  164. package/assets/tools/web_fetch.png +0 -0
  165. package/assets/tools/web_search.png +0 -0
  166. package/assets/tools/write.png +0 -0
  167. 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
@@ -120,14 +126,26 @@ shows as `¥<0.01` rather than `¥0.00`.
120
126
 
121
127
  DeepSeek needs no config file at all: the bar prices it from the published yuan
122
128
  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:
129
+ Those tables record the day they were last checked against the published tariff,
130
+ and `doctor` reports how old that is, so a re-pricing shows up as a warning instead
131
+ of a quietly wrong total. A `bytefly.json` can switch to the published dollar table,
132
+ add models of your own, or replace a built-in entry:
125
133
 
126
134
  ```
127
135
  ~/repo/project ⎇ main
128
136
  ▰▰▰▰▰▰▱▱▱▱ 62%/200k · ¥2.50 · ↑42k ↓8.0k · R96k CH 87% opus-4.5 · high
129
137
  ```
130
138
 
139
+ **Session budget.** `price.budget` puts a threshold under that total, in the same
140
+ currency: when the session's cost reaches it, one `bytefly · Session budget
141
+ reached` notice names what was spent against the budget, and one OS notification
142
+ follows when notify is enabled. It is an alert, not a limit — no turn is blocked or
143
+ aborted — and it fires once per session, on the turn where the crossing becomes
144
+ final (the run settling), because a warning per turn after that is noise. `0`, the
145
+ default, turns it off. The check reads the total the footer already priced, and
146
+ the footer exists only in `tui` sessions, so a print or JSON run has no budget
147
+ alert; the footer's own numbers are visible in every interactive session.
148
+
131
149
  **User message.** Every user message opens with a two-row `You` nameplate —
132
150
  the label over a rule that spans the width — above the message:
133
151
 
@@ -266,21 +284,25 @@ underneath, and `run_doctor` returns the same report as text and typed data.
266
284
  Only a missing
267
285
  *hard* requirement posts a notice at session start; everything else waits in the
268
286
  report.
269
- See [The bytefly screen](#the-bytefly-screen).
287
+ See [The control center](#the-control-center).
270
288
 
271
289
  **Notify.** Off by default, one `notify` section in `bytefly.json` turns on OS
272
290
  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
291
+ background job finishes, when the agent opens a prompt asking for input, and when
292
+ a `watch` wake match fires while the agent is idle. The
274
293
  method follows the terminal (OSC 777, OSC 99, a Windows toast) or falls back to
275
294
  the platform notifier (`osascript`, `notify-send`); see
276
295
  [Notifications](#notifications).
277
296
 
278
- **Update check.** Once per interactive session, in the background, the loaded
297
+ **Update check.** In the background at an interactive session's start, the loaded
279
298
  version is compared with the published `@gavin-hu/my-pi-agent` on npm. A newer
280
299
  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).
300
+ notice naming `pi update --extensions` and its release URL. The answer is cached
301
+ in `<agentDir>/bytefly/update.json` with the time it was read, so a session
302
+ starting inside the 24-hour window reuses it instead of repeating the request,
303
+ and `/bytefly update` re-runs the check on demand. The check is skipped when
304
+ `PI_OFFLINE` is set, makes no request outside `tui`, and a failed background
305
+ request is silent; see [Update check](#update-check).
284
306
 
285
307
  ## Commands
286
308
 
@@ -289,9 +311,10 @@ is set, makes no request outside `tui`, and a failed request is silent; see
289
311
  | `/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
312
  | `/bytefly settings` | Open the control center on Settings. Outside `tui` it prints the grouped settings listing with each row's value. |
291
313
  | `/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. |
314
+ | `/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
315
 
293
316
  Tab completes the subcommands, extension names after `doctor`, and `refresh`;
294
- `/bytefly settings` takes no name.
317
+ `/bytefly settings` and `/bytefly update` take no name.
295
318
 
296
319
  ## Behaviour by mode
297
320
 
@@ -328,7 +351,7 @@ separators, icons, status keys), and the turn separator in
328
351
  colors).
329
352
 
330
353
  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
354
+ [configuration reference](../../docs/references/bytefly-config.md); this section
332
355
  covers what that table cannot — when each section is read, and what the cost
333
356
  does when no rate matches.
334
357
 
@@ -392,7 +415,7 @@ named by `doctor`; a wrong-typed value falls back to its default, and a
392
415
  wrong-typed timezone drops the peak schedule rather than the session.
393
416
 
394
417
  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
418
+ needed for an unattended run, and the four trigger flags then narrow what is
396
419
  announced (see [Notifications](#notifications)).
397
420
 
398
421
  The proactive doctor check is part of the bytefly startup TUI, so it posts only
@@ -574,23 +597,29 @@ installs or fetches a package.
574
597
  without leaving the session.
575
598
 
576
599
  **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:
600
+ switch, with its own cursor. It lists sixteen preferences in six groups:
578
601
 
579
602
  ```
580
603
  bytefly ──────────────────────────────────────────────────────────────
581
- Extensions · Settings Global · 10 settings
604
+ Extensions · Settings Global · 16 settings
582
605
 
583
606
  Startup (1)
584
607
  ❯ ◻ Bytefly startup TUI Off
585
- Notifications (3)
608
+ Notifications (7)
586
609
  ◻ Desktop notifications Off
610
+ ◼ Turn finished On
611
+ ◼ Needs input On
612
+ ◼ Job finished On
613
+ ◼ Watcher matched On
587
614
  Delivery method Auto
588
615
  Notification title Pi
589
- Diagnostics (1)
616
+ Diagnostics (2)
590
617
  ◼ Proactive health check On
591
- Pricing (2)
618
+ Probe timeout 2000
619
+ Pricing (3)
592
620
  Currency CNY (¥)
593
621
  Cost decimals 2
622
+ Session budget 0
594
623
  Transcript (2)
595
624
  Rail glyph ╎
596
625
  Output padding 1
@@ -607,12 +636,18 @@ bytefly ────────────────────────
607
636
  |---|---|---|---|
608
637
  | Startup | Bytefly startup TUI | Pi `settings.json` (`quietStartup`) | Off, On |
609
638
  | Notifications | Desktop notifications | `notify.enabled` | Off, On |
639
+ | Notifications | Turn finished | `notify.turnFinished` | Off, On |
640
+ | Notifications | Needs input | `notify.needsInput` | Off, On |
641
+ | Notifications | Job finished | `notify.jobFinished` | Off, On |
642
+ | Notifications | Watcher matched | `notify.matchFound` | Off, On |
610
643
  | Notifications | Delivery method | `notify.method` | Auto, OSC 777, OSC 99, Windows toast, macOS, Linux |
611
644
  | Notifications | Notification title | `notify.title` | Text, up to 40 characters |
612
645
  | Diagnostics | Proactive health check | `doctor.proactive` | Off, On |
646
+ | Diagnostics | Probe timeout | `doctor.timeoutMs` | Number, 100–30000 ms |
613
647
  | Pricing | Currency | `price.currency.code` | CNY, USD |
614
648
  | Pricing | Cost decimals | `price.currency.decimals` | 0, 1, 2, 3, 4 |
615
- | Transcript | Rail glyph | `chrome.rail` | `╎`, `│`, `┃`, `▏` |
649
+ | Pricing | Session budget | `price.budget` | Number, `0` = off |
650
+ | Transcript | Rail glyph | `chrome.rail` | Text, exactly one column |
616
651
  | Transcript | Output padding | Pi `settings.json` | 0, 1 |
617
652
  | Terminal | Hardware cursor | Pi `settings.json` | Off, On |
618
653
 
@@ -620,7 +655,8 @@ Choosing **Currency** also writes that currency's default precision. Both rows
620
655
  stay editable afterwards, so the footer follows the explicit values.
621
656
 
622
657
  `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
658
+ forward, and a text row opens an inline editor on its value — the notification
659
+ title, the probe timeout, and the rail glyph. The detail pane names the source, describes the preference, and shows
624
660
  where the value is written — `Global · ~/.pi/agent/settings.json`, `Project ·
625
661
  .pi/bytefly.json`, or `Inherited from Global · .pi/bytefly.json`. A successful
626
662
  change marks the screen `Saved · reload to apply` — even when the terminal is too
@@ -635,7 +671,7 @@ with `true` or `false`.
635
671
 
636
672
  App rows are **Global only**: Pi's setters for them write the personal
637
673
  `settings.json`, and only resource paths have public project setters. Project
638
- scope therefore lists the six `bytefly.json` rows and a
674
+ scope therefore lists the thirteen `bytefly.json` rows and a
639
675
  `App settings are Global only.` note rather than promising a write that cannot
640
676
  happen. A `bytefly.json` row writes one leaf in the selected scope (a depth-1
641
677
  key, or a nested one such as `price.currency.decimals`) through a per-key
@@ -645,14 +681,19 @@ malformed document is refused with a notice instead of overwritten.
645
681
  The non-`tui` form prints the headline, the groups, and `Label: Value` rows:
646
682
 
647
683
  ```
648
- bytefly: Settings · Global · 9 settings
684
+ bytefly: Settings · Global · 16 settings
649
685
 
650
686
  Startup (1)
651
687
  Bytefly startup TUI: Off
652
688
 
653
- Notifications (2)
689
+ Notifications (7)
654
690
  Desktop notifications: Off
691
+ Turn finished: On
692
+ Needs input: On
693
+ Job finished: On
694
+ Watcher matched: On
655
695
  Delivery method: Auto
696
+ Notification title: Pi
656
697
  ...
657
698
  ```
658
699
 
@@ -664,6 +705,20 @@ means reduced capability — an optional package that is absent, a config file
664
705
  that fails to parse, a login that has not happened. Doctor never changes
665
706
  anything and never installs anything.
666
707
 
708
+ It also reports what has **aged**: the built-in DeepSeek rate tables carry the day
709
+ they were last checked against the published tariff (`PRICE_VERIFIED_AT` in
710
+ `footer/price/config.ts`), and the report says how old that is — `ok` inside the
711
+ 90-day window, `degraded` beyond it with the source URL to re-check. A tariff change
712
+ therefore becomes a visible warning instead of a quietly wrong cost total.
713
+
714
+ `web-access` publishes what it resolved at `session_start`, so the report names
715
+ the search provider and why it won (an explicit choice, the `auto` rule, or the
716
+ keyless default), and — when the provider takes a key and no literal `apiKey` is
717
+ set — its API-key variable and whether that variable is set. The key value is
718
+ never part of the report, only its variable name. Reachability is the one check
719
+ that touches the network, so it runs only on an explicit `/bytefly refresh`:
720
+ a session-start check stays offline.
721
+
667
722
  `/bytefly <extension>` narrows the same screen to that extension; `Enter`
668
723
  expands its findings, including the checks that passed, into the free height.
669
724
  `/bytefly refresh` ignores the findings cached earlier in the session. Every
@@ -718,7 +773,7 @@ the same entry in `lib/requirements.ts`, so the two cannot drift.
718
773
 
719
774
  The notify capability exists for unattended runs: start a long turn, leave the
720
775
  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
776
+ It fires on four events, each independently switchable in the `notify` section
722
777
  of `bytefly.json` (see [Configuration](#configuration)):
723
778
 
724
779
  - **A run settled** (`agent_settled`). Pi finished the turn and waits for the
@@ -731,6 +786,12 @@ of `bytefly.json` (see [Configuration](#configuration)):
731
786
  - **A background job finished.** The [job](../job/) extension announces each
732
787
  terminal transition on the shared `pi.events` bus
733
788
  (`my-pi-agent/job-finished`); bytefly listens and never imports it.
789
+ - **A watcher matched.** A [watch](../watch/) whose `wake` is on matched while the
790
+ agent was idle, and announced it on `my-pi-agent/watch-match` with the cycle's
791
+ count, the watcher, and its newest line. The body names that line, sanitized and
792
+ clipped to 80 characters, because the line is the alert: a watch usually tails a
793
+ build or an error log. A match the agent was busy for does not notify — it is
794
+ collected for the next turn instead.
734
795
 
735
796
  ### Delivery method
736
797
 
@@ -777,6 +838,7 @@ reports it once, in the two places it already owns:
777
838
  bytefly · 0.12.0 is available
778
839
  ╎ ◌ current · v0.11.4
779
840
  ╎ ◌ update · pi update --extensions
841
+ ╎ ◌ release · https://github.com/gavin-hu/my-pi-agent/releases/tag/v0.12.0
780
842
  ```
781
843
 
782
844
  The marker is the shared `↑` glyph and the newest version — the arrow is one cell
@@ -796,10 +858,27 @@ fired without awaiting at `session_start` (Pi awaits handlers, and a five-second
796
858
  registry call must not hold the first prompt) with a five-second abort. Only a
797
859
  strictly newer, parseable semver announces anything: a non-ok status, a body
798
860
  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.
861
+ version is silent. `PI_OFFLINE` (Pi's own switch) turns the check off;
862
+ `PI_SKIP_VERSION_CHECK` only governs Pi's `pi.dev` request.
863
+
864
+ The answer is cached in `<agentDir>/bytefly/update.json` as `latest` plus the
865
+ epoch milliseconds it was read at. A session that starts inside the 24-hour
866
+ window reuses that entry — painting the marker and posting the notice from it —
867
+ instead of repeating the request, so a working day of sessions costs one read.
868
+ Past the window the check runs again and rewrites the entry; a failed read writes
869
+ nothing, so the next session tries again. Offline, the cached entry is used with
870
+ no request and no write, because a stale version is still the best answer
871
+ available and the marker is advisory.
872
+
873
+ `/bytefly update` is the on-demand half: it ignores the window, reads the registry
874
+ now, records and paints the result, and prints one line — `up to date — v0.12.0
875
+ is the newest published version`, `v0.13.0 is available (current 0.12.0) —
876
+ https://…/releases/tag/v0.13.0`, `the update check was skipped (PI_OFFLINE is
877
+ set)`, or `could not read the published version`. It resolves no packages, probes
878
+ no extension, and posts no second notice, so a session open for days can see a
879
+ release without a restart. Opening the control center never checks: the screen has
880
+ no update surface, and the only request paths are `session_start` and this
881
+ command.
803
882
 
804
883
  ## Limitations
805
884
 
@@ -814,15 +893,16 @@ switch) turns the check off; `PI_SKIP_VERSION_CHECK` only governs Pi's
814
893
  - **Project overrides of app settings are not shown.** App rows read the
815
894
  personal file; a `.pi/settings.json` override still wins at runtime, and
816
895
  `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.
896
+ - **Arrays and tables stay JSON.** A row is one boolean, enum, string, or number.
897
+ `doctor.skip` (an array), the `rates` table, and `price.peak` (a nested object)
898
+ are still `bytefly.json` edits; every scalar preference has a row.
899
+ - **A custom rail glyph must be one column.** The editor refuses anything else,
900
+ because a two-column glyph would push every railed body one cell right; the
901
+ former four-glyph menu is now the description's example list.
902
+ - **`doctor`, `settings`, and `update` are reserved first arguments.**
903
+ `/bytefly doctor` always selects the report, so an extension literally named
904
+ `doctor` cannot be narrowed to by name; `settings` and `update` are reserved
905
+ the same way.
826
906
  - **The picker manages only the `extensions` resource type.** Skills, prompts,
827
907
  and themes are handled by Pi's own `pi config`.
828
908
  - **Built-in extensions are not listed** (`builtin:mcp`, `builtin:codemode`, …):
@@ -833,17 +913,19 @@ switch) turns the check off; `PI_SKIP_VERSION_CHECK` only governs Pi's
833
913
  published under another tag, or a checkout ahead of npm reports nothing, and
834
914
  there is no channel or prerelease preference: `0.11.4` is behind `0.12.0`
835
915
  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.
916
+ - **The cache window is fixed at 24 hours.** It runs at `session_start` in `tui`
917
+ mode and reuses an entry younger than that; there is no setting for it, and
918
+ `/bytefly` and `doctor` do not report the version. A failed or slow background
919
+ request is retried by the next session, or immediately by `/bytefly update`.
839
920
  - **The marker needs the bytefly startup TUI.** It lives on the custom header,
840
921
  so with `quietStartup` off the notice is the whole signal.
841
922
  - **Pi may report the same update.** An npm-installed copy can also show Pi's
842
923
  own `Package Updates Available` block; the two checks are independent and
843
924
  `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.
925
+ - **No auto-update, and the release URL is text only.** The notice names a
926
+ command instead of running one, and the notice renderer cannot open a link, so
927
+ the release row prints the URL and nothing more — no changelog body, no way to
928
+ click through from the transcript.
847
929
  - **Project toggles are two-state per press** (`enable`/`disable`). `x` clears
848
930
  the override, which is the third state; a resource with no project entry in
849
931
  `pi config` is still managed there.
@@ -915,8 +997,8 @@ switch) turns the check off; `PI_SKIP_VERSION_CHECK` only governs Pi's
915
997
  - No status chip for the doctor report: it is on demand plus one notice, not a
916
998
  permanent footer segment.
917
999
  - 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.
1000
+ the four triggers — a settled run, a prompt mid-run, a finished job, a watcher
1001
+ match — are the whole surface.
920
1002
  - No sound, action buttons, notification history, or de-duplication; a
921
1003
  notification is a fire-and-forget message, not an inbox.
922
1004
  - No frozen per-run working duration: the counter is live and is handed back to
@@ -947,10 +1029,10 @@ rather than showing a wrong value.
947
1029
  | 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
1030
  | 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
1031
  | 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. |
1032
+ | 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. |
1033
+ | 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
1034
  | 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. |
1035
+ | 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
1036
  | 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
1037
 
956
1038
  ## Design notes
@@ -1041,6 +1123,10 @@ rather than showing a wrong value.
1041
1123
  - **Two lines, four zones.** Line 1 is identity (`pwd` left, branch + worktree
1042
1124
  right); line 2 is resources (meters left, model + level right). This keeps
1043
1125
  "where am I / what state" separate from "how much am I burning".
1126
+ - **Child spend is a second figure.** The footer never adds a subagent's tokens to
1127
+ the session's; it prices the `my-pi-agent/subagent-finished` events with the
1128
+ same pricer and renders them as `↳`, so "this session" keeps meaning the
1129
+ dispatching process and the delegated work stays visible beside it.
1044
1130
  - **The gauge is anchored at column 0.** A leading mode slot would shift the
1045
1131
  context percentage left and right as plan mode came and went; trailing it
1046
1132
  after a `╎` keeps the percentage in a fixed place while the mode stays
@@ -1137,16 +1223,18 @@ rather than showing a wrong value.
1137
1223
  - **The install hint has one home.** `document` and `web-access` format their
1138
1224
  tool errors from the same `REQUIREMENTS` entries `doctor` prints, so a user
1139
1225
  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.
1226
+ - **Settings are a curated table, not a schema walker.** The rows cover every
1227
+ scalar preference across four kinds — a boolean, an enum, a string with
1228
+ its own rule, and a number — both scopes, both write paths, and one nested leaf
1229
+ (`price.currency.decimals`). A row states its section and the path to its leaf,
1230
+ and one reader per section resolves it project-over-global-over-defaults through
1231
+ the same normalizer the extension loads with, so the screen and the runtime
1232
+ cannot disagree about a value; a text row's rule calls the same predicate the
1233
+ loader uses, so an accepted value is never replaced on the next read. A generic
1234
+ walker would expose keys the screen cannot edit safely — arrays, nested
1235
+ tariffs, credentials — so the catalogue grows one proven family at a time. App
1236
+ rows are Global-only because Pi's setters write the personal file; Project
1237
+ scope lists only the `bytefly.json` rows it can actually write.
1150
1238
  - **The doctor is quiet unless the bytefly startup TUI is on.** Only `missing`
1151
1239
  findings post a notice, only for enabled extensions, only with a UI, only once
1152
1240
  per session, and only when Pi's `quietStartup` is `true` — the same state that
@@ -1190,7 +1278,7 @@ elapsed counter on Pi's working row), `panel/` (the
1190
1278
  unified screen and its merged model),
1191
1279
  `picker/` (discovery and the settings store), `doctor/` (the `run_doctor` tool
1192
1280
  and its probe runtime), `notify/`, and `update/` (the published-version check
1193
- behind the header marker). The chrome and the screen are drawn from
1281
+ behind the header marker, its disk cache, and the on-demand re-check). The chrome and the screen are drawn from
1194
1282
  the package's [`lib/tui/`](../../lib/tui/) design system (tokens, rules, rows,
1195
1283
  columns, gauge), so the look follows the active theme.
1196
1284
 
@@ -1257,13 +1345,14 @@ columns, gauge), so the look follows the active theme.
1257
1345
  | `notify/types.ts` | `NotifyConfig`, `NotifyMethod`, and the method list. |
1258
1346
  | `notify/config.ts` | The `notify` section of `bytefly.json`: defaults, validation, loader. |
1259
1347
  | `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. |
1348
+ | `notify/format.ts` | Pure notification text: session label and the turn / input / job / watch bodies. |
1349
+ | `notify/register.ts` | Registers the four triggers, loads the config at `session_start`, and owns the job- and watch-bus subscriptions. |
1262
1350
  | `update/check.ts` | The one registry read: the `latest` URL, the abort timeout, the `FetchLike` seam, and the failure-to-`undefined` mapping. |
1263
1351
  | `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. |
1352
+ | `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. |
1353
+ | `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. |
1354
+ | `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. |
1355
+ | `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
1356
 
1268
1357
  ## Testing
1269
1358
 
@@ -1303,13 +1392,16 @@ formatting (`doctor/format`), its config loader (`doctor/config`), and
1303
1392
  registration, the proactive notice, the session cache, and `run_doctor`
1304
1393
  through the factory (`doctor/register`). The notify capability is covered by its section loader
1305
1394
  (`notify/config`), the pure text builders (`notify/format`), method selection and
1306
- delivery over recording seams (`notify/dispatch`), and its three triggers,
1395
+ delivery over recording seams (`notify/dispatch`), and its four triggers,
1307
1396
  reload, and shutdown through `registerNotify` (`notify/register`). The update
1308
1397
  check is covered by the version-ordering table (`update/version`), the transport
1309
1398
  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`).
1399
+ marker, notice, and release-URL wording (`update/format`), the cache read, write,
1400
+ and window (`update/cache`), and its lifecycle — a fresh entry reused without a
1401
+ request, a stale one re-read and rewritten, silent on failure or an older version,
1402
+ cache-only offline, skipped outside `tui`, an explicit `recheck()` that ignores
1403
+ the window, and immunity to a reply that lands after shutdown
1404
+ (`update/register`).
1313
1405
  The user-message nameplate is covered by its pure transform
1314
1406
  (`message/format`, including a render through pi-tui's real `Markdown`) and its
1315
1407
  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,8 @@
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";
13
+ import { PRICE_VERIFIED_AT } from "../footer/price/config.ts";
12
14
  import { readJsonResult } from "../../../lib/config.ts";
13
15
  import { isExtensionEnabled } from "../../../lib/env.ts";
14
16
  import {
@@ -47,10 +49,28 @@ export interface DoctorRuntime {
47
49
  probeAll(options?: { refresh?: boolean }): Promise<ExtensionReport[]>;
48
50
  /** The cached reports from the last probe, if any. */
49
51
  peek(): ExtensionReport[] | undefined;
50
- /** Drop the cached reports; the configured cwd is kept. */
52
+ /** Remember the search provider `web-access` published for this session. */
53
+ noteSearchFacts(facts: unknown): void;
54
+ /** Drop the cached reports and the published facts; the configured cwd is kept. */
51
55
  reset(): void;
52
56
  }
53
57
 
58
+ /** How long a reachability probe may take; a doctor refresh is not a fetch. */
59
+ const REACH_TIMEOUT_MS = 3_000;
60
+
61
+ /**
62
+ * Probe a URL's reachability. Any HTTP answer proves the host is there; a 5xx is
63
+ * reported because it usually means the server is up but broken.
64
+ */
65
+ async function probeReach(url: string): Promise<{ ok: boolean; error?: string }> {
66
+ try {
67
+ const response = await fetch(url, { method: "GET", signal: AbortSignal.timeout(REACH_TIMEOUT_MS) });
68
+ return response.status < 500 ? { ok: true } : { ok: false, error: `HTTP ${response.status}` };
69
+ } catch (error) {
70
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
71
+ }
72
+ }
73
+
54
74
  /** Build a runtime over the host's `exec`, with `deps.seams` for tests. */
55
75
  export function createDoctorRuntime(
56
76
  pi: Pick<ExtensionAPI, "exec">,
@@ -59,9 +79,11 @@ export function createDoctorRuntime(
59
79
  let cwd = process.cwd();
60
80
  let config: DoctorConfig = DEFAULT_CONFIG;
61
81
  let cached: ExtensionReport[] | undefined;
82
+ /** What `web-access` published at session start; absent when it did not load. */
83
+ let searchProvider: WebSearchProviderEvent | undefined;
62
84
 
63
- /** The full seam set for the current session. */
64
- const seams = (): ProbeSeams => ({
85
+ /** The full seam set for the current session; `refresh` is the caller's. */
86
+ const seams = (refresh: boolean): ProbeSeams => ({
65
87
  exec: async (command, args) => {
66
88
  const result = await pi.exec(command, args, { timeout: config.timeoutMs });
67
89
  return { code: result.code };
@@ -72,6 +94,10 @@ export function createDoctorRuntime(
72
94
  isEnabled: (extension) => isExtensionEnabled(extension) && !config.skip.includes(extension),
73
95
  notify: notifySeam(cwd),
74
96
  isTTY: process.stdout.isTTY === true,
97
+ searchProvider: () => searchProvider,
98
+ reach: probeReach,
99
+ refresh,
100
+ priceVerifiedAt: () => PRICE_VERIFIED_AT,
75
101
  ...deps.seams,
76
102
  });
77
103
 
@@ -84,20 +110,26 @@ export function createDoctorRuntime(
84
110
 
85
111
  async probeRequired() {
86
112
  // 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())));
113
+ // not wait on the sum of every timeout. Never a refresh: this runs before
114
+ // anyone asked, so no probe may touch the network.
115
+ return Promise.all(hardRequirementExtensions().map((name) => probeExtension(name, seams(false))));
89
116
  },
90
117
 
91
118
  async probeAll(options = {}) {
92
119
  if (cached && !options.refresh) return cached;
93
- cached = await probeDeclared(seams(), Object.keys(REQUIREMENTS));
120
+ cached = await probeDeclared(seams(options.refresh === true), Object.keys(REQUIREMENTS));
94
121
  return cached;
95
122
  },
96
123
 
97
124
  peek: () => cached,
98
125
 
126
+ noteSearchFacts(facts) {
127
+ searchProvider = facts && typeof facts === "object" ? (facts as WebSearchProviderEvent) : undefined;
128
+ },
129
+
99
130
  reset() {
100
131
  cached = undefined;
132
+ searchProvider = undefined;
101
133
  },
102
134
  };
103
135
  }