@gavin-hu/my-pi-agent 0.18.0 → 0.20.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 (324) hide show
  1. package/README.md +66 -52
  2. package/README.zh-CN.md +56 -46
  3. package/docs/extensions/README.md +34 -0
  4. package/docs/extensions/board.md +507 -0
  5. package/{extensions/bytefly/README.md → docs/extensions/bytefly.md} +774 -574
  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 +561 -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} +62 -52
  13. package/{extensions/memory/README.md → docs/extensions/memory.md} +71 -60
  14. package/{extensions/plan/README.md → docs/extensions/plan.md} +440 -322
  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 +298 -0
  18. package/{extensions/subagent/README.md → docs/extensions/subagent.md} +165 -118
  19. package/docs/extensions/team.md +254 -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} +260 -163
  24. package/{extensions/worktree/README.md → docs/extensions/worktree.md} +137 -79
  25. package/extensions/board/commands.ts +105 -0
  26. package/extensions/board/config.ts +36 -0
  27. package/extensions/board/format.ts +170 -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 +388 -0
  32. package/extensions/board/result.ts +108 -0
  33. package/extensions/board/rows.ts +48 -0
  34. package/extensions/board/schema.ts +90 -0
  35. package/extensions/board/seat.ts +28 -0
  36. package/extensions/board/snapshot.ts +42 -0
  37. package/extensions/board/tools.ts +444 -0
  38. package/extensions/board/transport/router.ts +91 -0
  39. package/extensions/board/transport/server.ts +138 -0
  40. package/extensions/board/types.ts +121 -0
  41. package/extensions/board/view/client.ts +51 -0
  42. package/extensions/board/view/dialog.ts +43 -0
  43. package/extensions/board/view/keys.ts +125 -0
  44. package/extensions/board/view/live.ts +231 -0
  45. package/extensions/board/view/names.ts +91 -0
  46. package/extensions/board/view/page.ts +118 -0
  47. package/extensions/board/view/render.ts +519 -0
  48. package/extensions/board/view/styles.ts +228 -0
  49. package/extensions/board/view/team.ts +327 -0
  50. package/extensions/bytefly/footer/budget.ts +6 -5
  51. package/extensions/bytefly/footer/collect/entries.ts +12 -4
  52. package/extensions/bytefly/footer/collect/snapshot.ts +8 -17
  53. package/extensions/bytefly/footer/config.ts +19 -8
  54. package/extensions/bytefly/footer/price/price.ts +30 -8
  55. package/extensions/bytefly/footer/register.ts +5 -5
  56. package/extensions/bytefly/footer/render/format.ts +18 -0
  57. package/extensions/bytefly/footer/render/lines.ts +23 -7
  58. package/extensions/bytefly/footer/render/segments.ts +117 -31
  59. package/extensions/bytefly/footer/types.ts +29 -5
  60. package/extensions/bytefly/footer/unpriced.ts +42 -17
  61. package/extensions/bytefly/panel/commands.ts +67 -7
  62. package/extensions/bytefly/panel/filter.ts +68 -0
  63. package/extensions/bytefly/panel/format.ts +16 -15
  64. package/extensions/bytefly/panel/join.ts +56 -13
  65. package/extensions/bytefly/panel/prices.ts +43 -0
  66. package/extensions/bytefly/panel/settings-runtime.ts +0 -6
  67. package/extensions/bytefly/panel/settings.ts +75 -43
  68. package/extensions/bytefly/panel/tui/entries.ts +26 -8
  69. package/extensions/bytefly/panel/tui/frame.ts +130 -93
  70. package/extensions/bytefly/panel/tui/keys.ts +352 -0
  71. package/extensions/bytefly/panel/tui/list-view.ts +25 -46
  72. package/extensions/bytefly/panel/tui/panes.ts +76 -0
  73. package/extensions/bytefly/panel/tui/section-view.ts +126 -0
  74. package/extensions/bytefly/panel/tui/settings-section.ts +54 -125
  75. package/extensions/bytefly/panel/tui/settings-view.ts +30 -131
  76. package/extensions/bytefly/panel/tui/side-pane.ts +224 -0
  77. package/extensions/bytefly/panel/tui.ts +306 -259
  78. package/extensions/bytefly/picker/discovery.ts +0 -2
  79. package/extensions/bytefly/picker/types.ts +0 -2
  80. package/extensions/bytefly/update/check.ts +39 -0
  81. package/extensions/bytefly/update/format.ts +15 -0
  82. package/extensions/bytefly/update/register.ts +73 -34
  83. package/extensions/bytefly/update/state.ts +6 -3
  84. package/extensions/check/commands.ts +2 -1
  85. package/extensions/context/tui.ts +1 -1
  86. package/extensions/file-browse/{config.ts → config/config.ts} +1 -1
  87. package/extensions/file-browse/files/classify.ts +136 -0
  88. package/extensions/file-browse/files/files.ts +12 -0
  89. package/extensions/file-browse/files/index.ts +90 -0
  90. package/extensions/file-browse/files/list.ts +261 -0
  91. package/extensions/file-browse/{paths.ts → files/paths.ts} +1 -1
  92. package/extensions/file-browse/files/tree.ts +101 -0
  93. package/extensions/file-browse/{git.ts → git/git.ts} +10 -3
  94. package/extensions/file-browse/index.ts +4 -4
  95. package/extensions/file-browse/{live.ts → live/live.ts} +20 -69
  96. package/extensions/file-browse/{router.ts → transport/router.ts} +21 -17
  97. package/extensions/file-browse/transport/server.ts +110 -0
  98. package/extensions/file-browse/{chrome.ts → view/chrome.ts} +4 -4
  99. package/extensions/file-browse/{client.ts → view/client.ts} +1 -1
  100. package/extensions/file-browse/{diff-html.ts → view/diff-html.ts} +3 -3
  101. package/extensions/file-browse/view/emit.ts +51 -0
  102. package/extensions/file-browse/{html.ts → view/html.ts} +19 -18
  103. package/extensions/file-browse/{icons.ts → view/icons.ts} +1 -1
  104. package/extensions/file-browse/{markdown.ts → view/markdown.ts} +18 -20
  105. package/extensions/file-browse/{raw-html.ts → view/raw-html.ts} +19 -14
  106. package/extensions/file-browse/{styles.ts → view/styles.ts} +29 -5
  107. package/extensions/file-browse/{tree.ts → view/tree.ts} +1 -1
  108. package/extensions/file-browse/{urls.ts → view/urls.ts} +1 -1
  109. package/extensions/file-read/schema.ts +2 -1
  110. package/extensions/job/runtime.ts +4 -4
  111. package/extensions/memory/store.ts +12 -5
  112. package/extensions/plan/blocks.ts +23 -2
  113. package/extensions/plan/commands.ts +21 -16
  114. package/extensions/plan/feedback.ts +185 -52
  115. package/extensions/plan/format.ts +10 -3
  116. package/extensions/plan/index.ts +23 -5
  117. package/extensions/plan/plan-files.ts +88 -0
  118. package/extensions/plan/plan-index.ts +43 -9
  119. package/extensions/plan/plan-text.ts +33 -23
  120. package/extensions/plan/plans.ts +27 -34
  121. package/extensions/plan/policy.ts +30 -17
  122. package/extensions/plan/review.ts +3 -27
  123. package/extensions/plan/runtime.ts +52 -11
  124. package/extensions/plan/sections.ts +112 -0
  125. package/extensions/plan/steps.ts +3 -22
  126. package/extensions/plan/todos.ts +51 -3
  127. package/extensions/plan/tools/exit.ts +20 -95
  128. package/extensions/plan/tools/todo-seed.ts +4 -2
  129. package/extensions/plan/tools/write.ts +12 -1
  130. package/extensions/plan/tui.ts +5 -34
  131. package/extensions/plan/types.ts +15 -8
  132. package/extensions/plan/web-blocks.ts +89 -0
  133. package/extensions/plan/web-chrome.ts +3 -5
  134. package/extensions/plan/web-client.ts +179 -330
  135. package/extensions/plan/web-collab.ts +3 -3
  136. package/extensions/plan/web-comments.ts +18 -8
  137. package/extensions/plan/web-composer.ts +80 -0
  138. package/extensions/plan/web-draft.ts +40 -0
  139. package/extensions/plan/web-keys.ts +134 -0
  140. package/extensions/plan/web-outline.ts +192 -0
  141. package/extensions/plan/web-page.ts +4 -8
  142. package/extensions/plan/web-plan.ts +150 -0
  143. package/extensions/plan/web-router.ts +3 -58
  144. package/extensions/plan/web-settle.ts +194 -0
  145. package/extensions/plan/web-share-session.ts +24 -30
  146. package/extensions/plan/web-streams.ts +14 -14
  147. package/extensions/plan/web-tokens.ts +2 -2
  148. package/extensions/plan/web.ts +66 -120
  149. package/extensions/rewind/{commands.ts → commands/commands.ts} +5 -5
  150. package/extensions/rewind/{review.ts → commands/review.ts} +15 -14
  151. package/extensions/rewind/{rewind.ts → commands/rewind.ts} +6 -6
  152. package/extensions/rewind/{config.ts → config/config.ts} +1 -1
  153. package/extensions/rewind/index.ts +3 -3
  154. package/extensions/rewind/{policy.ts → state/policy.ts} +4 -4
  155. package/extensions/rewind/{runtime.ts → state/runtime.ts} +11 -11
  156. package/extensions/rewind/{snapshot-run.ts → state/snapshot-run.ts} +1 -1
  157. package/extensions/rewind/{timeline.ts → state/timeline.ts} +2 -2
  158. package/extensions/rewind/{format.ts → text/format.ts} +3 -3
  159. package/extensions/rewind/{git.ts → transport/git.ts} +2 -2
  160. package/extensions/rewind/{restore.ts → transport/restore.ts} +2 -2
  161. package/extensions/rewind/{snapshot.ts → transport/snapshot.ts} +1 -1
  162. package/extensions/rewind/{store.ts → transport/store.ts} +1 -1
  163. package/extensions/rewind/{checklist.ts → view/checklist.ts} +6 -6
  164. package/extensions/rewind/{hunk-picker.ts → view/hunk-picker.ts} +8 -8
  165. package/extensions/rewind/{path-picker.ts → view/path-picker.ts} +7 -7
  166. package/extensions/rewind/{review-tui.ts → view/review-tui.ts} +11 -11
  167. package/extensions/rewind/{rewind-tui.ts → view/rewind-tui.ts} +17 -11
  168. package/extensions/rewind/{status.ts → view/status.ts} +4 -4
  169. package/extensions/session-bus/commands.ts +17 -69
  170. package/extensions/session-bus/format.ts +22 -45
  171. package/extensions/session-bus/index.ts +13 -7
  172. package/extensions/session-bus/join-request.ts +40 -0
  173. package/extensions/session-bus/mail-watch.ts +134 -0
  174. package/extensions/session-bus/notice.ts +32 -4
  175. package/{lib/claims → extensions/session-bus}/overlap.ts +11 -18
  176. package/extensions/session-bus/protocol.ts +19 -49
  177. package/extensions/session-bus/resolve.ts +67 -0
  178. package/extensions/session-bus/result.ts +16 -51
  179. package/extensions/session-bus/rows.ts +11 -5
  180. package/extensions/session-bus/runtime.ts +220 -267
  181. package/extensions/session-bus/store.ts +55 -7
  182. package/extensions/session-bus/tools.ts +31 -66
  183. package/extensions/session-bus/tui.ts +5 -5
  184. package/extensions/session-bus/types.ts +23 -14
  185. package/extensions/subagent/agents.ts +9 -8
  186. package/extensions/subagent/commands.ts +2 -1
  187. package/extensions/subagent/invocation.ts +4 -2
  188. package/extensions/team/commands.ts +145 -0
  189. package/extensions/team/dispatch.ts +133 -0
  190. package/extensions/team/format.ts +119 -0
  191. package/extensions/team/index.ts +34 -0
  192. package/extensions/team/schema.ts +29 -0
  193. package/extensions/team/tools.ts +267 -0
  194. package/extensions/team/types.ts +65 -0
  195. package/extensions/todo/index.ts +4 -1
  196. package/extensions/todo/nudge.ts +8 -2
  197. package/extensions/web-access/fetch/page.ts +4 -1
  198. package/extensions/web-access/fetch/structured.ts +447 -0
  199. package/extensions/web-access/fetch/tool.ts +2 -1
  200. package/extensions/web-access/search/tool.ts +2 -1
  201. package/extensions/wechat/bridge.ts +106 -251
  202. package/extensions/wechat/{commands.ts → commands/commands.ts} +26 -18
  203. package/extensions/wechat/{doctor.ts → commands/doctor.ts} +5 -5
  204. package/extensions/wechat/{history.ts → commands/history.ts} +1 -1
  205. package/extensions/wechat/{login.ts → commands/login.ts} +3 -3
  206. package/extensions/wechat/{slash.ts → commands/slash.ts} +38 -2
  207. package/extensions/wechat/dialog/channels.ts +136 -0
  208. package/extensions/wechat/{interaction.ts → dialog/interaction.ts} +37 -37
  209. package/extensions/wechat/{prompt.ts → dialog/prompt.ts} +2 -2
  210. package/extensions/wechat/{remote-ui.ts → dialog/remote-ui.ts} +1 -1
  211. package/extensions/wechat/index.ts +10 -10
  212. package/extensions/wechat/intake.ts +245 -0
  213. package/extensions/wechat/{admission.ts → peer/admission.ts} +2 -2
  214. package/extensions/wechat/{peer-session.ts → peer/peer-session.ts} +2 -2
  215. package/extensions/wechat/{peer-worker.ts → peer/peer-worker.ts} +100 -22
  216. package/extensions/wechat/{presence.ts → peer/presence.ts} +8 -7
  217. package/extensions/wechat/{reply-pipeline.ts → peer/reply-pipeline.ts} +1 -1
  218. package/extensions/wechat/{credentials.ts → state/credentials.ts} +2 -2
  219. package/extensions/wechat/{state.ts → state/state.ts} +14 -2
  220. package/extensions/wechat/{format.ts → text/format.ts} +3 -3
  221. package/extensions/wechat/{notices.ts → text/notices.ts} +8 -0
  222. package/extensions/wechat/tools.ts +29 -12
  223. package/extensions/wechat/{client.ts → transport/client.ts} +26 -2
  224. package/extensions/wechat/{deliver.ts → transport/deliver.ts} +72 -28
  225. package/extensions/wechat/{inbound.ts → transport/inbound.ts} +5 -5
  226. package/extensions/wechat/{media.ts → transport/media.ts} +11 -5
  227. package/extensions/wechat/transport/outbound.ts +142 -0
  228. package/extensions/wechat/{poll.ts → transport/poll.ts} +3 -3
  229. package/extensions/wechat/types.ts +10 -1
  230. package/extensions/wechat/{login-screen.ts → view/login-screen.ts} +4 -4
  231. package/extensions/wechat/{status-screen.ts → view/status-screen.ts} +31 -26
  232. package/extensions/worktree/commands.ts +24 -17
  233. package/extensions/worktree/exit.ts +12 -6
  234. package/extensions/worktree/git.ts +22 -14
  235. package/extensions/worktree/guard.ts +18 -290
  236. package/extensions/worktree/handoff.ts +10 -2
  237. package/extensions/worktree/index.ts +2 -1
  238. package/extensions/worktree/lifecycle.ts +2 -0
  239. package/extensions/worktree/registry.ts +20 -11
  240. package/extensions/worktree/root-tools.ts +13 -3
  241. package/extensions/worktree/runtime.ts +8 -2
  242. package/extensions/worktree/screen.ts +15 -8
  243. package/extensions/worktree/shell.ts +303 -0
  244. package/extensions/worktree/state.ts +1 -1
  245. package/extensions/worktree/status.ts +42 -9
  246. package/extensions/worktree/tools.ts +19 -13
  247. package/lib/README.md +11 -8
  248. package/lib/board/card.ts +479 -0
  249. package/lib/board/links.ts +141 -0
  250. package/lib/board/page-pointer.ts +310 -0
  251. package/lib/board/round-text.ts +81 -0
  252. package/lib/board/round.ts +94 -0
  253. package/lib/board/rules.ts +437 -0
  254. package/lib/board/session.ts +105 -0
  255. package/lib/board/store.ts +497 -0
  256. package/lib/board/view.ts +234 -0
  257. package/lib/errors.ts +12 -4
  258. package/lib/format.ts +35 -0
  259. package/lib/http/guard.ts +112 -0
  260. package/lib/http/index.ts +16 -5
  261. package/lib/http/serve.ts +237 -0
  262. package/lib/http/server.ts +12 -10
  263. package/lib/http/sink.ts +135 -0
  264. package/lib/http/token.ts +38 -0
  265. package/lib/plan-entry.ts +11 -0
  266. package/lib/policy.ts +25 -2
  267. package/lib/requirements.ts +6 -7
  268. package/lib/session-label.ts +64 -0
  269. package/lib/session-roster.ts +201 -0
  270. package/lib/session-tool.ts +184 -0
  271. package/lib/spawn.ts +37 -3
  272. package/lib/stray-fields.ts +45 -0
  273. package/lib/tool-names.ts +38 -2
  274. package/lib/tui/glyphs.ts +3 -2
  275. package/lib/web/README.md +57 -4
  276. package/lib/web/client-runtime.ts +123 -0
  277. package/lib/web/components.ts +5 -0
  278. package/lib/web/index.ts +4 -2
  279. package/lib/web/markup.ts +88 -1
  280. package/lib/web/names.ts +42 -0
  281. package/lib/web/runtime.ts +7 -1
  282. package/lib/web/tokens.ts +25 -2
  283. package/package.json +26 -14
  284. package/skills/next/SKILL.md +4 -3
  285. package/skills/project-docs/SKILL.md +133 -0
  286. package/skills/project-docs/references/audit.md +131 -0
  287. package/skills/project-docs/references/changelog.md +105 -0
  288. package/skills/project-docs/references/discovery.md +127 -0
  289. package/skills/project-docs/references/readme.md +80 -0
  290. package/skills/project-docs/references/release.md +84 -0
  291. package/skills/project-docs/references/roadmap.md +93 -0
  292. package/skills/project-docs/scripts/state.ts +316 -0
  293. package/skills/refactor/SKILL.md +100 -125
  294. package/skills/refactor/references/handoff.md +35 -0
  295. package/skills/refactor/references/signals.md +54 -0
  296. package/skills/refactor/scripts/inventory.ts +587 -0
  297. package/extensions/bytefly/panel/tui/extensions-view.ts +0 -40
  298. package/extensions/bytefly/panel/tui/skills-view.ts +0 -50
  299. package/extensions/file-browse/README.md +0 -506
  300. package/extensions/file-browse/files.ts +0 -564
  301. package/extensions/file-browse/server.ts +0 -141
  302. package/extensions/job/process.ts +0 -35
  303. package/extensions/plan/split.ts +0 -87
  304. package/extensions/session-bus/README.md +0 -247
  305. package/extensions/session-bus/team.ts +0 -102
  306. package/extensions/wechat/outbound.ts +0 -100
  307. package/lib/plans/sections.ts +0 -172
  308. package/lib/plans/setting.ts +0 -52
  309. package/lib/plans/split.ts +0 -215
  310. package/lib/plans/store.ts +0 -19
  311. package/lib/plans/team.ts +0 -151
  312. package/skills/changelog/SKILL.md +0 -148
  313. package/skills/parallel-dev/SKILL.md +0 -150
  314. /package/extensions/file-browse/{rev.ts → live/rev.ts} +0 -0
  315. /package/extensions/file-browse/{diff.ts → text/diff.ts} +0 -0
  316. /package/extensions/file-browse/{fuzzy.ts → text/fuzzy.ts} +0 -0
  317. /package/extensions/file-browse/{live-client.ts → view/live-client.ts} +0 -0
  318. /package/extensions/file-browse/{palette.ts → view/palette.ts} +0 -0
  319. /package/extensions/rewind/{patch.ts → text/patch.ts} +0 -0
  320. /package/extensions/rewind/{paths.ts → text/paths.ts} +0 -0
  321. /package/extensions/rewind/{queue.ts → transport/queue.ts} +0 -0
  322. /package/extensions/rewind/{temp-index.ts → transport/temp-index.ts} +0 -0
  323. /package/extensions/wechat/{lock.ts → state/lock.ts} +0 -0
  324. /package/extensions/wechat/{channel.ts → text/channel.ts} +0 -0
@@ -0,0 +1,110 @@
1
+ /**
2
+ * The HTTP server for the browse extension.
3
+ *
4
+ * Wraps the pure `handleRequest` in `lib/http`'s loopback server: it resolves the
5
+ * root once, feeds the live status channel, and exposes an idempotent `close()`.
6
+ * The request handler is injectable so tests can avoid a socket when they only
7
+ * need routing.
8
+ *
9
+ * What stays here is this extension's own policy: the request handler, its
10
+ * request count, the catch that turns a thrown handler into a plain-text 500, the
11
+ * cap that refuses a page rather than opening a stream for it, and how long a
12
+ * server with nobody reading it is worth keeping.
13
+ */
14
+
15
+ import { realpathSync } from "node:fs";
16
+ import { type ServeResponse, serveLoopback, textResponse } from "../../../lib/http/index.ts";
17
+ import { IDLE_STOP_MS, type ServeConfig } from "../config/config.ts";
18
+ import { createFileIndex } from "../files/files.ts";
19
+ import type { GitDiffProvider, GitStatusProvider } from "../git/git.ts";
20
+ import { createLiveChannel, createLiveStatus } from "../live/live.ts";
21
+ import { handleRequest, type ServeContext } from "./router.ts";
22
+
23
+ export interface FileServer {
24
+ /** Origin URL, e.g. `http://127.0.0.1:49152/`. */
25
+ url: string;
26
+ /** The bound port. */
27
+ port: number;
28
+ /** Resolved absolute root that is actually served. */
29
+ root: string;
30
+ /** Stop listening. Safe to call more than once or before `listen` settles. */
31
+ close(): Promise<void>;
32
+ }
33
+
34
+ export interface FileServerOptions {
35
+ root: string;
36
+ config: ServeConfig;
37
+ /** Read-only git context for the page header; omitted when unavailable. */
38
+ git?: GitStatusProvider;
39
+ /** Read-only diff provider for the change and diff views; omitted when unavailable. */
40
+ diff?: GitDiffProvider;
41
+ /** Called once the last page has been gone for `idleStopMs`; omitted, the server waits for `close()`. */
42
+ onIdle?: () => void;
43
+ /** Grace before `onIdle`, in ms; defaults to `IDLE_STOP_MS`. */
44
+ idleStopMs?: number;
45
+ /** Override the request handler (tests). */
46
+ handler?: typeof handleRequest;
47
+ }
48
+
49
+ /** Start a read-only server rooted at `options.root` on 127.0.0.1. */
50
+ export async function createFileServer(options: FileServerOptions): Promise<FileServer> {
51
+ const root = realpathSync.native(options.root);
52
+ const { config } = options;
53
+ // The status feed's two server-owned facts: how long it has been up, and how
54
+ // many requests it has answered. Both are read per frame, never cached.
55
+ let requests = 0;
56
+ const status = config.live
57
+ ? createLiveStatus({
58
+ git: options.git,
59
+ diff: config.gitDiff ? options.diff : undefined,
60
+ startedAt: Date.now(),
61
+ requests: () => requests,
62
+ })
63
+ : undefined;
64
+ const live = status ? createLiveChannel({ status, intervalMs: config.liveIntervalMs }) : undefined;
65
+ const context: ServeContext = {
66
+ root,
67
+ config,
68
+ git: options.git,
69
+ diff: options.diff,
70
+ index: createFileIndex(root, config.maxIndexEntries),
71
+ live: status,
72
+ };
73
+ const handler = options.handler ?? handleRequest;
74
+
75
+ const server = await serveLoopback({
76
+ ports: [config.port],
77
+ idle: options.onIdle === undefined ? undefined : { ms: options.idleStopMs ?? IDLE_STOP_MS, onIdle: options.onIdle },
78
+ handler: async (request) => {
79
+ requests += 1;
80
+ let result: ServeResponse;
81
+ try {
82
+ result = await handler(context, request.method, request.url, request.headers);
83
+ } catch {
84
+ return { result: textResponse(500, "Internal error") };
85
+ }
86
+ if (!result.stream || !live || request.method.toUpperCase() === "HEAD") return { result };
87
+ // Checked before the head is written: a refusal must not leave a stream the
88
+ // client would wait on. `204` is the one status an event source does not
89
+ // reconnect after, so a page beyond the cap goes quiet instead of retrying.
90
+ if (live.atCapacity()) {
91
+ return { result: { status: 204, headers: { "Cache-Control": "no-store" }, body: "" } };
92
+ }
93
+ return {
94
+ result,
95
+ openSink: (sink) => live.attach({ write: (frame) => sink.write(frame), end: () => sink.end() }),
96
+ };
97
+ },
98
+ });
99
+
100
+ return {
101
+ url: `${server.origin}/`,
102
+ port: server.port,
103
+ root,
104
+ close: async () => {
105
+ // End the streams first, so a connected page does not hold the close open.
106
+ live?.close();
107
+ await server.close();
108
+ },
109
+ };
110
+ }
@@ -8,10 +8,10 @@
8
8
  * it reaches the page. The page shell and the page bodies stay in `html.ts`.
9
9
  */
10
10
 
11
- import { escapeHtml } from "../../lib/escape.ts";
12
- import { GLYPHS } from "../../lib/tui/glyphs.ts";
13
- import type { GitStatus } from "./git.ts";
14
- import type { ChangeIndex } from "./live.ts";
11
+ import { escapeHtml } from "../../../lib/escape.ts";
12
+ import { GLYPHS } from "../../../lib/tui/glyphs.ts";
13
+ import type { GitStatus } from "../git/git.ts";
14
+ import type { ChangeIndex } from "../live/live.ts";
15
15
 
16
16
  /** The header's git chip: the branch, or the detached head; changes are the changes chip's job. */
17
17
  export function renderGit(git: GitStatus | undefined): string {
@@ -12,7 +12,7 @@
12
12
  * and `bun build --no-bundle` needs no separate asset.
13
13
  */
14
14
 
15
- import { keysDialogScript, THEME_TOGGLE_JS } from "../../lib/web/index.ts";
15
+ import { keysDialogScript, THEME_TOGGLE_JS } from "../../../lib/web/index.ts";
16
16
  import { LIVE_CLIENT_JS } from "./live-client.ts";
17
17
  import { PALETTE_CLIENT_JS } from "./palette.ts";
18
18
  import { TREE_CLIENT_JS } from "./tree.ts";
@@ -8,9 +8,9 @@
8
8
  * hunk and never across the unchanged lines a hunk does not show.
9
9
  */
10
10
 
11
- import { escapeHtml } from "../../lib/escape.ts";
12
- import { highlightLines, type HighlightLanguage } from "../../lib/highlight.ts";
13
- import type { DiffLine } from "./diff.ts";
11
+ import { escapeHtml } from "../../../lib/escape.ts";
12
+ import { highlightLines, type HighlightLanguage } from "../../../lib/highlight.ts";
13
+ import type { DiffLine } from "../text/diff.ts";
14
14
 
15
15
  /**
16
16
  * The inner HTML per diff line. Hunk bodies are highlighted by rebuilding the
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The elements a page emits for a resolved Markdown link or image.
3
+ *
4
+ * Two renderers reach a link: the Markdown renderer (`markdown.ts`) and the raw-HTML
5
+ * sanitizer (`raw-html.ts`). Both ask `urls.ts` what a destination resolves to, and both
6
+ * then build the same anchor and the same image — the external-rel attributes, the
7
+ * `md-img` class, and the lazy decoding — so those live here rather than in a fourth and
8
+ * fifth copy. Values are escaped here; the callers hand in what the document said.
9
+ */
10
+
11
+ import { escapeHtml } from "../../../lib/escape.ts";
12
+
13
+ /**
14
+ * The attributes an external link carries, for a caller that is building the element
15
+ * itself: the raw-HTML sanitizer rewrites an existing tag's attribute list and has no
16
+ * content string to hand an emitter.
17
+ *
18
+ * `noopener` because the target is another origin, and `noreferrer` because the page is a
19
+ * local view of the reader's own repository and should not tell anyone where they read it.
20
+ */
21
+ export const EXTERNAL_ATTRS = ' target="_blank" rel="noopener noreferrer"';
22
+
23
+ /** An `<a>` for one destination; `title` and the rel attributes are omitted when absent. */
24
+ export function emitAnchor(input: { href: string; content: string; title?: string; external: boolean }): string {
25
+ const title = input.title === undefined ? "" : ` title="${escapeHtml(input.title)}"`;
26
+ return `<a href="${escapeHtml(input.href)}"${title}${input.external ? EXTERNAL_ATTRS : ""}>${input.content}</a>`;
27
+ }
28
+
29
+ /**
30
+ * A link standing in for an image the page may not show: an external `src` is blocked by
31
+ * the CSP, so the reader gets something to click, labelled by the alt text.
32
+ */
33
+ export function emitImageLink(href: string, label: string): string {
34
+ return `<a href="${escapeHtml(href)}"${EXTERNAL_ATTRS}>${escapeHtml(label)}</a>`;
35
+ }
36
+
37
+ /** An `<img>`: block-level spacing class, alt text, and the sizes a document may pin. */
38
+ export function emitImage(input: {
39
+ href: string;
40
+ alt: string;
41
+ title?: string;
42
+ /** A document may pin a pixel size; escaped like every other value the page emits. */
43
+ width?: string | number;
44
+ height?: string | number;
45
+ }): string {
46
+ const size = `${input.width === undefined ? "" : ` width="${escapeHtml(String(input.width))}"`}${
47
+ input.height === undefined ? "" : ` height="${escapeHtml(String(input.height))}"`
48
+ }`;
49
+ const title = input.title === undefined ? "" : ` title="${escapeHtml(input.title)}"`;
50
+ return `<img class="md-img" src="${escapeHtml(input.href)}" alt="${escapeHtml(input.alt)}"${size}${title} loading="lazy" decoding="async">`;
51
+ }
@@ -8,18 +8,19 @@
8
8
  * the page-only `browse-*` rules from `styles.ts`.
9
9
  */
10
10
 
11
- import { brandLockup, docHead, type KeyHint, keysSheet, themeToggle } from "../../lib/web/index.ts";
12
- import { escapeHtml } from "../../lib/escape.ts";
11
+ import { brandLockup, docHead, type KeyHint, keysSheet, tabTitle, themeToggle } from "../../../lib/web/index.ts";
12
+ import { escapeHtml } from "../../../lib/escape.ts";
13
13
  import { renderChanges, renderGit, renderLiveChrome, renderLiveStatus, type LivePage } from "./chrome.ts";
14
- import { formatSize, type DirEntry, type FileView, type Listing, type TreeNode } from "./files.ts";
15
- import { DIFF_MODES, statusLetter, type ChangedFile, type DiffMode } from "./diff.ts";
14
+ import { type DirEntry, type FileView, type Listing, type TreeNode } from "../files/files.ts";
15
+ import { formatSize } from "../../../lib/format.ts";
16
+ import { DIFF_MODES, statusLetter, type ChangedFile, type DiffMode } from "../text/diff.ts";
16
17
  import { renderDiffLines } from "./diff-html.ts";
17
- import type { DiffFileView, GitStatus } from "./git.ts";
18
- import type { ChangeIndex } from "./live.ts";
19
- import { detectLanguage, highlightLines, type HighlightLanguage } from "../../lib/highlight.ts";
18
+ import type { DiffFileView, GitStatus } from "../git/git.ts";
19
+ import type { ChangeIndex } from "../live/live.ts";
20
+ import { detectLanguage, highlightLines, type HighlightLanguage } from "../../../lib/highlight.ts";
20
21
  import { FOLDER_ICON, iconHref, languageOf, SPRITE, type IconSpec } from "./icons.ts";
21
22
  import { renderMarkdown } from "./markdown.ts";
22
- import { encodePath } from "./paths.ts";
23
+ import { encodePath } from "../files/paths.ts";
23
24
  import { BROWSE_CSS } from "./styles.ts";
24
25
 
25
26
  const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
@@ -86,7 +87,6 @@ function renderLayout(layout: Layout): string {
86
87
  <html lang="en">
87
88
  <head>
88
89
  ${docHead({ title: layout.title, css: BROWSE_CSS })}
89
- <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='6' fill='%2358a6ff'/%3E%3C/svg%3E">
90
90
  </head>
91
91
  <body class="bf-app">
92
92
  <a class="skip" href="#main">Skip to content</a>
@@ -220,7 +220,7 @@ ${page.listing.entries.map((entry) => renderRow(entry, page.maxThumbBytes, page.
220
220
  body += `<p class="bf-note">Showing the first ${page.listing.entries.length} of ${page.listing.total} entries.</p>`;
221
221
 
222
222
  return renderLayout({
223
- title: `browse · ${page.rel || folderName}`,
223
+ title: tabTitle("browse", page.rel || folderName),
224
224
  rootLabel: page.rootLabel,
225
225
  tree: page.tree,
226
226
  currentRel: page.rel,
@@ -286,8 +286,9 @@ function numberedLines(page: FilePage): boolean {
286
286
  return page.file.kind === "text" || (page.file.kind === "markdown" && page.markdownRaw === true);
287
287
  }
288
288
 
289
- /** The card-head controls: Markdown mode, line wrapping, and copy actions. */
290
- function renderFileActions(page: FilePage): string {
289
+ /** The card-head controls: Markdown mode, line wrapping, and the copy actions,
290
+ * plus the download the diff head keeps in the same group. */
291
+ function renderFileActions(page: FilePage, download: string): string {
291
292
  const isMarkdown = page.file.kind === "markdown";
292
293
  const rawHref = `/raw/${encodePath(page.rel)}`;
293
294
  const viewHref = `/view/${encodePath(page.rel)}`;
@@ -324,7 +325,7 @@ function renderFileActions(page: FilePage): string {
324
325
  `<button type="button" class="bf-btn" id="copy-reference" disabled title="Select lines in the file first">copy reference</button>`,
325
326
  );
326
327
  }
327
- return `<span class="bf-card__actions">${groups.join("")}</span>`;
328
+ return `<span class="bf-card__actions">${groups.join("")}${download}</span>`;
328
329
  }
329
330
 
330
331
  /** Render a file page (text, Markdown, image, binary, or over-size). */
@@ -336,7 +337,7 @@ export function renderFilePage(page: FilePage): string {
336
337
  // about the file; the language label, size, and date are the useful metadata.
337
338
  const meta = `${spec.label} · ${formatSize(page.file.size)} · ${formatDate(page.file.mtimeMs)}`;
338
339
  const download = `<a class="bf-btn" href="/raw/${encodePath(page.rel)}" download>download</a>`;
339
- const head = `<div class="bf-card__head">${iconMarkup(spec)}<h1 class="bf-card__title">${escapeHtml(name)}</h1><span class="bf-card__meta">${escapeHtml(meta)}</span><span class="bf-spacer"></span>${renderFileActions(page)}${download}</div>`;
340
+ const head = `<div class="bf-card__head">${iconMarkup(spec)}<h1 class="bf-card__title">${escapeHtml(name)}</h1><span class="bf-card__meta">${escapeHtml(meta)}</span><span class="bf-spacer"></span>${renderFileActions(page, download)}</div>`;
340
341
 
341
342
  const isMarkdown = page.file.kind === "markdown";
342
343
  const rendered = isMarkdown && !page.markdownRaw;
@@ -369,7 +370,7 @@ export function renderFilePage(page: FilePage): string {
369
370
  }
370
371
 
371
372
  return renderLayout({
372
- title: `browse · ${page.rel}`,
373
+ title: tabTitle("browse", page.rel),
373
374
  rootLabel: page.rootLabel,
374
375
  tree: page.tree,
375
376
  currentRel: page.rel,
@@ -442,7 +443,7 @@ ${page.files.map((file) => renderChangedRow(file, page.mode)).join("\n")}
442
443
  </table>`;
443
444
  const stat = page.stat ? `<pre class="browse-diff-stat">${escapeHtml(page.stat)}</pre>` : "";
444
445
  return renderLayout({
445
- title: "browse · changes",
446
+ title: tabTitle("browse", "changes"),
446
447
  rootLabel: page.rootLabel,
447
448
  tree: page.tree,
448
449
  currentRel: "",
@@ -514,7 +515,7 @@ export function renderFileDiffPage(page: FileDiffPage): string {
514
515
  if (page.diff.truncated) note = `<p class="bf-note">Showing the first ${page.diff.lines.length} diff lines.</p>`;
515
516
  }
516
517
  return renderLayout({
517
- title: `browse · ${page.rel}`,
518
+ title: tabTitle("browse", page.rel),
518
519
  rootLabel: page.rootLabel,
519
520
  tree: page.tree,
520
521
  currentRel: page.rel,
@@ -542,7 +543,7 @@ export interface ErrorPage {
542
543
  /** Render a status/error page with the same chrome. */
543
544
  export function renderErrorPage(page: ErrorPage): string {
544
545
  return renderLayout({
545
- title: `browse · ${page.status}`,
546
+ title: tabTitle("browse", String(page.status)),
546
547
  rootLabel: page.rootLabel,
547
548
  tree: page.tree,
548
549
  currentRel: "",
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import { extname } from "node:path";
8
- import { IMAGE_EXTENSIONS } from "./files.ts";
8
+ import { IMAGE_EXTENSIONS } from "../files/files.ts";
9
9
 
10
10
  export type IconName = "folder" | "file-code" | "file-text" | "file-image" | "file-json" | "file-archive" | "file";
11
11
 
@@ -25,8 +25,8 @@
25
25
  * policy and raw HTML do not.
26
26
  */
27
27
 
28
- import { decodeCodePoint, escapeHtml } from "../../lib/escape.ts";
29
- import { fenceLanguage, highlightLines } from "../../lib/highlight.ts";
28
+ import { decodeCodePoint, escapeHtml } from "../../../lib/escape.ts";
29
+ import { fenceLanguage, highlightLines } from "../../../lib/highlight.ts";
30
30
  import {
31
31
  closesSuppressed,
32
32
  closeHtmlSink,
@@ -38,9 +38,9 @@ import {
38
38
  sanitizeHtmlBlock,
39
39
  sanitizeInlineTag,
40
40
  } from "./raw-html.ts";
41
+ import { emitAnchor, emitImage, emitImageLink } from "./emit.ts";
41
42
  import { resolveImage, resolveLink } from "./urls.ts";
42
43
 
43
- const HTTP_SCHEME = /^https?:/i;
44
44
  /** A GFM extended autolink: `http(s)://` or `www.` followed by a domain. */
45
45
  const AUTOLINK = /^(?:https?:\/\/|www\.)[^\s<>]*/i;
46
46
 
@@ -126,8 +126,8 @@ function createContext(baseDir: string, defs: Map<string, LinkDefinition>, html:
126
126
  };
127
127
  }
128
128
 
129
- /** Fold a reference label the way CommonMark does: trim, collapse, case-fold. */
130
- function normalizeLabel(label: string): string {
129
+ /** Fold a reference label the way CommonMark does: trim, collapse, case-fold. Not `lib/session-label.ts`, which names a session. */
130
+ function referenceLabel(label: string): string {
131
131
  return label.trim().replace(/\s+/g, " ").toLowerCase();
132
132
  }
133
133
 
@@ -180,7 +180,7 @@ function collectDefinitions(lines: string[], html: boolean): { defs: Map<string,
180
180
  body.push(line);
181
181
  continue;
182
182
  }
183
- const key = normalizeLabel(match[1]);
183
+ const key = referenceLabel(match[1]);
184
184
  if (!defs.has(key)) {
185
185
  const dest = match[2].startsWith("<") && match[2].endsWith(">") ? match[2].slice(1, -1) : match[2];
186
186
  defs.set(key, { dest, title: match[3] });
@@ -630,11 +630,11 @@ function parseReferenceLink(text: string, start: number, ctx: Context): ParsedLi
630
630
  const end = matchBracket(text, close + 1);
631
631
  if (end !== -1) {
632
632
  const raw = text.slice(close + 2, end);
633
- const def = ctx.defs.get(normalizeLabel(raw === "" ? label : raw));
633
+ const def = ctx.defs.get(referenceLabel(raw === "" ? label : raw));
634
634
  if (def) return { label, dest: def.dest, title: def.title, end: end + 1 };
635
635
  }
636
636
  }
637
- const def = ctx.defs.get(normalizeLabel(label));
637
+ const def = ctx.defs.get(referenceLabel(label));
638
638
  if (def) return { label, dest: def.dest, title: def.title, end: close + 1 };
639
639
  return undefined;
640
640
  }
@@ -643,20 +643,17 @@ function renderLink(parsed: ParsedLink, ctx: Context): string {
643
643
  const label = renderInline(parsed.label, ctx);
644
644
  const target = resolveLink(parsed.dest, ctx.baseDir);
645
645
  if (!target) return label;
646
- const title = parsed.title ? ` title="${escapeHtml(parsed.title)}"` : "";
647
- const external = target.external ? ` target="_blank" rel="noopener noreferrer"` : "";
648
- return `<a href="${escapeHtml(target.href)}"${title}${external}>${label}</a>`;
646
+ // The anchor's shape is `emit.ts`'s, the same one the raw-HTML sanitizer emits: a link
647
+ // written in Markdown and the same link written as HTML are the same link.
648
+ return emitAnchor({ href: target.href, content: label, title: parsed.title, external: target.external });
649
649
  }
650
650
 
651
651
  function renderImage(parsed: ParsedLink, ctx: Context): string {
652
652
  const alt = escapeHtml(parsed.label);
653
653
  const target = resolveImage(parsed.dest, ctx.baseDir);
654
654
  if (!target) return alt;
655
- if (!target.image) {
656
- return `<a href="${escapeHtml(target.href)}" target="_blank" rel="noopener noreferrer">${alt || escapeHtml(parsed.dest)}</a>`;
657
- }
658
- const title = parsed.title ? ` title="${escapeHtml(parsed.title)}"` : "";
659
- return `<img class="md-img" src="${escapeHtml(target.href)}" alt="${alt}" loading="lazy" decoding="async"${title}>`;
655
+ if (!target.image) return emitImageLink(target.href, parsed.label || parsed.dest);
656
+ return emitImage({ href: target.href, alt, title: parsed.title });
660
657
  }
661
658
 
662
659
  function matchRun(text: string, start: number, char: string): number {
@@ -812,8 +809,10 @@ function renderInline(text: string, ctx: Context): string {
812
809
  const match = /^<((?:https?|mailto):[^>\s]+)>/i.exec(text.slice(i));
813
810
  if (match) {
814
811
  const href = match[1];
815
- const external = HTTP_SCHEME.test(href) ? ` target="_blank" rel="noopener noreferrer"` : "";
816
- out += `<a href="${escapeHtml(href)}"${external}>${escapeHtml(href)}</a>`;
812
+ // Resolved rather than pattern-matched: `urls.ts` owns which schemes open in a
813
+ // new tab, and a `<mailto:>` link is external but does not.
814
+ const external = resolveLink(href, ctx.baseDir)?.external ?? false;
815
+ out += emitAnchor({ href, content: escapeHtml(href), external });
817
816
  i += match[0].length;
818
817
  continue;
819
818
  }
@@ -836,8 +835,7 @@ function renderInline(text: string, ctx: Context): string {
836
835
  // Require a domain, so a bare scheme is not a link.
837
836
  if (dots > 0 && /\.[^./]/.test(label.slice(dots))) {
838
837
  const href = /^www\./i.test(label) ? `http://${label}` : label;
839
- const external = HTTP_SCHEME.test(href) ? ` target="_blank" rel="noopener noreferrer"` : "";
840
- out += `<a href="${escapeHtml(href)}"${external}>${escapeHtml(label)}</a>`;
838
+ out += emitAnchor({ href, content: escapeHtml(label), external: true });
841
839
  i += label.length;
842
840
  continue;
843
841
  }
@@ -25,7 +25,8 @@
25
25
  * how `<details>` wraps Markdown.
26
26
  */
27
27
 
28
- import { decodeCodePoint, escapeHtml } from "../../lib/escape.ts";
28
+ import { decodeCodePoint, escapeHtml } from "../../../lib/escape.ts";
29
+ import { emitImage, emitImageLink, EXTERNAL_ATTRS } from "./emit.ts";
29
30
  import { resolveImage, resolveLink } from "./urls.ts";
30
31
 
31
32
  /** Elements re-emitted with sanitized attributes. */
@@ -139,6 +140,12 @@ const BLOCK_OPEN = /^ {0,3}<(\/?)([a-zA-Z][a-zA-Z0-9-]*)/;
139
140
  const COMMENT_OPEN = /^ {0,3}<!--/;
140
141
  const DECLARATION_OPEN = /^ {0,3}<[!?]/;
141
142
  /** The five XML references, plus numeric ones, are the only refs decoded in a value. */
143
+ /**
144
+ * The five references an attribute value may carry, decoded.
145
+ *
146
+ * Not the Markdown renderer's table: a tag's attribute is not running text, and decoding
147
+ * `&nbsp;` or `&mdash;` inside one would invent characters the author did not write.
148
+ */
142
149
  const ATTRIBUTE_ENTITIES: Record<string, string> = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
143
150
  const ATTRIBUTE_ENTITY = /&(?:#([0-9]{1,7})|#[xX]([0-9a-fA-F]{1,6})|([a-zA-Z]+));/g;
144
151
 
@@ -407,7 +414,7 @@ function sanitizeOpenTag(tag: RawTag, baseDir: string): string {
407
414
  const target = href === undefined ? undefined : resolveLink(href, baseDir);
408
415
  if (target !== undefined) {
409
416
  out.push(` href="${escapeHtml(target.href)}"`);
410
- if (target.external) out.push(' target="_blank" rel="noopener noreferrer"');
417
+ if (target.external) out.push(EXTERNAL_ATTRS);
411
418
  }
412
419
  break;
413
420
  }
@@ -456,16 +463,14 @@ function sanitizeImage(tag: RawTag, baseDir: string): string {
456
463
  const src = attribute(tag, "src");
457
464
  const target = src === undefined ? undefined : resolveImage(src, baseDir);
458
465
  if (target === undefined) return alt === "" ? "" : escapeHtml(alt);
459
- if (!target.image) {
460
- const label = alt === "" ? target.href : alt;
461
- return `<a href="${escapeHtml(target.href)}" target="_blank" rel="noopener noreferrer">${escapeHtml(label)}</a>`;
462
- }
463
- const out: string[] = [` class="md-img" src="${escapeHtml(target.href)}" alt="${escapeHtml(alt)}"`];
464
- const width = numeric(attribute(tag, "width"));
465
- if (width !== undefined) out.push(` width="${width}"`);
466
- const height = numeric(attribute(tag, "height"));
467
- if (height !== undefined) out.push(` height="${height}"`);
468
- const title = attribute(tag, "title");
469
- if (title !== undefined) out.push(` title="${escapeHtml(title)}"`);
470
- return `<img${out.join("")} loading="lazy" decoding="async">`;
466
+ if (!target.image) return emitImageLink(target.href, alt === "" ? target.href : alt);
467
+ // The element is `emit.ts`'s, so an `<img>` written as HTML and the same one written as
468
+ // Markdown are the same element, down to the lazy decoding.
469
+ return emitImage({
470
+ href: target.href,
471
+ alt,
472
+ title: attribute(tag, "title"),
473
+ width: numeric(attribute(tag, "width")),
474
+ height: numeric(attribute(tag, "height")),
475
+ });
471
476
  }
@@ -12,7 +12,7 @@ import {
12
12
  WEB_COMPONENTS_CSS,
13
13
  WEB_LAYOUT_CSS,
14
14
  WEB_THEME_CSS,
15
- } from "../../lib/web/index.ts";
15
+ } from "../../../lib/web/index.ts";
16
16
 
17
17
  /** The shared shell, components, and the page-only `browse-*` rules. */
18
18
  export const BROWSE_CSS = `${WEB_BASE_CSS}
@@ -37,7 +37,6 @@ ${WEB_COMPONENTS_CSS}
37
37
  .browse-dotfile .bf-table__name a{opacity:.72}
38
38
  .browse-broken{color:var(--muted);text-decoration:line-through}
39
39
  .browse-link-mark{color:var(--muted);font-size:var(--fs-xs)}
40
- .browse-when{display:none}
41
40
  body.palette-open{overflow:hidden}
42
41
  .browse-diff{font-family:var(--font-mono);gap:5px}
43
42
  .browse-diff:hover{color:var(--accent);text-decoration:none}
@@ -93,8 +92,24 @@ body.palette-open{overflow:hidden}
93
92
  .browse-measure .bf-md>*{max-width:var(--browse-measure);margin-inline:auto}
94
93
  /* Tables start at the prose column and take only the gutter to its right, so a
95
94
  wide window widens them without a dead band on their left. Code stays inside
96
- the measure with the rest of the prose. */
97
- .browse-measure .bf-md>.md-table-frame{max-width:none;padding-left:var(--browse-gutter)}
95
+ the measure with the rest of the prose. The frame is also the container a
96
+ squeezed table asks about: it is the pane, not the window, that narrows one. */
97
+ .browse-measure .bf-md>.md-table-frame{max-width:none;padding-left:var(--browse-gutter);container-type:inline-size}
98
+ /* A table spans the reading column rather than stopping at its own content: a
99
+ shrink-wrapped table leaves a ragged band beside itself — 254px on a 2560px
100
+ screen, and 300px beside the three-column tables at 1440 — which the reader
101
+ cannot tell from a column that failed to draw. The card is wider than the text
102
+ it holds, so the measure is the ceiling: a table ends where the prose does, and
103
+ below the floor's threshold the minimum still wins and the wrapper scrolls. */
104
+ .browse-measure .bf-md .md-table-frame table{width:100%;max-width:var(--browse-measure)}
105
+ /* In a pane this narrow, auto layout gives the prose cell whatever an unbreakable
106
+ code span leaves — one word per line, and a four-column table read as a
107
+ 300px-tall row. A floor on the cells makes the table wider than the pane
108
+ instead, so every column keeps a readable width and the wrapper scrolls them;
109
+ a table that still fits is left as it was. */
110
+ @container (max-width:600px){
111
+ .browse-measure .bf-md .md-table-frame td{min-width:14em}
112
+ }
98
113
  /* A table wider than the pane scrolls, but an overlay scrollbar is invisible
99
114
  until it moves; a gradient on the frame marks the columns still out of view.
100
115
  It has to be the frame: the scroller clips its own content, so a pseudo-element
@@ -116,10 +131,19 @@ body.palette-open{overflow:hidden}
116
131
  .browse-side .bf-tree{flex:1 1 auto;min-height:0;overflow:auto}
117
132
  }
118
133
  @media (max-width:820px){
119
- .browse-root{order:9;flex:1 1 100%;direction:ltr;text-align:left}
134
+ .browse-root{order:9;flex:1 1 100%}
120
135
  }
121
136
  @media (max-width:520px){
122
137
  .bf-toolbar .bf-theme-toggle{margin-left:auto}
138
+ /* A phone has no room for four button rows above the file: the actions keep one
139
+ line and scroll instead, so the document starts where it should. A control
140
+ keeps its own width — shrunk, a label wraps and a segmented control clips. */
141
+ .bf-card__actions{max-width:100%;flex-wrap:nowrap;overflow-x:auto}
142
+ .bf-card__actions>*{flex:0 0 auto}
143
+ /* A trailing date or one-word change state costs the column a reader is choosing
144
+ between: at 390px the listing gave the name 106px and every entry truncated,
145
+ and the diff gave a path 140px while "modified" held 130. */
146
+ .browse-listing .bf-table__when,.browse-diff .browse-diff-word{display:none}
123
147
  }
124
148
  /* A printed page scrolls nothing: the card and its body open up, the pinned hunk
125
149
  header stops pinning, and the progress hairline belongs to the screen. */
@@ -8,7 +8,7 @@
8
8
  * intent, and the one row the keyboard is on.
9
9
  */
10
10
 
11
- import { SIDEBAR_COLLAPSE_MAX } from "../../lib/web/index.ts";
11
+ import { SIDEBAR_COLLAPSE_MAX } from "../../../lib/web/index.ts";
12
12
 
13
13
  export const TREE_CLIENT_JS = ` var tree = document.querySelector("nav.bf-tree");
14
14
  var filter = document.getElementById("filter");
@@ -14,7 +14,7 @@
14
14
  */
15
15
 
16
16
  import { posix } from "node:path";
17
- import { encodePath } from "./paths.ts";
17
+ import { encodePath } from "../files/paths.ts";
18
18
 
19
19
  const HAS_SCHEME = /^[a-zA-Z][a-zA-Z0-9+.-]*:/;
20
20
  const SAFE_LINK_SCHEME = /^(?:https?|mailto):/i;
@@ -3,10 +3,11 @@
3
3
  */
4
4
 
5
5
  import { Type, type Static } from "typebox";
6
+ import { READ_DOC_TOOL } from "../../lib/tool-names.ts";
6
7
  import type { FormatId } from "./formats.ts";
7
8
 
8
9
  /** The tool's registered name, shared by the definition and its renderer. */
9
- export const TOOL_NAME = "read_doc";
10
+ export const TOOL_NAME = READ_DOC_TOOL;
10
11
 
11
12
  export const MIN_CHARS = 200;
12
13
  export const MAX_CHARS = 100_000;
@@ -29,6 +29,7 @@ import {
29
29
  type KillTreeFn,
30
30
  type SpawnedProcess,
31
31
  type SpawnFn,
32
+ startTokenOf,
32
33
  type StdinSink,
33
34
  } from "../../lib/spawn.ts";
34
35
  import { loadConfig, type JobsConfig } from "./config.ts";
@@ -38,7 +39,6 @@ import { defaultInterval } from "../../lib/timer.ts";
38
39
  import { clipLabel } from "../../lib/tui/columns.ts";
39
40
  import { compareJobs, formatLogs } from "./format.ts";
40
41
  import { readLogTail } from "./logs.ts";
41
- import { defaultStartToken, type LivenessFn, type StartTokenFn } from "./process.ts";
42
42
  import {
43
43
  loadRegistries,
44
44
  mergeRecords,
@@ -114,9 +114,9 @@ export interface SendResult {
114
114
 
115
115
  export interface RuntimeOptions {
116
116
  spawn?: SpawnFn;
117
- liveness?: LivenessFn;
117
+ liveness?: typeof isPidAlive;
118
118
  killTree?: KillTreeFn;
119
- startToken?: StartTokenFn;
119
+ startToken?: typeof startTokenOf;
120
120
  now?: () => number;
121
121
  /** Platform seam: whether this is Windows. Defaults to the running platform. */
122
122
  isWindows?: () => boolean;
@@ -183,7 +183,7 @@ export function createJobsRuntime(options: RuntimeOptions = {}): JobsRuntime {
183
183
  const spawn = options.spawn ?? defaultSpawn;
184
184
  const liveness = options.liveness ?? isPidAlive;
185
185
  const killTree = options.killTree ?? defaultKillTree;
186
- const startToken = options.startToken ?? defaultStartToken;
186
+ const startToken = options.startToken ?? startTokenOf;
187
187
  const now = options.now ?? (() => Date.now());
188
188
  const isWindows = options.isWindows ?? (() => process.platform === "win32");
189
189