@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
@@ -12,10 +12,11 @@
12
12
  * pair — so the whole feed is tested without one.
13
13
  */
14
14
 
15
- import { defaultInterval, defaultTimer } from "../../lib/timer.ts";
16
- import { LIVE_MAX_CLIENTS } from "./config.ts";
17
- import { changeDirs } from "./diff.ts";
18
- import type { DiffList, GitDiffProvider, GitStatus, GitStatusProvider } from "./git.ts";
15
+ import type { EventSink } from "../../../lib/http/index.ts";
16
+ import { defaultInterval, type RepeatTimer } from "../../../lib/timer.ts";
17
+ import { LIVE_MAX_CLIENTS } from "../config/config.ts";
18
+ import { changeDirs } from "../text/diff.ts";
19
+ import type { DiffList, GitDiffProvider, GitStatus, GitStatusProvider } from "../git/git.ts";
19
20
  import { changeRevision } from "./rev.ts";
20
21
 
21
22
  /**
@@ -141,48 +142,25 @@ export function frameOf(snapshot: LiveSnapshot): string {
141
142
  return `event: status\ndata: ${JSON.stringify(snapshot)}\n\n`;
142
143
  }
143
144
 
144
- /** A connected page: the two things a server can do to its stream. */
145
- export interface LiveClient {
146
- /** Write one frame. */
147
- write(frame: string): void;
148
- /** End the stream. */
149
- end(): void;
150
- }
151
-
152
- /** Starts a repeating tick and returns its cancel. */
153
- export type LiveTimer = (tick: () => void, intervalMs: number) => () => void;
154
-
155
- /** Starts a one-shot delay and returns its cancel. */
156
- export type LiveTimeout = (run: () => void, delayMs: number) => () => void;
157
-
158
- /** The default ticker: a repeating timer that cannot hold the process open. */
159
- const defaultTicker: LiveTimer = (tick, intervalMs) => defaultInterval(intervalMs, tick);
160
-
161
- /** The default grace timer: one shot, and it cannot hold the process open either. */
162
- const defaultTimeout: LiveTimeout = (run, delayMs) => defaultTimer(delayMs, run);
145
+ /** A connected page, as the channel writes it: the sink's two verbs. */
146
+ export type LiveClient = EventSink;
163
147
 
164
148
  export interface LiveChannelOptions {
165
149
  /** What each frame carries. */
166
150
  status: LiveStatusSource;
167
151
  /** How far apart the frames are, in ms. */
168
152
  intervalMs: number;
169
- /** How long the last page's departure is held before `onIdle` is called, in ms. */
170
- idleMs: number;
171
153
  /** How many pages may attach at once; defaults to `LIVE_MAX_CLIENTS`. */
172
154
  maxClients?: number;
173
- /** Called once the last page has been gone for `idleMs`; omitted, the channel only idles. */
174
- onIdle?: () => void;
175
155
  /** Timer seam; defaults to an unref'd interval. */
176
- timer?: LiveTimer;
177
- /** Grace seam; defaults to an unref'd timeout. */
178
- timeout?: LiveTimeout;
156
+ timer?: RepeatTimer;
179
157
  }
180
158
 
181
159
  /** The fan-out for one server: pages attach, ticks publish, close ends them. */
182
160
  export interface LiveChannel {
183
161
  /** Add a connected page, send it the current status, and return its detach. */
184
162
  attach(client: LiveClient): () => void;
185
- /** End every connected page, stop the ticker, and drop a pending idle stop. */
163
+ /** End every connected page, and stop the ticker. */
186
164
  close(): void;
187
165
  /**
188
166
  * Whether another page must be refused. The caller asks before it opens the
@@ -192,18 +170,18 @@ export interface LiveChannel {
192
170
  }
193
171
 
194
172
  /**
195
- * Build a channel for one server. The ticker runs only while at least one page
196
- * is attached, so an idle server does no status work at all; the idle stop is one
197
- * timer armed when the last page leaves and cancelled by the next page, so a
198
- * reload keeps the server and a closed window releases it.
173
+ * Build a channel for one server. The ticker runs only while at least one page is
174
+ * attached, so an idle server does no status work at all; the last page leaving
175
+ * parks it and the next page starts it again. What happens to a server nobody is
176
+ * reading — the idle grace and the close it leads to — is the server's policy, so
177
+ * the caller hands it to `lib/http`'s `serveLoopback` instead of configuring it
178
+ * here.
199
179
  */
200
180
  export function createLiveChannel(options: LiveChannelOptions): LiveChannel {
201
- const timer = options.timer ?? defaultTicker;
202
- const timeout = options.timeout ?? defaultTimeout;
181
+ const timer = options.timer ?? defaultInterval;
203
182
  const maxClients = options.maxClients ?? LIVE_MAX_CLIENTS;
204
183
  const clients = new Set<LiveClient>();
205
184
  let cancel: (() => void) | undefined;
206
- let cancelIdle: (() => void) | undefined;
207
185
  let inFlight = false;
208
186
 
209
187
  const stop = (): void => {
@@ -211,34 +189,9 @@ export function createLiveChannel(options: LiveChannelOptions): LiveChannel {
211
189
  cancel = undefined;
212
190
  };
213
191
 
214
- const cancelIdleStop = (): void => {
215
- cancelIdle?.();
216
- cancelIdle = undefined;
217
- };
218
-
219
- const armIdleStop = (): void => {
220
- if (!options.onIdle || cancelIdle) return;
221
- cancelIdle = timeout(() => {
222
- cancelIdle = undefined;
223
- options.onIdle?.();
224
- }, options.idleMs);
225
- };
226
-
227
- /** Nobody is reading a frame any more: park the ticker and start the grace. */
228
- const pageLeft = (): void => {
229
- stop();
230
- armIdleStop();
231
- };
232
-
233
192
  const send = (frame: string): void => {
234
- for (const client of clients) {
235
- try {
236
- client.write(frame);
237
- } catch {
238
- clients.delete(client);
239
- }
240
- }
241
- if (clients.size === 0) pageLeft();
193
+ for (const client of clients) client.write(frame);
194
+ if (clients.size === 0) stop();
242
195
  };
243
196
 
244
197
  // One snapshot serves every attached page, so a tick is one git read and one
@@ -255,17 +208,15 @@ export function createLiveChannel(options: LiveChannelOptions): LiveChannel {
255
208
 
256
209
  return {
257
210
  attach(client) {
258
- cancelIdleStop();
259
211
  clients.add(client);
260
- cancel ??= timer(() => void publish(), options.intervalMs);
212
+ cancel ??= timer(options.intervalMs, () => void publish());
261
213
  void publish();
262
214
  return () => {
263
215
  if (!clients.delete(client)) return;
264
- if (clients.size === 0) pageLeft();
216
+ if (clients.size === 0) stop();
265
217
  };
266
218
  },
267
219
  close() {
268
- cancelIdleStop();
269
220
  stop();
270
221
  for (const client of clients) client.end();
271
222
  clients.clear();
@@ -19,14 +19,14 @@ import {
19
19
  parseRequestUrl,
20
20
  scriptHeaders,
21
21
  type ServeResponse,
22
- } from "../../lib/http/index.ts";
23
- import { THEME_JS, THEME_JS_PATH } from "../../lib/web/index.ts";
24
- import { CLIENT_JS } from "./client.ts";
25
- import type { LivePage } from "./chrome.ts";
26
- import type { ServeConfig } from "./config.ts";
27
- import { isDiffMode, type DiffMode } from "./diff.ts";
28
- import type { DiffList, GitDiffProvider, GitStatus, GitStatusProvider } from "./git.ts";
29
- import { changeIndexOf, type LiveStatusSource } from "./live.ts";
22
+ } from "../../../lib/http/index.ts";
23
+ import { THEME_JS, THEME_JS_PATH } from "../../../lib/web/index.ts";
24
+ import { CLIENT_JS } from "../view/client.ts";
25
+ import type { LivePage } from "../view/chrome.ts";
26
+ import type { ServeConfig } from "../config/config.ts";
27
+ import { isDiffMode, type DiffMode } from "../text/diff.ts";
28
+ import type { DiffList, GitDiffProvider, GitStatus, GitStatusProvider } from "../git/git.ts";
29
+ import { changeIndexOf, type LiveStatusSource } from "../live/live.ts";
30
30
  import {
31
31
  buildFileIndex,
32
32
  buildTree,
@@ -38,13 +38,19 @@ import {
38
38
  type DirEntry,
39
39
  type FileIndexProvider,
40
40
  type TreeNode,
41
- } from "./files.ts";
42
- import { FOLDER_ICON, iconHref, languageOf } from "./icons.ts";
43
- import { fuzzySearch } from "./fuzzy.ts";
44
- import { renderDirectoryPage, renderDiffPage, renderErrorPage, renderFileDiffPage, renderFilePage } from "./html.ts";
45
- import type { ChangeSummary } from "./html.ts";
46
- import { encodePath, HttpError, relativePath, resolveDecodedPath, resolveRequestPath } from "./paths.ts";
47
- import { listingRevision } from "./rev.ts";
41
+ } from "../files/files.ts";
42
+ import { FOLDER_ICON, iconHref, languageOf } from "../view/icons.ts";
43
+ import { fuzzySearch } from "../text/fuzzy.ts";
44
+ import {
45
+ renderDirectoryPage,
46
+ renderDiffPage,
47
+ renderErrorPage,
48
+ renderFileDiffPage,
49
+ renderFilePage,
50
+ } from "../view/html.ts";
51
+ import type { ChangeSummary } from "../view/html.ts";
52
+ import { encodePath, HttpError, relativePath, resolveDecodedPath, resolveRequestPath } from "../files/paths.ts";
53
+ import { listingRevision } from "../live/rev.ts";
48
54
 
49
55
  export interface ServeContext {
50
56
  root: string;
@@ -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
@@ -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, 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"];
@@ -286,8 +287,9 @@ function numberedLines(page: FilePage): boolean {
286
287
  return page.file.kind === "text" || (page.file.kind === "markdown" && page.markdownRaw === true);
287
288
  }
288
289
 
289
- /** The card-head controls: Markdown mode, line wrapping, and copy actions. */
290
- function renderFileActions(page: FilePage): string {
290
+ /** The card-head controls: Markdown mode, line wrapping, and the copy actions,
291
+ * plus the download the diff head keeps in the same group. */
292
+ function renderFileActions(page: FilePage, download: string): string {
291
293
  const isMarkdown = page.file.kind === "markdown";
292
294
  const rawHref = `/raw/${encodePath(page.rel)}`;
293
295
  const viewHref = `/view/${encodePath(page.rel)}`;
@@ -324,7 +326,7 @@ function renderFileActions(page: FilePage): string {
324
326
  `<button type="button" class="bf-btn" id="copy-reference" disabled title="Select lines in the file first">copy reference</button>`,
325
327
  );
326
328
  }
327
- return `<span class="bf-card__actions">${groups.join("")}</span>`;
329
+ return `<span class="bf-card__actions">${groups.join("")}${download}</span>`;
328
330
  }
329
331
 
330
332
  /** Render a file page (text, Markdown, image, binary, or over-size). */
@@ -336,7 +338,7 @@ export function renderFilePage(page: FilePage): string {
336
338
  // about the file; the language label, size, and date are the useful metadata.
337
339
  const meta = `${spec.label} · ${formatSize(page.file.size)} · ${formatDate(page.file.mtimeMs)}`;
338
340
  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>`;
341
+ 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
342
 
341
343
  const isMarkdown = page.file.kind === "markdown";
342
344
  const rendered = isMarkdown && !page.markdownRaw;
@@ -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,
@@ -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
  }
@@ -25,7 +25,7 @@
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
29
  import { resolveImage, resolveLink } from "./urls.ts";
30
30
 
31
31
  /** Elements re-emitted with sanitized attributes. */
@@ -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;
@@ -74,17 +74,24 @@ export function readMemoryFile(path: string): string {
74
74
  return readFileSync(path, "utf-8");
75
75
  }
76
76
 
77
- /** Coarse identity of a store file, for cheap change detection. */
77
+ /**
78
+ * Identity of a store file, for cheap change detection.
79
+ *
80
+ * The stamp is nanoseconds, not milliseconds: an edit that keeps the file's size
81
+ * is otherwise invisible for a whole millisecond, which is long enough for an
82
+ * editor that writes in place. `size` stays a number so the doctor's byte counts
83
+ * read the same.
84
+ */
78
85
  export interface MemoryFileStamp {
79
- mtimeMs: number;
86
+ mtimeNs: bigint;
80
87
  size: number;
81
88
  }
82
89
 
83
90
  /** Freshness stamp for a store file; `undefined` when it is missing or unstattable. */
84
91
  export function memoryFileStamp(path: string): MemoryFileStamp | undefined {
85
92
  try {
86
- const info = statSync(path);
87
- return { mtimeMs: info.mtimeMs, size: info.size };
93
+ const info = statSync(path, { bigint: true });
94
+ return { mtimeNs: info.mtimeNs, size: Number(info.size) };
88
95
  } catch {
89
96
  return undefined;
90
97
  }
@@ -93,7 +100,7 @@ export function memoryFileStamp(path: string): MemoryFileStamp | undefined {
93
100
  /** Whether two stamps describe the same file state. */
94
101
  export function sameStamp(a: MemoryFileStamp | undefined, b: MemoryFileStamp | undefined): boolean {
95
102
  if (a === undefined || b === undefined) return a === b;
96
- return a.mtimeMs === b.mtimeMs && a.size === b.size;
103
+ return a.mtimeNs === b.mtimeNs && a.size === b.size;
97
104
  }
98
105
 
99
106
  /** Note text of every bullet line, sanitized to one terminal-safe line. */