@gavin-hu/my-pi-agent 0.18.0 → 0.19.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 (249) hide show
  1. package/README.md +52 -44
  2. package/README.zh-CN.md +46 -41
  3. package/docs/extensions/README.md +34 -0
  4. package/docs/extensions/board.md +344 -0
  5. package/{extensions/bytefly/README.md → docs/extensions/bytefly.md} +446 -404
  6. package/{extensions/check/README.md → docs/extensions/check.md} +83 -68
  7. package/{extensions/context/README.md → docs/extensions/context.md} +40 -23
  8. package/docs/extensions/file-browse.md +554 -0
  9. package/{extensions/file-open/README.md → docs/extensions/file-open.md} +31 -23
  10. package/{extensions/file-read/README.md → docs/extensions/file-read.md} +47 -55
  11. package/{extensions/goal/README.md → docs/extensions/goal.md} +115 -131
  12. package/{extensions/job/README.md → docs/extensions/job.md} +61 -50
  13. package/{extensions/memory/README.md → docs/extensions/memory.md} +71 -60
  14. package/{extensions/plan/README.md → docs/extensions/plan.md} +390 -317
  15. package/{extensions/question/README.md → docs/extensions/question.md} +54 -31
  16. package/{extensions/rewind/README.md → docs/extensions/rewind.md} +115 -122
  17. package/docs/extensions/session-bus.md +285 -0
  18. package/{extensions/subagent/README.md → docs/extensions/subagent.md} +165 -118
  19. package/docs/extensions/team.md +226 -0
  20. package/{extensions/todo/README.md → docs/extensions/todo.md} +85 -88
  21. package/{extensions/watch/README.md → docs/extensions/watch.md} +54 -36
  22. package/{extensions/web-access/README.md → docs/extensions/web-access.md} +93 -50
  23. package/{extensions/wechat/README.md → docs/extensions/wechat.md} +258 -162
  24. package/{extensions/worktree/README.md → docs/extensions/worktree.md} +115 -72
  25. package/extensions/board/commands.ts +87 -0
  26. package/extensions/board/config.ts +36 -0
  27. package/extensions/board/format.ts +72 -0
  28. package/extensions/board/index.ts +40 -0
  29. package/extensions/board/input.ts +54 -0
  30. package/extensions/board/join.ts +42 -0
  31. package/extensions/board/page.ts +312 -0
  32. package/extensions/board/result.ts +99 -0
  33. package/extensions/board/rows.ts +48 -0
  34. package/extensions/board/schema.ts +75 -0
  35. package/extensions/board/seat.ts +29 -0
  36. package/extensions/board/snapshot.ts +41 -0
  37. package/extensions/board/tools.ts +279 -0
  38. package/extensions/board/transport/router.ts +172 -0
  39. package/extensions/board/transport/server.ts +149 -0
  40. package/extensions/board/types.ts +54 -0
  41. package/extensions/board/view/actions.ts +306 -0
  42. package/extensions/board/view/client.ts +41 -0
  43. package/extensions/board/view/keys.ts +121 -0
  44. package/extensions/board/view/live.ts +397 -0
  45. package/extensions/board/view/page.ts +158 -0
  46. package/extensions/board/view/render.ts +496 -0
  47. package/extensions/board/view/styles.ts +211 -0
  48. package/extensions/board/view/team.ts +305 -0
  49. package/extensions/board/writes.ts +99 -0
  50. package/extensions/bytefly/panel/commands.ts +40 -7
  51. package/extensions/bytefly/panel/settings-runtime.ts +0 -6
  52. package/extensions/bytefly/panel/settings.ts +3 -20
  53. package/extensions/bytefly/panel/tui/settings-view.ts +1 -1
  54. package/extensions/bytefly/picker/discovery.ts +0 -2
  55. package/extensions/bytefly/picker/types.ts +0 -2
  56. package/extensions/bytefly/update/check.ts +39 -0
  57. package/extensions/bytefly/update/format.ts +15 -0
  58. package/extensions/bytefly/update/register.ts +73 -34
  59. package/extensions/bytefly/update/state.ts +6 -3
  60. package/extensions/context/tui.ts +1 -1
  61. package/extensions/file-browse/{config.ts → config/config.ts} +1 -1
  62. package/extensions/file-browse/{files.ts → files/files.ts} +1 -14
  63. package/extensions/file-browse/{paths.ts → files/paths.ts} +1 -1
  64. package/extensions/file-browse/{git.ts → git/git.ts} +10 -3
  65. package/extensions/file-browse/index.ts +4 -4
  66. package/extensions/file-browse/{live.ts → live/live.ts} +20 -69
  67. package/extensions/file-browse/{router.ts → transport/router.ts} +21 -15
  68. package/extensions/file-browse/transport/server.ts +110 -0
  69. package/extensions/file-browse/{chrome.ts → view/chrome.ts} +4 -4
  70. package/extensions/file-browse/{client.ts → view/client.ts} +1 -1
  71. package/extensions/file-browse/{diff-html.ts → view/diff-html.ts} +3 -3
  72. package/extensions/file-browse/{html.ts → view/html.ts} +14 -12
  73. package/extensions/file-browse/{icons.ts → view/icons.ts} +1 -1
  74. package/extensions/file-browse/{markdown.ts → view/markdown.ts} +7 -7
  75. package/extensions/file-browse/{raw-html.ts → view/raw-html.ts} +1 -1
  76. package/extensions/file-browse/{styles.ts → view/styles.ts} +29 -5
  77. package/extensions/file-browse/{tree.ts → view/tree.ts} +1 -1
  78. package/extensions/file-browse/{urls.ts → view/urls.ts} +1 -1
  79. package/extensions/file-read/schema.ts +2 -1
  80. package/extensions/memory/store.ts +12 -5
  81. package/extensions/plan/blocks.ts +23 -2
  82. package/extensions/plan/commands.ts +21 -16
  83. package/extensions/plan/feedback.ts +147 -38
  84. package/extensions/plan/index.ts +6 -4
  85. package/extensions/plan/plan-index.ts +43 -9
  86. package/extensions/plan/plan-text.ts +33 -23
  87. package/extensions/plan/plans.ts +8 -11
  88. package/extensions/plan/policy.ts +20 -12
  89. package/extensions/plan/review.ts +3 -27
  90. package/extensions/plan/runtime.ts +21 -9
  91. package/extensions/plan/sections.ts +112 -0
  92. package/extensions/plan/steps.ts +3 -22
  93. package/extensions/plan/tools/exit.ts +11 -95
  94. package/extensions/plan/tui.ts +5 -34
  95. package/extensions/plan/types.ts +0 -8
  96. package/extensions/plan/web-client.ts +1 -1
  97. package/extensions/plan/web-page.ts +0 -7
  98. package/extensions/plan/web-router.ts +3 -57
  99. package/extensions/plan/web-share-session.ts +24 -30
  100. package/extensions/plan/web-streams.ts +14 -14
  101. package/extensions/plan/web-tokens.ts +2 -2
  102. package/extensions/plan/web.ts +66 -120
  103. package/extensions/rewind/{commands.ts → commands/commands.ts} +5 -5
  104. package/extensions/rewind/{review.ts → commands/review.ts} +13 -13
  105. package/extensions/rewind/{rewind.ts → commands/rewind.ts} +6 -6
  106. package/extensions/rewind/{config.ts → config/config.ts} +1 -1
  107. package/extensions/rewind/index.ts +3 -3
  108. package/extensions/rewind/{policy.ts → state/policy.ts} +4 -4
  109. package/extensions/rewind/{runtime.ts → state/runtime.ts} +11 -11
  110. package/extensions/rewind/{snapshot-run.ts → state/snapshot-run.ts} +1 -1
  111. package/extensions/rewind/{timeline.ts → state/timeline.ts} +2 -2
  112. package/extensions/rewind/{format.ts → text/format.ts} +3 -3
  113. package/extensions/rewind/{git.ts → transport/git.ts} +2 -2
  114. package/extensions/rewind/{restore.ts → transport/restore.ts} +2 -2
  115. package/extensions/rewind/{snapshot.ts → transport/snapshot.ts} +1 -1
  116. package/extensions/rewind/{store.ts → transport/store.ts} +1 -1
  117. package/extensions/rewind/{checklist.ts → view/checklist.ts} +6 -6
  118. package/extensions/rewind/{hunk-picker.ts → view/hunk-picker.ts} +8 -8
  119. package/extensions/rewind/{path-picker.ts → view/path-picker.ts} +7 -7
  120. package/extensions/rewind/{review-tui.ts → view/review-tui.ts} +11 -11
  121. package/extensions/rewind/{rewind-tui.ts → view/rewind-tui.ts} +17 -11
  122. package/extensions/rewind/{status.ts → view/status.ts} +4 -4
  123. package/extensions/session-bus/commands.ts +17 -69
  124. package/extensions/session-bus/format.ts +22 -45
  125. package/extensions/session-bus/index.ts +13 -7
  126. package/extensions/session-bus/join-request.ts +40 -0
  127. package/extensions/session-bus/mail-watch.ts +134 -0
  128. package/extensions/session-bus/notice.ts +32 -4
  129. package/{lib/claims → extensions/session-bus}/overlap.ts +11 -18
  130. package/extensions/session-bus/protocol.ts +19 -49
  131. package/extensions/session-bus/resolve.ts +67 -0
  132. package/extensions/session-bus/result.ts +16 -51
  133. package/extensions/session-bus/rows.ts +4 -2
  134. package/extensions/session-bus/runtime.ts +129 -238
  135. package/extensions/session-bus/store.ts +42 -6
  136. package/extensions/session-bus/tools.ts +31 -66
  137. package/extensions/session-bus/tui.ts +4 -4
  138. package/extensions/session-bus/types.ts +13 -13
  139. package/extensions/subagent/agents.ts +9 -8
  140. package/extensions/subagent/invocation.ts +4 -2
  141. package/extensions/team/commands.ts +112 -0
  142. package/extensions/team/dispatch.ts +133 -0
  143. package/extensions/team/format.ts +105 -0
  144. package/extensions/team/index.ts +33 -0
  145. package/extensions/team/schema.ts +29 -0
  146. package/extensions/team/tools.ts +246 -0
  147. package/extensions/team/types.ts +48 -0
  148. package/extensions/web-access/fetch/page.ts +4 -1
  149. package/extensions/web-access/fetch/structured.ts +447 -0
  150. package/extensions/web-access/fetch/tool.ts +2 -1
  151. package/extensions/web-access/search/tool.ts +2 -1
  152. package/extensions/wechat/bridge.ts +76 -92
  153. package/extensions/wechat/{commands.ts → commands/commands.ts} +26 -18
  154. package/extensions/wechat/{doctor.ts → commands/doctor.ts} +5 -5
  155. package/extensions/wechat/{history.ts → commands/history.ts} +1 -1
  156. package/extensions/wechat/{login.ts → commands/login.ts} +3 -3
  157. package/extensions/wechat/{slash.ts → commands/slash.ts} +38 -2
  158. package/extensions/wechat/dialog/channels.ts +136 -0
  159. package/extensions/wechat/{interaction.ts → dialog/interaction.ts} +37 -37
  160. package/extensions/wechat/{prompt.ts → dialog/prompt.ts} +2 -2
  161. package/extensions/wechat/{remote-ui.ts → dialog/remote-ui.ts} +1 -1
  162. package/extensions/wechat/index.ts +10 -10
  163. package/extensions/wechat/{admission.ts → peer/admission.ts} +2 -2
  164. package/extensions/wechat/{peer-session.ts → peer/peer-session.ts} +2 -2
  165. package/extensions/wechat/{peer-worker.ts → peer/peer-worker.ts} +100 -22
  166. package/extensions/wechat/{presence.ts → peer/presence.ts} +8 -7
  167. package/extensions/wechat/{reply-pipeline.ts → peer/reply-pipeline.ts} +1 -1
  168. package/extensions/wechat/{credentials.ts → state/credentials.ts} +2 -2
  169. package/extensions/wechat/{state.ts → state/state.ts} +14 -2
  170. package/extensions/wechat/{format.ts → text/format.ts} +3 -3
  171. package/extensions/wechat/{notices.ts → text/notices.ts} +8 -0
  172. package/extensions/wechat/tools.ts +29 -12
  173. package/extensions/wechat/{client.ts → transport/client.ts} +26 -2
  174. package/extensions/wechat/{deliver.ts → transport/deliver.ts} +72 -28
  175. package/extensions/wechat/{inbound.ts → transport/inbound.ts} +5 -5
  176. package/extensions/wechat/{media.ts → transport/media.ts} +11 -5
  177. package/extensions/wechat/transport/outbound.ts +142 -0
  178. package/extensions/wechat/{poll.ts → transport/poll.ts} +3 -3
  179. package/extensions/wechat/types.ts +10 -1
  180. package/extensions/wechat/{login-screen.ts → view/login-screen.ts} +4 -4
  181. package/extensions/wechat/{status-screen.ts → view/status-screen.ts} +31 -26
  182. package/extensions/worktree/guard.ts +3 -5
  183. package/lib/README.md +8 -6
  184. package/lib/board/card.ts +290 -0
  185. package/lib/board/links.ts +117 -0
  186. package/lib/board/page-pointer.ts +256 -0
  187. package/lib/board/round-text.ts +72 -0
  188. package/lib/board/round.ts +89 -0
  189. package/lib/board/rules.ts +304 -0
  190. package/lib/board/session.ts +105 -0
  191. package/lib/board/store.ts +310 -0
  192. package/lib/board/view.ts +150 -0
  193. package/lib/errors.ts +12 -4
  194. package/lib/format.ts +13 -0
  195. package/lib/http/guard.ts +112 -0
  196. package/lib/http/index.ts +16 -5
  197. package/lib/http/serve.ts +234 -0
  198. package/lib/http/server.ts +12 -10
  199. package/lib/http/sink.ts +110 -0
  200. package/lib/http/token.ts +38 -0
  201. package/lib/policy.ts +12 -2
  202. package/lib/requirements.ts +6 -7
  203. package/lib/session-label.ts +64 -0
  204. package/lib/session-roster.ts +130 -0
  205. package/lib/session-tool.ts +184 -0
  206. package/lib/tool-names.ts +25 -2
  207. package/lib/tui/glyphs.ts +2 -0
  208. package/lib/web/README.md +5 -2
  209. package/lib/web/components.ts +5 -0
  210. package/lib/web/runtime.ts +7 -1
  211. package/lib/web/tokens.ts +25 -2
  212. package/package.json +26 -14
  213. package/skills/next/SKILL.md +4 -3
  214. package/skills/project-docs/SKILL.md +133 -0
  215. package/skills/project-docs/references/audit.md +131 -0
  216. package/skills/project-docs/references/changelog.md +105 -0
  217. package/skills/project-docs/references/discovery.md +127 -0
  218. package/skills/project-docs/references/readme.md +80 -0
  219. package/skills/project-docs/references/release.md +84 -0
  220. package/skills/project-docs/references/roadmap.md +93 -0
  221. package/skills/project-docs/scripts/state.ts +316 -0
  222. package/skills/refactor/SKILL.md +100 -125
  223. package/skills/refactor/references/handoff.md +35 -0
  224. package/skills/refactor/references/signals.md +54 -0
  225. package/skills/refactor/scripts/inventory.ts +587 -0
  226. package/extensions/file-browse/README.md +0 -506
  227. package/extensions/file-browse/server.ts +0 -141
  228. package/extensions/plan/split.ts +0 -87
  229. package/extensions/session-bus/README.md +0 -247
  230. package/extensions/session-bus/team.ts +0 -102
  231. package/extensions/wechat/outbound.ts +0 -100
  232. package/lib/plans/sections.ts +0 -172
  233. package/lib/plans/setting.ts +0 -52
  234. package/lib/plans/split.ts +0 -215
  235. package/lib/plans/store.ts +0 -19
  236. package/lib/plans/team.ts +0 -151
  237. package/skills/changelog/SKILL.md +0 -148
  238. package/skills/parallel-dev/SKILL.md +0 -150
  239. /package/extensions/file-browse/{rev.ts → live/rev.ts} +0 -0
  240. /package/extensions/file-browse/{diff.ts → text/diff.ts} +0 -0
  241. /package/extensions/file-browse/{fuzzy.ts → text/fuzzy.ts} +0 -0
  242. /package/extensions/file-browse/{live-client.ts → view/live-client.ts} +0 -0
  243. /package/extensions/file-browse/{palette.ts → view/palette.ts} +0 -0
  244. /package/extensions/rewind/{patch.ts → text/patch.ts} +0 -0
  245. /package/extensions/rewind/{paths.ts → text/paths.ts} +0 -0
  246. /package/extensions/rewind/{queue.ts → transport/queue.ts} +0 -0
  247. /package/extensions/rewind/{temp-index.ts → transport/temp-index.ts} +0 -0
  248. /package/extensions/wechat/{lock.ts → state/lock.ts} +0 -0
  249. /package/extensions/wechat/{channel.ts → text/channel.ts} +0 -0
@@ -1,506 +0,0 @@
1
- # file-browse — view the working directory in a browser
2
-
3
- `/browse` starts a **read-only** HTTP server rooted at the effective working
4
- directory, opens the default browser at it, and prints the URL. The page is a
5
- two-pane file browser: a collapsible path tree on the left, directory listings
6
- and file views on the right, with image thumbnails, rendered Markdown,
7
- highlighted code, per-language icons, a fuzzy quick-open palette, and a
8
- read-only git diff view. It is
9
- bound to `127.0.0.1`
10
- and ships no dependencies — `node:http` plus one small vanilla script.
11
-
12
- <img src="../../assets/file-browse.png" alt="The loopback file browser, showing the path filter, the file tree, and a rendered Markdown preview" width="100%">
13
-
14
- ```bash
15
- pi --extension ./extensions/file-browse # load just this extension
16
- pi -e . # load the whole @gavin-hu/my-pi-agent package
17
- ```
18
-
19
- ## What it does
20
-
21
- - **One-command browsing.** `/browse` starts the server, prints the URL, and
22
- opens the browser (unless `autoOpen: false`).
23
- - **Two-pane UI.** A server-rendered tree, directory listings, and file pages,
24
- progressively enhanced by one first-party script (filter, lazy tree
25
- expansion, thumbnail fallback). A folder folds from its twisty, and a
26
- `fold all` / `expand all` button beside the filter folds the whole tree at
27
- once. The tree scrolls in
28
- its own pane with quick open and the filter pinned above it; below 860px it
29
- collapses into a `Files` disclosure above the listing. A hairline progress bar
30
- tracks whichever column is scrolling (the card body for a file or diff, the
31
- main column otherwise), and the served folder names the root page.
32
- - **Rich previews.** Text, rendered Markdown, images (thumbnails), and
33
- binary/over-size download cards, with per-language icons.
34
- - **First-party syntax highlighting.** Code files render with keywords,
35
- strings, numbers, and comments colored, in the dark and light palettes:
36
- JavaScript/TypeScript (`js`, `mjs`, `cjs`, `jsx`, `ts`, `mts`, `cts`, `tsx`),
37
- JSON, CSS (`css`, `scss`, `sass`, `less`), HTML/XML and template components
38
- (`html`, `htm`, `xml`, `vue`, `svelte`), shell (`sh`, `bash`, `zsh`),
39
- Python (`py`, `pyw`, `pyi`), Java, YAML (`yaml`, `yml`), and SQL. Other text
40
- types stay plain.
41
- - **Rendered Markdown.** `.md`/`.markdown` files render as headings, lists,
42
- tables, code, inline and reference links, bare-URL autolinks, and images, with
43
- a one-click **raw** view (`?raw=1`). The page fills the card, so the scroll
44
- bar sits at the card edge; headings, prose, and images keep a centred reading
45
- measure of at most 1012px. Code blocks stay
46
- inside that column; tables start at it and take the free gutter to its right,
47
- and a table wider than the pane scrolls, its right edge fading until the last
48
- column is reached. A task list's checkbox is drawn by the page, so a done
49
- item carries the success colour in both themes. Tables size their columns to
50
- the content, and named and numeric character references are decoded. A
51
- README's raw HTML is re-emitted through an allowlist (`<details>`,
52
- `<summary>`, tables, `<kbd>`, images), so it renders without becoming an
53
- injection path. Fenced code blocks are syntax-highlighted when the fence
54
- names a supported language. Headings get stable anchor ids, so in-document
55
- `#anchor` links work. Raw HTML in the source is escaped, never executed, and
56
- external images are shown as links rather than fetched.
57
- - **Sortable listings.** The Name / Size / Modified headers sort the current
58
- directory, directories first, and the choice is remembered per browser.
59
- - **Copy and wrap.** Copy a file's root-relative path or its contents, copy a
60
- `path:12-30` reference for the lines you selected, and toggle line wrapping for
61
- text and raw views.
62
- - **Theme toggle.** A header button switches between the dark and light
63
- palettes and remembers the choice per browser; until it is used, the palette
64
- follows the system `prefers-color-scheme`.
65
- - **Quick open.** `/` (or `Ctrl/Cmd+K`, or the sidebar button) opens a fuzzy
66
- path finder over a capped index of the tree, with keyboard navigation and a
67
- highlighted match; Enter opens the file.
68
- - **Keyboard.** `/` or `Ctrl/Cmd+K` opens quick open, `-` folds or expands
69
- every folder in the tree, `u` toggles line wrapping, `r` switches rendered and
70
- raw Markdown, `y` copies a reference to the selected lines, `n`/`p` step
71
- between a diff's hunks, and `?` opens a shortcut
72
- sheet. A diff's hunk headers pin to the top of the code viewport while its
73
- body scrolls.
74
- - **Read-only and loopback-scoped.** No editing, no writing, no LAN exposure.
75
- - **Git context.** A read-only header chip shows the checked-out branch (or
76
- detached head) and reflows on narrow screens. Uncommitted changes are stated
77
- once, by the changes chip below; turn the diff view off (`gitDiff: false`) and
78
- the header reports no changes at all.
79
- - **Git diff view.** A `● N changes +A −D` chip links to `/diff`, a change
80
- overview with `git diff --stat` and a per-file status table, and `/diff/<rel>`
81
- renders one file's unified diff, whose head carries its own `+A −D` and hunk
82
- count. Changed lines are syntax-highlighted with the
83
- file's language, and dirty files and their directories carry a marker in the
84
- listing and the tree. The scope is switchable between `head` / `staged` /
85
- `worktree`; untracked files show as all-add, an unborn `HEAD` falls back to the
86
- index, and a non-repository is a plain empty state. Read-only, capped, and
87
- rendered with the shared diff colors.
88
- - **Live status.** A page left open keeps up with the session: a `◉ live`
89
- chip beside `read-only` says whether status is flowing (green), still
90
- connecting (accent), stalled (amber, after three missed frames), or offline
91
- (red); the branch chip, the `● N changes +A −D` chip, and the change markers in
92
- the tree and the listing update in place; and the footer line reports `up 2m
93
- 14s · 37 requests`. The chip names its state and never counts, and a frame
94
- whose change set did not move repaints nothing at all. One Server-Sent Events
95
- stream per page carries it, on the same loopback server, so nothing extra is
96
- opened and nothing is written; that stream is also how the server notices the
97
- last page closing its window, after which it stops listening. A page that lists
98
- a folder also checks that folder, and offers a `⟳ reload` control when a file
99
- has appeared or gone on disk since it drew its rows.
100
- - **No dependencies.** `node:http` plus the inline sprite, CSS, and client
101
- script.
102
-
103
- ## Commands
104
-
105
- ```bash
106
- /browse start at the working directory, open the browser, print the URL
107
- /browse <path> start with a subdirectory of the working directory as the root
108
- /browse status print the root and URL, or "not running"
109
- /browse stop stop the server
110
- ```
111
-
112
- - The root is the effective cwd, so an active `worktree` session serves the
113
- worktree. `<path>` must resolve inside it.
114
- - The port is fixed for the session: the first `/browse` picks a random port
115
- in `4780`–`5779` and every later `/browse` in that session reuses it, so the
116
- URL stays stable across roots and restarts. Set `"port"` to pin an explicit
117
- port; a busy port is reported, not silently changed.
118
- - Starting while already running with the same root reprints the URL without
119
- restarting; a different root (or a changed pinned port) restarts the server.
120
- - The server is closed when its last page has been gone for ten seconds, so
121
- closing the browser window ends the session's browse server and `/browse`
122
- starts it again on the same port. A reload or a new tab inside that grace keeps
123
- it. `session_shutdown` closes it too, and `/browse stop` closes it at once.
124
-
125
- ### Status
126
-
127
- While a server is running, the extension publishes a two-tone `◉ <port>` chip
128
- (a green circle-dot and a blue port) under the shared `browse` status key. The
129
- [`bytefly`](../bytefly/) extension routes it to line 1's right zone
130
- beside `⎇ branch` and `⑂ worktree`; it
131
- is cleared on stop and on `session_shutdown`. The chip is session-local: it
132
- reflects only this session's server, with no cross-session sharing.
133
-
134
- The page has its own live status, which is what a left-open tab reads. The
135
- header chip starts hidden and is painted by the client from `/events`: `◉ live`
136
- in the success colour while frames arrive, `◉ connecting` before the first
137
- one, `◉ stale` in the warning colour when three intervals pass without a frame
138
- on an open connection, and `◉ offline` in the danger colour once the stream
139
- errors. The chip names its state rather than counting, so it is repainted only
140
- when that state changes. The same frame carries the branch (or detached head), the
141
- uncommitted-change totals and paths, the server's uptime, and the requests it
142
- has answered, so the branch chip, the changes chip, the markers, and the footer
143
- line are painted from one snapshot rather than four requests. Each change set
144
- also carries a short revision, and a frame whose revision the page already
145
- painted does no work at all — so an idle tree costs no DOM, however many rows the
146
- page drew. Losing the stream leaves everything the page last showed in place: it
147
- is read-only, so a stale label is never a stale action. `"live": false` turns the
148
- whole surface off — no chip, no stream, no `/events` route — and so does
149
- JavaScript off.
150
-
151
- A page that lists a folder carries the revision of the names it drew, and asks
152
- `/api/dir-rev` for the folder's current one on the frame cadence — one directory
153
- read every five seconds by default, skipped while the tab is hidden and stopped
154
- for good once it has an answer. A difference reveals `⟳ reload` beside the live
155
- chip, which reloads the page; the rows themselves are never touched, because a
156
- page that could grow a row is a page that writes. A folder whose listing was
157
- truncated, and a file, `/diff`, or error page, has nothing to compare and shows
158
- no control. At most eight pages may hold the stream at once: a ninth is refused
159
- and its chip reads `offline`, which keeps one local page from pinning the server
160
- — and the git read behind each frame — alive.
161
-
162
- The stream is also how the server knows a page is still there: when the last one
163
- leaves, the port is released after ten seconds and the chip is cleared, because
164
- nothing is left to serve. That is why a page with JavaScript off — or with live
165
- status off — never counts as a reader, and the server then waits for
166
- `/browse stop` or the end of the session.
167
-
168
- ### The page
169
-
170
- The page is keyboard-first, and the tree is one tab stop rather than two per
171
- folder:
172
-
173
- | Key | What it does in the tree |
174
- |---|---|
175
- | `↑` / `↓` | Move between the rows on screen |
176
- | `→` | Open a folder (reading its children first if needed), or enter it |
177
- | `←` | Close the folder, or step out to its parent |
178
- | `Space` | Open or close the focused folder |
179
- | `Enter` | Follow the focused row's link |
180
- | `Home` / `End` | First or last row on screen |
181
-
182
- The folder's own link carries `aria-expanded`, so a screen reader hears its
183
- state once; the twisty beside it is a pointer control kept out of the tab order.
184
- A mouse click moves the tab stop to that row, so `Tab` comes back to where you
185
- were. Elsewhere the page keeps its own keys: `/` or `Ctrl`/`Cmd`+`K` for quick
186
- open, `-` to fold, `u` to wrap, `r` for raw Markdown, `n`/`p` between diff hunks,
187
- and `?` for the sheet, whose dialog now traps `Tab` and returns focus to its
188
- button.
189
-
190
- Printing is supported: the paper gets the light palette whatever the screen is
191
- showing, the chrome and the tree are dropped, and a file or diff prints whole
192
- instead of inside its scroll box.
193
-
194
- ## Routes
195
-
196
- | Route | Result |
197
- |---|---|
198
- | `/`, `/browse[/<rel>]` | tree + directory listing |
199
- | `/view/<rel>` | tree + file page (text, Markdown, image, binary, or over-size) |
200
- | `/view/<rel>?raw=1` | raw Markdown/text source instead of the rendered view |
201
- | `/raw/<rel>` | raw bytes, streamed; downloads and image sources |
202
- | `/diff[?mode=head\|staged\|worktree]` | changed-file overview with `--stat` |
203
- | `/diff/<rel>[?mode=…]` | one file's unified diff |
204
- | `/api/tree?path=<rel>` | JSON children for lazy tree expansion |
205
- | `/api/files?q=<query>` | JSON fuzzy path matches for the quick-open palette |
206
- | `/events` | Server-Sent Events stream of `status` frames for the open page |
207
- | `/app.js` | the first-party client script |
208
- | `/theme.js` | the pre-paint theme bootstrap |
209
- | `/favicon.ico` | `204` (the icon is a data URI) |
210
-
211
- ## Configuration
212
-
213
- The `file-browse` section of the shared `bytefly.json`, merged from the global
214
- document and the project document (project wins):
215
-
216
- ```jsonc
217
- // bytefly.json → "file-browse"
218
- {
219
- "port": 0, // 0 = one random port fixed for the session
220
- "autoOpen": true, // open the browser on start; false for headless
221
- "maxFileBytes": 1048576, // larger files show a download card only
222
- "maxThumbBytes": 5242880, // larger images get an icon, not a thumbnail
223
- "maxTextLines": 5000, // rendered lines before truncation
224
- "maxDirEntries": 2000, // listed entries before truncation
225
- "thumbnails": true,
226
- "markdown": true, // render Markdown files as formatted HTML
227
- "markdownHtml": true, // re-emit sanitized raw HTML in rendered Markdown
228
- "maxIndexEntries": 20000, // files collected for quick open before truncation
229
- "gitDiff": true, // offer the read-only diff view
230
- "diffContext": 3, // unified diff context lines
231
- "maxDiffLines": 4000, // diff lines rendered before truncation
232
- "live": true, // stream live status to open pages
233
- "liveIntervalMs": 5000 // ms between status frames while a page is connected
234
- }
235
- ```
236
-
237
- ## Security
238
-
239
- - **Loopback only.** Binds `127.0.0.1`; no auth and no LAN exposure.
240
- - **Method and Host.** Only `GET`/`HEAD` are served (else `405`); a `Host`
241
- header that is not `127.0.0.1[:port]`, `localhost[:port]`, or `[::1][:port]`
242
- is rejected (else `403`, DNS-rebinding defense), and no CORS headers are sent.
243
- - **Path guard.** Every path is decoded once, NUL and absolute paths are
244
- rejected, the path is resolved under the root, and the target (or nearest
245
- existing ancestor) is `realpath`-checked for containment. This blocks `../`,
246
- encoded traversal, and symlink escapes. Symlinks are listed but only followed
247
- when their real path stays inside the root.
248
- - **CSP.** Pages allow only same-origin scripts and inline styles, and refuse to
249
- be framed (`frame-ancestors 'none'`, `X-Frame-Options: DENY`), so a local page
250
- cannot clickjack the controls or re-point relative URLs through an injected
251
- `<base>`; the policy comes from `lib/http/headers.ts`, shared with `plan`, so
252
- the two surfaces cannot drift. `/raw` uses
253
- `default-src 'none'; script-src 'none'; sandbox` plus `nosniff`, so a served
254
- SVG/HTML file cannot execute.
255
- - **Markdown safety.** Rendered Markdown is escape-first, and raw HTML passes an
256
- allowlist (`raw-html.ts`): only known elements and attributes are re-emitted,
257
- rebuilt from the parsed names rather than copied, so `script`, `style`,
258
- `iframe`, `svg`, event handlers, `style=` and `class=` never reach the page.
259
- Link schemes are allowlisted (`javascript:`/`data:` dropped), an external
260
- image renders as a link instead of loading, and open elements are balanced at
261
- the end of the document so an unclosed `<div>` cannot swallow the page chrome.
262
- A suppressed element (`script`, `style`, `svg`, …) is dropped with its body;
263
- any other unknown tag is escaped and stays visible. `markdownHtml: false`
264
- restores escape-only rendering. The page CSP keeps the rest inert.
265
- - **Highlighting safety.** The highlighter escapes every literal before it
266
- wraps it in a token span, so a code file's content cannot inject markup or
267
- attributes; only the fixed `tok-*` class names reach the page.
268
- - **Event stream.** `/events` is a `GET` on the same loopback server, behind
269
- the same method and `Host` checks, and it only reads: the frame is the branch,
270
- the change paths and their revision, an uptime, and a request count, all of
271
- which the page it feeds already renders. It carries no page policy
272
- (`text/event-stream` is not a document), sends `Cache-Control: no-store`, and
273
- cannot be cached or replayed. A tick runs only while a page is attached, and
274
- the timer is `unref`ed, so an idle server does no work and a forgotten tab
275
- cannot hold the process open. At most eight pages may attach; a ninth is
276
- answered `204`, the one status an event source never reconnects after.
277
- - **Directory check.** `/api/dir-rev` answers one folder's listing revision, so
278
- the page can tell whether its rows are stale. It goes through the same path
279
- guard as every other route and reads one directory; its answer is a hash of the
280
- names, not the names themselves, so it discloses nothing the page above it does
281
- not already show.
282
- - **Caps.** `maxFileBytes` (inline rendering), `maxTextLines` (rendered lines),
283
- `maxThumbBytes` (thumbnails), and `maxDirEntries` (listings) bound every read.
284
- - **Git reads.** The header chip and the diff view run read-only git
285
- (`rev-parse`, `status --porcelain=v1 -z`, `diff --stat`, `diff -U<n>`) with a
286
- timeout and a short cache; any failure simply omits the chip or the diff. A
287
- per-file diff sets `GIT_LITERAL_PATHSPECS=1`, so a filename containing glob
288
- characters is matched literally.
289
- - **Read-only.** Nothing is ever written.
290
-
291
- ## Limitations
292
-
293
- - Syntax highlighting covers the built-in language set below; any other text
294
- type renders exactly as before. State that the tokenizer does not carry
295
- across lines (shell heredocs, interpolation inside a template literal) is
296
- approximated rather than parsed.
297
- - Live status updates what the page already contains: the header chips, the
298
- markers on the rows it rendered, and the footer line. A file that appears on
299
- disk does not join the listing by itself — the page offers `⟳ reload` instead —
300
- and the check covers the folder the page lists, by name, not the folders inside
301
- it, a listed file's contents, the `/diff` table, or a file page's `changes`
302
- action, which stay page-load snapshots. The branch chip is only patched when
303
- the page was served inside a repository.
304
- - `/browse` has no auth; a malicious local page could probe the port. The `Host`
305
- allowlist and the absence of CORS headers mitigate reads, and the server is
306
- read-only.
307
- - Serving the root exposes whatever is on disk to the local user who started it;
308
- this is intentional and scoped to loopback.
309
-
310
- ## Non-goals
311
-
312
- - No editing or writing from the browser.
313
- - No third-party assets: the Markdown renderer, its raw-HTML sanitizer, and the
314
- syntax highlighter are first-party, so raw HTML in a Markdown file is
315
- allowlisted and never executed.
316
- - No auth and no LAN exposure: loopback only.
317
- - Not a general static-file server: the root is confined to the effective cwd.
318
-
319
- ## Pi integration
320
-
321
- | Integration point | How |
322
- |---|---|
323
- | Command | `/browse` registers a single command from the factory; no tools are exposed. |
324
- | 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. |
325
- | 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. |
326
-
327
- ## Design notes
328
-
329
- - **Lifecycle.** `/browse` resolves `resolveEffectiveCwd(ctx.cwd)`, loads config,
330
- selects the port, parses args, and `await createFileServer(...)`: `realpath`
331
- the root, bind `127.0.0.1`, await `listening`, return `{ url, port, root,
332
- close }`. The port is chosen once for the session: a pinned config `port`
333
- wins, otherwise a random port is drawn and cached after the first successful
334
- bind, so later starts reuse it (a conflict is reported and the next `/browse`
335
- rolls another). `/browse stop` and `session_shutdown` await the idempotent
336
- `close()`; `session_shutdown` also clears the cached port. The page is the
337
- server's only reader, so the channel reports the last page's departure after
338
- `IDLE_STOP_MS` and the command closes the server and clears the chip — the
339
- cached port survives, so the next `/browse` binds the same one.
340
- - **Pure routing.** `handleRequest(context, method, rawUrl, headers)` validates
341
- method and `Host`, parses the URL, matches a route, and runs every path
342
- through `paths.ts`. It returns a plain `ServeResponse`, so routing is tested
343
- without a socket; `server.ts` is the only part that binds a port.
344
- - **One snapshot, one frame.** The status source is shared by every attached
345
- page, so a tick costs one `git status`, one `git diff --stat`, and one
346
- serialization however many tabs are open. The frame carries a revision of the
347
- change set, and the client compares it before painting, which is why an idle
348
- page does no DOM work. The cadence is what bounds those reads, so the default
349
- is five seconds rather than matching the providers' two-second render cache.
350
- - **A poll, not a watcher.** The folder check is one request from the page to
351
- `/api/dir-rev`, not an `fs.watch`: recursive watching is the one part of
352
- `node:fs` whose availability and event semantics differ per platform, and it
353
- would add a long-lived resource to start and release around `close()`. The
354
- check compares names, so it is a directory read rather than a stat pass, and it
355
- stops at the first difference because the control, not the checker, is the fix.
356
- - **Progressive UI.** The page is server-rendered HTML with an inline style
357
- block and inline SVG icons. `client.ts` enhances it: the filter hides
358
- non-matching rows while keeping ancestors, disclosure triangles lazily
359
- `fetch("/api/tree")`, broken thumbnails fall back to the category icon, the
360
- listing headers sort in place, the wrap toggle is remembered, and the copy
361
- buttons use the clipboard. On wide screens the tree is its own scroll pane
362
- under the pinned filter and quick-open trigger; the sidebar is a `<details>`
363
- whose content wrapper has no definite height, so the panel fills it
364
- absolutely and the tree flexes inside. Without JS the page still browses and
365
- renders Markdown, and the JS-only controls (filter, disclosure, actions, quick
366
- open) are hidden; the sort buttons lose their pointer so they do not look
367
- live. The palette is the shared Bytefly web theme (`lib/web/tokens.ts`),
368
- derived from the `bytefly-dark`/`bytefly-light` terminal themes; it follows
369
- `prefers-color-scheme` until an explicit choice sets `data-theme` on the root:
370
- `/theme.js` applies the saved choice in the head before first paint, and the
371
- header toggle flips and stores it (`pi-web-theme` in `localStorage`, the key
372
- shared with plan's review page).
373
- - **Markdown rendering.** `markdown.ts` is a first-party, escape-first renderer
374
- for the constructs that show up in repository READMEs. It emits only a known
375
- tag set, escapes every literal character, allowlists URL schemes (dropping
376
- `javascript:`/`data:`), rewrites local links and images to `/view`/`/raw`, and
377
- renders an external image as a link instead of loading it (the page CSP is
378
- `img-src 'self' data:`). Link reference definitions (`[id]: url`), including
379
- the full `[text][id]`, collapsed `[text][]`, and shortcut `[text]` forms, and
380
- numeric and common named character references are resolved. The Rendered/Raw
381
- switch is a server-side `?raw=1`
382
- link, so it works without JavaScript. Headings are demoted one level so the
383
- card title stays the page's only `h1`, and each gets a slug `id` (unique in
384
- the document). Set `markdown: false` to fall back to the plain-text view. A
385
- fenced code block
386
- whose info string names a supported
387
- language is highlighted through `lib/highlight.ts` (either an extension or a
388
- long-form alias, `fenceLanguage`); an absent or unknown language stays plain.
389
- - **Raw HTML.** A line that starts with an allowed block tag (`<details>`,
390
- `<table>`, `<div>`, `<pre>`, …), a comment, or a closing tag opens an HTML
391
- block that runs to the next blank line; text inside it stays text, and a
392
- suppressed tag (`<script>`, `<style>`, `<svg>`) drops its body through the
393
- matching close tag. `raw-html.ts` re-emits only allowlisted elements and
394
- attributes: `a`/`img` go through the same `/view`/`/raw` resolution as
395
- Markdown, an external image becomes a link, and `class`/`style`/handlers are
396
- dropped. Phrasing tags are also accepted inside a line, while a block tag
397
- mid-paragraph stays escaped. Open elements are balanced across blocks, which
398
- is how a `<details>` can wrap Markdown paragraphs, and `markdownHtml: false`
399
- turns the whole path off.
400
- - **Syntax highlighting.** `lib/highlight.ts` is a first-party, escape-first
401
- tokenizer for the file types that dominate a repository: JavaScript and
402
- TypeScript, JSON, CSS (incl. SCSS/Sass/Less), HTML/XML and template
403
- components, shell, Python, Java, YAML, and SQL. `detectLanguage` maps a
404
- filename to a tokenizer and an unmapped type stays plain. `highlightLines`
405
- walks the whole file, so block comments, template literals, open HTML tags,
406
- CSS brace depth, and Python triple-quoted strings carry across lines; every
407
- literal is escaped with the same `escapeHtml` the plain view uses, and a
408
- line's markup is only the fixed `tok-*` classes the stylesheet colors. It
409
- backs both the plain text view and fenced code blocks in the rendered
410
- Markdown view.
411
- - **Quick open.** `/api/files` ranks the cached index with `fuzzy.ts` and
412
- returns the matched character positions; the client fetches on an 80 ms
413
- debounce and builds rows with `textContent`, so a file name cannot inject
414
- markup. The index skips `.git`, `node_modules`, and symlinks, is capped by
415
- `maxIndexEntries`, and is cached per server for 10 s by `createFileIndex`. The
416
- palette is a `dialog`/`combobox`/`listbox` with `aria-activedescendant` and
417
- `Esc` / arrow / `Enter` handling.
418
- - **Diff view.** `git.ts` extends the read-only provider set with a TTL-cached
419
- `git status --porcelain=v1 -z` and a per-mode `git diff`; `diff.ts` parses the
420
- unified diff and the porcelain records into plain view models, so `router.ts`
421
- and `html.ts` stay pure. `head` compares against `HEAD` (falling back to
422
- `--cached` before the first commit), `staged` against the index, and
423
- `worktree` against the index from the other side. An untracked file renders as
424
- all-add through `readFileView`, so binary and over-size handling is shared
425
- with the file view. `git diff --stat` supplies the chip totals and
426
- `changeDirs` derives the directory markers; the changed lines are highlighted
427
- per hunk by rebuilding the old and new sides for `lib/highlight`.
428
- - **Live status.** `live.ts` builds one snapshot per tick — the git status,
429
- the `head` change index, the server's uptime, and its request count — and
430
- `frameOf` serializes it as one `status` event. `createLiveChannel` holds the
431
- attached pages and starts the ticker on the first attach, so an idle server
432
- does no work; it shares a single in-flight snapshot between pages, drops a
433
- page whose write fails, and stops on the last detach and on `close()`, which
434
- `server.ts` calls before closing the socket. The last detach also arms one
435
- idle-stop timer — a reload re-attaches and cancels it — and `server.ts` turns
436
- that into the extension's `close()` and a cleared chip. Both providers are already
437
- TTL-cached, so a tick re-reads at most one `git status`, and every read
438
- failure degrades to `null` rather than a failed frame. The event-stream
439
- headers and the `retry:` preamble live in `lib/http` with the rest of the
440
- loopback policy, shared with plan's review page. The client half is
441
- `live-client.ts`: one `EventSource`, one snapshot paint (`textContent` only,
442
- never markup), one staleness timeout armed per frame, and a `pagehide` cleanup.
443
- - **Streaming responses.** The server streams `filePath` responses and
444
- suppresses bodies for `HEAD`, so large `/raw` files are not buffered.
445
- - **Root confinement over feature breadth.** The root is deliberately confined
446
- to the effective cwd rather than serving an arbitrary directory, so a
447
- worktree session serves the worktree and nothing above it.
448
-
449
- ## Files
450
-
451
- | File | Purpose |
452
- |---|---|
453
- | `index.ts` | `/browse` command, running-server closure, shutdown cleanup |
454
- | `config.ts` | Config defaults, validation, global/project merge |
455
- | `paths.ts` | Decode/encode, resolve, and `realpath`-containment guard |
456
- | [`../../lib/escape.ts`](../../lib/escape.ts) | `escapeHtml`, the shared HTML escaping used by every renderer |
457
- | [`../../lib/highlight.ts`](../../lib/highlight.ts) | First-party escape-first syntax tokenizer for the code view (shared with `plan`) |
458
- | [`../../lib/web/index.ts`](../../lib/web/index.ts) | The shared Bytefly web brand system: tokens, base, components, layout, runtime, and markup |
459
- | `files.ts` | Classification (incl. Markdown), text reads, and one page-level walk (`directoryView`) that returns a directory's listing and its sidebar tree |
460
- | `diff.ts` | Pure unified-diff and porcelain-status parsing for the diff view |
461
- | `fuzzy.ts` | Fuzzy path matching and ranking for the quick-open palette |
462
- | `markdown.ts` | First-party escape-first Markdown-to-HTML renderer (fenced code highlighted; raw HTML through `raw-html.ts`) |
463
- | `raw-html.ts` | Allowlisted raw-HTML sanitizer: element/attribute policy, suppressed bodies, and cross-block balancing |
464
- | `urls.ts` | Shared link/image URL policy: scheme allowlist and `/view`/`/raw` rewriting |
465
- | `git.ts` | Read-only branch/dirty provider for the header chip |
466
- | `icons.ts` | Inline SVG sprite and the per-language mapping |
467
- | `client.ts` | The `/app.js` source: sort, wrap, copy, hunk navigation, reading progress, and the page keymap |
468
- | `tree.ts` | The sidebar block: path filter, fold-all, lazy tree loading, and the tree's keyboard model (interpolated into `client.ts`) |
469
- | `palette.ts` | The quick-open palette: fuzzy search, the combobox wiring, and the `window.bfPalette` contract (interpolated into `client.ts`) |
470
- | `live-client.ts` | The client's live status block: the event stream, the chips, the markers, and the footer line (interpolated into `client.ts`) |
471
- | `live.ts` | The live status snapshot, its frame, and the fan-out channel that ticks it |
472
- | `styles.ts` | The page-only `browse-*` stylesheet (the `bf-*` kit comes from `lib/web`) |
473
- | `html.ts` | Page renderers: the shared shell, the two-pane layout, the listing, the cards, and the diff body |
474
- | `router.ts` | Pure request handler: routes, the `/raw` sandbox policy, and the error page |
475
- | `server.ts` | `node:http` server: this extension's handler over the shared bind, writer, and close |
476
- | [`../../lib/http/index.ts`](../../lib/http/index.ts) | Loopback HTTP layer shared with `plan`: page/script/JSON/event-stream headers, `ServeResponse`, the request-envelope helpers, bind/respond/close, and `openEventStream` |
477
- | [`../../lib/browser.ts`](../../lib/browser.ts) | Cross-platform browser opener (shared with `plan`) |
478
-
479
- ## Testing
480
-
481
- `extensions/file-browse/` covers the path guard (traversal, encoded, NUL, malformed,
482
- absolute, symlink escape and in-root symlink), file classification/listings/
483
- reads/tree and the quick-open file index, the fuzzy matcher, the syntax
484
- highlighter (language detection, fence aliases, token classes, cross-line
485
- state, and escaping), the Markdown renderer (blocks, inline, fenced-code
486
- highlighting, safety, raw HTML, and path resolution), the raw-HTML sanitizer
487
- (allowlist, suppressed bodies, attribute policy, balancing, and a seeded
488
- invariant test over fragment soup),
489
- the icon mapping and sprite, HTML escaping/encoding/rendering, the client script
490
- and its serving, the platform browser commands with an injected spawn, the router
491
- (routes, `/api/tree`, `/api/files`, `?raw=1`, status codes, `Host`, `HEAD`, `/raw`
492
- headers, and the `/diff` view routes and mode validation)
493
- without a socket, a real `127.0.0.1:0` server, the session-port lifecycle (fixed
494
- on first start, reused, and re-rolled after a conflict), the git status provider
495
- (branch, dirty, detached, non-repo, failure, and TTL cache), the git diff parser
496
- and provider (line kinds, line numbers, status and rename parsing, mode
497
- filtering, untracked all-add, unborn-HEAD fallback, literal pathspec, and TTL
498
- cache), the diff renderers (highlighted changed lines, marker sets, and chip
499
- totals), the live status feed (snapshot fields, degraded providers, the frame,
500
- and the channel's ticker driven by an injected timer, plus the idle stop armed on
501
- the last detach and cancelled by the next page or by `close()`), the `/events`
502
- route and
503
- its 404 when the channel is off, a real socket that reads a page's first frame
504
- and the ticker's next one and sees the stream end when the server closes, the
505
- live chrome a page renders when on and not when off, and the command
506
- lifecycle with injected seams.
@@ -1,141 +0,0 @@
1
- /**
2
- * The HTTP server for the browse extension.
3
- *
4
- * Wraps `node:http` around the pure `handleRequest`: it resolves the root once,
5
- * binds `127.0.0.1`, streams `/raw` responses from disk, feeds the live status
6
- * channel, and exposes an idempotent `close()`. The request handler is injectable
7
- * so tests can avoid a socket when they only need routing.
8
- *
9
- * The bind, the response writer, the event-stream head, and the close come from
10
- * `lib/http`, which `plan` shares; what stays here is this server's own request
11
- * handler, its request count, and the catch that turns a thrown handler into a
12
- * plain-text 500.
13
- */
14
-
15
- import { realpathSync } from "node:fs";
16
- import { createServer } from "node:http";
17
- import {
18
- bindLoopback,
19
- createCloser,
20
- openEventStream,
21
- respond,
22
- textResponse,
23
- type ServeResponse,
24
- } from "../../lib/http/index.ts";
25
- import { IDLE_STOP_MS, type ServeConfig } from "./config.ts";
26
- import { createFileIndex } from "./files.ts";
27
- import type { GitDiffProvider, GitStatusProvider } from "./git.ts";
28
- import { createLiveChannel, createLiveStatus } from "./live.ts";
29
- import { handleRequest, type ServeContext } from "./router.ts";
30
-
31
- export interface FileServer {
32
- /** Origin URL, e.g. `http://127.0.0.1:49152/`. */
33
- url: string;
34
- /** The bound port. */
35
- port: number;
36
- /** Resolved absolute root that is actually served. */
37
- root: string;
38
- /** Stop listening. Safe to call more than once or before `listen` settles. */
39
- close(): Promise<void>;
40
- }
41
-
42
- export interface FileServerOptions {
43
- root: string;
44
- config: ServeConfig;
45
- /** Read-only git context for the page header; omitted when unavailable. */
46
- git?: GitStatusProvider;
47
- /** Read-only diff provider for the change and diff views; omitted when unavailable. */
48
- diff?: GitDiffProvider;
49
- /** Called once the last page has been gone for `idleStopMs`; omitted, the server waits for `close()`. */
50
- onIdle?: () => void;
51
- /** Grace before `onIdle`, in ms; defaults to `IDLE_STOP_MS`. */
52
- idleStopMs?: number;
53
- /** Override the request handler (tests). */
54
- handler?: typeof handleRequest;
55
- }
56
-
57
- /** Start a read-only server rooted at `options.root` on 127.0.0.1. */
58
- export async function createFileServer(options: FileServerOptions): Promise<FileServer> {
59
- const root = realpathSync.native(options.root);
60
- const { config } = options;
61
- // The status feed's two server-owned facts: how long it has been up, and how
62
- // many requests it has answered. Both are read per frame, never cached.
63
- let requests = 0;
64
- const status = config.live
65
- ? createLiveStatus({
66
- git: options.git,
67
- diff: config.gitDiff ? options.diff : undefined,
68
- startedAt: Date.now(),
69
- requests: () => requests,
70
- })
71
- : undefined;
72
- const live = status
73
- ? createLiveChannel({
74
- status,
75
- intervalMs: config.liveIntervalMs,
76
- idleMs: options.idleStopMs ?? IDLE_STOP_MS,
77
- onIdle: options.onIdle,
78
- })
79
- : undefined;
80
- const context: ServeContext = {
81
- root,
82
- config,
83
- git: options.git,
84
- diff: options.diff,
85
- index: createFileIndex(root, config.maxIndexEntries),
86
- live: status,
87
- };
88
- const handler = options.handler ?? handleRequest;
89
-
90
- const server = createServer((request, response) => {
91
- requests += 1;
92
- const answer = async (): Promise<ServeResponse> => {
93
- try {
94
- return await handler(context, request.method ?? "GET", request.url ?? "/", request.headers);
95
- } catch {
96
- return textResponse(500, "Internal error");
97
- }
98
- };
99
- void answer().then((result) => {
100
- // A HEAD of the stream is answered as a plain response: there is no body to
101
- // hold open, and the client would otherwise wait on frames it asked not to get.
102
- if (!result.stream || !live || (request.method ?? "GET").toUpperCase() === "HEAD") {
103
- respond(request, response, result);
104
- return;
105
- }
106
- // Checked before the head is written: a refusal must not leave a stream the
107
- // client would wait on. `204` is the one status an event source does not
108
- // reconnect after, so a page beyond the cap goes quiet instead of retrying.
109
- if (live.atCapacity()) {
110
- respond(request, response, { status: 204, headers: { "Cache-Control": "no-store" }, body: "" });
111
- return;
112
- }
113
- openEventStream(response, result);
114
- const detach = live.attach({
115
- write: (frame) => {
116
- response.write(frame);
117
- },
118
- end: () => {
119
- if (!response.writableEnded) response.end();
120
- },
121
- });
122
- request.on("close", () => {
123
- detach();
124
- if (!response.writableEnded) response.end();
125
- });
126
- });
127
- });
128
-
129
- const port = await bindLoopback(server, config.port);
130
- const closeServer = createCloser(server);
131
- return {
132
- url: `http://127.0.0.1:${port}/`,
133
- port,
134
- root,
135
- close: async () => {
136
- // End the streams first, so a connected page does not hold the close open.
137
- live?.close();
138
- await closeServer();
139
- },
140
- };
141
- }