@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
@@ -0,0 +1,554 @@
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 bound to `127.0.0.1` and ships no dependencies —
9
+ the shared loopback layer plus one small vanilla script.
10
+
11
+ <img src="../../assets/screens/file-browse.png" alt="The loopback file browser, showing the path filter, the file tree, and a rendered Markdown preview" width="100%">
12
+
13
+ ## Capabilities
14
+
15
+ Status records what a reader can rely on: **Shipped** is in a tagged release, **Unreleased** is complete in the current tree but not yet tagged, and **Partial** has one released part and one unreleased part. Rows are ordered by user value, highest first.
16
+
17
+ | Capability | How | What you get | Status |
18
+ |---|---|---|---|
19
+ | One-command browsing | `/browse` | The server starts at the effective working directory, prints its URL, and opens the browser. | Shipped |
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 |
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 |
24
+ | Git diff view | `/diff`, `/diff/<rel>` | A change overview and one file's unified diff, scoped to `head`, `staged`, or `worktree`. | Shipped |
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
+ | 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 |
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
+ | 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
+ | Diff hunk navigation | `n`/`p` in `/diff` | Jump between changed hunks without scrolling the whole file. | Shipped |
31
+ | Fold and expand all | the `fold all` control | Collapse or expand the whole tree in one click. | Shipped |
32
+ | Reading progress | `browse-progress` hairline | A long directory or diff read shows how much has loaded, and printing hides the indicator. | Shipped |
33
+ | Printing | `@media print` styles | The page prints without the toolbar, progress hairline, or other screen-only chrome. | Shipped |
34
+ | Syntax highlighting | `/view/<rel>`, `/view/<rel>?raw=1` | Supported code files show keywords, strings, numbers, and comments in colour in both themes; other text stays plain. | Shipped |
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
+ | 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
+ | 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 |
39
+
40
+ ## Quickstart
41
+
42
+ ```bash
43
+ pi --extension ./extensions/file-browse # load just this extension
44
+ pi -e . # load the whole @gavin-hu/my-pi-agent package
45
+ pi install ./ # install the package for a user
46
+ ```
47
+
48
+ ```text
49
+ /browse # start at the working directory and open the browser
50
+ /browse status # print the root and URL, or "not running"
51
+ ```
52
+
53
+ `/browse <path>` starts the same server with a subdirectory as its root.
54
+
55
+ ## Commands
56
+
57
+ `/browse` is the only command: it starts, reports, or stops the server, and
58
+ selects the root.
59
+
60
+ ```bash
61
+ /browse start at the working directory, open the browser, print the URL
62
+ /browse <path> start with a subdirectory of the working directory as the root
63
+ /browse status print the root and URL, or "not running"
64
+ /browse stop stop the server
65
+ ```
66
+
67
+ - The root is the effective cwd, so an active `worktree` session serves the
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.
73
+ - 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.
75
+ - The server is closed when its last page has been gone for ten seconds, so
76
+ 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
78
+ it. `session_shutdown` closes it too, and `/browse stop` closes it at once.
79
+
80
+ ## Routes
81
+
82
+ | Route | Result |
83
+ |---|---|
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) |
97
+
98
+ ## Configuration
99
+
100
+ The `file-browse` section of the shared `bytefly.json`. Resolution rules —
101
+ global document then project document, project wins — are in
102
+ [`../configuration.md`](../configuration.md#file-browse--the-local-file-server).
103
+
104
+ | Key | Default | Meaning |
105
+ |---|---|---|
106
+ | `port` | `0` | `0` picks one random port fixed for the session; a positive value pins it. |
107
+ | `autoOpen` | `true` | Open the browser on start; `false` for headless. |
108
+ | `maxFileBytes` | `1048576` | Larger files show a download card only. |
109
+ | `maxThumbBytes` | `5242880` | Larger images get an icon, not a thumbnail. |
110
+ | `maxTextLines` | `5000` | Rendered lines before truncation. |
111
+ | `maxDirEntries` | `2000` | Listed entries before truncation. |
112
+ | `thumbnails` | `true` | Render inline image thumbnails in listings. |
113
+ | `markdown` | `true` | Render Markdown files as formatted HTML. |
114
+ | `markdownHtml` | `true` | Re-emit sanitized raw HTML in rendered Markdown. |
115
+ | `maxIndexEntries` | `20000` | Files collected for quick open before truncation. |
116
+ | `gitDiff` | `true` | Offer the read-only diff view; `false` also drops the changes chip. |
117
+ | `diffContext` | `3` | Unified diff context lines. |
118
+ | `maxDiffLines` | `4000` | Diff lines rendered before truncation. |
119
+ | `live` | `true` | Stream live status to open pages. |
120
+ | `liveIntervalMs` | `5000` | Milliseconds between status frames while a page is connected. |
121
+
122
+ ## Security
123
+
124
+ - **Loopback only.** Binds `127.0.0.1`; no auth and no LAN exposure.
125
+ - **Method and Host.** Only `GET`/`HEAD` are served (else `405`); a `Host`
126
+ header that is not `127.0.0.1[:port]`, `localhost[:port]`, or `[::1][:port]`
127
+ is rejected (else `403`, DNS-rebinding defense), and no CORS headers are sent.
128
+ - **Path guard.** Every path is decoded once, NUL and absolute paths are
129
+ rejected, the path is resolved under the root, and the target (or nearest
130
+ existing ancestor) is `realpath`-checked for containment. This blocks `../`,
131
+ encoded traversal, and symlink escapes. Symlinks are listed but only followed
132
+ when their real path stays inside the root.
133
+ - **CSP.** Pages allow only same-origin scripts and inline styles, and refuse to
134
+ be framed (`frame-ancestors 'none'`, `X-Frame-Options: DENY`), so a local page
135
+ cannot clickjack the controls or re-point relative URLs through an injected
136
+ `<base>`; the policy comes from `lib/http/headers.ts`, shared with `plan`, so
137
+ the two surfaces cannot drift. `/raw` uses
138
+ `default-src 'none'; script-src 'none'; sandbox` plus `nosniff`, so a served
139
+ SVG/HTML file cannot execute.
140
+ - **Markdown safety.** Rendered Markdown is escape-first, and raw HTML passes an
141
+ allowlist (`raw-html.ts`): only known elements and attributes are re-emitted,
142
+ rebuilt from the parsed names rather than copied, so `script`, `style`,
143
+ `iframe`, `svg`, event handlers, `style=` and `class=` never reach the page.
144
+ Link schemes are allowlisted (`javascript:`/`data:` dropped), an external
145
+ image renders as a link instead of loading, and open elements are balanced at
146
+ the end of the document so an unclosed `<div>` cannot swallow the page chrome.
147
+ A suppressed element (`script`, `style`, `svg`, …) is dropped with its body;
148
+ any other unknown tag is escaped and stays visible. `markdownHtml: false`
149
+ restores escape-only rendering. The page CSP keeps the rest inert.
150
+ - **Highlighting safety.** The highlighter escapes every literal before it
151
+ wraps it in a token span, so a code file's content cannot inject markup or
152
+ attributes; only the fixed `tok-*` class names reach the page.
153
+ - **Event stream.** `/events` is a `GET` on the same loopback server, behind
154
+ the same method and `Host` checks, and it only reads: the frame is the branch,
155
+ the change paths and their revision, an uptime, and a request count, all of
156
+ which the page it feeds already renders. It carries no page policy
157
+ (`text/event-stream` is not a document), sends `Cache-Control: no-store`, and
158
+ cannot be cached or replayed. A tick runs only while a page is attached, and
159
+ the timer is `unref`ed, so an idle server does no work and a forgotten tab
160
+ cannot hold the process open. At most eight pages may attach; a ninth is
161
+ answered `204`, the one status an event source never reconnects after.
162
+ - **Caps.** `maxFileBytes` (inline rendering), `maxTextLines` (rendered lines),
163
+ `maxThumbBytes` (thumbnails), and `maxDirEntries` (listings) bound every read.
164
+ - **Git reads.** The header chip and the diff view run read-only git
165
+ (`rev-parse`, `status --porcelain=v1 -z`, `diff --stat`, `diff -U<n>`) with a
166
+ timeout and a short cache; any failure simply omits the chip or the diff. A
167
+ per-file diff sets `GIT_LITERAL_PATHSPECS=1`, so a filename containing glob
168
+ characters is matched literally.
169
+ - **Read-only.** Nothing is ever written.
170
+
171
+ ## The page
172
+
173
+ 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
175
+ folds from its twisty, and a `fold all` / `expand all` button beside the filter
176
+ folds the whole tree at once. The tree scrolls in its own pane with quick open
177
+ and the filter pinned above it; below 860px it collapses into a `Files`
178
+ disclosure above the listing. Below 820px the served folder's path takes a row of
179
+ its own and keeps its last segments, an ellipsis cutting the leading directories;
180
+ below 520px the card's controls hold one scrolling row above the file instead of
181
+ stacking. A hairline progress bar tracks whichever column is scrolling (the card
182
+ body for a file or diff, the main column otherwise), and the served folder names
183
+ the root page. The sidebar is a `<details>` whose content wrapper has no definite
184
+ height, so the panel fills it absolutely and the tree flexes inside.
185
+
186
+ The filter hides non-matching rows while keeping ancestors, and disclosure
187
+ triangles lazily fetch `/api/tree`. Without JavaScript the page still browses and
188
+ renders Markdown, and the JS-only controls (filter, disclosure, actions, quick
189
+ open) are hidden; the sort buttons lose their pointer so they do not look live.
190
+
191
+ The page palette is the shared Bytefly web theme (`lib/web/tokens.ts`), derived
192
+ from the `bytefly-dark`/`bytefly-light` terminal themes. It follows
193
+ `prefers-color-scheme` until an explicit choice sets `data-theme` on the root:
194
+ `/theme.js` applies the saved choice in the head before first paint, and the
195
+ header toggle flips and stores it (`pi-web-theme` in `localStorage`, the key
196
+ shared with plan's review page).
197
+
198
+ The page is keyboard-first, and the tree is one tab stop rather than two per
199
+ folder:
200
+
201
+ | Key | What it does in the tree |
202
+ |---|---|
203
+ | `↑` / `↓` | Move between the rows on screen |
204
+ | `→` | Open a folder (reading its children first if needed), or enter it |
205
+ | `←` | Close the folder, or step out to its parent |
206
+ | `Space` | Open or close the focused folder |
207
+ | `Enter` | Follow the focused row's link |
208
+ | `Home` / `End` | First or last row on screen |
209
+
210
+ The folder's own link carries `aria-expanded`, so a screen reader hears its
211
+ state once; the twisty beside it is a pointer control kept out of the tab order.
212
+ A mouse click moves the tab stop to that row, so `Tab` comes back to where you
213
+ were. Elsewhere the page keeps its own keys: `/` or `Ctrl`/`Cmd`+`K` for quick
214
+ open, `-` to fold, `u` to wrap, `r` for raw Markdown, `y` to copy a line
215
+ reference, `n`/`p` between diff hunks, and `?` for the sheet, whose dialog traps
216
+ `Tab` and returns focus to its button.
217
+
218
+ Printing is supported: the paper gets the light palette whatever the screen is
219
+ showing, the chrome and the tree are dropped, and a file or diff prints whole
220
+ instead of inside its scroll box.
221
+
222
+ ## Previews
223
+
224
+ Text, rendered Markdown, images (thumbnails), and binary/over-size download
225
+ cards, with per-language icons. Broken thumbnails fall back to the category
226
+ icon.
227
+
228
+ Code files render with keywords, strings, numbers, and comments coloured, in the
229
+ dark and light palettes: JavaScript/TypeScript (`js`, `mjs`, `cjs`, `jsx`, `ts`,
230
+ `mts`, `cts`, `tsx`), JSON, CSS (`css`, `scss`, `sass`, `less`), HTML/XML and
231
+ template components (`html`, `htm`, `xml`, `vue`, `svelte`), shell (`sh`,
232
+ `bash`, `zsh`), Python (`py`, `pyw`, `pyi`), Java, YAML (`yaml`, `yml`), and
233
+ SQL. Other text types stay plain.
234
+
235
+ `lib/highlight.ts` is a first-party, escape-first tokenizer for those file
236
+ types. `detectLanguage` maps a filename to a tokenizer and an unmapped type
237
+ stays plain. `highlightLines` walks the whole file, so block comments, template
238
+ literals, open HTML tags, CSS brace depth, and Python triple-quoted strings
239
+ carry across lines; every literal is escaped with the same `escapeHtml` the
240
+ plain view uses, and a line's markup is only the fixed `tok-*` classes the
241
+ stylesheet colours. It backs both the plain text view and fenced code blocks in
242
+ the rendered Markdown view.
243
+
244
+ The Name / Size / Modified headers sort the current directory, directories
245
+ first, and the choice is remembered per browser. Below 520px the Modified column
246
+ is dropped, so the name keeps the row instead of truncating to the first few
247
+ characters. Copy a file's root-relative path or its contents, copy a
248
+ `path:12-30` reference for the lines you selected, and toggle line wrapping for
249
+ text and raw views.
250
+
251
+ ## Markdown preview
252
+
253
+ `.md`, `.markdown`, `.mdown`, `.mkd`, and `.mkdn` files render as headings,
254
+ lists, tables, code, inline and reference links, bare-URL autolinks, and images,
255
+ with a one-click **raw** view (`?raw=1`). The page fills the card, so the scroll
256
+ bar sits at the card edge; headings, prose, and images keep a centred reading
257
+ measure of at most 1012px. Code blocks stay inside that column; tables start at
258
+ it and are never wider than it, so every table in a document ends on the same
259
+ line as the prose above it, and one wider than the pane scrolls, its right edge
260
+ fading until the last column is reached. Columns size to
261
+ the content, and in a pane narrower than 600px they stop shrinking: each cell
262
+ keeps a readable width, so the table scrolls instead of stacking one word per
263
+ line. A task list's checkbox is drawn by the page, so a done item carries the
264
+ success colour in both themes. Named and numeric character references are
265
+ decoded. Headings get stable anchor ids, so in-document `#anchor` links work.
266
+ Fenced code blocks are syntax-highlighted when the fence names a supported
267
+ language.
268
+
269
+ A README's raw HTML is re-emitted through an allowlist (`<details>`,
270
+ `<summary>`, tables, `<kbd>`, images), so it renders without becoming an
271
+ injection path. Raw HTML in the source is escaped, never executed, and external
272
+ images are shown as links rather than fetched. `markdownHtml: false` restores
273
+ escape-only rendering; `markdown: false` falls back to the plain-text view.
274
+
275
+ `markdown.ts` is a first-party, escape-first renderer for the constructs that
276
+ show up in repository READMEs. It emits only a known tag set, escapes every
277
+ literal character, allowlists URL schemes (dropping `javascript:`/`data:`),
278
+ rewrites local links and images to `/view`/`/raw`, and renders an external image
279
+ as a link instead of loading it (the page CSP is `img-src 'self' data:`). Link
280
+ reference definitions (`[id]: url`), including the full `[text][id]`, collapsed
281
+ `[text][]`, and shortcut `[text]` forms, and numeric and common named character
282
+ references are resolved. The Rendered/Raw switch is a server-side `?raw=1` link,
283
+ so it works without JavaScript. Headings are demoted one level so the card title
284
+ stays the page's only `h1`, and each gets a slug `id` (unique in the document). A
285
+ fenced code block whose info string names a supported language is highlighted
286
+ through `lib/highlight.ts` (either an extension or a long-form alias,
287
+ `fenceLanguage`); an absent or unknown language stays plain.
288
+
289
+ A line that starts with an allowed block tag (`<details>`, `<table>`, `<div>`,
290
+ `<pre>`, …), a comment, or a closing tag opens an HTML block that runs to the
291
+ next blank line; text inside it stays text, and a suppressed tag (`<script>`,
292
+ `<style>`, `<svg>`) drops its body through the matching close tag. `raw-html.ts`
293
+ re-emits only allowlisted elements and attributes: `a`/`img` go through the same
294
+ `/view`/`/raw` resolution as Markdown, an external image becomes a link, and
295
+ `class`/`style`/handlers are dropped. Phrasing tags are also accepted inside a
296
+ line, while a block tag mid-paragraph stays escaped. Open elements are balanced
297
+ across blocks, which is how a `<details>` can wrap Markdown paragraphs.
298
+
299
+ ## Quick open
300
+
301
+ `/` (or `Ctrl/Cmd+K`, or the sidebar button) opens a fuzzy path finder over a
302
+ capped index of the tree, with keyboard navigation and a highlighted match;
303
+ Enter opens the file. `/api/files` ranks the cached index with `fuzzy.ts` and
304
+ returns the matched character positions; the client fetches on an 80 ms debounce
305
+ and builds rows with `textContent`, so a file name cannot inject markup. The
306
+ index skips `.git`, `node_modules`, and symlinks, is capped by
307
+ `maxIndexEntries`, and is cached per server for 10 s by `createFileIndex`. The
308
+ palette is a `dialog`/`combobox`/`listbox` with `aria-activedescendant` and
309
+ `Esc` / arrow / `Enter` handling.
310
+
311
+ ## Git context and diff view
312
+
313
+ A read-only header chip shows the checked-out branch (or detached head) and
314
+ reflows on narrow screens. Uncommitted changes are stated once, by the changes
315
+ chip below; turn the diff view off (`gitDiff: false`) and the header reports no
316
+ changes at all.
317
+
318
+ A `● N changes +A −D` chip links to `/diff`, a change overview with
319
+ `git diff --stat` and a per-file status table, and `/diff/<rel>` renders one
320
+ file's unified diff, whose head carries its own `+A −D` and hunk count. Below
321
+ 520px that table's change word is dropped, where the badge already carries the
322
+ same letter and the path needs the room. Changed lines are syntax-highlighted
323
+ with the file's language, and dirty files and their directories carry a marker
324
+ in the listing and the tree. The scope is switchable between `head` / `staged` /
325
+ `worktree`; untracked files show as all-add, an unborn `HEAD` falls back to the
326
+ index, and a non-repository is a plain empty
327
+ state. Read-only, capped, and rendered with the shared diff colours. A diff's
328
+ hunk headers pin to the top of the code viewport while its body scrolls.
329
+
330
+ `git.ts` extends the read-only provider set with a TTL-cached
331
+ `git status --porcelain=v1 -z` and a per-mode `git diff`; `diff.ts` parses the
332
+ unified diff and the porcelain records into plain view models, so `router.ts`
333
+ and `html.ts` stay pure. `head` compares against `HEAD` (falling back to
334
+ `--cached` before the first commit), `staged` against the index, and `worktree`
335
+ against the index from the other side. An untracked file renders as all-add
336
+ through `readFileView`, so binary and over-size handling is shared with the file
337
+ view. `git diff --stat` supplies the chip totals and `changeDirs` derives the
338
+ directory markers; the changed lines are highlighted per hunk by rebuilding the
339
+ old and new sides for `lib/highlight`.
340
+
341
+ ## Live status
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.
349
+
350
+ The page has its own live status, which is what a left-open tab reads. A `◉ live`
351
+ chip beside `read-only` says whether status is flowing (green), still
352
+ connecting (accent), stalled (amber, after three missed frames), or offline
353
+ (red); the branch chip, the `● N changes +A −D` chip, and the change markers in
354
+ the tree and the listing update in place; and the footer line reports `up 2m
355
+ 14s · 37 requests`. The chip names its state and never counts, and a frame whose
356
+ change set did not move repaints nothing at all. One Server-Sent Events stream
357
+ per page carries it, on the same loopback server, so nothing extra is opened and
358
+ nothing is written; that stream is also how the server notices the last page
359
+ closing its window, after which it stops listening.
360
+
361
+ The header chip starts hidden and is painted by the client from `/events`:
362
+ `◉ live` in the success colour while frames arrive, `◉ connecting` before the
363
+ first one, `◉ stale` in the warning colour when three intervals pass without a
364
+ frame on an open connection, and `◉ offline` in the danger colour once the
365
+ stream errors. The chip names its state rather than counting, so it is repainted
366
+ only when that state changes. The same frame carries the branch (or detached
367
+ head), the uncommitted-change totals and paths, the server's uptime, and the
368
+ requests it has answered, so the branch chip, the changes chip, the markers, and
369
+ the footer line are painted from one snapshot rather than four requests. Each
370
+ change set also carries a short revision, and a frame whose revision the page
371
+ already painted does no work at all — so an idle tree costs no DOM, however many
372
+ rows the page drew. Losing the stream leaves everything the page last showed in
373
+ place: it is read-only, so a stale label is never a stale action. `"live": false`
374
+ turns the whole surface off — no chip, no stream, no `/events` route — and so
375
+ does JavaScript off.
376
+
377
+ At most eight pages may hold the stream at once: a ninth is refused and its chip
378
+ reads `offline`, which keeps one local page from pinning the server — and the
379
+ git read behind each frame — alive. The stream is also how the server knows a
380
+ page is still there: when the last one leaves, the port is released after ten
381
+ seconds and the chip is cleared, because nothing is left to serve. That is why a
382
+ page with JavaScript off — or with live status off — never counts as a reader,
383
+ and the server then waits for `/browse stop` or the end of the session.
384
+
385
+ `live.ts` builds one snapshot per tick — the git status, the `head` change
386
+ index, the server's uptime, and its request count — and `frameOf` serializes it
387
+ as one `status` event. `createLiveChannel` holds the attached pages and starts
388
+ the ticker on the first attach, so an idle server does no work; it shares a
389
+ single in-flight snapshot between pages and stops the ticker on the last detach
390
+ and on `close()`, which `server.ts` calls before closing the socket. The idle
391
+ stop is one layer down: `lib/http` arms a single timer when a server's last
392
+ stream ends, a reload re-attaches and cancels it, and `server.ts` turns that into
393
+ the extension's `close()` and a cleared chip. Both providers are already
394
+ TTL-cached, so a tick
395
+ re-reads at most one `git status`, and every read failure degrades to `null`
396
+ rather than a failed frame. The event-stream headers and the `retry:` preamble
397
+ live in `lib/http` with the rest of the loopback policy, shared with plan's
398
+ review page. The client half is `live-client.ts`: one `EventSource`, one
399
+ snapshot paint (`textContent` only, never markup), one staleness timeout armed
400
+ per frame, and a `pagehide` cleanup.
401
+
402
+ ## Reload hint
403
+
404
+ A page that lists a folder carries the revision of the names it drew, and asks
405
+ `/api/dir-rev` for the folder's current one on the frame cadence — one directory
406
+ read every five seconds by default, skipped while the tab is hidden and stopped
407
+ for good once it has an answer. A difference reveals `⟳ reload` beside the live
408
+ chip, which reloads the page; the rows themselves are never touched, because a
409
+ page that could grow a row is a page that writes. A folder whose listing was
410
+ truncated, and a file, `/diff`, or error page, has nothing to compare and shows
411
+ no control.
412
+
413
+ `/api/dir-rev` answers one folder's listing revision, so the page can tell
414
+ whether its rows are stale. It goes through the same path guard as every other
415
+ route and reads one directory; its answer is a hash of the names, not the names
416
+ themselves, so it discloses nothing the page above it does not already show. The
417
+ check compares names, so it is a directory read rather than a stat pass, and it
418
+ stops at the first difference because the control, not the checker, is the fix.
419
+
420
+ ## Limitations
421
+
422
+ - Syntax highlighting covers the built-in language set above; any other text
423
+ type renders exactly as before. State that the tokenizer does not carry
424
+ across lines (shell heredocs, interpolation inside a template literal) is
425
+ approximated rather than parsed.
426
+ - Live status updates what the page already contains: the header chips, the
427
+ markers on the rows it rendered, and the footer line. A file that appears on
428
+ disk does not join the listing by itself — the page offers `⟳ reload` instead —
429
+ and the check covers the folder the page lists, by name, not the folders inside
430
+ it, a listed file's contents, the `/diff` table, or a file page's `changes`
431
+ action, which stay page-load snapshots. The branch chip is only patched when
432
+ the page was served inside a repository.
433
+ - `/browse` has no auth; a malicious local page could probe the port. The `Host`
434
+ allowlist and the absence of CORS headers mitigate reads, and the server is
435
+ read-only.
436
+ - Serving the root exposes whatever is on disk to the local user who started it;
437
+ this is intentional and scoped to loopback.
438
+
439
+ ## Non-goals
440
+
441
+ - No editing or writing from the browser.
442
+ - No third-party assets: the Markdown renderer, its raw-HTML sanitizer, and the
443
+ syntax highlighter are first-party, so raw HTML in a Markdown file is
444
+ allowlisted and never executed.
445
+ - No auth and no LAN exposure: loopback only.
446
+ - Not a general static-file server: the root is confined to the effective cwd.
447
+
448
+ ## Pi integration
449
+
450
+ | Integration point | How |
451
+ |---|---|
452
+ | 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. |
454
+ | 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
+
456
+ ## Design notes
457
+
458
+ - **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.
469
+ - **Pure routing.** `handleRequest(context, method, rawUrl, headers)` validates
470
+ method and `Host`, parses the URL, matches a route, and runs every path
471
+ through `paths.ts`. It returns a plain `ServeResponse`, so routing is tested
472
+ without a socket; `server.ts` is the only part that hands those routes to
473
+ `lib/http`'s loopback server.
474
+ - **One snapshot, one frame.** The status source is shared by every attached
475
+ page, so a tick costs one `git status`, one `git diff --stat`, and one
476
+ serialization however many tabs are open. The frame carries a revision of the
477
+ change set, and the client compares it before painting, which is why an idle
478
+ page does no DOM work. The cadence is what bounds those reads, so the default
479
+ is five seconds rather than matching the providers' two-second render cache.
480
+ - **A poll, not a watcher.** The folder check is one request from the page to
481
+ `/api/dir-rev`, not an `fs.watch`: recursive watching is the one part of
482
+ `node:fs` whose availability and event semantics differ per platform, and it
483
+ would add a long-lived resource to start and release around `close()`. The
484
+ check compares names, so it is a directory read rather than a stat pass, and it
485
+ stops at the first difference because the control, not the checker, is the fix.
486
+ - **Progressive enhancement.** The page is server-rendered first, so it browses
487
+ and renders Markdown without JavaScript; the client only adds the filter, lazy
488
+ disclosure, sorting, wrap, copy, and quick open.
489
+ - **Streaming responses.** The server streams `filePath` responses and
490
+ suppresses bodies for `HEAD`, so large `/raw` files are not buffered.
491
+ - **Root confinement over feature breadth.** The root is deliberately confined
492
+ to the effective cwd rather than serving an arbitrary directory, so a
493
+ worktree session serves the worktree and nothing above it.
494
+
495
+ ## Files
496
+
497
+ | File | Purpose |
498
+ |---|---|
499
+ | `index.ts` | `/browse` command, running-server closure, shutdown cleanup |
500
+ | `config/config.ts` | Config defaults, validation, global/project merge |
501
+ | `files/paths.ts` | Decode/encode, resolve, and `realpath`-containment guard |
502
+ | [`../../lib/escape.ts`](../../lib/escape.ts) | `escapeHtml`, the shared HTML escaping used by every renderer |
503
+ | [`../../lib/highlight.ts`](../../lib/highlight.ts) | First-party escape-first syntax tokenizer for the code view (shared with `plan`) |
504
+ | [`../../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 |
506
+ | `text/diff.ts` | Pure unified-diff and porcelain-status parsing for the diff view |
507
+ | `text/fuzzy.ts` | Fuzzy path matching and ranking for the quick-open palette |
508
+ | `view/markdown.ts` | First-party escape-first Markdown-to-HTML renderer (fenced code highlighted; raw HTML through `raw-html.ts`) |
509
+ | `view/raw-html.ts` | Allowlisted raw-HTML sanitizer: element/attribute policy, suppressed bodies, and cross-block balancing |
510
+ | `view/urls.ts` | Shared link/image URL policy: scheme allowlist and `/view`/`/raw` rewriting |
511
+ | `git/git.ts` | Read-only branch/dirty provider for the header chip |
512
+ | `view/icons.ts` | Inline SVG sprite and the per-language mapping |
513
+ | `view/client.ts` | The `/app.js` source: sort, wrap, copy, hunk navigation, reading progress, and the page keymap |
514
+ | `view/tree.ts` | The sidebar block: path filter, fold-all, lazy tree loading, and the tree's keyboard model (interpolated into `client.ts`) |
515
+ | `view/palette.ts` | The quick-open palette: fuzzy search, the combobox wiring, and the `window.bfPalette` contract (interpolated into `client.ts`) |
516
+ | `view/live-client.ts` | The client's live status block: the event stream, the chips, the markers, and the footer line (interpolated into `client.ts`) |
517
+ | `live/live.ts` | The live status snapshot, its frame, and the fan-out channel that ticks it |
518
+ | `live/rev.ts` | Short digests of the change set and a folder's listing names, so an unchanged frame is skipped |
519
+ | `view/chrome.ts` | Header chrome: the git and changes chips, the live chip, the reload control, and the footer line |
520
+ | `view/styles.ts` | The page-only `browse-*` stylesheet (the `bf-*` kit comes from `lib/web`) |
521
+ | `view/html.ts` | Page renderers: the shared shell, the two-pane layout, the listing, and the cards |
522
+ | `view/diff-html.ts` | The unified-diff body: the gutter, the sign, the hunk sections, and per-hunk highlighting |
523
+ | `transport/router.ts` | Pure request handler: routes, the `/raw` sandbox policy, and the error page |
524
+ | `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 |
526
+ | [`../../lib/browser.ts`](../../lib/browser.ts) | Cross-platform browser opener (shared with `plan`) |
527
+
528
+ ## Testing
529
+
530
+ `extensions/file-browse/` covers the path guard (traversal, encoded, NUL, malformed,
531
+ absolute, symlink escape and in-root symlink), file classification/listings/
532
+ reads/tree and the quick-open file index, listing revisions, the fuzzy matcher,
533
+ the syntax highlighter (language detection, fence aliases, token classes,
534
+ cross-line state, and escaping), the Markdown renderer (blocks, inline,
535
+ fenced-code highlighting, safety, raw HTML, and path resolution), the raw-HTML
536
+ sanitizer (allowlist, suppressed bodies, attribute policy, balancing, and a
537
+ seeded invariant test over fragment soup),
538
+ the icon mapping and sprite, the page chrome and page renderers, HTML
539
+ escaping/encoding, the client script and its serving, the platform browser
540
+ commands with an injected spawn, the router (routes, `/api/tree`, `/api/files`,
541
+ `/api/dir-rev`, `?raw=1`, status codes, `Host`, `HEAD`, `/raw` headers, and the
542
+ `/diff` view routes and mode validation) without a socket, a real
543
+ `127.0.0.1:0` server, the session-port lifecycle (fixed on first start, reused,
544
+ and re-rolled after a conflict), config normalization and clamping, the git
545
+ status provider (branch, dirty, detached, non-repo, failure, and TTL cache), the
546
+ git diff parser and provider (line kinds, line numbers, status and rename
547
+ parsing, mode filtering, untracked all-add, unborn-HEAD fallback, literal
548
+ pathspec, and TTL cache), the diff renderers (highlighted changed lines, marker
549
+ sets, and chip totals), the live status feed (snapshot fields, degraded
550
+ providers, the frame, and the channel's ticker driven by an injected timer, plus
551
+ the ticker parking on the last detach and on `close()`), the `/events` route and its 404 when the channel is off, a real
552
+ socket that reads a page's first frame and the ticker's next one and sees the
553
+ stream end when the server closes, the live chrome a page renders when on and
554
+ not when off, and the command lifecycle with injected seams.
@@ -5,11 +5,19 @@ handler, or reveals it in the file manager. `/open` does it interactively and
5
5
  the `open_path` tool does it from a script, both resolving the path against the
6
6
  effective working directory.
7
7
 
8
- ## Tool rendering
8
+ ## Capabilities
9
9
 
10
- How the `open_path` call and its result look in the transcript — the call line as a headline and the body indented under it:
10
+ Status records what a reader can rely on: **Shipped** is in a tagged release, **Unreleased** is complete in the current tree but not yet tagged, and **Partial** has one released part and one unreleased part. Rows are ordered by user value, highest first.
11
11
 
12
- <img src="../../assets/tools/open_path.png" alt="The open_path tool's call and result, drawn by the generic fallback" width="100%">
12
+ | Capability | How | What you get | Status |
13
+ |---|---|---|---|
14
+ | Native open | `/open`, `open_path` | The file or directory opens in the application the OS associates with its type. | Shipped |
15
+ | Reveal in the file manager | `/open -r`, `open_path` with `reveal: true` | The target is selected in the file manager instead of opened. | Shipped |
16
+ | Working-directory resolution | `/open`, `open_path` | A relative path resolves against the effective working directory, so under [`worktree`](./worktree.md) `/open .` opens the isolated worktree. | Shipped |
17
+ | Codemode-only exposure | `exposure: "codemode"` | The model cannot call `open_path` directly unless a codemode script asks for it; the table does not promise otherwise. | Shipped |
18
+ | Pasted-path cleanup | `/open`, `open_path` | The editor's `@` mention prefix, surrounding quotes, and control characters are stripped before the path is used. | Shipped |
19
+ | Failure reporting | `/open`, `open_path` | A missing path, a launcher that cannot start, and a non-zero exit after a successful start on macOS/Linux are reported. | Shipped |
20
+ | Session-outliving launch | `/open`, `open_path` | The handler runs detached, so the application outlives the Pi session. | Shipped |
13
21
 
14
22
  ## Quickstart
15
23
 
@@ -25,21 +33,6 @@ pi install ./ # install the package for a user
25
33
  /open -r src reveal src in the file manager
26
34
  ```
27
35
 
28
- ## What it does
29
-
30
- - Opens a file or directory with the OS-native application: `open` on macOS,
31
- `explorer` on Windows, `xdg-open` on Linux.
32
- - Reveals it in the file manager instead with `-r` (or `reveal: true`): `open -R`
33
- on macOS, `explorer /select,` on Windows, and the containing directory on
34
- Linux, which has no "select" verb.
35
- - Unwraps the editor's `@` file-mention prefix and surrounding quotes, so
36
- `/open @src/app.ts` and `/open @"my file.txt"` both work.
37
- - Reports a launcher that cannot be started, and (on macOS and Linux) a launcher
38
- that exits non-zero because no handler accepted the target.
39
- - Resolves relative paths against the effective working directory, so under the
40
- `worktree` extension `/open .` opens the isolated worktree.
41
- - Launches the handler detached, so it outlives the Pi session.
42
-
43
36
  ## Commands and flags
44
37
 
45
38
  ```bash
@@ -55,6 +48,8 @@ needs an interactive terminal UI.
55
48
 
56
49
  ## Tool
57
50
 
51
+ <img src="../../assets/tools/open_path.png" alt="The open_path tool's call and result, drawn by the generic fallback" width="100%">
52
+
58
53
  `open_path` — `exposure: "codemode"`, inactive by default, not read-only,
59
54
  open-world (`openWorldHint: true` because it starts an OS process). It is listed
60
55
  by the `codemode` tool for a script to call as `tools.open_path(...)`, and an
@@ -106,6 +101,19 @@ JSON, and print modes; a codemode script can call it in any of them. Only the
106
101
  non-zero after a successful open, so only a failure to start is reported
107
102
  there. Nothing is retried.
108
103
 
104
+ ## Native handlers
105
+
106
+ Both surfaces run the same command for a platform; the mode alone chooses
107
+ between open and reveal.
108
+
109
+ | Platform | Open | Reveal |
110
+ |---|---|---|
111
+ | macOS | `open <path>` | `open -R <path>` |
112
+ | Windows | `explorer <path>` | `explorer /select,<path>` |
113
+ | Linux | `xdg-open <path>` | `xdg-open <dir>` |
114
+
115
+ Reveal has no native meaning on Linux, so there the containing directory opens.
116
+
109
117
  ## Limitations
110
118
 
111
119
  - Reveal has no native meaning on Linux, so it opens the containing directory.
@@ -127,7 +135,7 @@ JSON, and print modes; a codemode script can call it in any of them. Only the
127
135
  | Command | `/open [path]` and `/open -r [path]`; reports through `ctx.ui.notify`. |
128
136
  | State | None persisted, and none needed: every call is independent. |
129
137
  | Lifecycle | No `session_start` / `session_shutdown` hooks; the factory holds no long-lived resources. |
130
- | Host seams | `resolveEffectiveCwd` and `isExtensionEnabled` (`lib/env.ts`), `stripControlChars` (`lib/format.ts`), `spawn` / `open` / `xdg-open` / `explorer` (via the injected launcher in `open.ts`). |
138
+ | Host seams | `resolveEffectiveCwd` and `isExtensionEnabled` ([`lib/env.ts`](../../lib/env.ts)), `stripControlChars` ([`lib/format.ts`](../../lib/format.ts)), `spawn` / `open` / `xdg-open` / `explorer` (via the injected launcher in [`open.ts`](../../extensions/file-open/open.ts)). |
131
139
  | Config | None. |
132
140
 
133
141
  ## Design notes
@@ -149,7 +157,7 @@ of the model's active tool set unless a script explicitly reaches for it.
149
157
 
150
158
  | Module | Responsibility |
151
159
  |---|---|
152
- | `index.ts` | The extension factory: registers `open_path` and the `/open` command, resolves the target, and routes a launch failure to `ctx.ui.notify`. |
153
- | `open.ts` | `normalizeTarget` and `buildCommand` (pure), `resolveTarget` (existence check), and `openTarget` with the injectable launcher, which reports a start failure and a meaningful non-zero exit. |
154
- | `schema.ts` | `TOOL_NAME`, the TypeBox `parameters` / `outputSchema`, and the `OpenPathArgs` / `OpenPathResult` types. |
155
- | `extension.test.ts` | The capability suite: normalization, resolution, per-platform commands, the launcher seam, registration, disabling, the tool call, and the command. |
160
+ | [`index.ts`](../../extensions/file-open/index.ts) | The extension factory: registers `open_path` and the `/open` command, resolves the target, and routes a launch failure to `ctx.ui.notify`. |
161
+ | [`open.ts`](../../extensions/file-open/open.ts) | `normalizeTarget` and `buildCommand` (pure), `resolveTarget` (existence check), and `openTarget` with the injectable launcher, which reports a start failure and a meaningful non-zero exit. |
162
+ | [`schema.ts`](../../extensions/file-open/schema.ts) | `TOOL_NAME`, the TypeBox `parameters` / `outputSchema`, and the `OpenPathArgs` / `OpenPathResult` types. |
163
+ | [`extension.test.ts`](../../extensions/file-open/extension.test.ts) | The capability suite: normalization, resolution, per-platform commands, the launcher seam, registration, disabling, the tool call, and the command. |