@gavin-hu/my-pi-agent 0.19.0 → 0.21.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 (208) hide show
  1. package/README.md +19 -12
  2. package/README.zh-CN.md +15 -9
  3. package/docs/extensions/README.md +2 -1
  4. package/docs/extensions/board.md +329 -151
  5. package/docs/extensions/bytefly.md +427 -273
  6. package/docs/extensions/file-browse.md +46 -44
  7. package/docs/extensions/job.md +1 -2
  8. package/docs/extensions/plan.md +86 -24
  9. package/docs/extensions/serve.md +129 -0
  10. package/docs/extensions/session-bus.md +23 -10
  11. package/docs/extensions/team.md +56 -28
  12. package/docs/extensions/wechat.md +2 -1
  13. package/docs/extensions/worktree.md +30 -15
  14. package/extensions/board/commands.ts +48 -17
  15. package/extensions/board/format.ts +107 -9
  16. package/extensions/board/index.ts +14 -2
  17. package/extensions/board/join.ts +31 -10
  18. package/extensions/board/page.ts +202 -156
  19. package/extensions/board/result.ts +10 -24
  20. package/extensions/board/rows.ts +1 -1
  21. package/extensions/board/schema.ts +31 -10
  22. package/extensions/board/seat.ts +9 -10
  23. package/extensions/board/snapshot.ts +7 -5
  24. package/extensions/board/tools.ts +211 -34
  25. package/extensions/board/transport/router.ts +27 -110
  26. package/extensions/board/transport/server.ts +76 -56
  27. package/extensions/board/types.ts +57 -0
  28. package/extensions/board/view/client.ts +24 -13
  29. package/extensions/board/view/dialog.ts +43 -0
  30. package/extensions/board/view/keys.ts +28 -24
  31. package/extensions/board/view/live.ts +6 -178
  32. package/extensions/board/view/names.ts +91 -0
  33. package/extensions/board/view/page.ts +60 -89
  34. package/extensions/board/view/render.ts +228 -200
  35. package/extensions/board/view/styles.ts +72 -49
  36. package/extensions/board/view/team.ts +63 -41
  37. package/extensions/bytefly/footer/budget.ts +6 -5
  38. package/extensions/bytefly/footer/collect/entries.ts +12 -4
  39. package/extensions/bytefly/footer/collect/snapshot.ts +8 -17
  40. package/extensions/bytefly/footer/config.ts +19 -11
  41. package/extensions/bytefly/footer/price/price.ts +30 -8
  42. package/extensions/bytefly/footer/register.ts +5 -5
  43. package/extensions/bytefly/footer/render/footer.ts +18 -4
  44. package/extensions/bytefly/footer/render/format.ts +18 -0
  45. package/extensions/bytefly/footer/render/key.ts +46 -0
  46. package/extensions/bytefly/footer/render/layout.ts +13 -3
  47. package/extensions/bytefly/footer/render/lines.ts +23 -7
  48. package/extensions/bytefly/footer/render/segments.ts +93 -31
  49. package/extensions/bytefly/footer/types.ts +29 -5
  50. package/extensions/bytefly/footer/unpriced.ts +42 -17
  51. package/extensions/bytefly/header/component.ts +4 -20
  52. package/extensions/bytefly/panel/commands.ts +28 -1
  53. package/extensions/bytefly/panel/filter.ts +68 -0
  54. package/extensions/bytefly/panel/format.ts +16 -15
  55. package/extensions/bytefly/panel/join.ts +56 -13
  56. package/extensions/bytefly/panel/prices.ts +43 -0
  57. package/extensions/bytefly/panel/settings.ts +72 -23
  58. package/extensions/bytefly/panel/tui/entries.ts +26 -8
  59. package/extensions/bytefly/panel/tui/frame.ts +130 -93
  60. package/extensions/bytefly/panel/tui/keys.ts +352 -0
  61. package/extensions/bytefly/panel/tui/list-view.ts +25 -46
  62. package/extensions/bytefly/panel/tui/panes.ts +76 -0
  63. package/extensions/bytefly/panel/tui/section-view.ts +126 -0
  64. package/extensions/bytefly/panel/tui/settings-section.ts +54 -125
  65. package/extensions/bytefly/panel/tui/settings-view.ts +30 -131
  66. package/extensions/bytefly/panel/tui/side-pane.ts +224 -0
  67. package/extensions/bytefly/panel/tui.ts +306 -259
  68. package/extensions/check/commands.ts +2 -1
  69. package/extensions/check/render.ts +10 -1
  70. package/extensions/file-browse/config/config.ts +4 -19
  71. package/extensions/file-browse/files/classify.ts +136 -0
  72. package/extensions/file-browse/files/files.ts +8 -547
  73. package/extensions/file-browse/files/index.ts +90 -0
  74. package/extensions/file-browse/files/list.ts +261 -0
  75. package/extensions/file-browse/files/tree.ts +101 -0
  76. package/extensions/file-browse/index.ts +27 -48
  77. package/extensions/file-browse/live/live.ts +1 -1
  78. package/extensions/file-browse/transport/router.ts +12 -6
  79. package/extensions/file-browse/transport/server.ts +61 -28
  80. package/extensions/file-browse/view/chrome.ts +4 -4
  81. package/extensions/file-browse/view/client.ts +3 -1
  82. package/extensions/file-browse/view/emit.ts +51 -0
  83. package/extensions/file-browse/view/html.ts +78 -46
  84. package/extensions/file-browse/view/live-client.ts +2 -2
  85. package/extensions/file-browse/view/markdown.ts +23 -26
  86. package/extensions/file-browse/view/palette.ts +2 -2
  87. package/extensions/file-browse/view/raw-html.ts +27 -22
  88. package/extensions/file-browse/view/tree.ts +1 -1
  89. package/extensions/file-browse/view/urls.ts +20 -6
  90. package/extensions/goal/tui.ts +12 -2
  91. package/extensions/job/runtime.ts +4 -4
  92. package/extensions/job/tools.ts +19 -7
  93. package/extensions/job/tui.ts +2 -28
  94. package/extensions/plan/commands.ts +2 -2
  95. package/extensions/plan/feedback.ts +88 -18
  96. package/extensions/plan/format.ts +10 -3
  97. package/extensions/plan/index.ts +24 -2
  98. package/extensions/plan/plan-files.ts +88 -0
  99. package/extensions/plan/plan-text.ts +114 -0
  100. package/extensions/plan/plans.ts +19 -23
  101. package/extensions/plan/policy.ts +10 -5
  102. package/extensions/plan/runtime.ts +43 -4
  103. package/extensions/plan/todos.ts +51 -3
  104. package/extensions/plan/tools/exit.ts +10 -1
  105. package/extensions/plan/tools/todo-seed.ts +4 -2
  106. package/extensions/plan/tools/write.ts +12 -1
  107. package/extensions/plan/tools.ts +2 -2
  108. package/extensions/plan/types.ts +15 -0
  109. package/extensions/plan/web-blocks.ts +160 -0
  110. package/extensions/plan/web-chrome.ts +7 -5
  111. package/extensions/plan/web-client.ts +271 -325
  112. package/extensions/plan/web-collab.ts +7 -7
  113. package/extensions/plan/web-comments.ts +57 -16
  114. package/extensions/plan/web-composer.ts +91 -0
  115. package/extensions/plan/web-draft.ts +40 -0
  116. package/extensions/plan/web-keys.ts +258 -0
  117. package/extensions/plan/web-markdown.ts +1 -1
  118. package/extensions/plan/web-outline.ts +212 -0
  119. package/extensions/plan/web-page.ts +59 -86
  120. package/extensions/plan/web-payload.ts +188 -0
  121. package/extensions/plan/web-plan.ts +160 -0
  122. package/extensions/plan/web-router.ts +0 -1
  123. package/extensions/plan/web-settle.ts +201 -0
  124. package/extensions/plan/web-share-session.ts +24 -55
  125. package/extensions/plan/web-share.ts +19 -37
  126. package/extensions/plan/web-write.ts +112 -0
  127. package/extensions/plan/web.ts +77 -130
  128. package/extensions/rewind/commands/review.ts +2 -1
  129. package/extensions/serve/config.ts +37 -0
  130. package/extensions/serve/index.ts +71 -0
  131. package/extensions/serve/registry.ts +251 -0
  132. package/extensions/session-bus/join-request.ts +16 -9
  133. package/extensions/session-bus/result.ts +1 -27
  134. package/extensions/session-bus/rows.ts +7 -3
  135. package/extensions/session-bus/runtime.ts +100 -38
  136. package/extensions/session-bus/store.ts +14 -2
  137. package/extensions/session-bus/tools.ts +5 -4
  138. package/extensions/session-bus/tui.ts +1 -1
  139. package/extensions/session-bus/types.ts +10 -1
  140. package/extensions/subagent/commands.ts +2 -1
  141. package/extensions/team/commands.ts +44 -9
  142. package/extensions/team/format.ts +14 -0
  143. package/extensions/team/index.ts +2 -1
  144. package/extensions/team/tools.ts +30 -9
  145. package/extensions/team/types.ts +17 -0
  146. package/extensions/todo/index.ts +4 -1
  147. package/extensions/todo/nudge.ts +8 -2
  148. package/extensions/todo/tui.ts +21 -3
  149. package/extensions/wechat/bridge.ts +43 -172
  150. package/extensions/wechat/intake.ts +245 -0
  151. package/extensions/worktree/commands.ts +24 -17
  152. package/extensions/worktree/exit.ts +12 -6
  153. package/extensions/worktree/git.ts +22 -14
  154. package/extensions/worktree/guard.ts +15 -285
  155. package/extensions/worktree/handoff.ts +10 -2
  156. package/extensions/worktree/index.ts +2 -1
  157. package/extensions/worktree/lifecycle.ts +2 -0
  158. package/extensions/worktree/registry.ts +20 -11
  159. package/extensions/worktree/root-tools.ts +13 -3
  160. package/extensions/worktree/runtime.ts +8 -2
  161. package/extensions/worktree/screen.ts +15 -8
  162. package/extensions/worktree/shell.ts +303 -0
  163. package/extensions/worktree/state.ts +1 -1
  164. package/extensions/worktree/status.ts +42 -9
  165. package/extensions/worktree/tools.ts +19 -13
  166. package/lib/README.md +9 -7
  167. package/lib/board/card.ts +192 -3
  168. package/lib/board/links.ts +26 -2
  169. package/lib/board/page-pointer.ts +111 -32
  170. package/lib/board/round-text.ts +11 -2
  171. package/lib/board/round.ts +25 -20
  172. package/lib/board/rules.ts +144 -11
  173. package/lib/board/session.ts +84 -18
  174. package/lib/board/store.ts +268 -55
  175. package/lib/board/view.ts +114 -24
  176. package/lib/bus-request.ts +83 -0
  177. package/lib/format.ts +22 -0
  178. package/lib/http/index.ts +11 -7
  179. package/lib/http/mount.ts +142 -0
  180. package/lib/http/port.ts +25 -0
  181. package/lib/http/serve.ts +170 -43
  182. package/lib/http/server.ts +3 -3
  183. package/lib/http/sink.ts +34 -9
  184. package/lib/plan-entry.ts +20 -13
  185. package/lib/policy.ts +14 -1
  186. package/lib/requirements.ts +2 -2
  187. package/lib/session-label.ts +23 -7
  188. package/lib/session-roster.ts +74 -3
  189. package/lib/session-tool.ts +37 -21
  190. package/lib/spawn.ts +37 -3
  191. package/lib/stray-fields.ts +45 -0
  192. package/lib/tool-names.ts +13 -0
  193. package/lib/tui/README.md +6 -3
  194. package/lib/tui/glyphs.ts +3 -6
  195. package/lib/tui/render-cache.ts +29 -0
  196. package/lib/tui/tool.ts +30 -1
  197. package/lib/web/README.md +52 -2
  198. package/lib/web/client-runtime.ts +149 -0
  199. package/lib/web/index.ts +4 -2
  200. package/lib/web/markup.ts +106 -2
  201. package/lib/web/names.ts +42 -0
  202. package/package.json +3 -2
  203. package/extensions/board/config.ts +0 -36
  204. package/extensions/board/view/actions.ts +0 -306
  205. package/extensions/board/writes.ts +0 -99
  206. package/extensions/bytefly/panel/tui/extensions-view.ts +0 -40
  207. package/extensions/bytefly/panel/tui/skills-view.ts +0 -50
  208. package/extensions/job/process.ts +0 -35
@@ -18,13 +18,13 @@ Status records what a reader can rely on: **Shipped** is in a tagged release, **
18
18
  |---|---|---|---|
19
19
  | One-command browsing | `/browse` | The server starts at the effective working directory, prints its URL, and opens the browser. | Shipped |
20
20
  | Read-only serving | `/browse` | Nothing is edited or written, and the server binds `127.0.0.1` only. | Shipped |
21
- | Two-pane page | `/`, `/browse[/<rel>]` | A collapsible tree beside the current folder's listing, with the tree, filter, and quick-open trigger pinned in their own pane. | Shipped |
21
+ | Two-pane page | `/files/`, `/files/browse[/<rel>]` | A collapsible tree beside the current folder's listing, with the tree, filter, and quick-open trigger pinned in their own pane. | Shipped |
22
22
  | File previews | `/view/<rel>` | Text, rendered Markdown, image thumbnails, and download cards for binary or over-size files, each with a per-language icon. | Shipped |
23
- | Quick open | `/`, `Ctrl/Cmd+K`, `/api/files` | A fuzzy path finder over the tree opens a file without walking the tree. | Shipped |
23
+ | Quick open | `/files/`, `Ctrl/Cmd+K`, `/files/api/files` | A fuzzy path finder over the tree opens a file without walking the tree. | Shipped |
24
24
  | Git diff view | `/diff`, `/diff/<rel>` | A change overview and one file's unified diff, scoped to `head`, `staged`, or `worktree`. | Shipped |
25
25
  | Live status | `/events`, the `◉ live` chip | An open page keeps its branch, change markers, and footer up to date without a reload. | Shipped |
26
26
  | Reload hint | `/api/dir-rev`, `⟳ reload` | A page whose folder gained or lost a file offers a reload control instead of changing its rows. | Shipped |
27
- | Terminal session chip | `browse` status key | The terminal footer shows `◉ <port>` while the page is served, so the session names its live server. | Shipped |
27
+ | The session's page port | `serve`'s `◉ <port>` chip | The footer names the one port every page of the session answers on, this one included. | Shipped |
28
28
  | Listing controls | the `Name`, `Size`, and `Modified` headers | The current folder sorts by name, size, or modified time, directories first, and the choice is remembered per browser. | Shipped |
29
29
  | Keyboard navigation | the tree's keymap, `?` | The tree is one tab stop with arrow, fold, and open keys; a shortcut sheet lists the page's own keys. | Shipped |
30
30
  | Diff hunk navigation | `n`/`p` in `/diff` | Jump between changed hunks without scrolling the whole file. | Shipped |
@@ -35,7 +35,7 @@ Status records what a reader can rely on: **Shipped** is in a tagged release, **
35
35
  | Markdown preview | `/view/<rel>`, `/view/<rel>?raw=1` | A README renders as headings, lists, tables, code, and links, with a one-click raw view. | Shipped |
36
36
  | Copy and wrap | the page toolbar, `y`, `u` | Copy a file's path or contents or a `path:12-30` line reference, and toggle line wrapping for text and raw views. | Shipped |
37
37
  | Theme toggle | the header button, `/theme.js` | Dark and light palettes, following the system until an explicit choice is remembered per browser. | Shipped |
38
- | Idle stop and port reuse | `/browse stop`, the session port | The server stops when idle and reuses its port for the next session instead of leaking listeners. | Shipped |
38
+ | Idle stop and prefix reuse | `/browse stop`, the idle grace | The page gives its prefix back when nobody is reading it, so a closed tab never holds the session's server. | Shipped |
39
39
 
40
40
  ## Quickstart
41
41
 
@@ -66,34 +66,32 @@ selects the root.
66
66
 
67
67
  - The root is the effective cwd, so an active `worktree` session serves the
68
68
  worktree. `<path>` must resolve inside it.
69
- - The port is fixed for the session: the first `/browse` picks a random port
70
- in `4780`–`5779` and every later `/browse` in that session reuses it, so the
71
- URL stays stable across roots and restarts. Set `"port"` to pin an explicit
72
- port; a busy port is reported, not silently changed.
69
+ - The port is the session's: `serve` picks one in `4780`–`5779` (and `serve.port` pins
70
+ it), so the origin stays stable across roots and restarts. A busy pinned port is
71
+ reported, not silently changed.
73
72
  - Starting while already running with the same root reprints the URL without
74
- restarting; a different root (or a changed pinned port) restarts the server.
73
+ restarting; a different root restarts the page.
75
74
  - The server is closed when its last page has been gone for ten seconds, so
76
75
  closing the browser window ends the session's browse server and `/browse`
77
- starts it again on the same port. A reload or a new tab inside that grace keeps
76
+ starts it again on the session's port. A reload or a new tab inside that grace keeps
78
77
  it. `session_shutdown` closes it too, and `/browse stop` closes it at once.
79
78
 
80
79
  ## Routes
81
80
 
82
81
  | Route | Result |
83
82
  |---|---|
84
- | `/`, `/browse[/<rel>]` | tree + directory listing |
85
- | `/view/<rel>` | tree + file page (text, Markdown, image, binary, or over-size) |
86
- | `/view/<rel>?raw=1` | raw Markdown/text source instead of the rendered view |
87
- | `/raw/<rel>` | raw bytes, streamed; downloads and image sources |
88
- | `/diff[?mode=head\|staged\|worktree]` | changed-file overview with `--stat` |
89
- | `/diff/<rel>[?mode=…]` | one file's unified diff |
90
- | `/api/tree?path=<rel>` | JSON children for lazy tree expansion |
91
- | `/api/files?q=<query>` | JSON fuzzy path matches for the quick-open palette |
92
- | `/api/dir-rev?path=<rel>` | JSON listing revision for the reload check |
93
- | `/events` | Server-Sent Events stream of `status` frames for the open page |
94
- | `/app.js` | the first-party client script |
95
- | `/theme.js` | the pre-paint theme bootstrap |
96
- | `/favicon.ico` | `204` (the icon is a data URI) |
83
+ | `/files/`, `/files/browse[/<rel>]` | tree + directory listing |
84
+ | `/files/view/<rel>` | tree + file page (text, Markdown, image, binary, or over-size) |
85
+ | `/files/view/<rel>?raw=1` | raw Markdown/text source instead of the rendered view |
86
+ | `/files/raw/<rel>` | raw bytes, streamed; downloads and image sources |
87
+ | `/files/diff[?mode=head\|staged\|worktree]` | changed-file overview with `--stat` |
88
+ | `/files/diff/<rel>[?mode=…]` | one file's unified diff |
89
+ | `/files/api/tree?path=<rel>` | JSON children for lazy tree expansion |
90
+ | `/files/api/files?q=<query>` | JSON fuzzy path matches for the quick-open palette |
91
+ | `/files/api/dir-rev?path=<rel>` | JSON listing revision for the reload check |
92
+ | `/files/events` | Server-Sent Events stream of `status` frames for the open page |
93
+ | `/files/app.js` | the first-party client script |
94
+ | `/files/theme.js` | the pre-paint theme bootstrap |
97
95
 
98
96
  ## Configuration
99
97
 
@@ -103,7 +101,6 @@ global document then project document, project wins — are in
103
101
 
104
102
  | Key | Default | Meaning |
105
103
  |---|---|---|
106
- | `port` | `0` | `0` picks one random port fixed for the session; a positive value pins it. |
107
104
  | `autoOpen` | `true` | Open the browser on start; `false` for headless. |
108
105
  | `maxFileBytes` | `1048576` | Larger files show a download card only. |
109
106
  | `maxThumbBytes` | `5242880` | Larger images get an icon, not a thumbnail. |
@@ -171,7 +168,10 @@ global document then project document, project wins — are in
171
168
  ## The page
172
169
 
173
170
  The page is server-rendered HTML with an inline style block and inline SVG
174
- icons, progressively enhanced by one first-party script (`client.ts`). A folder
171
+ icons, progressively enhanced by one first-party script (`client.ts`). Each page
172
+ names its own subject in the tab — `browse · <folder>`, `browse · <rel>` for a file, the
173
+ `<page> · <subject>` shape the kit's `tabTitle` gives every served page — and links the
174
+ brand mark as an SVG data URI, in the appearance the system is in. A folder
175
175
  folds from its twisty, and a `fold all` / `expand all` button beside the filter
176
176
  folds the whole tree at once. The tree scrolls in its own pane with quick open
177
177
  and the filter pinned above it; below 860px it collapses into a `Files`
@@ -340,12 +340,11 @@ old and new sides for `lib/highlight`.
340
340
 
341
341
  ## Live status
342
342
 
343
- While a server is running, the extension publishes a two-tone `◉ <port>` chip
344
- (a green circle-dot and a blue port) under the shared `browse` status key. The
345
- [`bytefly`](./bytefly.md) extension routes it to line 1's right zone
346
- beside `⎇ branch` and `⑂ worktree`; it is cleared on stop and on
347
- `session_shutdown`. The chip is session-local: it reflects only this session's
348
- server, with no cross-session sharing.
343
+ The page answers at `/files` on the session's one server, so the footer's one
344
+ `◉ <port>` chip is the session's, not this page's: [`serve`](./serve.md) publishes it
345
+ while anything is mounted, and [`bytefly`](./bytefly.md) routes it to line 1's right
346
+ zone, after `⎇ branch` and `⑂ worktree`. It is cleared when the session's server closes,
347
+ so a session with no page open shows nothing at all.
349
348
 
350
349
  The page has its own live status, which is what a left-open tab reads. A `◉ live`
351
350
  chip beside `read-only` says whether status is flowing (green), still
@@ -450,22 +449,20 @@ stops at the first difference because the control, not the checker, is the fix.
450
449
  | Integration point | How |
451
450
  |---|---|
452
451
  | Command | `/browse` registers a single command from the factory; no tools are exposed. |
453
- | State | The running server, its root, and the session port live in a module-local closure; nothing is persisted and no session entries are written. |
452
+ | State | The running mount, its root, and the session id live in a module-local closure; nothing is persisted and no session entries are written. The port is `serve`'s, not this extension's. |
454
453
  | Lifecycle | The factory opens no socket; the server is created on `/browse` and closed idempotently by `/browse stop`, by `session_shutdown`, and once its last page has been gone for the idle grace, which also ends every attached status stream. |
455
454
 
456
455
  ## Design notes
457
456
 
458
457
  - **Lifecycle.** `/browse` resolves `resolveEffectiveCwd(ctx.cwd)`, loads config,
459
- selects the port, parses args, and `await createFileServer(...)`: `realpath`
460
- the root, bind `127.0.0.1`, await `listening`, return `{ url, port, root,
461
- close }`. The port is chosen once for the session: a pinned config `port`
462
- wins, otherwise a random port is drawn and cached after the first successful
463
- bind, so later starts reuse it (a conflict is reported and the next `/browse`
464
- rolls another). `/browse stop` and `session_shutdown` await the idempotent
465
- `close()`; `session_shutdown` also clears the cached port. The page is the
466
- server's only reader, so the channel reports the last page's departure after
467
- `IDLE_STOP_MS` and the command closes the server and clears the chip — the
468
- cached port survives, so the next `/browse` binds the same one.
458
+ parses args, and mounts the page through `requestMount`: `realpath` the root, claim
459
+ `/files` on the session's server, return `{ url, port, root, close }`. The
460
+ port belongs to `serve` — `serve.port` pins the session's origin — so nothing here
461
+ picks or retries one. `/browse stop` and `session_shutdown` await the idempotent
462
+ `close()`, which gives the prefix back. The page is the mount's only reader, so the
463
+ channel reports the last page's departure after `IDLE_STOP_MS` and the command
464
+ releases the prefix — and the session's port is `serve`'s, remembered per session,
465
+ so the next `/browse` answers on the same origin.
469
466
  - **Pure routing.** `handleRequest(context, method, rawUrl, headers)` validates
470
467
  method and `Host`, parses the URL, matches a route, and runs every path
471
468
  through `paths.ts`. It returns a plain `ServeResponse`, so routing is tested
@@ -502,12 +499,17 @@ stops at the first difference because the control, not the checker, is the fix.
502
499
  | [`../../lib/escape.ts`](../../lib/escape.ts) | `escapeHtml`, the shared HTML escaping used by every renderer |
503
500
  | [`../../lib/highlight.ts`](../../lib/highlight.ts) | First-party escape-first syntax tokenizer for the code view (shared with `plan`) |
504
501
  | [`../../lib/web/index.ts`](../../lib/web/index.ts) | The shared Bytefly web brand system: tokens, base, components, layout, runtime, and markup |
505
- | `files/files.ts` | Classification (incl. Markdown), text reads, and one page-level walk (`directoryView`) that returns a directory's listing and its sidebar tree |
502
+ | `files/files.ts` | The barrel over the four reads: one surface for the callers, four modules behind it |
503
+ | `files/classify.ts` | What a name is: kind, Markdown, image, MIME type, and the text sample |
504
+ | `files/list.ts` | Capped directory listings and one file's bytes, over injected filesystem reads |
505
+ | `files/tree.ts` | The sidebar tree and the page-level walk (`directoryView`) that returns it with the listing |
506
+ | `files/index.ts` | The quick-open file index, walked once per root up to a cap |
506
507
  | `text/diff.ts` | Pure unified-diff and porcelain-status parsing for the diff view |
507
508
  | `text/fuzzy.ts` | Fuzzy path matching and ranking for the quick-open palette |
508
509
  | `view/markdown.ts` | First-party escape-first Markdown-to-HTML renderer (fenced code highlighted; raw HTML through `raw-html.ts`) |
509
510
  | `view/raw-html.ts` | Allowlisted raw-HTML sanitizer: element/attribute policy, suppressed bodies, and cross-block balancing |
510
511
  | `view/urls.ts` | Shared link/image URL policy: scheme allowlist and `/view`/`/raw` rewriting |
512
+ | `view/emit.ts` | The anchor and image elements both renderers emit, so a link written in Markdown and the same one written as HTML are one element |
511
513
  | `git/git.ts` | Read-only branch/dirty provider for the header chip |
512
514
  | `view/icons.ts` | Inline SVG sprite and the per-language mapping |
513
515
  | `view/client.ts` | The `/app.js` source: sort, wrap, copy, hunk navigation, reading progress, and the page keymap |
@@ -522,7 +524,7 @@ stops at the first difference because the control, not the checker, is the fix.
522
524
  | `view/diff-html.ts` | The unified-diff body: the gutter, the sign, the hunk sections, and per-hunk highlighting |
523
525
  | `transport/router.ts` | Pure request handler: routes, the `/raw` sandbox policy, and the error page |
524
526
  | `transport/server.ts` | This extension's routes over `lib/http`'s loopback server: the request count, the stream cap, and the idle policy |
525
- | [`../../lib/http/index.ts`](../../lib/http/index.ts) | Loopback HTTP layer shared with `plan`: one server entry (`serveLoopback`), page/script/JSON/event-stream headers, `ServeResponse`, the request-envelope helpers, the event-stream sink and its idle grace |
527
+ | [`../../lib/http/index.ts`](../../lib/http/index.ts) | Loopback HTTP layer shared with `plan`: one server entry (`serveHttp`) and the mount contract a page claims a prefix with (`requestMount`), page/script/JSON/event-stream headers, `ServeResponse`, the request-envelope helpers, the event-stream sink and its idle grace |
526
528
  | [`../../lib/browser.ts`](../../lib/browser.ts) | Cross-platform browser opener (shared with `plan`) |
527
529
 
528
530
  ## Testing
@@ -365,7 +365,7 @@ rewritten.
365
365
  cached from the stdout stream for `status`/`wait`/completion notes; only the
366
366
  `/jobs` log pane reads the file.
367
367
  - **Pure logic split from IO.** `format.ts` and the reconciliation in
368
- `registry.ts` are pure; `process.ts` and the runtime take injectable
368
+ `registry.ts` are pure; the runtime takes injectable
369
369
  spawn/liveness/kill/clock functions, so the whole suite runs without
370
370
  launching a process.
371
371
  - **The transcript reuses the `/jobs` grammar.** `job list` renders the same
@@ -388,7 +388,6 @@ rewritten.
388
388
  | `schema.ts` | Parameter schema and pure validation (a `JobCall` union). |
389
389
  | `config.ts` | The `job` section of `bytefly.json`: loading and clamping. |
390
390
  | `format.ts` | Pure formatting, duration, and transcript outcome-line helpers; sanitization is `lib/format.ts`'s `sanitizeUntrustedLine`. |
391
- | `process.ts` | The job-only process remainder: the `LivenessFn` seam and the `ps` start-time token. Spawn, tree-kill, and liveness itself come from `lib/spawn.ts`. |
392
391
  | `paths.ts` | Session/marker/registry/status file-name helpers. |
393
392
  | `registry.ts` | Per-session registry load/merge, legacy migration, and pure reconciliation. |
394
393
  | `reservations.ts` | Atomic `O_EXCL` job-id reservation and release. |
@@ -22,7 +22,7 @@ Status records what a reader can rely on: **Shipped** is in a tagged release, **
22
22
  | One writable artifact | `write_plan` | The plan lands as markdown under the agent directory, one store per repository, shared by its worktrees. | Shipped |
23
23
  | Approval gate | `exit_plan_mode` | The plan is read back and presented; nothing is written until the user approves it. | Shipped |
24
24
  | Session goal at approval | `goal` at approval | A `planning` goal is promoted to the plan's objective, and a session with none gets it, without a second dialog; an active goal is left alone. | Shipped |
25
- | Steps as todos | `exit_plan_mode` | Approval seeds the plan's steps into the todo list, with the verification section as the last item. | Shipped |
25
+ | Steps as todos | `exit_plan_mode` | Approval seeds the plan's steps into the todo list, with the verification section as the last item; approving the same plan again replaces the items its earlier approval seeded. | Shipped |
26
26
  | Plan kept current | `set_plan_steps` | Ticking a step rewrites its checkbox in the file and its todo item in one call. | Shipped |
27
27
  | Stale-step protection | `expected` in `set_plan_steps` | A step whose number moved, is out of range, or no longer matches its text refuses the whole batch instead of ticking the wrong line. | Shipped |
28
28
  | Cancel without a plan | `cancel_plan_mode`, `/plan cancel` | Write access returns, and no todo items or goal are recorded. | Shipped |
@@ -36,11 +36,13 @@ Status records what a reader can rely on: **Shipped** is in a tagged release, **
36
36
  | Capability | How | What you get | Status |
37
37
  |---|---|---|---|
38
38
  | Review screen | `exit_plan_mode` | A scrollable, themed plan with approve, refine, keep, browser, and exit in the footer. | Shipped |
39
- | Browser page | `/plan review`, `w` on the review screen | The plan opens in a local browser as a reading-first page with live section and step progress. | Shipped |
39
+ | Browser page | `/plan review`, `w` on the review screen | The plan opens in a local browser as a reading-first page with live section and step progress. | Partial |
40
40
  | Approval in the browser | the `/plan review` page's **Approve & run** | The plan is approved without returning to the terminal, and the steps are seeded. | Shipped |
41
41
  | Inline comments | the page's `+ Comment`, **Save feedback** | Comments are saved into the plan file as `> [!REVIEW]` lines and returned to the model. | Shipped |
42
42
  | Live page | the page's `events` stream, the plan-file poll | An edit made elsewhere repaints every open page without a reload. | Shipped |
43
43
  | Changed sections | the `exit_plan_mode` review screen, the `/plan review` outline | After a refinement, only the sections that changed are marked. | Shipped |
44
+ | Block-level change marks | the `/plan review` page's **Changed** view | After a refinement, each block that is new or edited is marked — an inserted step marks itself and leaves the rest of its section alone — and a section that lost blocks says how much shorter it became. **All** / **Changed** / **Comments** switch what the page shows, `v` cycles them, `n`/`N` walk the blocks that carry a comment, and a printed review is always the whole plan. | Unreleased |
45
+ | Settled comments | the page's `Resolve` / `Reopen` on a note you wrote | The note keeps its place, its quote and its history and is written `> [!REVIEW resolved]`, where a later review and the model read it as closed; the panel counts `2 open · 1 resolved`, and every card names who wrote it. | Unreleased |
44
46
  | Section navigation | the review screen, `n`/`p` | The screen names the section you are in and jumps between headings. | Shipped |
45
47
  | Remote review | `exit_plan_mode` on a WeChat turn | The plan file is sent, then one compact prompt to approve, keep planning, or exit. | Shipped |
46
48
 
@@ -140,7 +142,15 @@ the cap and the fix named, and nothing is written.
140
142
 
141
143
  The file is the source of truth. On approval the model follows the plan file's
142
144
  steps, and refinements rewrite the same file (`write_plan` with `plan_path`), so
143
- the reviewed artifact is the executed artifact.
145
+ the reviewed artifact is the executed artifact. The text a refinement replaces is
146
+ kept beside it, as a dotfile named for the plan and the revision
147
+ (`.2026-10-26-1008-add-rate-limiting-r001.md`), up to the newest five: that is
148
+ what makes two revisions diffable, and what lets a comment `carryFeedback` moved
149
+ onto another block be checked against the text the reviewer actually read. A
150
+ revision is a dotfile, so no scan lists it as a plan, no `plan_path` opens it, and
151
+ the review never offers it for approval — and a refinement that changed nothing
152
+ keeps nothing, because the copy would be the plan itself. The write's result names
153
+ the revision it kept.
144
154
 
145
155
  ### `exit_plan_mode`
146
156
 
@@ -171,6 +181,13 @@ the todo list:
171
181
  todos, and its verification bullets are already covered.
172
182
  - Each seeded item is matched back to its step by text, so ticking a step with
173
183
  `set_plan_steps` completes the item too.
184
+ - An approval records the step texts it seeded, per plan path, as a `plan-seeded`
185
+ entry. A plan refined after approval is approved again — `write_plan` needs plan
186
+ mode, so a second round is the way back — and that second approval **replaces the
187
+ leftovers its own earlier approval made**: an untouched item whose step was
188
+ renamed or dropped goes, while one somebody started or finished is kept and
189
+ named, because deleting it would erase work a reader did. `write_plan` says the
190
+ list is stale when it refines a plan that was approved before.
174
191
 
175
192
  **Goal-aware.** While planning, the injected context restates a pursued session
176
193
  goal as one bullet, so the plan is designed against the objective instead of
@@ -265,7 +282,8 @@ its plans hold and how old the oldest one is, both derived from the walk that ju
265
282
  happened. The directory stays the index: no registry file, nothing deletes a plan,
266
283
  and one bounded read per plan.
267
284
 
268
- `/plan review` is the only subcommand that takes an argument. Bare `/plan review`
285
+ `/plan review` is the only subcommand that takes a path; `/plan share` takes the
286
+ one word `stop` after it, and `list` and `cancel` take nothing. Bare `/plan review`
269
287
  reviews the plan this session wrote, or, in a later session, the newest plan file
270
288
  in the plans directory (newest modification time); `/plan review <path>.md` (or a
271
289
  path with a separator) reviews that plan. A line that merely starts with the word
@@ -531,8 +549,12 @@ line range it shows, and the footer lists only the keys that apply.
531
549
  <img src="../../assets/screens/plan-review.png" alt="The browser-based plan review: a sticky header with the plan title, a status chip, and the Approve & run, Save feedback, and Back to Pi actions, a Share action, a side panel with step progress and a section outline, and the plan as one measured column with a comment rail holding the owner's own comment and a teammate's @priya comment" width="100%">
532
550
 
533
551
  Press `w` on the review screen — or run `/plan review [path]` — to open the plan
534
- in the default browser on `127.0.0.1`. The page is a reading-first review
535
- surface: a sticky header carrying the plan's H1 (falling back to the file-name
552
+ in the default browser on `127.0.0.1`. Its tab reads `plan review · <the plan's
553
+ title>` — the page's name as the header's lockup says it, then the subject, the shape
554
+ `lib/web/markup.ts`'s `tabTitle` gives every served page — rendered with the document
555
+ rather than set by a script, so a strip of tabs names the plan before the page has
556
+ finished loading. The page is a
557
+ reading-first review surface: a sticky header carrying the plan's H1 (falling back to the file-name
536
558
  slug), a live status chip (connection, comment count, and unsaved changes), the
537
559
  theme toggle, and the review actions, over a hairline reading-progress bar. A
538
560
  sticky side panel shows the plan's `Steps` section progress and comment count above a
@@ -542,6 +564,12 @@ reveals a **+ Comment** action on hover; select
542
564
  text first to quote it. **Save feedback** writes the comments into the plan
543
565
  file and returns them to the model.
544
566
 
567
+ - **A comment can be settled.** **Resolve** (on your own note) keeps it — its place, its
568
+ quote, its history — and marks it `> [!REVIEW resolved]` in the file, where a later review
569
+ and the model both read it as closed; **Reopen** puts it back. The panel counts
570
+ `2 open · 1 resolved`. A note you wrote is labelled **You** on your page and **The owner**
571
+ on somebody else's, a reviewer's is `@name`, and one sentence beside the facts says what
572
+ **Approve & run** and **Save feedback** each do.
545
573
  - **Shared theme.** The page uses the same Bytefly tokens, base rules, and
546
574
  syntax-token colours as `file-browse` (`lib/web/`), follows the system light/dark
547
575
  preference until the header toggle sets an explicit choice, and remembers that
@@ -601,7 +629,9 @@ file and returns them to the model.
601
629
  `DOMParser`, so a code line is never parsed as markup.
602
630
  - **The file is the store.** Each comment is written as
603
631
  `> [!REVIEW] <comment>` on the line after the block it anchors to, tagged with
604
- its author when a teammate wrote it (`> [!REVIEW] @alice: <comment>`). Saving is
632
+ its author when a teammate wrote it (`> [!REVIEW] @alice: <comment>`), and
633
+ marked `> [!REVIEW moved]` when a refinement carried it off the text it was
634
+ written on. Saving is
605
635
  idempotent per writer: that writer's previous markers are replaced and everyone
606
636
  else's are kept, so two people reviewing the same plan cannot erase each other.
607
637
  Stripping is fence-aware: a `> [!REVIEW]` line inside a code block is
@@ -617,7 +647,11 @@ file and returns them to the model.
617
647
  comment whose section is gone too moves to the plan's last block rather than
618
648
  disappearing, and every one of those placements lands beside a block, never after
619
649
  a blank line, so the page keeps the thread under its block. Only a plan with no
620
- block at all leaves a comment in the unattached list.
650
+ block at all leaves a comment in the unattached list. Everything past the first
651
+ step is a guess, so a placement that is not the anchored text itself is marked:
652
+ the card carries a `moved` chip whose tooltip says the block changed and the
653
+ note was placed here, and the marker records it in the file, so the fact
654
+ survives a reload, a hand-off, and a save by its own author.
621
655
  - **Anchored to source.** `blocks.ts` splits the plan (fence-aware) into
622
656
  source-line ranges; a comment is placed after the whole anchored block, so a
623
657
  fenced code block is never split. A save re-checks the file against what the
@@ -628,14 +662,15 @@ file and returns them to the model.
628
662
  save that carries no comments asks for nothing, so the review screen comes back
629
663
  instead. `/plan review` sends them as a user message (a plan-mode revision or a
630
664
  plain update, depending on the mode).
631
- - **Loopback first, tokenized.** The server binds `127.0.0.1`, carries a random
632
- per-session token in the URL, rejects a `Host` header it was not bound on,
633
- refuses a cross-origin POST, serves only `GET`, `HEAD`, and `POST`, and caps the
634
- request body. Sharing adds a second listener (below); it never widens the first. A save or cancel
635
- resolves the session only after its HTTP response is delivered, so the browser
636
- never sees a false "Failed to fetch". The last bound port is remembered per
637
- plan, so reviewing the same plan again reuses the origin (and the browser's
638
- saved theme with it). It is closed on the owner's save or cancel, on the owner's
665
+ - **Loopback first, tokenized.** The review mounts `/plan/<id>` on the session's one
666
+ server, which binds `127.0.0.1`; the URL carries a random per-session token, a `Host`
667
+ header the server was not bound on is rejected, a cross-origin POST is refused, only
668
+ `GET`, `HEAD`, and `POST` are served, and the request body is capped. Sharing asks that
669
+ mount for one LAN listener (below); it never widens the loopback socket. A save or
670
+ cancel resolves the session only after its HTTP response is delivered, so the browser
671
+ never sees a false "Failed to fetch". The prefix is the review's, on the session's one
672
+ origin, so the browser's saved theme survives a review. It is closed on the owner's
673
+ save or cancel, on the owner's
639
674
  page going away, or on `session_shutdown`; a reload reconnects inside that grace,
640
675
  so refreshing or reloading a conflicted page does not end the review. A
641
676
  reviewer's tab opening or closing is not the review's lifetime.
@@ -705,6 +740,12 @@ both.
705
740
  Git Bash (`bash`), which must be installed for any shell command to run.
706
741
  - The read-only guarantee is a guard rail against accidental writes, not a
707
742
  security boundary (see [Security](#security)).
743
+ - **A carried comment's new home is a guess, and only the page says so.** A note a
744
+ refinement could not pin to surviving text is placed by wording overlap, by the
745
+ gap it left, or at the plan's last block; the `moved` chip is what tells a
746
+ reviewer that. A reader of the plan file alone sees `> [!REVIEW moved]`, and the
747
+ terminal review shows the comment as a quote with no chip, so the marker is the
748
+ fact and the chip is one reader's way of showing it.
708
749
  - The plans directory is the only index. `/plan list` walks it on every open and
709
750
  keeps no registry, so it sees an external edit or a hand-copied plan
710
751
  immediately; it lists a file over the 256 KiB plan limit without reading it,
@@ -734,6 +775,10 @@ both.
734
775
  refuses a file it did not render (`409`). It is a poll rather than a file
735
776
  watcher, so a change on a filesystem that reports nothing at all is still seen
736
777
  at the next request, because every page read re-compares the file.
778
+ - **A settled note is a flag in the marker.** `> [!REVIEW resolved]` (and
779
+ `moved resolved`) is read as open when absent; a build from before this one does not know
780
+ the flag and shows the marker line as body text, so a file reviewed on both sides of the
781
+ upgrade reads differently there.
737
782
  - A shared review holds at most eight links. Each is a live bearer credential, so
738
783
  a ninth is refused with the cap named rather than minted.
739
784
 
@@ -897,7 +942,8 @@ both.
897
942
  | `types.ts` | `PlanModeEntry`, `EnterPlanModeDetails`, `WritePlanDetails`, `ExitPlanModeDetails`, `CancelPlanModeDetails`, `SetPlanStepsDetails`. |
898
943
  | `policy.ts` | Plan mode's read-only policy, the shared prompt summary, and the bash-refusal guidance. |
899
944
  | `git-bash.ts` | `checkReadOnlyGit`: the pure allowlist that restricts `bash` to read-only git commands. |
900
- | `plans.ts` | Plan-file store: slug/stamp naming, directory resolution (repository, active worktree, or agent directory), containment, atomic `write`/`setSteps`/`saveFeedback`, and `read`/`latest`. |
945
+ | `plans.ts` | Plan-file store: slug/stamp naming, directory resolution (repository, active worktree, or agent directory), containment, `write`/`setSteps`/`saveFeedback`, and `read`/`latest`. |
946
+ | `plan-files.ts` | How a plan file reaches the disk: `writeAtomic` (a dotfile temp, then a rename, so a crash cannot truncate the plan a review is reading) and `keepRevision`, which keeps the text a refinement replaces beside it, numbered, up to the newest five. |
901
947
  | `todos.ts` | Approval-time todo merge (pure): the last list replayed from the branch, and the merge of the plan's steps into it with the cap respected. |
902
948
  | `steps.ts` | Plan-document structure: the outline, step extraction and the `Steps`-section scope, the plan's own goal (`goalSection`), and the checkbox edit. |
903
949
  | `sections.ts` | The one fence-aware heading scan: `extractSections`, `sectionKeys` (the key that tells two identically titled sections apart), and the `HEADING` / `FENCE` / `normalizeHeadingText` vocabulary `blocks.ts` reads a heading through. |
@@ -913,18 +959,27 @@ both.
913
959
  | `plan-index.ts` | `/plan list`: the notice text and the docked index screen (store size and oldest plan, title, step progress, age, focused path; Enter reviews). |
914
960
  | `blocks.ts` | Source-line block model (fence-aware headings, paragraphs, list items, code, quotes, tables, rules, blanks), and `contentBlockAt`: the block with content a comment beside blank space belongs to. |
915
961
  | `feedback.ts` | Comment codec: authored markers, `stripFeedback`, `readFeedback`/`parseFeedback` (body, blocks, and comments out of one parse), `formatFeedback` (writer-scoped), `displayFeedback`, `carryFeedback`, `feedbackSummary`, and payload validation. |
916
- | `web.ts` | `createPlanWebReview`: the review session (per-token roles, the change stream, the plan-file poll, the body cap, settle-after-flush, abort) over `lib/http`'s one loopback server. |
962
+ | `web.ts` | `createPlanWebReview`: the review session (per-token roles, the change stream, the plan-file poll, the body cap, settle-after-flush, abort) over `lib/http`'s one loopback server, and the router wiring — the island is `web-payload.ts` and the writes are `web-write.ts`. |
917
963
  | `web-tokens.ts` | The token table: the role each token names, the collision rule that keeps two roles off one token, and the names the other live links have bound. |
918
964
  | `web-streams.ts` | The event streams: who is listening, what a frame is addressed to, and the closed-tab grace that ends the review when the owner's page goes away. |
919
965
  | `web-router.ts` | Pure request handler: token role, `Host` allowlist, origin, and method checks through one POST-and-role guard, then the page, comment, owner-save, approve, cancel, and the four share routes. |
920
- | `web-page.ts` | The review page shell (header, reading progress, review facts, section outline, shortcut sheet, approve confirmation, share and name dialogs), its data island, and inline CSS. |
966
+ | `web-page.ts` | The review page shell (header, reading progress, review facts, section outline, shortcut sheet, approve confirmation, share and name dialogs) and inline CSS. |
967
+ | `web-payload.ts` | The page's data island: `ReviewPageData` and `reviewPageData`, which builds every fact the browser renders — the blocks, the stored comments, the outline and its change marks, the step progress, the highlighted code, the caps, and who is asking — from one read of the plan, with no server or socket in sight. |
968
+ | `web-write.ts` | The review's two write paths over the plan store: `saveOwnerAnnotations` (the owner's save, which returns the plan it wrote) and `postReviewerAnnotations` (a reviewer's post, signed and scoped to its link), sharing one 404/409 check so a comment can never land beside text nobody read. |
921
969
  | `web-share.ts` | Pure sharing rules: the reachable LAN addresses from an injected snapshot, one tokenized URL, the link table (mint, find, revoke, list, state) with its cap, and the reasons a share is unavailable. |
922
970
  | `web-share-session.ts` | The shared listener: its bound address, the link table over the session's tokens, `ShareState`, and invite/revoke/stop/close, injected with the request handler, role map, and frame hooks. |
923
- | `web-client.ts` | The `/client.js` source: state and wiring (comment rail, save/approve/back, keyboard nav, the stream's lifetime) over the three fragments below. |
971
+ | `web-keys.ts` | The client's keyboard as typed source (`WEB_KEYS_JS`), and the one table the page's keys come from: `webKeys()` gives each binding its keys, the shortcut sheet's sentence, the sidebar hint's words and whether the page offers it — a function rather than a constant, because the bundle carries a body and `String()` of a table would drop the closures — read by `handleKey` (Esc peels the composer before closing, a field and the sheet own their keys, `s` is the write the page offers) and by the sheet and hint line the page renders; plus `rememberSelection`, which keeps the text a reader selected as the next comment's quote, and the `keyRows`/`surfaceOf` the handler and the page's own sheet both read, spliced with them for the same reason. |
972
+ | `web-settle.ts` | The client's settlement as typed source (`WEB_SETTLE_JS`): `submitAnnotations` — one POST shape whose 409 disables the writes, offers a reload and says the server's own sentence — and the four ways a review ends, of which only the owner's save, approve and close settle it. |
973
+ | `web-plan.ts` | The client's frame as typed source (`WEB_PLAN_JS`, `renderPlanFrame`): it draws every block and its comment rail, then puts the reader back — focus only when it was inside the plan, the active block in place rather than by scrolling. Reads arrive as getters, mutations leave as callbacks, so the bundle keeps every variable. |
974
+ | `web-composer.ts` | The client's composer lifecycle as typed source (`WEB_COMPOSER_JS`): `closeComposer` (over one `ComposerDeps` the client builds where the state is), `reopenComposer` for the draft a reload interrupted, and `commentOnActive`, which returns the index so the client keeps its one `activeIndex`. |
975
+ | `web-draft.ts` | The client's draft guard as typed source (`WEB_DRAFT_JS`): `draftTextarea` answers the open composer's textarea when it holds text, and `guardDraft` refuses the ways off the page, names the reason once, and puts the cursor back in it. |
976
+ | `web-blocks.ts` | The client's block navigation as typed source (`WEB_BLOCKS_JS`): `blockElements`, `setActive` and `moveActive` — which return the index they computed, so the client keeps the one `activeIndex` — and `blockAtLine`, which needs both the rendered block and its island entry. |
977
+ | `web-client.ts` | The `/client.js` source: the composition root, and nothing else by decision — the element handles, the state each rule is handed, one zero-argument adapter per rule, the listeners that attach them, the `EventSource` that keeps a tab's presence known, and the boot order. Every rule it used to hold lives in the `web-*.ts` modules below, where it can be typed and driven. |
924
978
  | `web-comments.ts` | The client's comment half: selection quotes, the composer, authored cards and who may edit them, delete-with-undo. |
925
979
  | `web-collab.ts` | The client's collaboration half: the link list with copy and per-link revoke, the reviewer's name, and the island refetch a stream frame triggers. |
926
980
  | `web-markdown.ts` | The client's document half: `safeHref` and the inline/table/code/list rendering (interpolated into `web-client.ts`). |
927
981
  | `web-chrome.ts` | The client's chrome half: the state chip, the status line, the header, and reading progress (interpolated into `web-client.ts`). |
982
+ | `web-outline.ts` | The client's outline as typed source (`WEB_OUTLINE_JS`): `sectionFor` — the last heading at or before a line — `renderOutline`, which builds the list and hands back the counters it made, `observeSections`, which watches the blocks and returns the one watcher the client keeps, and `renderFacts`, which paints the step progress and counts each comment under its heading. |
928
983
  | `path-guard.ts` | Recursive path-argument detection for the read-only backstop. |
929
984
  | [`../../lib/browser.ts`](../../lib/browser.ts) | Cross-platform default-browser opener (shared with `file-browse`). |
930
985
  | [`../../lib/plan-entry.ts`](../../lib/plan-entry.ts) | Plan-mode entry shared with `goal`: the read-only summary, the consent sentence, and the `pi.events` request `plan` answers by enabling the mode. |
@@ -943,7 +998,7 @@ both.
943
998
  bun test extensions/plan
944
999
  ```
945
1000
 
946
- Eleven co-located suites, declared in `test/suite-manifest.ts`:
1001
+ Twelve co-located suites, declared in `test/suite-manifest.ts`:
947
1002
 
948
1003
  - `plans.test.ts` — the plan-file store: naming, directory resolution, atomic
949
1004
  writes, `read`/`latest`/`index`, containment, `setSteps`, `saveFeedback`, and
@@ -955,9 +1010,16 @@ Eleven co-located suites, declared in `test/suite-manifest.ts`:
955
1010
  `todo`, the session goal, and the call/result renderings.
956
1011
  - `review-prompt.test.ts` — the compact remote prompt and its reply parser
957
1012
  against one action table.
958
- - `web.test.ts` — the browser review: the comment codec, the router, the page
959
- roles and page shell, the client script, the stream, the plan-file poll, the
960
- share rules, and a real loopback session.
1013
+ - `web-*.test.ts` — the browser review, one capability per suite: the comment
1014
+ codec and the router (`web-router`), the page roles and its shell (`web-page`),
1015
+ the client script's rules (`web-client`, `web-outline`, `web-composer`,
1016
+ `web-settlement`), the stream and the plan-file poll (`web-stream`), the
1017
+ loopback session (`web-session`), and the share rules (`web-shared-review`).
1018
+ - `web-behaviour.test.ts` — the served review, driven in a real browser through
1019
+ the router: the blocks and the plan's name, a block's comment control opening the
1020
+ composer with Escape peeling it before the review, a stored comment in its block's
1021
+ rail and the header's count, and the outline jumping to the section it names. It
1022
+ skips where no browser is installed, naming `PI_CHROME`.
961
1023
  - `web-share.test.ts` — the LAN address selector, `shareUrl`, the link table with
962
1024
  its cap, and the reasons a share is unavailable.
963
1025
  - `commands.test.ts` — `/plan review`, `/plan list`, `/plan share`, and the one
@@ -0,0 +1,129 @@
1
+ # serve — the session's one loopback server
2
+
3
+ ## Capabilities
4
+
5
+ | Capability | How | What you get |
6
+ |---|---|---|
7
+ | One port for every page of a session | automatic: the first page that opens binds it | the file browser at `/files/`, the board at `/board/`, and a review at `/plan/<id>/` answer on one origin, so a bookmark, a firewall rule, and a port to remember are one thing |
8
+ | One chip for that port | `◉ <port>` in the status bar | the session's port while anything is served on it, and nothing when no page is open |
9
+ | A port that survives a restart | `serve.port` in `bytefly.json` | the same address after `/resume`, so a saved URL still opens the page it names |
10
+ | A second session on its own port | automatic: servers are keyed by Pi session | a WeChat peer conversation never answers on this session's port |
11
+ | One page reachable on the LAN, on request | the asking page calls `mount.share()` | a teammate's link names exactly one address, and only that page answers there |
12
+
13
+ ## Quickstart
14
+
15
+ There is nothing to run: `serve` is infrastructure, and the pages that use it are
16
+ `/board`, `/browse`, and `/plan review`.
17
+
18
+ ```bash
19
+ /board # http://127.0.0.1:4780/board/?token=…
20
+ /browse # http://127.0.0.1:4780/files/
21
+ /plan review # http://127.0.0.1:4780/plan/3f9c21aa/?token=…
22
+ ```
23
+
24
+ All three answer on `:4780`; the footer shows `◉ 4780` while any of them is open
25
+ and drops the chip when the last one closes.
26
+
27
+ ## Routes
28
+
29
+ The server holds prefixes, not pages. Each one is claimed by the extension that
30
+ serves it, and a request reaches that extension with its prefix stripped:
31
+
32
+ | Prefix | Served by | Note |
33
+ |---|---|---|
34
+ | `/files/` | `file-browse` | the file browser: its tree, its file pages, its own script, and the links it rewrites out of Markdown, all built from this prefix; `/files/browse/<path>` is one directory |
35
+ | `/board/` | `board` | the read-only board page, with its token in the query |
36
+ | `/plan/<id>/` | `plan` | one review; the id is the review's, so two reviews never share a namespace |
37
+
38
+ Anything else is a `404` with a plain-text body, and a prefix named without its
39
+ trailing slash (`/board`) is redirected to the slashed form, because a page's own
40
+ links are relative to its directory. A prefix another page already holds is
41
+ refused by name rather than served twice.
42
+
43
+ ## Behaviour by mode
44
+
45
+ The server runs in every mode, but the chip needs a UI: RPC and JSON sessions serve
46
+ their pages and skip it. Nothing here reads or writes the transcript.
47
+
48
+ ## Configuration
49
+
50
+ The `serve` section of the shared `bytefly.json` (global, then project, project wins:
51
+
52
+ | Key | Default | What it does |
53
+ |---|---|---|
54
+ | `port` | `0` | `0` takes one free port in `4780`–`5779` for the session and remembers it; a positive value pins the session's server so a bookmarked address survives a restart |
55
+
56
+ The pin belongs to the first session in a process that binds: a second session — a
57
+ WeChat peer conversation loads every extension except the bridge itself — takes an
58
+ OS-chosen port instead of failing on a port the host already holds. A second
59
+ *process* that finds the pinned port taken is told so rather than moved silently off
60
+ the address a bookmark names.
61
+
62
+ ## Security
63
+
64
+ The server binds `127.0.0.1`. The unit of exposure is the mount: a prefix answers
65
+ only what its page routes, and each page keeps its own `Host` allowlist, its own
66
+ token, and its own method list in `lib/http`.
67
+
68
+ A shared review is the one exception, and it is the reviewing page's own: it asks
69
+ for a listener on a single LAN address, and that listener carries only the prefixes
70
+ that asked for it. `file-browse` and `board` are never reachable that way, and a page
71
+ that stops sharing leaves the listener at once — the socket closes with the last page
72
+ that was still sharing it.
73
+
74
+ ## Limitations
75
+
76
+ - A page needs `serve` loaded. Without it every page refuses with a sentence naming
77
+ the extension, instead of binding a rival socket.
78
+ - One pin is one port for the whole session: two repositories running pages on this
79
+ machine at once need a project-level `serve.port` each, or neither pinned.
80
+ - The port moves to an OS-chosen one when the pinned port is taken, so an old
81
+ bookmark keeps failing until the setting or the other process is dealt with.
82
+
83
+ ## Non-goals
84
+
85
+ - It serves no page of its own, and registers no tool and no command.
86
+ - It is not a general web server: loopback, a page's own routes, one optional LAN
87
+ address per shared page, and nothing else.
88
+ - It does not proxy or rewrite; a page's paths are its own, under its prefix.
89
+
90
+ ## Pi integration
91
+
92
+ - Registers `pi.events.on(HTTP_MOUNT_CHANNEL, …)` in the factory — registration only.
93
+ Binding happens on the first mount, never at load and never on `session_start`.
94
+ - `session_start` records the session's working directory and its context, so the
95
+ pin is read from that session's config and the chip is drawn in its UI.
96
+ - `session_shutdown` closes the session's socket, its LAN listener, and its chip.
97
+ It is idempotent.
98
+
99
+ ## Design notes
100
+
101
+ A session has one port, so the socket cannot belong to a page. The page asks for a
102
+ prefix over `pi.events` — the mechanism the repository already uses for a
103
+ cross-extension request that must be answered — and `lib/http` owns the socket, the
104
+ dispatch, the body cap per mount, and the idle grace per mount.
105
+
106
+ The handshake is *claimed*, not awaited: `server.listen` cannot answer synchronously,
107
+ so the owner responds with the pending bind and only a request nobody claimed is
108
+ refused. Without that distinction a bind that is still starting looks exactly like a
109
+ session with no server.
110
+
111
+ ## Files
112
+
113
+ | Path | Responsibility |
114
+ |---|---|
115
+ | [`../../extensions/serve/index.ts`](../../extensions/serve/index.ts) | The Pi wiring: the bus listener, the per-session directory and context, the port chip, and the shutdown |
116
+ | [`../../extensions/serve/registry.ts`](../../extensions/serve/registry.ts) | The session's servers: binding on first mount, the pin's owner, the remembered port, prefix refusals, the LAN listener a shared page borrows, and closing with the last mount |
117
+ | [`../../extensions/serve/config.ts`](../../extensions/serve/config.ts) | The `serve` section: `port`, its clamp, and the two documents it resolves over |
118
+
119
+ ## Testing
120
+
121
+ `config.test.ts`, `registry.test.ts`, and `extension.test.ts`, declared under
122
+ `serve` in `test/suite-manifest.ts`.
123
+
124
+ The registry cases bind real sockets on `127.0.0.1`: one port for two pages of a
125
+ session, a second session on its own port, a refused duplicate prefix, the socket
126
+ closing with its last page and reopening where it was, the pin belonging to the
127
+ first session that binds, and a LAN share reaching one page only. The extension
128
+ cases drive the shipped bus path through `requestMount`, including the chip and the
129
+ shutdown.