@gavin-hu/my-pi-agent 0.13.0 → 0.14.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 (64) hide show
  1. package/README.md +14 -11
  2. package/README.zh-CN.md +12 -12
  3. package/extensions/bytefly/README.md +163 -61
  4. package/extensions/bytefly/doctor/config.ts +4 -1
  5. package/extensions/bytefly/doctor/runtime.ts +2 -0
  6. package/extensions/bytefly/footer/budget.ts +64 -0
  7. package/extensions/bytefly/footer/collect/snapshot.ts +22 -4
  8. package/extensions/bytefly/footer/price/config.ts +18 -1
  9. package/extensions/bytefly/footer/price/types.ts +5 -0
  10. package/extensions/bytefly/footer/register.ts +34 -3
  11. package/extensions/bytefly/header/component.ts +30 -8
  12. package/extensions/bytefly/header/config.ts +22 -0
  13. package/extensions/bytefly/header/register.ts +2 -1
  14. package/extensions/bytefly/index.ts +7 -5
  15. package/extensions/bytefly/notify/config.ts +1 -1
  16. package/extensions/bytefly/notify/register.ts +15 -1
  17. package/extensions/bytefly/panel/commands.ts +1 -1
  18. package/extensions/bytefly/panel/format.ts +12 -19
  19. package/extensions/bytefly/panel/settings-runtime.ts +62 -18
  20. package/extensions/bytefly/panel/settings.ts +269 -42
  21. package/extensions/bytefly/panel/tui/settings-view.ts +49 -3
  22. package/extensions/bytefly/panel/tui.ts +87 -19
  23. package/extensions/bytefly/separator/component.ts +7 -6
  24. package/extensions/bytefly/separator/config.ts +15 -0
  25. package/extensions/bytefly/separator/format.ts +60 -6
  26. package/extensions/bytefly/separator/register.ts +68 -9
  27. package/extensions/bytefly/separator/types.ts +8 -0
  28. package/extensions/bytefly/working/config.ts +4 -5
  29. package/extensions/bytefly/working/format.ts +24 -7
  30. package/extensions/bytefly/working/register.ts +34 -3
  31. package/extensions/job/format.ts +4 -7
  32. package/extensions/memory/README.md +2 -0
  33. package/extensions/plan/README.md +12 -0
  34. package/extensions/subagent/README.md +60 -5
  35. package/extensions/subagent/config.ts +58 -0
  36. package/extensions/subagent/index.ts +27 -1
  37. package/extensions/subagent/orchestrate.ts +16 -3
  38. package/extensions/subagent/render.ts +12 -0
  39. package/extensions/subagent/run.ts +100 -37
  40. package/extensions/subagent/transcripts.ts +202 -0
  41. package/extensions/subagent/types.ts +12 -0
  42. package/extensions/todo/nudge.ts +5 -1
  43. package/extensions/watch/README.md +13 -5
  44. package/extensions/watch/commands.ts +4 -3
  45. package/extensions/watch/config.ts +1 -1
  46. package/extensions/watch/format.ts +4 -7
  47. package/extensions/watch/runtime.ts +83 -12
  48. package/extensions/wechat/README.md +2 -0
  49. package/extensions/worktree/README.md +7 -1
  50. package/extensions/worktree/enter.ts +10 -3
  51. package/extensions/worktree/exit.ts +25 -5
  52. package/extensions/worktree/git.ts +28 -0
  53. package/extensions/worktree/index.ts +2 -0
  54. package/extensions/worktree/root-tools.ts +1 -1
  55. package/extensions/worktree/runtime.ts +5 -0
  56. package/lib/README.md +5 -3
  57. package/lib/config.ts +33 -9
  58. package/lib/format.ts +6 -0
  59. package/lib/requirements.ts +80 -1
  60. package/lib/tui/columns.ts +17 -0
  61. package/lib/tui/tool.ts +6 -2
  62. package/package.json +2 -1
  63. package/themes/bytefly-dark.json +2 -3
  64. package/themes/bytefly-light.json +2 -3
package/README.md CHANGED
@@ -31,13 +31,16 @@ want the agent to *think first and work safely*:
31
31
  - **Plan, then execute.** A read-only planning mode turns a task into a reviewed
32
32
  plan file that the agent then follows step by step.
33
33
  - **Delegate and parallelize.** Run specialist subagents in their own Pi
34
- processes to explore, review, test, or implement while you keep the thread.
35
- - **Never wreck the checkout.** Work happens in an isolated `git worktree`, and
36
- every prompt is snapshotted so a bad turn is one `/rewind` away.
34
+ processes, push a long build into a background job, and let a watcher tail a
35
+ log until it matters — all while you keep the thread.
36
+ - **Never wreck the checkout.** Isolation is one command: the task moves onto its
37
+ own branch in a `git worktree`, and every prompt is snapshotted so a bad turn
38
+ is one `/rewind` away.
37
39
  - **Remember across sessions.** Durable notes live in plain markdown that you
38
40
  can read, edit, and version.
39
41
  - **Reach the web and your files.** Search, fetch, browse, open, and read
40
- documents — all from inside the session.
42
+ documents from inside the session, and answer a WeChat chat through a bridge to
43
+ the live session.
41
44
 
42
45
  Everything is optional and independently toggleable. Nothing is patched or
43
46
  forked: Pi discovers the resources through the `pi` manifest in
@@ -49,13 +52,13 @@ other.
49
52
  Requires a working [Pi](https://pi.dev) installation.
50
53
 
51
54
  ```bash
52
- pi install npm:@gavin-hu/my-pi-agent # install from npm
53
- pi install git:github.com/gavin-hu/my-pi-agent # install from git
54
- pi install git:github.com/gavin-hu/my-pi-agent@v0.13.0 # pin a release
55
- pi install ./ -l # project-local (.pi/settings.json)
56
- pi -e . # try it for one run
55
+ pi install npm:@gavin-hu/my-pi-agent
57
56
  ```
58
57
 
58
+ [Getting started](./docs/getting-started.md) has the other sources — git, a
59
+ pinned release, a project-local install, and a trial that writes nothing — plus
60
+ the fit decision, the first workflows, and how to remove the pack.
61
+
59
62
  `git` on `PATH` is needed by [`worktree`](./extensions/worktree/) and
60
63
  [`rewind`](./extensions/rewind/); everything else degrades or disables cleanly,
61
64
  and `/bytefly doctor` names what is missing, including optional runtime pieces.
@@ -121,7 +124,7 @@ extension's own `README.md` has the full tool and command surface.
121
124
 
122
125
  | Extension | Tools and commands | What it does |
123
126
  |---|---|---|
124
- | [`bytefly`](./extensions/bytefly/) | `/bytefly` (with the `doctor` and `settings` subcommands), `run_doctor` | The visible layer: a themed header with a width-adaptive footer, a live elapsed counter on Pi's working row, and a transcript where every tool block leads with its call line and rails its result under it. `/bytefly` switches extensions and holds the curated settings; opt-in `notify` raises OS notifications when a run, a job, or a question needs you. `bytefly.json` is the config document every extension reads. |
127
+ | [`bytefly`](./extensions/bytefly/) | `/bytefly` (with the `doctor` and `settings` subcommands), `run_doctor` | The visible layer: a themed header with a width-adaptive footer, a live run line on Pi's working row (tool, tool count, and elapsed time), and a transcript where every tool block leads with its call line and rails its result under it. `/bytefly` switches extensions and holds the curated settings; opt-in `notify` raises OS notifications when a run, a job, or a question needs you. `bytefly.json` is the config document every extension reads. |
125
128
 
126
129
  ### Themes
127
130
 
@@ -184,7 +187,7 @@ stops the factories from registering; to drop the whole package, use Pi's
184
187
 
185
188
  | Guide | What it covers |
186
189
  |---|---|
187
- | [Getting started](./docs/getting-started.md) | Requirements, install options, verifying the load, themes. |
190
+ | [Getting started](./docs/getting-started.md) | Whether the pack fits, a trial run, install options, verifying the load, first workflows, and removal. |
188
191
  | [Extensions](./docs/extensions.md) | The full catalog of every extension and theme. |
189
192
  | [Configuration](./docs/configuration.md) | Config-file resolution, the per-extension index, environment variables. |
190
193
  | [Configuration reference](./docs/references/bytefly-config.md) | Every `bytefly.json` section and key, with its type, default, and bounds. |
package/README.zh-CN.md CHANGED
@@ -29,12 +29,13 @@ Pi 提供一个精简而锐利的内核。这个包是在其之上的一层,
29
29
 
30
30
  - **先规划,再执行。** 只读的规划模式把一次任务变成一份可审阅的计划文件,智能体
31
31
  随后按步骤执行。
32
- - **委派与并行。** 在各自的 Pi 进程中运行专职子智能体来探索、审阅、测试或实现,
33
- 你继续把握主线。
34
- - **绝不搞乱检出区。** 工作发生在隔离的 `git worktree` 中,每次提示都会生成快照,
35
- 因此一次糟糕的回合只需一个 `/rewind` 就能回退。
32
+ - **委派与并行。** 在各自的 Pi 进程中运行专职子智能体,把耗时构建交给后台任务,
33
+ 让 watcher 盯住日志直到出现关键行 —— 你继续把握主线。
34
+ - **绝不搞乱检出区。** 隔离只需一条命令:任务切到自己分支的 `git worktree` 中,
35
+ 每次提示还会生成快照,因此一次糟糕的回合只需一个 `/rewind` 就能回退。
36
36
  - **跨会话记忆。** 持久笔记以纯 Markdown 保存,可读、可改、可纳入版本控制。
37
- - **连通网络与文件。** 搜索、抓取、浏览、打开和读取文档 —— 全部在会话内完成。
37
+ - **连通网络与文件。** 在会话内搜索、抓取、浏览、打开和读取文档,还可通过桥接
38
+ 在微信里直接回答当前会话。
38
39
 
39
40
  一切都是可选的,且可独立开关。没有任何东西被补丁或分叉:Pi 通过
40
41
  [`package.json`](./package.json) 里的 `pi` manifest 发现这些资源,因此该包能像
@@ -45,13 +46,12 @@ Pi 提供一个精简而锐利的内核。这个包是在其之上的一层,
45
46
  需要一个可用的 [Pi](https://pi.dev) 安装。
46
47
 
47
48
  ```bash
48
- pi install npm:@gavin-hu/my-pi-agent # 从 npm 安装
49
- pi install git:github.com/gavin-hu/my-pi-agent # 从 git 安装
50
- pi install git:github.com/gavin-hu/my-pi-agent@v0.13.0 # 固定到某个版本
51
- pi install ./ -l # 仅项目本地(.pi/settings.json)
52
- pi -e . # 试运行一次
49
+ pi install npm:@gavin-hu/my-pi-agent
53
50
  ```
54
51
 
52
+ [快速开始](./docs/getting-started.md) 介绍其余安装方式——git、固定版本、
53
+ 仅项目本地安装,以及不写入任何内容的试用——还有适用性判断、首个工作流和移除方式。
54
+
55
55
  `git` 需在 `PATH` 上,供 [`worktree`](./extensions/worktree/) 与
56
56
  [`rewind`](./extensions/rewind/) 使用;其余部分会优雅降级或禁用,
57
57
  `/bytefly doctor` 会指出缺少什么,包括可选的运行时组件。
@@ -112,7 +112,7 @@ Windows、macOS 和 Linux 均可运行,完整检查套件在每个平台上都
112
112
 
113
113
  | 扩展 | 工具与命令 | 说明 |
114
114
  |---|---|---|
115
- | [`bytefly`](./extensions/bytefly/) | `/bytefly`(`doctor`、`settings` 子命令)、`run_doctor` | 可见的那一层:主题化头部配宽度自适应页脚,Pi 工作行上实时计时,转录中每个工具块以调用行为标题、结果正文挂在其下方。`/bytefly` 可开关扩展并集中存放精选设置;可选的 `notify` 会在一次运行、后台任务或待回答的问题需要你时发出系统通知。`bytefly.json` 是每个扩展都会读取的配置文档。 |
115
+ | [`bytefly`](./extensions/bytefly/) | `/bytefly`(`doctor`、`settings` 子命令)、`run_doctor` | 可见的那一层:主题化头部配宽度自适应页脚,Pi 工作行上实时显示工具、调用次数与计时,转录中每个工具块以调用行为标题、结果正文挂在其下方。`/bytefly` 可开关扩展并集中存放精选设置;可选的 `notify` 会在一次运行、后台任务或待回答的问题需要你时发出系统通知。`bytefly.json` 是每个扩展都会读取的配置文档。 |
116
116
 
117
117
  ### 主题
118
118
 
@@ -171,7 +171,7 @@ Pi 的 `--no-extensions`,或从你的设置中删除该包。
171
171
 
172
172
  | 指南 | 内容 |
173
173
  |---|---|
174
- | [快速开始](./docs/getting-started.md) | 环境要求、安装选项、验证加载、主题。 |
174
+ | [快速开始](./docs/getting-started.md) | 是否适合你、试用、安装选项、验证加载、首个工作流、移除。 |
175
175
  | [扩展](./docs/extensions.md) | 所有扩展与主题的完整目录。 |
176
176
  | [配置](./docs/configuration.md) | 配置文件解析、按扩展索引、环境变量。 |
177
177
  | [配置参考](./docs/references/bytefly-config.md) | 列出 `bytefly.json` 的每个 section 与键,包括类型、默认值和取值范围。 |
@@ -3,8 +3,8 @@
3
3
  `bytefly` owns the terminal chrome, configuration, and diagnostics surfaces
4
4
  this package adds: one themed startup header whose two-tone nameplate carries
5
5
  the agent and chrome identities, one compact, colorful, width-adaptive two-line
6
- footer, a two-row `You` nameplate that opens each user message, a labeled rule that
7
- marks each completed turn in the transcript, one keyboard-driven screen with two
6
+ footer, a two-row `You` nameplate that opens each user message, a receipt rule that
7
+ records each completed turn in the transcript, one keyboard-driven screen with two
8
8
  sections — Extensions, every resolved Pi extension with its enable checkbox and
9
9
  its health, and Settings, the common app and `bytefly.json` preferences — and
10
10
  an opt-in notify capability that raises an OS notification when a run finishes,
@@ -33,7 +33,7 @@ How the `run_doctor` call and its result look in the transcript — the call lin
33
33
 
34
34
  The extension owns ten surfaces: the startup header, the footer, the
35
35
  user-message marker, the rule between completed turns, the notice block for
36
- shared transcript notices, the tool-block frame, the live working counter, the
36
+ shared transcript notices, the tool-block frame, the live working line, the
37
37
  two-section control center, notifications, and the published-version check.
38
38
 
39
39
  **Header.** A nameplate: a two-tone half-block mark from the active theme's
@@ -59,10 +59,15 @@ that cannot render half blocks the mark falls back to a two-tone `Pi` wordmark.
59
59
 
60
60
  As the terminal narrows, the identity sheds the update marker, then the chrome
61
61
  version, then the chrome line, then the agent name, and the help rows drop whole
62
- keys, so nothing wraps or clips mid-word. The nameplate sits on the transcript margin, which follows Pi's
63
- `outputPad` (the value the tool frame and the notice use too), and the help hangs
64
- one body indent inside it. The mark, tokens, identity, and grouped help are tuned
65
- in [`header/config.ts`](./header/config.ts).
62
+ keys, so nothing wraps or clips mid-word. Height sheds the same way: at 28 rows
63
+ all three help groups print, below 28 the middle group (the expand and model
64
+ chords) goes first, below 22 only the `/` commands row survives, and below 16 the
65
+ help block and the leading blank row go, leaving the nameplate and its rule. An
66
+ unknown height keeps the full header. The nameplate sits on the transcript
67
+ margin, which follows Pi's `outputPad` (the value the tool frame and the notice
68
+ use too), and the help hangs one body indent inside it. The mark, tokens,
69
+ identity, and grouped help are tuned in
70
+ [`header/config.ts`](./header/config.ts).
66
71
 
67
72
  **Footer.** Two lines, each with a left and right zone:
68
73
 
@@ -126,14 +131,26 @@ shows as `¥<0.01` rather than `¥0.00`.
126
131
 
127
132
  DeepSeek needs no config file at all: the bar prices it from the published yuan
128
133
  rates and peak tariff built into [`footer/price/config.ts`](./footer/price/config.ts).
129
- A `bytefly.json` can switch to the published dollar table, add models of your own,
130
- or replace a built-in entry:
134
+ Those tables record the day they were last checked against the published tariff,
135
+ and `doctor` reports how old that is, so a re-pricing shows up as a warning instead
136
+ of a quietly wrong total. A `bytefly.json` can switch to the published dollar table,
137
+ add models of your own, or replace a built-in entry:
131
138
 
132
139
  ```
133
140
  ~/repo/project ⎇ main
134
141
  ▰▰▰▰▰▰▱▱▱▱ 62%/200k · ¥2.50 · ↑42k ↓8.0k · R96k CH 87% opus-4.5 · high
135
142
  ```
136
143
 
144
+ **Session budget.** `price.budget` puts a threshold under that total, in the same
145
+ currency: when the session's cost reaches it, one `bytefly · Session budget
146
+ reached` notice names what was spent against the budget, and one OS notification
147
+ follows when notify is enabled. It is an alert, not a limit — no turn is blocked or
148
+ aborted — and it fires once per session, on the turn where the crossing becomes
149
+ final (the run settling), because a warning per turn after that is noise. `0`, the
150
+ default, turns it off. The check reads the total the footer already priced, and
151
+ the footer exists only in `tui` sessions, so a print or JSON run has no budget
152
+ alert; the footer's own numbers are visible in every interactive session.
153
+
137
154
  **User message.** Every user message opens with a two-row `You` nameplate —
138
155
  the label over a rule that spans the width — above the message:
139
156
 
@@ -156,26 +173,36 @@ as it would without it, and a soft wrap of one long line stays at the margin
156
173
  because the renderer wraps it, not bytefly. The label and the rule are tuned
157
174
  in [`message/config.ts`](./message/config.ts).
158
175
 
159
- **Working timer.** Pi's working row shows a static `Working`, so a long run
176
+ **Working line.** Pi's working row shows a static `Working`, so a long run
160
177
  reads the same as a short one. While the row is up, bytefly advances its
161
- message once a second and hands it back to Pi when the row comes down:
178
+ message once a second and hands it back to Pi when the row comes down. Before
179
+ the first tool starts it is a count:
162
180
 
163
181
  ```
164
182
  Working… 12s
165
183
  ```
166
184
 
167
- The count is tied to the row's visibility: it starts on `turn_start`, the event
168
- the host shows the working indicator on, and stops on `agent_end`, where the
169
- host clears it, then again on `session_shutdown`. Turns inside one run do not
170
- restart it — the host keeps the same row up across turns — so the count covers
171
- the whole run, tool calls included. A retry is a new run, so the count restarts
172
- when its row reappears. The word and the interval are tuned in
173
- [`working/config.ts`](./working/config.ts).
185
+ Once a top-level tool starts, the row becomes a live run line: the tool, how
186
+ many top-level tools this run has started, and the elapsed time.
187
+
188
+ ```
189
+ edit · 3 tools · 12s
190
+ ```
191
+
192
+ A nested tool call (one carrying `parentToolCallId`, such as a codemode script's
193
+ inner call) neither counts nor takes over the line, so the number matches the
194
+ tool blocks in the transcript. The line keeps the last tool name while the
195
+ model streams after it, and a tool event repaints the row immediately instead
196
+ of waiting for the next tick. The line covers the whole run, tool calls
197
+ included. It is tied to the row's visibility: it starts on `turn_start`, stops
198
+ on `agent_end`, and is reset on `session_shutdown`; turns inside one run do not
199
+ restart it, and a retry starts a new run. The words and the interval are tuned
200
+ in [`working/config.ts`](./working/config.ts).
174
201
 
175
202
  **Turn separator.** After each completed turn the extension appends an inert
176
- entry and draws a centered `╌╌╌ turn N ╌╌╌` rule across the transcript; see
177
- [Turn separator](#turn-separator). No config file is needed and there is no
178
- command.
203
+ entry and draws a centered receipt rule — `╌╌╌ turn N · duration · N tools ╌╌╌`
204
+ — across the transcript; see [Turn separator](#turn-separator). No config file is
205
+ needed and there is no command.
179
206
 
180
207
  **Notices.** Any extension in the package can post a transcript notice under the
181
208
  shared `my-pi-agent/notice` type; bytefly draws it at the transcript's margin —
@@ -207,6 +234,15 @@ are tuned in [`notice/config.ts`](./notice/config.ts). Every field is sanitized
207
234
  and clipped at the boundary, and a message without `details` keeps Pi's default
208
235
  custom-message box.
209
236
 
237
+ **Compaction.** `/compact` is Pi's own surface: its summary block is a built-in
238
+ message component, so bytefly cannot replace, hide, or shorten it. The shipped
239
+ themes only remove its fill — `customMessageBg` is the terminal default — and
240
+ paint the `[compaction]` label in the accent, so the block sits on the canvas
241
+ like the rest of the transcript. Pi's branch summaries and skill invocations
242
+ share the same tokens.
243
+
244
+ <img src="../../assets/compaction.png" alt="The /compact summary block: a [compaction] label, the compacted token count, and the summary Markdown on the terminal canvas, with no background band" width="100%">
245
+
210
246
  **Tool blocks.** Every tool block leads with the tool's own call line as the
211
247
  headline — the tool name in the theme's tool-title colour — and its result body
212
248
  sits two columns in directly beneath it, led by the transcript rail, instead of
@@ -336,7 +372,7 @@ identity tokens, tagline, grouped help), the footer in
336
372
  [`footer/config.ts`](./footer/config.ts) (thresholds, gauge glyphs and widths,
337
373
  separators, icons, status keys), and the turn separator in
338
374
  [`separator/config.ts`](./separator/config.ts) (glyph, label prefix, padding,
339
- colors).
375
+ minimum duration, outcome wording, colors).
340
376
 
341
377
  Every key of every section is listed with its type, default, and bounds in the
342
378
  [configuration reference](../../docs/references/bytefly-config.md); this section
@@ -495,32 +531,46 @@ the model, which sits at the end of the line.
495
531
  `agent_settled` fires once after an agent run fully settles — exactly one user
496
532
  turn — which is why it is used instead of `turn_end` (that fires once per
497
533
  assistant message, so tool-calling rounds would each get a line). On each
498
- settled run the extension appends an inert custom entry and a renderer draws
499
- it:
534
+ settled run the extension appends an inert custom entry and a renderer draws it
535
+ as a receipt: the turn number, then the run's duration, top-level tool count,
536
+ and outcome as the width allows.
500
537
 
501
538
  ```
502
- ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ turn 3 ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
539
+ ╌╌╌╌╌╌╌╌╌╌╌╌ turn 3 · 1m12s · 7 tools ╌╌╌╌╌╌╌╌╌╌╌╌
503
540
  ```
504
541
 
505
542
  - The turn number is `count(separators on the current branch) + 1`, so a
506
543
  resumed or forked session continues the parent's numbering and abandoned
507
544
  branches do not leak counts.
545
+ - The receipt opens on the run's first `agent_start` and closes on
546
+ `agent_settled`, so a retry stays inside one duration. It counts only
547
+ top-level tool calls; a nested call (one carrying `parentToolCallId`) is an
548
+ implementation detail of its parent and does not inflate the number.
549
+ - The outcome is part of the receipt: a failed tool adds `✕N`, and an aborted
550
+ run adds `aborted` instead of the failure count. A sub-second run prints no
551
+ duration, so quick turns stay quiet. Entries written before the receipt
552
+ existed have only a turn number and still render as `turn N`.
553
+ - The label sheds fields rather than overflowing: the tool count first, then
554
+ the duration, then the outcome. When even the turn number does not fit, the
555
+ line becomes a plain dashed rule instead of clipping.
556
+ - A clean turn paints the label `meta`, a turn with a failed tool paints it
557
+ `bad`, and an aborted turn paints it `warn`.
508
558
  - The entry is excluded from LLM context, so it costs no tokens, and it
509
559
  survives resume and fork. It is drawn under the `bytefly-separator` custom
510
560
  entry type.
511
- - The line is centered and width-adaptive: the label is filled out with the
512
- glyph, and when it cannot fit the line becomes a plain dashed rule rather
513
- than overflowing.
514
561
  - The rule is installed only in interactive (`tui`) sessions; RPC, JSON, and
515
562
  print sessions append nothing.
516
563
  - Separators recorded by the retired `turn-separator` extension are not
517
564
  rendered, and `PI_DISABLED_EXTENSIONS=turn-separator` no longer affects the
518
565
  rule; disable the whole extension with `bytefly` instead.
519
- - There is no runtime command; the glyph, label prefix, padding, and colors are
520
- tuned in [`separator/config.ts`](./separator/config.ts).
566
+ - There is no runtime command; the glyph, label prefix, padding, colors, and
567
+ minimum duration are tuned in
568
+ [`separator/config.ts`](./separator/config.ts).
521
569
 
522
570
  ## The control center
523
571
 
572
+ <img src="../../assets/bytefly.png" alt="The /bytefly control center: the Extensions and Settings section bar with the `Global · 10 extensions · 2 off · 1 missing` counts, one checkbox row per extension with its verdict, the focused extension's report, and the key hints" width="100%">
573
+
524
574
  `/bytefly [extension] [refresh]` opens the control center on **Extensions**: a
525
575
  keyboard-driven dock list of every extension Pi resolves for the session, so the
526
576
  list matches what actually loads. `←`/`→` switch to **Settings**, where the same
@@ -585,11 +635,11 @@ installs or fetches a package.
585
635
  without leaving the session.
586
636
 
587
637
  **Settings.** The second section uses the same frame and the same `Tab` scope
588
- switch, with its own cursor. It lists fifteen preferences in six groups:
638
+ switch, with its own cursor. It lists twenty-eight preferences in ten groups:
589
639
 
590
640
  ```
591
641
  bytefly ──────────────────────────────────────────────────────────────
592
- Extensions · Settings Global · 15 settings
642
+ Extensions · Settings Global · 28 settings
593
643
 
594
644
  Startup (1)
595
645
  ❯ ◻ Bytefly startup TUI Off
@@ -604,19 +654,36 @@ bytefly ────────────────────────
604
654
  Diagnostics (2)
605
655
  ◼ Proactive health check On
606
656
  Probe timeout 2000
607
- Pricing (2)
657
+ Pricing (3)
608
658
  Currency CNY (¥)
609
659
  Cost decimals 2
610
- Transcript (2)
660
+ Session budget 0
661
+ Transcript (3)
611
662
  Rail glyph ╎
663
+ Achieved goal Hide
612
664
  Output padding 1
613
665
  Terminal (1)
614
666
  ◻ Hardware cursor Off
667
+ Worktree (4)
668
+ ◼ Fetch remote On
669
+ Base ref Fresh
670
+ Branch prefix worktree-
671
+ Prune after (days) 7
672
+ Snapshots (3)
673
+ ◼ Automatic snapshots On
674
+ Snapshots kept 20
675
+ ◼ Safety snapshot On
676
+ Jobs and watchers (3)
677
+ ◻ Wake when a job finishes Off
678
+ ◻ Keep jobs after exit Off
679
+ ◻ Wake on a match Off
680
+ Memory (1)
681
+ ◼ Inject notes On
615
682
  Bytefly startup TUI — Pi setting
616
683
  Use bytefly's startup header in place of Pi's startup TUI.
617
684
  Default · ~/.pi/agent/settings.json
618
685
 
619
- Esc close · ←/→ · Space change · Tab scope · ↑/↓ select
686
+ Esc close · ←/→ · Space change · Tab scope · u reset · J JSON · [ ] group
620
687
  ```
621
688
 
622
689
  | Group | Row | Source | Values |
@@ -633,22 +700,45 @@ bytefly ────────────────────────
633
700
  | Diagnostics | Probe timeout | `doctor.timeoutMs` | Number, 100–30000 ms |
634
701
  | Pricing | Currency | `price.currency.code` | CNY, USD |
635
702
  | Pricing | Cost decimals | `price.currency.decimals` | 0, 1, 2, 3, 4 |
703
+ | Pricing | Session budget | `price.budget` | Number, `0` = off |
636
704
  | Transcript | Rail glyph | `chrome.rail` | Text, exactly one column |
705
+ | Transcript | Achieved goal | `goal.achieved` | Show, Hide |
637
706
  | Transcript | Output padding | Pi `settings.json` | 0, 1 |
638
707
  | Terminal | Hardware cursor | Pi `settings.json` | Off, On |
708
+ | Worktree | Base ref | `worktree.baseRef` | Fresh, Head |
709
+ | Worktree | Branch prefix | `worktree.branchPrefix` | Text, trimmed, required |
710
+ | Worktree | Fetch remote | `worktree.fetchRemote` | Off, On |
711
+ | Worktree | Prune after (days) | `worktree.pruneAfterDays` | Number, 0–3650 |
712
+ | Snapshots | Automatic snapshots | `rewind.autoSnapshots` | Off, On |
713
+ | Snapshots | Snapshots kept | `rewind.max` | Number, 0–1000 |
714
+ | Snapshots | Safety snapshot | `rewind.safetySnapshot` | Off, On |
715
+ | Jobs and watchers | Wake when a job finishes | `job.wakeOnFinish` | Off, On |
716
+ | Jobs and watchers | Keep jobs after exit | `job.detachedByDefault` | Off, On |
717
+ | Jobs and watchers | Wake on a match | `watch.wakeOnMatch` | Off, On |
718
+ | Memory | Inject notes | `memory.inject` | Off, On |
639
719
 
640
720
  Choosing **Currency** also writes that currency's default precision. Both rows
641
721
  stay editable afterwards, so the footer follows the explicit values.
642
722
 
643
723
  `Space` (or `Enter`) changes the focused row: a boolean toggles, an enum cycles
644
724
  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
725
+ title, the probe timeout, the branch prefix, and the rail glyph. `u` deletes the
726
+ focused row's key so its default applies again (`Already at its default.` when
727
+ nothing is stored), `J` swaps the detail pane for the focused row's whole section
728
+ as the active scope stores it — the only view of the keys this screen leaves to
729
+ JSON — and `[` / `]` jump to the previous or next group. The detail pane names
730
+ the source, describes the preference, and shows
646
731
  where the value is written — `Global · ~/.pi/agent/settings.json`, `Project ·
647
732
  .pi/bytefly.json`, or `Inherited from Global · .pi/bytefly.json`. A successful
648
733
  change marks the screen `Saved · reload to apply` — even when the terminal is too
649
734
  short for the detail body — and closing offers the same
650
735
  reload confirm as an extension toggle.
651
736
 
737
+ Six of the ten groups belong to other extensions. The screen cannot import their
738
+ normalizers, so it reports the leaf as the file states it and declares the
739
+ loader's default; `test/settings-coverage.test.ts` holds the catalogue to every
740
+ loader's defaults and bounds in both directions.
741
+
652
742
  **Bytefly startup TUI** is the one row whose label differs from the Pi key it
653
743
  writes: On writes `"quietStartup": true` (bytefly's header replaces Pi's startup
654
744
  detail), and Off writes `false` (Pi's built-in header stays). A hand-set
@@ -657,17 +747,18 @@ with `true` or `false`.
657
747
 
658
748
  App rows are **Global only**: Pi's setters for them write the personal
659
749
  `settings.json`, and only resource paths have public project setters. Project
660
- scope therefore lists the twelve `bytefly.json` rows and a
750
+ scope therefore lists the twenty-five `bytefly.json` rows and a
661
751
  `App settings are Global only.` note rather than promising a write that cannot
662
752
  happen. A `bytefly.json` row writes one leaf in the selected scope (a depth-1
663
753
  key, or a nested one such as `price.currency.decimals`) through a per-key
664
754
  writer, so every sibling key, section, `$schema`, and `version` is preserved. A
665
- malformed document is refused with a notice instead of overwritten.
755
+ malformed document is refused with a notice instead of overwritten, and `u`
756
+ removes that one leaf through the same writer's `unsetConfigValueAt`.
666
757
 
667
758
  The non-`tui` form prints the headline, the groups, and `Label: Value` rows:
668
759
 
669
760
  ```
670
- bytefly: Settings · Global · 15 settings
761
+ bytefly: Settings · Global · 28 settings
671
762
 
672
763
  Startup (1)
673
764
  Bytefly startup TUI: Off
@@ -691,6 +782,12 @@ means reduced capability — an optional package that is absent, a config file
691
782
  that fails to parse, a login that has not happened. Doctor never changes
692
783
  anything and never installs anything.
693
784
 
785
+ It also reports what has **aged**: the built-in DeepSeek rate tables carry the day
786
+ they were last checked against the published tariff (`PRICE_VERIFIED_AT` in
787
+ `footer/price/config.ts`), and the report says how old that is — `ok` inside the
788
+ 90-day window, `degraded` beyond it with the source URL to re-check. A tariff change
789
+ therefore becomes a visible warning instead of a quietly wrong cost total.
790
+
694
791
  `web-access` publishes what it resolved at `session_start`, so the report names
695
792
  the search provider and why it won (an explicit choice, the `auto` rule, or the
696
793
  keyless default), and — when the provider takes a key and no literal `apiKey` is
@@ -946,6 +1043,9 @@ command.
946
1043
  - **Notifications are not focus-aware.** They fire whether or not the terminal
947
1044
  has focus, and once per trigger rather than once per unseen event. There is
948
1045
  no focus API to consult.
1046
+ - **`/compact` is Pi's block.** The summary component is built in, so bytefly
1047
+ cannot render, hide, or shorten it; the themes only choose its fill and label
1048
+ color. Branch summaries and skill invocations share the same theme tokens.
949
1049
  - **A background job cannot ask for input.** The `needsInput` trigger is a
950
1050
  prompt raised during a run, not a process waiting on stdin: jobs are
951
1051
  fire-and-observe, so that state does not exist.
@@ -953,11 +1053,11 @@ command.
953
1053
  host lacks (or an OSC method off a TTY) and the notification is dropped; only
954
1054
  `auto` inspects the environment. `doctor` now reports the mismatch as a
955
1055
  `degraded` finding, which says what to change but does not fix it for you.
956
- - **The working counter is one global row.** `setWorkingMessage` drives the
1056
+ - **The working line is one global row.** `setWorkingMessage` drives the
957
1057
  single working row, so bytefly and any other extension setting it race — last
958
1058
  writer wins — and the message is reset to Pi's default at `agent_end`. If the
959
- row is hidden (`ctx.ui.setWorkingVisible(false)`) the counter has no surface,
960
- and a reload mid-run does not resume it until the next turn.
1059
+ row is hidden (`ctx.ui.setWorkingVisible(false)`) the line has no surface,
1060
+ and a reload mid-run does not resume the count until the next turn.
961
1061
 
962
1062
  ## Non-goals
963
1063
 
@@ -981,7 +1081,7 @@ command.
981
1081
  match — are the whole surface.
982
1082
  - No sound, action buttons, notification history, or de-duplication; a
983
1083
  notification is a fire-and-forget message, not an inbox.
984
- - No frozen per-run working duration: the counter is live and is handed back to
1084
+ - No frozen per-run working duration: the line is live and is handed back to
985
1085
  Pi when the working row comes down, because the extension API exposes no hook
986
1086
  to keep a finished run's total.
987
1087
 
@@ -994,18 +1094,18 @@ rather than showing a wrong value.
994
1094
 
995
1095
  | Integration point | Details |
996
1096
  |---|---|
997
- | Header | `ctx.ui.setHeader` with a custom component, replacing Pi's built-in header; installed only when `ctx.mode === "tui"` and `pi.getSettings().quietStartup === true` (the **Bytefly startup TUI** row). `false`, `"header"`, and unset call `setHeader(undefined)`, leaving Pi's built-in header in place. The nameplate insets by Pi's `outputPad` (read through `pi.getSettings()`), so it sits on the transcript margin, and the help hangs one body indent inside it. The mark and text are drawn from the active theme's tokens, the help keys resolve live through `keyText`, and the mark falls back to a wordmark on Apple Terminal (`isAppleTerminalSession`), mirroring Pi's built-in. The chrome version gains `↑<latest>` from the `UpdateSource`, and `registerHeader` returns a `refresh()` handle that re-installs the last context's header when that value arrives. |
1097
+ | Header | `ctx.ui.setHeader` with a custom component, replacing Pi's built-in header; installed only when `ctx.mode === "tui"` and `pi.getSettings().quietStartup === true` (the **Bytefly startup TUI** row). `false`, `"header"`, and unset call `setHeader(undefined)`, leaving Pi's built-in header in place. The nameplate insets by Pi's `outputPad` (read through `pi.getSettings()`), so it sits on the transcript margin, and the help hangs one body indent inside it. The mark and text are drawn from the active theme's tokens, the help keys resolve live through `keyText`, and the mark falls back to a wordmark on Apple Terminal (`isAppleTerminalSession`), mirroring Pi's built-in. The factory reads `tui.terminal.rows` on every render, so the height ladder in `header/config.ts` sheds help groups and then the leading blank row as the terminal shortens (unknown height keeps the full header). The chrome version gains `↑<latest>` from the `UpdateSource`, and `registerHeader` returns a `refresh()` handle that re-installs the last context's header when that value arrives. |
998
1098
  | Footer | `ctx.ui.setFooter` with a custom component, replacing Pi's built-in footer; installed only when `ctx.mode === "tui"`. |
999
1099
  | User message | `pi.registerMarkdownTransformer` puts a two-row `You` nameplate (the label over a full-width rule) above each user message; one padded level-2 heading carries the accent through `mdHeading` for both rows, and the shipped themes set `userMessageBg` to the terminal default, so the block has no fill. Pi renders the result as Markdown, so the surface adds no component or color of its own. |
1000
1100
  | Notice | `registerMessageRenderer(NOTICE_TYPE)` draws every shared transcript notice at the margin from its `details`; the headline stays at the margin and its rows hang from the transcript rail, painted by the notice's worst severity, whose glyph comes from the shared `RailSource`. `job` and `watch` post one when their work settles, and `doctor` posts one at `session_start` for a missing hard requirement. The type and shape come from `lib/notice.ts`. A message without `details` falls back to Pi's default custom-message box. |
1001
1101
  | Tool blocks | `registerToolRenderer` returns, for every tool, `renderShell: "self"`; `renderCall` leaves the tool's own first line as the block's headline (a shell prompt shown as the tool name, its command repainted in the body colour, and a primary subject demoted to the body colour) and indents the rest of the call two columns under it, `renderResult` indents its body the same and leads it with the status-coloured rail, and each wraps the tool's own renderer (or a generic fallback). `edit` and `write` carry their result on the call body, so that body is railed too. |
1002
1102
  | Screen command | `pi.registerCommand("bytefly")` takes `[doctor] [settings] [extension] [refresh]`: `tui` mounts a dock screen via `ctx.ui.custom()` with the rails suppressed; other modes print through `ctx.ui.notify()` — the grouped listing, `run_doctor`'s report for `doctor`, or the grouped settings listing with values for `settings`. A named extension is checked against the discovered set before any probe. Tab completes the subcommands, extension names after `doctor`, and `refresh`; `/bytefly settings` rejects a name. |
1003
- | Turn separator | `registerEntryRenderer("bytefly-separator")` draws each rule; `agent_settled` appends `pi.appendEntry("bytefly-separator", { turn })` in `tui` mode only. |
1004
- | Working timer | `turn_start` starts a 1s tick that drives `ctx.ui.setWorkingMessage("Working… Ns")` in `tui` mode; `agent_end` restores Pi's default and cancels the tick (also on `session_shutdown`). No config file and no stored state. |
1103
+ | Turn separator | `registerEntryRenderer("bytefly-separator")` draws each stored receipt; `agent_start` opens it, top-level `tool_execution_start`/`tool_execution_end` count tools and failures, and `agent_settled` appends `pi.appendEntry("bytefly-separator", { turn, durationMs, tools, failures, aborted })` in `tui` mode only. The receipt fields are omitted when no `agent_start` was seen, so an older or partially observed entry stays `{ turn }`. |
1104
+ | Working line | `turn_start` starts a 1s tick that drives `ctx.ui.setWorkingMessage` in `tui` mode; before a tool runs the message is `Working… Ns`, and each top-level `tool_execution_start` repaints it as `tool · N tools · Ns` (nested calls carrying `parentToolCallId` are skipped). `agent_end` restores Pi's default and cancels the tick (also on `session_shutdown`). No config file and no persisted state; the count lives in the register's closure. |
1005
1105
  | Notifications | `agent_settled` (skipped when aborted), `ui_prompt_start` only while `agent_start`…`agent_settled` marks a run active, and `my-pi-agent/job-finished` on `pi.events`; the `notify` section of `bytefly.json` gates each. Delivery writes an OSC 777/99 sequence to stdout or spawns `osascript`, `notify-send`, or `powershell.exe`. The bus subscription is created in `session_start` and released in `session_shutdown`, and the `job` extension never imports bytefly. |
1006
1106
  | Picker discovery | `SettingsManager.create(...)` plus `DefaultPackageManager.resolve({ onMissing: "skip" })`, both public exports; both views drop `bytefly`'s own entrypoint. |
1007
1107
  | Picker writes | `SettingsManager` setters (`setExtensionPaths`, `setPackages`, `setProjectExtensionPaths`, `setProjectPackages`) then `flush()`; `ctx.reload()` after the screen closes when changes are pending. |
1008
- | Settings writes | App rows call `SettingsManager` setters (`setQuietStartup`, `setOutputPad`, `setShowHardwareCursor`) on the global store and `flush()`, then surface `drainErrors()`; `bytefly.json` rows call `writeConfigValueAt` on the selected scope's document with the row's leaf path, so a nested key (`price.currency.decimals`) and a flat one (`notify.method`) share one writer. A failed or refused write shows a notice in the screen's hint row and leaves the displayed value unchanged; a successful one marks the screen dirty. |
1108
+ | Settings writes | App rows call `SettingsManager` setters (`setQuietStartup`, `setOutputPad`, `setShowHardwareCursor`) on the global store and `flush()`, then surface `drainErrors()`; `bytefly.json` rows call `writeConfigValueAt` on the selected scope's document with the row's leaf path, so a nested key (`price.currency.decimals`) and a flat one (`notify.method`) share one writer, and `u` calls `unsetConfigValueAt` with the same path. A failed or refused write shows a notice in the screen's hint row and leaves the displayed value unchanged; a successful one marks the screen dirty. |
1009
1109
  | 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. |
1010
1110
  | 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. |
1011
1111
  | 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. |
@@ -1098,8 +1198,10 @@ rather than showing a wrong value.
1098
1198
  and a grouped help block. It takes its margin from Pi's `outputPad`, like the
1099
1199
  tool frame and the notice, and hangs the help one body indent inside it, so the
1100
1200
  header follows a non-default pad instead of assuming a column. It adds no
1101
- session or model data — that is the footer's job — and it stays a fixed
1102
- four-plus rows so the startup transcript never reflows.
1201
+ session or model data — that is the footer's job — and its shape follows the
1202
+ terminal: the height ladder drops whole help groups, then the leading blank
1203
+ row, before the nameplate gives up anything, so a short pane spends its rows
1204
+ on the transcript instead of chrome.
1103
1205
  - **Two lines, four zones.** Line 1 is identity (`pwd` left, branch + worktree
1104
1206
  right); line 2 is resources (meters left, model + level right). This keeps
1105
1207
  "where am I / what state" separate from "how much am I burning".
@@ -1254,7 +1356,7 @@ rather than showing a wrong value.
1254
1356
  The surfaces are folders: `header/`, `footer/` (the status bar and its price
1255
1357
  calculation), `message/` (the user-message marker), `notice/` (the shared notice),
1256
1358
  `block/` (the tool-block frame), `separator/`, `working/` (the live
1257
- elapsed counter on Pi's working row), `panel/` (the
1359
+ run line on Pi's working row), `panel/` (the
1258
1360
  unified screen and its merged model),
1259
1361
  `picker/` (discovery and the settings store), `doctor/` (the `run_doctor` tool
1260
1362
  and its probe runtime), `notify/`, and `update/` (the published-version check
@@ -1296,14 +1398,14 @@ columns, gauge), so the look follows the active theme.
1296
1398
  | `block/component.ts` | Pure `toolBlockLines`, `headlineWithToolName`, `recolorShellCall`, `recolorCallHeadline`, and `ToolBlock`: the headline, the body's two-column indent (optionally rail-led), the reduced-width inner render, the leading-blank trim, the shell-prompt rewrite (the name keeps the accent, the command reopens in `label`), the subject demotion (a path, URL, query, or pattern reads in `label` so the name is the line's only accent, including the tail a bare `edit <path>` headline wraps onto the body), and the image-line passthrough. |
1297
1399
  | `block/fallback.ts` | Generic `callFallback` / `resultFallback` for a tool with no renderer: the subject in the body colour, its options faint, and a capped result preview. |
1298
1400
  | `block/register.ts` | The `registerToolRenderer` resolver that frames every tool; it recolours a shell call's string before the frame wraps it, rails the result body by status, and rails the call body for `edit`/`write` (the two whose call body carries the result). |
1299
- | `separator/config.ts` | Turn-rule tuning: `customType`, glyph, label prefix, padding, and roles. |
1300
- | `separator/register.ts` | Registers the entry renderer and appends a separator on `agent_settled`. |
1401
+ | `separator/config.ts` | Turn-rule tuning: `customType`, glyph, label prefix, padding, receipt minimum duration and outcome wording, and roles. |
1402
+ | `separator/register.ts` | Registers the entry renderer, tracks the run's tools and failures, and appends the receipt on `agent_settled`. |
1301
1403
  | `separator/component.ts` | `SeparatorComponent`: the stateless entry renderer. |
1302
- | `separator/format.ts` | Pure `separatorLine`: the centered turn rule, drawn by the kit's `centeredRule`. |
1303
- | `separator/types.ts` | `TurnSeparatorData`. |
1304
- | `working/config.ts` | Counter tuning (`WORKING_CONFIG`): the word, the ellipsis, and the tick interval. |
1305
- | `working/format.ts` | Pure `formatElapsed` (whole seconds) and the working-message shape. |
1306
- | `working/register.ts` | Starts the tick on `turn_start` and cancels it on `agent_end`/`session_shutdown`. |
1404
+ | `separator/format.ts` | Pure `receiptLabel` (the width ladder), `receiptRole`, and `separatorLine`, drawn by the kit's `centeredRule`. |
1405
+ | `separator/types.ts` | `TurnSeparatorData`: the turn plus the optional receipt fields. |
1406
+ | `working/config.ts` | Counter tuning (`WORKING_CONFIG`): the words, the tool singular/plural, and the tick interval. |
1407
+ | `working/format.ts` | Pure `formatElapsed` (whole seconds) and the working-message shape: the plain count and the `tool · N tools · elapsed` run line. |
1408
+ | `working/register.ts` | Starts the tick on `turn_start`, tracks top-level tools on `tool_execution_start`, and cancels on `agent_end`/`session_shutdown`. |
1307
1409
  | `panel/types.ts` | `PanelSection` (`extensions` | `settings`), `PanelRow`, and `PanelModel`, the merged row model. |
1308
1410
  | `panel/join.ts` | The item/report union with the unambiguous-name health rule and unique row keys, groups, and ordering. |
1309
1411
  | `panel/format.ts` | Non-TUI text: the extension headline and grouped listing with a per-row health suffix, plus the section segments, the Settings counts/headline, and the grouped Settings listing. |
@@ -1355,10 +1457,10 @@ folding across a width sweep from 6 to 120 plus wide characters
1355
1457
  (`footer/render/layout`), exact
1356
1458
  golden output at fixed widths (`footer/render/render`), and the install/skip/
1357
1459
  restore/toggle lifecycle plus a config-driven price (`extension`). The turn
1358
- separator
1359
- is covered by its pure formatting (`separator/format`) and its
1360
- register/append/render/disable lifecycle (`separator/register`). The picker
1361
- helpers are covered by the pure helpers (`picker/resources`), the toggle
1460
+ separator is covered by its pure formatting (`separator/format`, including the
1461
+ receipt's width ladder), its register/append/render/disable lifecycle
1462
+ (`separator/register`), and the run stats it records. The picker helpers are
1463
+ covered by the pure helpers (`picker/resources`), the toggle
1362
1464
  transforms (`picker/toggle`), discovery with an injected factory
1363
1465
  (`picker/discovery`), and the runtime state machine (`picker/runtime`). The
1364
1466
  unified screen is covered by its pure model merge (`panel/join`), its text
@@ -14,6 +14,9 @@ import { clampInteger, loadConfigSection } from "../../../lib/config.ts";
14
14
  /** How long a single probe may take before it counts as unusable. */
15
15
  const DEFAULT_TIMEOUT_MS = 2_000;
16
16
 
17
+ /** The probe deadline range, in milliseconds; the panel's editor offers the same bounds. */
18
+ export const TIMEOUT_MS_RANGE = { min: 100, max: 30_000 } as const;
19
+
17
20
  export interface DoctorConfig {
18
21
  /** Probe enabled extensions at session start and warn when one is broken. */
19
22
  proactive: boolean;
@@ -34,7 +37,7 @@ export function normalizeConfig(raw: Record<string, unknown> | undefined, base:
34
37
  if (!raw) return base;
35
38
  return {
36
39
  proactive: typeof raw.proactive === "boolean" ? raw.proactive : base.proactive,
37
- timeoutMs: clampInteger(raw.timeoutMs, base.timeoutMs, 100, 30_000),
40
+ timeoutMs: clampInteger(raw.timeoutMs, base.timeoutMs, TIMEOUT_MS_RANGE.min, TIMEOUT_MS_RANGE.max),
38
41
  skip: Array.isArray(raw.skip)
39
42
  ? raw.skip.filter((name): name is string => typeof name === "string" && name.trim().length > 0)
40
43
  : base.skip,
@@ -10,6 +10,7 @@
10
10
 
11
11
  import { CONFIG_DIR_NAME, getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
12
12
  import type { WebSearchProviderEvent } from "../../../lib/events.ts";
13
+ import { PRICE_VERIFIED_AT } from "../footer/price/config.ts";
13
14
  import { readJsonResult } from "../../../lib/config.ts";
14
15
  import { isExtensionEnabled } from "../../../lib/env.ts";
15
16
  import {
@@ -96,6 +97,7 @@ export function createDoctorRuntime(
96
97
  searchProvider: () => searchProvider,
97
98
  reach: probeReach,
98
99
  refresh,
100
+ priceVerifiedAt: () => PRICE_VERIFIED_AT,
99
101
  ...deps.seams,
100
102
  });
101
103