@gavin-hu/my-pi-agent 0.7.0 → 0.8.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 (34) hide show
  1. package/README.md +10 -10
  2. package/README.zh-CN.md +153 -0
  3. package/extensions/bytefly/README.md +129 -122
  4. package/extensions/bytefly/doctor/format.ts +3 -22
  5. package/extensions/bytefly/doctor/register.ts +10 -8
  6. package/extensions/bytefly/footer/register.ts +17 -91
  7. package/extensions/bytefly/header/component.ts +8 -4
  8. package/extensions/bytefly/header/config.ts +11 -6
  9. package/extensions/bytefly/header/register.ts +7 -27
  10. package/extensions/bytefly/index.ts +15 -14
  11. package/extensions/bytefly/panel/commands.ts +147 -0
  12. package/extensions/bytefly/panel/format.ts +135 -0
  13. package/extensions/bytefly/panel/join.ts +103 -0
  14. package/extensions/bytefly/panel/tui.ts +416 -0
  15. package/extensions/bytefly/panel/types.ts +44 -0
  16. package/extensions/bytefly/picker/resources.ts +0 -11
  17. package/extensions/file-browse/README.md +20 -7
  18. package/extensions/file-browse/html.ts +29 -6
  19. package/extensions/file-browse/markdown.ts +288 -46
  20. package/extensions/plan/README.md +16 -12
  21. package/extensions/plan/plans.ts +16 -17
  22. package/extensions/plan/runtime.ts +1 -1
  23. package/extensions/plan/steps.ts +31 -19
  24. package/extensions/plan/tools.ts +62 -32
  25. package/extensions/plan/types.ts +6 -6
  26. package/lib/README.md +1 -1
  27. package/lib/requirements.ts +3 -3
  28. package/lib/tui.ts +7 -8
  29. package/lib/web/components.ts +11 -0
  30. package/package.json +2 -2
  31. package/extensions/bytefly/doctor/commands.ts +0 -68
  32. package/extensions/bytefly/doctor/tui.ts +0 -140
  33. package/extensions/bytefly/picker/commands.ts +0 -73
  34. package/extensions/bytefly/picker/tui.ts +0 -260
@@ -1,16 +1,15 @@
1
- # bytefly — the TUI header, footer, turn rule, extensions picker, doctor, and notifications
1
+ # bytefly — the TUI header, footer, turn rule, extension screen, and notifications
2
2
 
3
3
  `bytefly` owns the terminal chrome, configuration, and diagnostics surfaces
4
- this package adds: one themed startup header that recolors Pi's brand mark and
5
- version, one compact, colorful, width-adaptive two-line footer, a labeled rule
6
- that marks each completed turn in the transcript, the `/extensions` picker that
7
- lists and toggles every resolved Pi extension, the `/doctor` surface that
8
- reports whether each extension has what it needs, and an opt-in notify
9
- capability that raises an OS notification when a run finishes, a job finishes,
10
- or the agent asks for input. DeepSeek is priced from the provider's published
11
- yuan rates and peak tariff by default, and any other model can be priced from
12
- your own rates when Pi's catalog does not match what a provider actually
13
- charges.
4
+ this package adds: one themed startup header whose two-tone nameplate carries
5
+ the agent and chrome identities, one compact, colorful, width-adaptive two-line
6
+ footer, a labeled rule that marks each completed turn in the transcript, one
7
+ keyboard-driven screen that lists every resolved Pi extension with its enable
8
+ checkbox and its health, and an opt-in notify capability that raises an OS
9
+ notification when a run finishes, a job finishes, or the agent asks for input.
10
+ DeepSeek is priced from the provider's published yuan rates and peak tariff by
11
+ default, and any other model can be priced from your own rates when Pi's catalog
12
+ does not match what a provider actually charges.
14
13
 
15
14
  ```
16
15
  pi --extension ./extensions/bytefly # load just this extension
@@ -21,7 +20,7 @@ pi install ./ # install the package
21
20
  ## What it does
22
21
 
23
22
  The extension owns five surfaces: the startup header, the footer, the rule
24
- between completed turns, the `/extensions` picker, and the `/doctor` report.
23
+ between completed turns, the extension screen, and notifications.
25
24
 
26
25
  **Header.** A nameplate: a two-tone half-block mark from the active theme's
27
26
  `accent` / `warning` tokens, the agent identity (`Pi` and its version), the
@@ -34,11 +33,11 @@ two-tone `Pi` wordmark.
34
33
 
35
34
  ```
36
35
  █▀█ Pi v1.2.0
37
- █▀ █ enhanced by bytefly v0.5.1
36
+ █▀ █ enhanced by bytefly v0.7.0
38
37
  ─────────────────────────────────────────────────
39
- escape to interrupt · ctrl+c to clear · ctrl+d to exit
40
- ctrl+o more · ctrl+t to think · ctrl+l models · ctrl+g editor
41
- / commands · ! bash · /bytefly chrome · /doctor checks
38
+ escape to interrupt · ctrl+c to clear · ctrl+d to exit (empty)
39
+ ctrl+o to expand tools · ctrl+t to expand thinking · ctrl+l to select model
40
+ / commands · ! bash · !! bash (no context) · /bytefly extensions
42
41
  ```
43
42
 
44
43
  As the terminal narrows, the identity sheds the chrome version, then the chrome
@@ -113,17 +112,14 @@ entry and draws a centered `╌╌╌ turn N ╌╌╌` rule across the transcri
113
112
  [Turn separator](#turn-separator). No config file is needed and there is no
114
113
  command.
115
114
 
116
- **Extensions picker.** `/extensions` opens a keyboard-driven dock list of every
117
- Pi extension the session resolves — across personal settings and, when trusted,
118
- the project — and writes a `+pattern` / `-pattern` toggle into the matching
119
- `settings.json`; see [Extensions picker](#extensions-picker). There is no
120
- config file, and changes apply on reload.
121
-
122
- **Doctor.** `/doctor` reports whether each extension has what it needs at
123
- runtime — a binary on `PATH`, an optional package, a login/state file, or a
124
- config file that fails to parse — and `run_doctor` returns the same report as
125
- text and typed data. Only a missing *hard* requirement warns at session start;
126
- everything else waits in the report. See [Doctor](#doctor).
115
+ **Extension screen.** `/bytefly [extension] [refresh]` opens a keyboard-driven
116
+ dock list of every Pi extension the session resolves — across personal settings
117
+ and, when trusted, the project — one row per extension with its enable checkbox
118
+ and its worst-severity health, and writes a `+pattern` / `-pattern` toggle into
119
+ the matching `settings.json`. The focused extension's findings sit underneath,
120
+ and `run_doctor` returns the same report as text and typed data. Only a missing
121
+ *hard* requirement warns at session start; everything else waits in the report.
122
+ See [The bytefly screen](#the-bytefly-screen).
127
123
 
128
124
  **Notify.** Off by default, one `notify` section in `bytefly.json` turns on OS
129
125
  notifications for unattended runs: when a full agent run settles, when a
@@ -136,26 +132,23 @@ the platform notifier (`osascript`, `notify-send`); see
136
132
 
137
133
  | Command | What it does |
138
134
  |---|---|
139
- | `/bytefly` | Toggle the bytefly chrome — header and footer — on or off. |
140
- | `/bytefly header` | Toggle the header alone. |
141
- | `/bytefly footer` | Toggle the footer alone. |
142
- | `/bytefly status` | Report whether the header and footer are on. |
143
- | `/extensions` | Open the extensions picker in the TUI, or print the grouped list elsewhere. |
144
- | `/doctor [extension] [refresh]` | Probe the extensions and show the report; in the TUI this opens a docked list screen. |
135
+ | `/bytefly [extension] [refresh]` | Open the extension screen. In `tui` it mounts a docked list; every other mode prints the grouped listing, or one extension's full findings when named. |
136
+ | `/extensions` | Alias for `/bytefly`: the same screen and the same handler. |
137
+ | `/doctor [extension] [refresh]` | Alias for `/bytefly`: the same screen and the same handler. |
145
138
 
146
- The `status` subcommand completes with Tab.
139
+ Tab completes extension names and `refresh` for all three commands.
147
140
 
148
141
  ## Behaviour by mode
149
142
 
150
143
  The header, footer, and turn rule are installed only in interactive (`tui`)
151
- sessions; `/bytefly` is a no-op elsewhere. The picker is available in every
152
- mode: in `tui` it opens the dock screen, while in RPC, JSON, and print modes
153
- `/extensions` prints the same grouped list and does not toggle. `/doctor` probes
154
- in every mode: in `tui` it opens its list screen, while RPC, JSON, and print
155
- modes print the same report, and the proactive session-start warning is skipped
156
- when there is no UI. Notify runs in every mode and picks its method from the
157
- environment; the OSC methods need a TTY, so a print/JSON/RPC run gets an
158
- OS-level notification or none.
144
+ sessions and are always on; every mode loads the extension, and non-TUI modes
145
+ change nothing. The screen is available in every mode: in `tui`, `/bytefly` and
146
+ its `/extensions` and `/doctor` aliases mount the docked screen, while RPC,
147
+ JSON, and print modes print the grouped listing with a per-row health suffix and
148
+ the headline, or that extension's full findings when one is named. The
149
+ proactive session-start warning is skipped when there is no UI. Notify runs in
150
+ every mode and picks its method from the environment; the OSC methods need a
151
+ TTY, so a print/JSON/RPC run gets an OS-level notification or none.
159
152
 
160
153
  ## Configuration
161
154
 
@@ -226,8 +219,9 @@ Rates merge key by key across the two documents, so a project document can
226
219
  override one model without restating the rest; a key present in both is replaced,
227
220
  not merged field by field. No windows means every moment is peak, so a rate with
228
221
  `offPeakMultiplier: 0.5` and no schedule stays flat. The `price` section is read
229
- when the footer is installed, so a new session — or `/bytefly` toggled off and on
230
- — picks up an edit.
222
+ once per install — at `session_start`, and again on a `session_tree` change — so
223
+ a new session or tree change picks up an edit; there is no toggle, and nothing
224
+ watches the file.
231
225
 
232
226
  **Mixing currencies is the sharp edge of the built-in default.** A record priced
233
227
  from `rates` or the built-in table is in `currency.symbol` units — yuan for
@@ -301,8 +295,8 @@ new session or `/reload` picks up an edit.
301
295
  - Project settings are only read or written when the project is trusted
302
296
  (`ctx.isProjectTrusted()`); an untrusted session offers Global scope only.
303
297
  - Discovery never installs or fetches a package: missing sources are skipped.
304
- - The picker registers no model-facing tool, so the model cannot change resource
305
- configuration.
298
+ - The extension screen registers no model-facing tool; toggling writes only
299
+ through Pi's `settings.json`, and `run_doctor` is read-only.
306
300
 
307
301
  ## Cost calculation
308
302
 
@@ -394,70 +388,69 @@ it:
394
388
  - There is no runtime command; the glyph, label prefix, padding, and colors are
395
389
  tuned in [`separator/config.ts`](./separator/config.ts).
396
390
 
397
- ## Extensions picker
391
+ ## The bytefly screen
392
+
393
+ `/bytefly [extension] [refresh]` is a keyboard-driven dock list of every
394
+ extension Pi resolves for the session, so the list matches what actually loads.
395
+ It merges the former extensions picker and doctor report into one row per
396
+ extension: the picker's enable checkbox and label, the doctor's worst-severity
397
+ glyph and summary. `/extensions` and `/doctor [extension] [refresh]` are
398
+ registered against the same handler, so all three names open the same screen;
399
+ `[extension]` narrows it to one extension and `refresh` re-probes the doctor
400
+ before it opens. Tab completes extension names and `refresh`.
398
401
 
399
- `/extensions` is a keyboard-driven dock list of every extension Pi resolves for
400
- the session, so the list matches what actually loads. It reuses Pi's own settings
401
- and package resolution (`SettingsManager` plus `DefaultPackageManager` with
402
- `onMissing: "skip"`), so opening the screen never installs or fetches a package.
402
+ The screen reuses Pi's own settings and package resolution (`SettingsManager`
403
+ plus `DefaultPackageManager` with `onMissing: "skip"`), so opening it never
404
+ installs or fetches a package.
403
405
 
404
406
  - Lists every resolved extension, grouped by source (each package, user
405
407
  settings, project settings), sorted packages-first and then by display name.
406
- - Omits `bytefly` itself, so the tool that manages the list is never a row.
407
- Turn the chrome off with `/bytefly`; use `pi config` to disable bytefly
408
- entirely.
408
+ - Omits `bytefly`'s own entrypoint from the picker groups, so the tool that
409
+ manages the list is never a selectable row; its doctor report still appears in
410
+ the trailing `Not resolved` group.
409
411
  - Shows whether each one is enabled (`[x]`) or disabled (`[ ]`).
410
- - Toggles the focused extension with `Space` / `Enter`, writing `+pattern` /
411
- `-pattern` into `extensions` (and package filter objects) in the active
412
- scope.
413
- - Switches the write scope with `Tab`: **Global** (personal
414
- `settings.json`) or **Project** (`.pi/settings.json`, only when the project
415
- is trusted).
412
+ - Shows the worst severity for a reported extension (`✓` ok, `!` degraded, `✕`
413
+ missing, `∅` disabled) and its summary; an extension with no doctor report
414
+ leaves the glyph column and the summary blank rather than claiming `ok`.
415
+ - Toggles the focused extension with `Space`, writing `+pattern` / `-pattern`
416
+ into `extensions` (and package filter objects) in the active scope.
417
+ - Switches the write scope with `Tab`: **Global** (personal `settings.json`) or
418
+ **Project** (`.pi/settings.json`, only when the project is trusted; an
419
+ untrusted project refuses with a notice).
420
+ - Expands or collapses the focused extension's findings with `Enter`, and
421
+ re-probes with `r`. While a re-probe is in flight the hint shows `refreshing…`
422
+ and a second `r` is ignored, so probes cannot stack.
416
423
  - Offers a reload after pending changes, so the new selection takes effect
417
424
  without leaving the session.
418
425
 
419
- ### Keys
420
-
421
- | Key | Effect |
422
- |---|---|
423
- | `Space` / `Enter` | Toggle the focused extension. |
424
- | `Tab` | Switch between Global and Project scope. |
425
- | `↑`/`↓`, `j`/`k` | Move the selection. |
426
- | `PgUp`/`PgDn` | Move a page. |
427
- | `g` / `G`, `Home` / `End` | Jump to the first or last extension. |
428
- | `Esc` / `Ctrl+C` | Close the screen. |
429
-
430
- Mouse wheel scrolling works; every action also has a keyboard path.
431
-
432
- ## Doctor
433
-
434
- `/doctor` probes every declared extension and reports each check as `ok`,
426
+ The doctor side probes every declared extension and reports each check as `ok`,
435
427
  `missing`, `degraded`, or `info`; `run_doctor` returns the same report as text
436
428
  plus a typed `structuredContent` report. Only `missing` and `degraded` are
437
429
  problems: `missing` means an enabled extension cannot do its job, `degraded`
438
430
  means reduced capability — an optional package that is absent, a config file
439
- that fails to parse, a login that has not happened. `doctor` never changes
431
+ that fails to parse, a login that has not happened. Doctor never changes
440
432
  anything and never installs anything.
441
433
 
442
- `/doctor <extension>` narrows the report to one extension — including the
443
- checks that passed — and `/doctor refresh` ignores the findings cached earlier
444
- in the session. In the TUI the report is a docked list screen: one row per
445
- extension with a severity glyph, the focused extension's findings underneath.
446
- `Enter` switches between problems only and every check, `r` re-probes, `Esc`
447
- closes; while a re-probe is in flight the hint shows `refreshing…` and a second
448
- `r` is ignored, so probes cannot stack. Every other mode prints the same report
449
- as text.
434
+ `/bytefly <extension>` narrows the same screen to that extension, including the
435
+ checks that passed, and `/bytefly refresh` ignores the findings cached earlier
436
+ in the session. Every other mode prints the grouped listing with a health suffix
437
+ on each row and the headline; a named extension prints its full findings
438
+ instead.
450
439
 
451
440
  ### Keys
452
441
 
453
442
  | Key | Effect |
454
443
  |---|---|
444
+ | `Space` | Toggle the focused extension. |
445
+ | `Enter` | Expand or collapse the focused extension's findings. |
446
+ | `Tab` | Switch between Global and Project scope. |
447
+ | `r` | Re-probe the doctor. |
455
448
  | `↑`/`↓`, `j`/`k` | Move the selection. |
456
449
  | `PgUp`/`PgDn` | Move a page. |
457
450
  | `g` / `G`, `Home` / `End` | Jump to the first or last extension. |
458
- | `Enter` | Toggle between problems only and every check. |
459
- | `r` | Re-probe. |
460
- | `Esc` | Close the screen. |
451
+ | `Esc` / `Ctrl+C` | Close the screen. |
452
+
453
+ Mouse wheel scrolling works; every action also has a keyboard path.
461
454
 
462
455
  ### What is checked
463
456
 
@@ -474,7 +467,7 @@ as text.
474
467
  | wechat | — | `wechat/credentials.json` (not logged in) |
475
468
  | job | — | Windows has no `EXIT` trap; `ps` is POSIX-only |
476
469
  | todo, goal | — | their config files |
477
- | bytefly | — | TUI only for the header, footer, and turn rule; the picker's project scope needs a trusted project; `bytefly.json` |
470
+ | bytefly | — | TUI only for the header, footer, and turn rule; the screen's project scope needs a trusted project; `bytefly.json` |
478
471
  | question | — | needs an interactive UI |
479
472
 
480
473
  Every extension that reads a config file also gets the malformed-JSON check:
@@ -579,13 +572,14 @@ control-free line before it reaches an escape sequence or a shell argument.
579
572
 
580
573
  - No per-segment CLI: the bar's look lives in `footer/config.ts`, the price in
581
574
  `bytefly.json`.
582
- - No per-model cost breakdown, no `/bytefly price`, and no on-screen marker
583
- for which tariff or which source produced a number.
575
+ - No per-model cost breakdown and no on-screen marker for which tariff or
576
+ which source produced a number.
584
577
  - No todo-count segment; the [todo](../todo/) widget already shows progress.
585
578
  - No shelling out for git ahead/behind; the footer data provider exposes only
586
579
  the branch.
587
- - No session or model data in the header: it shows Pi's brand mark and version
588
- only. The footer owns the session, model, and working-directory data.
580
+ - No session or model data in the header: it shows the nameplate and the grouped
581
+ help block only. The footer owns the session, model, and working-directory
582
+ data.
589
583
  - No auto-fix: `doctor` reports the install command; it never runs it.
590
584
  - No aggregation of runtime failures: a tool that errored earlier in the session
591
585
  is not a prerequisite problem, and `doctor` does not pretend to know about it.
@@ -607,20 +601,18 @@ rather than showing a wrong value.
607
601
  | Integration point | Details |
608
602
  |---|---|
609
603
  | Header | `ctx.ui.setHeader` with a custom component, replacing Pi's built-in header; installed only when `ctx.mode === "tui"`. 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. |
610
- | Footer | `ctx.ui.setFooter` with a custom component, replacing Pi's built-in footer; installed only when `ctx.mode === "tui"` and the footer is on. |
611
- | Command | `/bytefly [header|footer|status]` toggles the header and footer together (no argument) or one surface, and reports their state (tui only; a no-op elsewhere), with Tab completion. |
604
+ | Footer | `ctx.ui.setFooter` with a custom component, replacing Pi's built-in footer; installed only when `ctx.mode === "tui"`. |
605
+ | Screen command | `pi.registerCommand("bytefly"`, `"extensions"`, and `"doctor"`) share one handler: `tui` mounts a dock screen via `ctx.ui.custom()` with the rails suppressed, other modes print through `ctx.ui.notify()`. Tab completes extension names and `refresh`. |
612
606
  | Turn separator | `registerEntryRenderer("bytefly-separator")` draws each rule; `agent_settled` appends `pi.appendEntry("bytefly-separator", { turn })` in `tui` mode only. |
613
607
  | 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. |
614
- | Picker command | `pi.registerCommand("extensions")`; `tui` opens a dock screen via `ctx.ui.custom()` with the rails suppressed, other modes print through `ctx.ui.notify()`. |
615
608
  | Picker discovery | `SettingsManager.create(...)` plus `DefaultPackageManager.resolve({ onMissing: "skip" })`, both public exports; both views drop `bytefly`'s own entrypoint. |
616
609
  | Picker writes | `SettingsManager` setters (`setExtensionPaths`, `setPackages`, `setProjectExtensionPaths`, `setProjectPackages`) then `flush()`; `ctx.reload()` after the screen closes when changes are pending. |
617
610
  | Doctor tool | `pi.registerTool("run_doctor")`, read-only (`readOnlyHint: true`), `exposure: "direct"`, active by default, with a typed `outputSchema` mirroring `structuredContent`. Its `readOnlyHint` is what the shared default-deny policy in `lib/policy.ts` reads to allow it in plan mode. |
618
- | Doctor command | `pi.registerCommand("doctor")`; `tui` opens a dock screen via `ctx.ui.custom()` with the rails suppressed, other modes print through `ctx.ui.notify()`. |
619
- | Doctor probes | `run_doctor` and `/doctor` share the session cache; `/doctor 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. |
611
+ | Doctor probes | `run_doctor` and the screen share the session cache created by `registerDoctor`; `/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. |
620
612
  | 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. |
621
- | Config | `bytefly.json` from the agent directory and `<cwd>/.pi`; the `price` section is read when the footer is installed (a new session, or `/bytefly` toggled back on), and the `notify` and `doctor` sections at `session_start`. Nothing watches the document. |
613
+ | 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), and the `notify` and `doctor` sections at `session_start`. Nothing watches the document. |
622
614
  | Data sources | `footerData.getGitBranch()`, `footerData.getAvailableProviderCount()`, `ctx.getContextUsage()`, `ctx.sessionManager.getEntries()`, `ctx.sessionManager.getSessionName()`, `ctx.model`; subscribes to `footerData.onBranchChange` to repaint. |
623
- | 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 picker derives its screen from live runtime state and persists only through `settings.json`. 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. |
615
+ | 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 only through `settings.json`. 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. |
624
616
  | Lifecycle | `session_start` installs the header and footer, points the doctor runtime at the session cwd, probes the extensions with hard requirements when `bytefly.json`'s `doctor.proactive` is on, and loads the notify config and subscribes to the job bus; `session_tree` reinstalls the header and footer; `session_shutdown` restores Pi's built-in header and footer, disposes the branch subscription, drops doctor's cached findings, and drops the notify bus subscription. |
625
617
 
626
618
  ## Design notes
@@ -700,7 +692,7 @@ rather than showing a wrong value.
700
692
  - **A record is dated, not the frame.** The peak schedule is evaluated against
701
693
  each entry's own timestamp, so a total is stable as the clock crosses a
702
694
  boundary — and memoizing the scan by `sessionId:leafId` stays valid.
703
- - **The picker reuses Pi's resolution; only the toggle is ported.** Reading
695
+ - **The screen reuses Pi's resolution; only the toggle is ported.** Reading
704
696
  `SettingsManager` and `DefaultPackageManager` (public exports) makes the list
705
697
  match what Pi loads; the `+`/`-` pattern arithmetic is a trimmed port of Pi's
706
698
  config selector, kept pure and separately tested so a future drift is easy to
@@ -710,6 +702,17 @@ rather than showing a wrong value.
710
702
  screen shows the view for the active scope and writes through the matching
711
703
  store. A project package override is written with `autoload: false`, so it
712
704
  filters the personal entry instead of replacing it.
705
+ - **The merge keys on a directory name only when it is unambiguous.** A
706
+ subfolder resource displays as `web-access/index.ts` while its doctor report
707
+ is named `web-access`, so the join starts from `basename(dirname(path))`. A
708
+ name shared by more than one resource — a user `plan` beside the package's
709
+ `plan`, or the `extensions` directory every flat file shares — claims no
710
+ report, so no row is lent another resource's health; the unclaimed report
711
+ appends in the trailing `Not resolved` group with its findings intact. Rows are keyed
712
+ by the unique resource identity (`source:path`, or `report:<name>` for a
713
+ report with no item) so the cursor survives a rebuild even when two rows
714
+ share a directory name, and a row without a report shows no glyph and no
715
+ summary rather than a false `ok`.
713
716
  - **Doctor declarations live in `lib/`, probes in the extension, presentation in
714
717
  a third layer.** An extension cannot import another extension's modules, so
715
718
  the only legal home for a shared prerequisite table is `lib/requirements.ts`;
@@ -731,16 +734,18 @@ rather than showing a wrong value.
731
734
 
732
735
  ## Files
733
736
 
734
- The six surfaces are folders: `header/`, `footer/` (the status bar and its
735
- price calculation), `separator/`, `picker/`, `doctor/`, and `notify/`.
737
+ The surfaces are folders: `header/`, `footer/` (the status bar and its price
738
+ calculation), `separator/`, `panel/` (the unified screen and its merged model),
739
+ `picker/` (discovery and the settings store), `doctor/` (the `run_doctor` tool
740
+ and its probe runtime), and `notify/`.
736
741
 
737
742
  | File | Responsibility |
738
743
  |---|---|
739
744
  | `index.ts` | Composition root: the disable guard and the six `register*` calls. |
740
745
  | `header/config.ts` | Header tuning (`HEADER_CONFIG`): mark and identity tokens, padding, tagline, and the grouped help entries. |
741
746
  | `header/component.ts` | `ByteflyHeader`: the mark, identity rows, rule, and grouped help, plus its factory. |
742
- | `header/register.ts` | Installs/restores the header, exposes its enabled state, and owns its session lifecycle. |
743
- | `footer/register.ts` | Installs/restores the footer, registers `/bytefly [header|footer|status]`, and owns its session lifecycle. |
747
+ | `header/register.ts` | Installs/restores the header and owns its session lifecycle. |
748
+ | `footer/register.ts` | Installs/restores the footer and owns its session lifecycle. |
744
749
  | `footer/config.ts` | Bar tuning (`CONFIG`): thresholds, gauge, separators, icons, status keys, labels. |
745
750
  | `footer/types.ts` | `StatusSnapshot`, `Segment`, `LineSpec`, `UsageRecord`, and footer/TUI shapes. |
746
751
  | `footer/collect/entries.ts` | Session entries → usage records, each resolved to the model in effect. |
@@ -761,21 +766,22 @@ price calculation), `separator/`, `picker/`, `doctor/`, and `notify/`.
761
766
  | `separator/component.ts` | `SeparatorComponent`: the stateless entry renderer. |
762
767
  | `separator/format.ts` | Pure `separatorLabel` / `separatorParts` / `separatorLine`. |
763
768
  | `separator/types.ts` | `TurnSeparatorData`, `SeparatorParts`. |
769
+ | `panel/types.ts` | `PanelRow` and `PanelModel`, the merged row model. |
770
+ | `panel/join.ts` | The item/report union with the unambiguous-name health rule and unique row keys, groups, and ordering. |
771
+ | `panel/format.ts` | Non-TUI text: the headline and the grouped listing with a per-row health suffix. |
772
+ | `panel/tui.ts` | `PanelComponent`, the unified dock screen. |
773
+ | `panel/commands.ts` | `/bytefly`, `/extensions`, and `/doctor`: one runner for the screen and its text modes. |
764
774
  | `picker/types.ts` | Picker host seams (`SettingsStore`, `ExtensionResolver`, `Discovery`) and row types. |
765
- | `picker/resources.ts` | Pure grouping, labels, display names, pattern helpers, text listing. |
775
+ | `picker/resources.ts` | Pure grouping, labels, display names, and pattern helpers. |
766
776
  | `picker/toggle.ts` | Pure next-settings transforms for global and project toggles. |
767
777
  | `picker/discovery.ts` | Host wiring: builds the global and effective views from Pi's managers. |
768
778
  | `picker/runtime.ts` | Picker scope, live view copies, dirty tracking, and persistence. |
769
- | `picker/tui.ts` | `ExtensionsListComponent`, the dock screen. |
770
- | `picker/commands.ts` | `/extensions`: TUI path and text fallback. |
771
- | `doctor/register.ts` | Registers `run_doctor`, `/doctor`, and the proactive session-start check; returns the runtime. |
779
+ | `doctor/register.ts` | Registers `run_doctor` and the proactive session-start check; returns the runtime. |
772
780
  | `doctor/runtime.ts` | Host seams, the session cache, and the two probe modes. |
773
781
  | `doctor/tools.ts` | The `run_doctor` tool and its renderers. |
774
- | `doctor/commands.ts` | The `/doctor` command and its TUI wiring. |
775
782
  | `doctor/schema.ts` | Tool parameters and the structured output schema. |
776
783
  | `doctor/config.ts` | `DEFAULT_CONFIG`, `normalizeConfig`, `loadConfig` for the `doctor` section of `bytefly.json`. |
777
- | `doctor/format.ts` | Pure report text, rows, glyphs, and the proactive warning sentence. |
778
- | `doctor/tui.ts` | `DoctorScreen`, the docked list screen with its detail pane. |
784
+ | `doctor/format.ts` | Pure report text, summary, glyphs, detail lines, and the proactive warning sentence. |
779
785
  | `notify/types.ts` | `NotifyConfig`, `NotifyMethod`, and the method list. |
780
786
  | `notify/config.ts` | The `notify` section of `bytefly.json`: defaults, validation, loader. |
781
787
  | `notify/dispatch.ts` | Method selection and delivery: OSC 777/99, Windows toast, `osascript`, `notify-send`, over injected seams. |
@@ -805,16 +811,17 @@ golden output at fixed widths (`footer/render/render`), and the install/skip/
805
811
  restore/toggle lifecycle plus a config-driven price (`extension`). The turn
806
812
  separator
807
813
  is covered by its pure formatting (`separator/format`) and its
808
- register/append/render/disable lifecycle (`separator/register`). The picker is
809
- covered by the pure helpers (`picker/resources`), the toggle transforms
810
- (`picker/toggle`), discovery with an injected factory (`picker/discovery`), the
811
- runtime state machine (`picker/runtime`), the screen's rendering and key
812
- handling (`picker/tui`), and the command through the bytefly factory
813
- (`picker/commands`). The doctor surface is covered by its pure report
814
- formatting (`doctor/format`), the screen's rendering and key handling
815
- (`doctor/tui`), its config loader (`doctor/config`), and registration, the
816
- proactive warning, the session cache, and `run_doctor` through the factory
817
- (`doctor/register`). The notify capability is covered by its section loader
814
+ register/append/render/disable lifecycle (`separator/register`). The picker
815
+ helpers are covered by the pure helpers (`picker/resources`), the toggle
816
+ transforms (`picker/toggle`), discovery with an injected factory
817
+ (`picker/discovery`), and the runtime state machine (`picker/runtime`). The
818
+ unified screen is covered by its pure model merge (`panel/join`), its text
819
+ formatter (`panel/format`), the screen's rendering and key handling
820
+ (`panel/tui`), and the command through the bytefly factory (`panel/commands`).
821
+ The doctor surface is covered by its pure report
822
+ formatting (`doctor/format`), its config loader (`doctor/config`), and
823
+ registration, the proactive warning, the session cache, and `run_doctor`
824
+ through the factory (`doctor/register`). The notify capability is covered by its section loader
818
825
  (`notify/config`), the pure text builders (`notify/format`), method selection and
819
826
  delivery over recording seams (`notify/dispatch`), and its three triggers,
820
827
  reload, and shutdown through `registerNotify` (`notify/register`).
@@ -2,16 +2,15 @@
2
2
  * Pure formatting for doctor's reports.
3
3
  *
4
4
  * Two shapes share these helpers: the plain multi-line text every non-TUI mode
5
- * shows, and the single row per extension the TUI screen renders. Nothing here
5
+ * shows, and the per-extension summary the unified panel renders. Nothing here
6
6
  * touches the filesystem or the host, and every string that came from a probe (a
7
7
  * parse error, a captured path) goes through {@link sanitizeLine} — probe text can carry
8
8
  * control characters, a parse error quotes the file's own bytes.
9
9
  */
10
10
 
11
11
  import type { Theme } from "@earendil-works/pi-coding-agent";
12
- import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
12
+ import { wrapTextWithAnsi } from "@earendil-works/pi-tui";
13
13
  import { sanitizeLine } from "../../../lib/format.ts";
14
- import { selectionMarker } from "../../../lib/list-cursor.ts";
15
14
  import type { ExtensionReport, Finding, FindingSeverity } from "../../../lib/requirements.ts";
16
15
  import { GLYPHS } from "../../../lib/ui.ts";
17
16
 
@@ -123,7 +122,7 @@ export function reportSeverity(report: ExtensionReport): FindingSeverity {
123
122
  }
124
123
 
125
124
  /** A short right-hand status for one extension row. */
126
- function reportSummary(report: ExtensionReport): string {
125
+ export function reportSummary(report: ExtensionReport): string {
127
126
  if (!report.enabled) return "disabled";
128
127
  const missing = report.findings.filter((finding) => finding.severity === "missing").length;
129
128
  const degraded = report.findings.filter((finding) => finding.severity === "degraded").length;
@@ -132,24 +131,6 @@ function reportSummary(report: ExtensionReport): string {
132
131
  return "ok";
133
132
  }
134
133
 
135
- /** One extension row: marker, severity glyph, name, and a summary. */
136
- export function extensionRow(report: ExtensionReport, theme: Theme, width: number, selected = false): string {
137
- const severity = reportSeverity(report);
138
- const glyph = severityGlyph(severity, theme);
139
- const head = `${selectionMarker(theme, selected)}${glyph} `;
140
- const summary = reportSummary(report);
141
- const available = Math.max(1, width - visibleWidth(head) - visibleWidth(summary) - 1);
142
- const name = truncateToWidth(sanitizeLine(report.extension), available, "…");
143
- const pad = " ".repeat(Math.max(0, available - visibleWidth(name)));
144
- const tail =
145
- severity === "missing"
146
- ? theme.fg("error", summary)
147
- : severity === "degraded"
148
- ? theme.fg("warning", summary)
149
- : theme.fg("dim", summary);
150
- return truncateToWidth(`${head}${name}${pad} ${tail}`, width);
151
- }
152
-
153
134
  /**
154
135
  * The focused extension's findings, wrapped to `width` and capped at `rows`
155
136
  * lines. Problems come first: `includeInfo` is the screen's expand toggle, and
@@ -1,20 +1,22 @@
1
1
  /**
2
- * bytefly's doctor surface: the `run_doctor` tool, the `/doctor` command, and
3
- * the proactive session-start check.
2
+ * bytefly's doctor surface: the `run_doctor` tool and the proactive
3
+ * session-start check.
4
4
  *
5
5
  * An extension that is enabled but inert is the failure this exists to prevent:
6
6
  * a missing extractor CLI, an optional package that was never installed, a
7
7
  * config file with a typo, or a runtime the extension needs but cannot reach.
8
8
  * `doctor` knows the answer because each extension declares its prerequisites in
9
- * `lib/requirements.ts`, and the same probes feed `/doctor` and `run_doctor`.
9
+ * `lib/requirements.ts`, and the same probes feed the unified panel and
10
+ * `run_doctor`.
10
11
  *
11
12
  * This module registers itself from bytefly's factory; it has no env guard of
12
- * its own because `PI_DISABLED_EXTENSIONS=bytefly` is what disables it.
13
+ * its own because `PI_DISABLED_EXTENSIONS=bytefly` is what disables it. It
14
+ * creates the session-scoped runtime and returns it so the composition root can
15
+ * hand the same cache to the panel.
13
16
  */
14
17
 
15
18
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
16
19
  import { missingFindings } from "../../../lib/requirements.ts";
17
- import { registerCommands } from "./commands.ts";
18
20
  import { loadConfig } from "./config.ts";
19
21
  import { formatWarning } from "./format.ts";
20
22
  import { createDoctorRuntime, type DoctorRuntime } from "./runtime.ts";
@@ -28,14 +30,14 @@ export interface DoctorDeps {
28
30
  /**
29
31
  * Register the doctor surface and return its runtime.
30
32
  *
31
- * Registers the read-only tool and the command, points the runtime at each
32
- * session, and drops the cached findings on shutdown.
33
+ * Registers the read-only tool, points the runtime at each session, and drops
34
+ * the cached findings on shutdown. `/bytefly` and its aliases are registered by
35
+ * the panel, which shares this runtime.
33
36
  */
34
37
  export function registerDoctor(pi: ExtensionAPI, deps: DoctorDeps = {}): DoctorRuntime {
35
38
  const runtime = deps.runtime ?? createDoctorRuntime(pi);
36
39
 
37
40
  registerTools(pi, runtime);
38
- registerCommands(pi, runtime);
39
41
 
40
42
  pi.on("session_start", async (_event, ctx) => {
41
43
  const config = loadConfig(ctx.cwd);