@gavin-hu/my-pi-agent 0.17.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 (274) hide show
  1. package/README.md +61 -45
  2. package/README.zh-CN.md +54 -42
  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} +465 -416
  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} +66 -53
  13. package/{extensions/memory/README.md → docs/extensions/memory.md} +74 -63
  14. package/docs/extensions/plan.md +976 -0
  15. package/{extensions/question/README.md → docs/extensions/question.md} +54 -28
  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} +166 -119
  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} +60 -38
  22. package/{extensions/web-access/README.md → docs/extensions/web-access.md} +93 -50
  23. package/{extensions/wechat/README.md → docs/extensions/wechat.md} +275 -192
  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/notify/types.ts +1 -1
  51. package/extensions/bytefly/panel/commands.ts +40 -7
  52. package/extensions/bytefly/panel/settings-runtime.ts +0 -6
  53. package/extensions/bytefly/panel/settings.ts +11 -21
  54. package/extensions/bytefly/panel/tui/settings-view.ts +1 -1
  55. package/extensions/bytefly/picker/discovery.ts +0 -2
  56. package/extensions/bytefly/picker/types.ts +0 -2
  57. package/extensions/bytefly/update/check.ts +39 -0
  58. package/extensions/bytefly/update/format.ts +15 -0
  59. package/extensions/bytefly/update/register.ts +73 -34
  60. package/extensions/bytefly/update/state.ts +6 -3
  61. package/extensions/context/tui.ts +1 -1
  62. package/extensions/file-browse/{config.ts → config/config.ts} +13 -4
  63. package/extensions/file-browse/{files.ts → files/files.ts} +1 -14
  64. package/extensions/file-browse/{paths.ts → files/paths.ts} +1 -1
  65. package/extensions/file-browse/{git.ts → git/git.ts} +10 -3
  66. package/extensions/file-browse/index.ts +4 -4
  67. package/extensions/file-browse/{live.ts → live/live.ts} +54 -82
  68. package/extensions/file-browse/live/rev.ts +42 -0
  69. package/extensions/file-browse/{router.ts → transport/router.ts} +44 -17
  70. package/extensions/file-browse/transport/server.ts +110 -0
  71. package/extensions/file-browse/view/chrome.ts +83 -0
  72. package/extensions/file-browse/{client.ts → view/client.ts} +63 -1
  73. package/extensions/file-browse/view/diff-html.ts +94 -0
  74. package/extensions/file-browse/{html.ts → view/html.ts} +32 -158
  75. package/extensions/file-browse/{icons.ts → view/icons.ts} +1 -1
  76. package/extensions/file-browse/{live-client.ts → view/live-client.ts} +47 -1
  77. package/extensions/file-browse/{markdown.ts → view/markdown.ts} +7 -7
  78. package/extensions/file-browse/{raw-html.ts → view/raw-html.ts} +1 -1
  79. package/extensions/file-browse/{styles.ts → view/styles.ts} +31 -5
  80. package/extensions/file-browse/{tree.ts → view/tree.ts} +1 -1
  81. package/extensions/file-browse/{urls.ts → view/urls.ts} +1 -1
  82. package/extensions/file-read/schema.ts +2 -1
  83. package/extensions/goal/plan-route.ts +8 -2
  84. package/extensions/job/index.ts +15 -7
  85. package/extensions/memory/store.ts +12 -5
  86. package/extensions/plan/blocks.ts +23 -2
  87. package/extensions/plan/commands.ts +77 -21
  88. package/extensions/plan/feedback.ts +409 -63
  89. package/extensions/plan/index.ts +7 -5
  90. package/extensions/plan/plan-index.ts +44 -10
  91. package/extensions/plan/plan-text.ts +51 -24
  92. package/extensions/plan/plans.ts +162 -57
  93. package/extensions/plan/policy.ts +20 -12
  94. package/extensions/plan/review-prompt.ts +93 -0
  95. package/extensions/plan/review.ts +51 -95
  96. package/extensions/plan/runtime.ts +22 -10
  97. package/extensions/plan/sections.ts +112 -0
  98. package/extensions/plan/steps.ts +63 -95
  99. package/extensions/plan/tools/enter.ts +8 -3
  100. package/extensions/plan/tools/exit.ts +13 -19
  101. package/extensions/plan/tools/write.ts +8 -3
  102. package/extensions/plan/tools.ts +1 -1
  103. package/extensions/plan/tui.ts +29 -15
  104. package/extensions/plan/types.ts +29 -3
  105. package/extensions/plan/web-chrome.ts +2 -1
  106. package/extensions/plan/web-client.ts +122 -178
  107. package/extensions/plan/web-collab.ts +207 -0
  108. package/extensions/plan/web-comments.ts +269 -0
  109. package/extensions/plan/web-page.ts +96 -17
  110. package/extensions/plan/web-router.ts +109 -47
  111. package/extensions/plan/web-share-session.ts +191 -0
  112. package/extensions/plan/web-share.ts +169 -0
  113. package/extensions/plan/web-streams.ts +124 -0
  114. package/extensions/plan/web-tokens.ts +63 -0
  115. package/extensions/plan/web.ts +245 -151
  116. package/extensions/question/dialogs.ts +91 -1
  117. package/extensions/question/tools.ts +2 -1
  118. package/extensions/rewind/{commands.ts → commands/commands.ts} +5 -5
  119. package/extensions/rewind/{review.ts → commands/review.ts} +13 -13
  120. package/extensions/rewind/{rewind.ts → commands/rewind.ts} +6 -6
  121. package/extensions/rewind/{config.ts → config/config.ts} +1 -1
  122. package/extensions/rewind/index.ts +3 -3
  123. package/extensions/rewind/{policy.ts → state/policy.ts} +4 -4
  124. package/extensions/rewind/{runtime.ts → state/runtime.ts} +11 -11
  125. package/extensions/rewind/{snapshot-run.ts → state/snapshot-run.ts} +1 -1
  126. package/extensions/rewind/{timeline.ts → state/timeline.ts} +2 -2
  127. package/extensions/rewind/{format.ts → text/format.ts} +3 -3
  128. package/extensions/rewind/{git.ts → transport/git.ts} +2 -2
  129. package/extensions/rewind/{restore.ts → transport/restore.ts} +2 -2
  130. package/extensions/rewind/{snapshot.ts → transport/snapshot.ts} +1 -1
  131. package/extensions/rewind/{store.ts → transport/store.ts} +1 -1
  132. package/extensions/rewind/{checklist.ts → view/checklist.ts} +6 -6
  133. package/extensions/rewind/{hunk-picker.ts → view/hunk-picker.ts} +8 -8
  134. package/extensions/rewind/{path-picker.ts → view/path-picker.ts} +7 -7
  135. package/extensions/rewind/{review-tui.ts → view/review-tui.ts} +11 -11
  136. package/extensions/rewind/{rewind-tui.ts → view/rewind-tui.ts} +17 -11
  137. package/extensions/rewind/{status.ts → view/status.ts} +4 -4
  138. package/extensions/session-bus/commands.ts +113 -0
  139. package/extensions/session-bus/format.ts +101 -0
  140. package/extensions/session-bus/index.ts +75 -0
  141. package/extensions/session-bus/join-request.ts +40 -0
  142. package/extensions/session-bus/mail-watch.ts +134 -0
  143. package/extensions/session-bus/notice.ts +86 -0
  144. package/extensions/session-bus/overlap.ts +36 -0
  145. package/extensions/session-bus/protocol.ts +104 -0
  146. package/extensions/session-bus/resolve.ts +67 -0
  147. package/extensions/session-bus/result.ts +208 -0
  148. package/extensions/session-bus/rows.ts +171 -0
  149. package/extensions/session-bus/runtime.ts +466 -0
  150. package/extensions/session-bus/store.ts +365 -0
  151. package/extensions/session-bus/tools.ts +192 -0
  152. package/extensions/session-bus/tui.ts +394 -0
  153. package/extensions/session-bus/types.ts +98 -0
  154. package/extensions/subagent/agents.ts +9 -8
  155. package/extensions/subagent/invocation.ts +4 -2
  156. package/extensions/team/commands.ts +112 -0
  157. package/extensions/team/dispatch.ts +133 -0
  158. package/extensions/team/format.ts +105 -0
  159. package/extensions/team/index.ts +33 -0
  160. package/extensions/team/schema.ts +29 -0
  161. package/extensions/team/tools.ts +246 -0
  162. package/extensions/team/types.ts +48 -0
  163. package/extensions/watch/index.ts +15 -9
  164. package/extensions/web-access/fetch/page.ts +4 -1
  165. package/extensions/web-access/fetch/structured.ts +447 -0
  166. package/extensions/web-access/fetch/tool.ts +2 -1
  167. package/extensions/web-access/search/tool.ts +2 -1
  168. package/extensions/wechat/bridge.ts +76 -92
  169. package/extensions/wechat/{commands.ts → commands/commands.ts} +26 -18
  170. package/extensions/wechat/{doctor.ts → commands/doctor.ts} +5 -5
  171. package/extensions/wechat/{history.ts → commands/history.ts} +1 -1
  172. package/extensions/wechat/{login.ts → commands/login.ts} +3 -3
  173. package/extensions/wechat/{slash.ts → commands/slash.ts} +38 -2
  174. package/extensions/wechat/dialog/channels.ts +136 -0
  175. package/extensions/wechat/{interaction.ts → dialog/interaction.ts} +66 -51
  176. package/extensions/wechat/{prompt.ts → dialog/prompt.ts} +40 -25
  177. package/extensions/wechat/{remote-ui.ts → dialog/remote-ui.ts} +1 -1
  178. package/extensions/wechat/index.ts +10 -10
  179. package/extensions/wechat/{admission.ts → peer/admission.ts} +2 -2
  180. package/extensions/wechat/{peer-session.ts → peer/peer-session.ts} +2 -2
  181. package/extensions/wechat/{peer-worker.ts → peer/peer-worker.ts} +119 -56
  182. package/extensions/wechat/{presence.ts → peer/presence.ts} +11 -11
  183. package/extensions/wechat/{reply-pipeline.ts → peer/reply-pipeline.ts} +13 -5
  184. package/extensions/wechat/{credentials.ts → state/credentials.ts} +2 -2
  185. package/extensions/wechat/{state.ts → state/state.ts} +14 -2
  186. package/extensions/wechat/{format.ts → text/format.ts} +3 -3
  187. package/extensions/wechat/{notices.ts → text/notices.ts} +15 -7
  188. package/extensions/wechat/tools.ts +29 -12
  189. package/extensions/wechat/{client.ts → transport/client.ts} +26 -2
  190. package/extensions/wechat/{deliver.ts → transport/deliver.ts} +104 -40
  191. package/extensions/wechat/{inbound.ts → transport/inbound.ts} +5 -5
  192. package/extensions/wechat/{media.ts → transport/media.ts} +11 -5
  193. package/extensions/wechat/transport/outbound.ts +142 -0
  194. package/extensions/wechat/{poll.ts → transport/poll.ts} +3 -3
  195. package/extensions/wechat/types.ts +10 -1
  196. package/extensions/wechat/{login-screen.ts → view/login-screen.ts} +4 -4
  197. package/extensions/wechat/{status-screen.ts → view/status-screen.ts} +31 -26
  198. package/extensions/worktree/git.ts +5 -0
  199. package/extensions/worktree/guard.ts +3 -5
  200. package/extensions/worktree/registry.ts +11 -5
  201. package/extensions/worktree/status.ts +1 -3
  202. package/lib/README.md +12 -6
  203. package/lib/board/card.ts +290 -0
  204. package/lib/board/links.ts +117 -0
  205. package/lib/board/page-pointer.ts +256 -0
  206. package/lib/board/round-text.ts +72 -0
  207. package/lib/board/round.ts +89 -0
  208. package/lib/board/rules.ts +304 -0
  209. package/lib/board/session.ts +105 -0
  210. package/lib/board/store.ts +310 -0
  211. package/lib/board/view.ts +150 -0
  212. package/lib/errors.ts +12 -4
  213. package/lib/format.ts +13 -0
  214. package/lib/http/guard.ts +112 -0
  215. package/lib/http/index.ts +16 -5
  216. package/lib/http/router.ts +40 -13
  217. package/lib/http/serve.ts +234 -0
  218. package/lib/http/server.ts +29 -13
  219. package/lib/http/sink.ts +110 -0
  220. package/lib/http/token.ts +38 -0
  221. package/lib/interaction.ts +10 -0
  222. package/lib/notice.ts +29 -2
  223. package/lib/plan-entry.ts +7 -2
  224. package/lib/policy.ts +12 -2
  225. package/lib/qr.ts +64 -0
  226. package/lib/repo-key.ts +74 -0
  227. package/lib/report.ts +52 -33
  228. package/lib/requirements.ts +7 -9
  229. package/lib/session-label.ts +64 -0
  230. package/lib/session-roster.ts +130 -0
  231. package/lib/session-tool.ts +184 -0
  232. package/lib/tool-names.ts +25 -2
  233. package/lib/tui/README.md +16 -2
  234. package/lib/tui/dock-screen.ts +85 -17
  235. package/lib/tui/glyphs.ts +2 -0
  236. package/lib/tui/line-pane.ts +15 -4
  237. package/lib/tui/tool.ts +23 -4
  238. package/lib/web/README.md +5 -2
  239. package/lib/web/components.ts +6 -1
  240. package/lib/web/runtime.ts +7 -1
  241. package/lib/web/tokens.ts +25 -2
  242. package/package.json +27 -14
  243. package/skills/init/SKILL.md +80 -45
  244. package/skills/init/references/agents-md-quality.md +26 -2
  245. package/skills/init/references/interview-questions.md +36 -14
  246. package/skills/next/SKILL.md +77 -51
  247. package/skills/project-docs/SKILL.md +133 -0
  248. package/skills/project-docs/references/audit.md +131 -0
  249. package/skills/project-docs/references/changelog.md +105 -0
  250. package/skills/project-docs/references/discovery.md +127 -0
  251. package/skills/project-docs/references/readme.md +80 -0
  252. package/skills/project-docs/references/release.md +84 -0
  253. package/skills/project-docs/references/roadmap.md +93 -0
  254. package/skills/project-docs/scripts/state.ts +316 -0
  255. package/skills/refactor/SKILL.md +100 -125
  256. package/skills/refactor/references/handoff.md +35 -0
  257. package/skills/refactor/references/signals.md +54 -0
  258. package/skills/refactor/scripts/inventory.ts +587 -0
  259. package/extensions/file-browse/README.md +0 -467
  260. package/extensions/file-browse/server.ts +0 -134
  261. package/extensions/plan/README.md +0 -709
  262. package/extensions/plan/config.ts +0 -27
  263. package/extensions/wechat/outbound.ts +0 -100
  264. package/extensions/wechat/qrcode.ts +0 -46
  265. package/skills/changelog/SKILL.md +0 -148
  266. /package/extensions/file-browse/{diff.ts → text/diff.ts} +0 -0
  267. /package/extensions/file-browse/{fuzzy.ts → text/fuzzy.ts} +0 -0
  268. /package/extensions/file-browse/{palette.ts → view/palette.ts} +0 -0
  269. /package/extensions/rewind/{patch.ts → text/patch.ts} +0 -0
  270. /package/extensions/rewind/{paths.ts → text/paths.ts} +0 -0
  271. /package/extensions/rewind/{queue.ts → transport/queue.ts} +0 -0
  272. /package/extensions/rewind/{temp-index.ts → transport/temp-index.ts} +0 -0
  273. /package/extensions/wechat/{lock.ts → state/lock.ts} +0 -0
  274. /package/extensions/wechat/{channel.ts → text/channel.ts} +0 -0
@@ -0,0 +1,94 @@
1
+ /**
2
+ * The browse pages' unified-diff body: one line per row, with the gutter, the
3
+ * sign, and the hunk sections.
4
+ *
5
+ * Split from `html.ts` when that module reached its named ceiling: the diff body
6
+ * is the one part of the page renderers with its own rule — old and new sides are
7
+ * highlighted as two documents per hunk, so the tokenizer carries state within a
8
+ * hunk and never across the unchanged lines a hunk does not show.
9
+ */
10
+
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
+
15
+ /**
16
+ * The inner HTML per diff line. Hunk bodies are highlighted by rebuilding the
17
+ * old side (context + deleted) and the new side (context + added), so the
18
+ * tokenizer carries state within a hunk; meta, hunk, and note lines stay plain.
19
+ * State does not cross hunks, since the unchanged lines between them are absent.
20
+ */
21
+ function highlightDiffText(lines: DiffLine[], language?: HighlightLanguage): string[] {
22
+ if (!language) return lines.map((line) => escapeHtml(line.text));
23
+ const out: string[] = new Array(lines.length);
24
+ let index = 0;
25
+ while (index < lines.length) {
26
+ const kind = lines[index].kind;
27
+ if (kind !== "ctx" && kind !== "add" && kind !== "del") {
28
+ out[index] = escapeHtml(lines[index].text);
29
+ index++;
30
+ continue;
31
+ }
32
+ const start = index;
33
+ const oldDoc: string[] = [];
34
+ const newDoc: string[] = [];
35
+ while (index < lines.length) {
36
+ const body = lines[index].kind;
37
+ if (body !== "ctx" && body !== "add" && body !== "del") break;
38
+ if (body === "ctx") {
39
+ oldDoc.push(lines[index].text);
40
+ newDoc.push(lines[index].text);
41
+ } else if (body === "add") {
42
+ newDoc.push(lines[index].text);
43
+ } else {
44
+ oldDoc.push(lines[index].text);
45
+ }
46
+ index++;
47
+ }
48
+ const oldHtml = highlightLines(oldDoc, language);
49
+ const newHtml = highlightLines(newDoc, language);
50
+ let oldIndex = 0;
51
+ let newIndex = 0;
52
+ for (let cursor = start; cursor < index; cursor++) {
53
+ const body = lines[cursor].kind;
54
+ if (body === "ctx") {
55
+ out[cursor] = newHtml[newIndex];
56
+ newIndex++;
57
+ oldIndex++;
58
+ } else if (body === "add") {
59
+ out[cursor] = newHtml[newIndex];
60
+ newIndex++;
61
+ } else {
62
+ out[cursor] = oldHtml[oldIndex];
63
+ oldIndex++;
64
+ }
65
+ }
66
+ }
67
+ return out;
68
+ }
69
+
70
+ /** Render unified-diff lines with a two-column gutter and per-kind styling. */
71
+ export function renderDiffLines(lines: DiffLine[], language?: HighlightLanguage): string {
72
+ const html = highlightDiffText(lines, language);
73
+ // Every hunk is a section so its sticky header is bounded by its own body;
74
+ // the section also gives the keyboard a stable, non-sticky scroll target.
75
+ const out: string[] = [];
76
+ let hunks = 0;
77
+ let open = false;
78
+ lines.forEach((line, index) => {
79
+ if (line.kind === "hunk") {
80
+ if (open) out.push("</span>");
81
+ hunks += 1;
82
+ out.push(`<span class="bf-diff-hunk" id="hunk-${hunks}" data-hunk="${hunks}">`);
83
+ open = true;
84
+ }
85
+ const sign = line.kind === "add" ? "+" : line.kind === "del" ? "-" : line.kind === "ctx" ? " " : "";
86
+ const gutter = `<span class="browse-diff-no" aria-hidden="true">${line.oldNo ?? ""}</span><span class="browse-diff-no" aria-hidden="true">${line.newNo ?? ""}</span>`;
87
+ const marker = `<span class="browse-diff-sign" aria-hidden="true">${sign}</span>`;
88
+ out.push(
89
+ `<span class="bf-code__line bf-diff-line bf-diff-line--${line.kind}">${gutter}${marker}${html[index]}</span>`,
90
+ );
91
+ });
92
+ if (open) out.push("</span>");
93
+ return out.join("");
94
+ }
@@ -8,17 +8,19 @@
8
8
  * the page-only `browse-*` rules from `styles.ts`.
9
9
  */
10
10
 
11
- import { GLYPHS } from "../../lib/tui/glyphs.ts";
12
- import { brandLockup, docHead, type KeyHint, keysSheet, themeToggle } from "../../lib/web/index.ts";
13
- import { escapeHtml } from "../../lib/escape.ts";
14
- import { formatSize, type DirEntry, type FileView, type Listing, type TreeNode } from "./files.ts";
15
- import { DIFF_MODES, statusLetter, type ChangedFile, type DiffLine, type DiffMode } from "./diff.ts";
16
- import type { DiffFileView, GitStatus } from "./git.ts";
17
- import type { ChangeIndex } from "./live.ts";
18
- import { detectLanguage, highlightLines, type HighlightLanguage } from "../../lib/highlight.ts";
11
+ import { brandLockup, docHead, type KeyHint, keysSheet, themeToggle } from "../../../lib/web/index.ts";
12
+ import { escapeHtml } from "../../../lib/escape.ts";
13
+ import { renderChanges, renderGit, renderLiveChrome, renderLiveStatus, type LivePage } from "./chrome.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";
17
+ import { renderDiffLines } from "./diff-html.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";
19
21
  import { FOLDER_ICON, iconHref, languageOf, SPRITE, type IconSpec } from "./icons.ts";
20
22
  import { renderMarkdown } from "./markdown.ts";
21
- import { encodePath } from "./paths.ts";
23
+ import { encodePath } from "../files/paths.ts";
22
24
  import { BROWSE_CSS } from "./styles.ts";
23
25
 
24
26
  const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
@@ -36,16 +38,6 @@ function iconMarkup(spec: IconSpec, hidden = false): string {
36
38
  return `<svg class="browse-cat ${spec.colorClass}"${hidden ? " hidden" : ""} aria-hidden="true" title="${escapeHtml(spec.label)}"><use href="${iconHref(spec.icon)}"/></svg>`;
37
39
  }
38
40
 
39
- /** The header's git chip: the branch, or the detached head; changes are the changes chip's job. */
40
- function renderGit(git: GitStatus | undefined): string {
41
- if (!git) return "";
42
- const name = git.branch ?? git.head;
43
- if (!name) return "";
44
- const glyph = git.branch ? GLYPHS.branch : GLYPHS.detached;
45
- const aria = git.branch ? `git branch ${git.branch}` : `git detached at ${git.head ?? ""}`;
46
- return `<span class="bf-chip browse-git" title="${escapeHtml(aria)}" aria-label="${escapeHtml(aria)}"><span class="browse-git__glyph" aria-hidden="true">${escapeHtml(glyph)}</span><span class="browse-git__name">${escapeHtml(name)}</span></span>`;
47
- }
48
-
49
41
  /**
50
42
  * The changed-path index for markers, plus the totals shown in the chip. It is
51
43
  * the same index the live status feed sends, so the page and its updates agree
@@ -66,55 +58,6 @@ function changeMark(summary: ChangeSummary | undefined, rel: string, isDir: bool
66
58
  : "";
67
59
  }
68
60
 
69
- /** The live chrome a page renders: how often its stream is sent a frame. */
70
- export interface LivePage {
71
- intervalMs: number;
72
- }
73
-
74
- /**
75
- * The header's connection chip. It starts hidden and says "connecting": the
76
- * client reveals and paints it from the event stream, so a page with JS off — or
77
- * with live status disabled — never shows a state it cannot keep true.
78
- *
79
- * It is deliberately not a `role="status"` region: the header is where a reader
80
- * looks for the page's state, not a place to announce every change to it.
81
- */
82
- function renderLiveChip(live: LivePage | undefined): string {
83
- if (!live) return "";
84
- return `<span class="bf-chip bf-chip--accent browse-live" id="live-state" title="Live status from the browse server" data-interval="${live.intervalMs}" hidden><span class="browse-live__glyph" aria-hidden="true">${escapeHtml(GLYPHS.browse)}</span><span class="browse-live__text">connecting</span></span>`;
85
- }
86
-
87
- /**
88
- * The footer's live line, hidden until the first frame: uptime and requests are
89
- * server facts the rendered page does not know at build time.
90
- */
91
- function renderLiveStatus(): string {
92
- return `<span class="browse-live-line" id="live-status" hidden></span>`;
93
- }
94
-
95
- /**
96
- * The header's changes chip, linking to the diff overview. With live status on,
97
- * a hidden shell is always rendered so the chip can appear and disappear as the
98
- * working tree changes, without the client inserting elements into the header.
99
- */
100
- function renderChanges(summary: ChangeSummary | undefined, live: LivePage | undefined): string {
101
- const count = summary?.count ?? 0;
102
- if (count <= 0) {
103
- return live
104
- ? `<a class="bf-chip browse-diff" id="browse-changes" href="/diff" title="View uncommitted changes" hidden></a>`
105
- : "";
106
- }
107
- const label = `${count} change${count === 1 ? "" : "s"}`;
108
- const insertions = summary?.insertions ?? 0;
109
- const deletions = summary?.deletions ?? 0;
110
- const totals =
111
- insertions + deletions > 0
112
- ? ` <span class="browse-diff-totals"><span class="browse-diff-add">+${insertions}</span> <span class="browse-diff-del">−${deletions}</span></span>`
113
- : "";
114
- const aria = `${label} against HEAD${insertions + deletions > 0 ? `, ${insertions} insertions and ${deletions} deletions` : ""}`;
115
- return `<a class="bf-chip browse-diff" id="browse-changes" href="/diff" title="View uncommitted changes" aria-label="${escapeHtml(aria)}"><span class="browse-diff__dot" aria-hidden="true">●</span>${escapeHtml(label)}${totals}</a>`;
116
- }
117
-
118
61
  function hrefFor(entry: { rel: string; isDir: boolean }): string {
119
62
  return entry.isDir ? `/browse/${encodePath(entry.rel)}` : `/view/${encodePath(entry.rel)}`;
120
63
  }
@@ -157,7 +100,7 @@ ${SPRITE}
157
100
  ${renderGit(layout.git)}
158
101
  ${renderChanges(layout.summary, layout.live)}
159
102
  <span class="bf-chip bf-chip--accent">read-only</span>
160
- ${renderLiveChip(layout.live)}
103
+ ${renderLiveChrome(layout.live)}
161
104
  <button type="button" class="bf-btn bf-btn--icon browse-keys-btn" id="keys-open" aria-label="Keyboard shortcuts" title="Keyboard shortcuts">?</button>
162
105
  ${themeToggle()}
163
106
  </div><div class="browse-progress" aria-hidden="true"><span class="browse-progress__fill" id="progress-fill"></span></div></header>
@@ -339,8 +282,14 @@ function pageKeys(...extra: KeyHint[]): KeyHint[] {
339
282
  ];
340
283
  }
341
284
 
342
- /** The card-head controls: Markdown mode, line wrapping, and copy actions. */
343
- function renderFileActions(page: FilePage): string {
285
+ /** Whether the page renders lines with a number beside them: plain text, or raw Markdown. */
286
+ function numberedLines(page: FilePage): boolean {
287
+ return page.file.kind === "text" || (page.file.kind === "markdown" && page.markdownRaw === true);
288
+ }
289
+
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 {
344
293
  const isMarkdown = page.file.kind === "markdown";
345
294
  const rawHref = `/raw/${encodePath(page.rel)}`;
346
295
  const viewHref = `/view/${encodePath(page.rel)}`;
@@ -359,7 +308,7 @@ function renderFileActions(page: FilePage): string {
359
308
  : `<a class="bf-segmented__item" href="${viewHref}?raw=1">raw</a>`;
360
309
  groups.push(`<span class="bf-segmented" role="group" aria-label="Markdown view">${rendered}${raw}</span>`);
361
310
  }
362
- if (page.file.kind === "text" || (isMarkdown && page.markdownRaw)) {
311
+ if (numberedLines(page)) {
363
312
  groups.push(
364
313
  `<button type="button" class="bf-btn" id="wrap-toggle" aria-pressed="false" title="Toggle line wrapping">wrap</button>`,
365
314
  );
@@ -372,7 +321,12 @@ function renderFileActions(page: FilePage): string {
372
321
  groups.push(
373
322
  `<button type="button" class="bf-btn" data-copy="path" data-value="${escapeHtml(page.rel)}" title="Copy the root-relative path">copy path</button>`,
374
323
  );
375
- return `<span class="bf-card__actions">${groups.join("")}</span>`;
324
+ if (numberedLines(page)) {
325
+ groups.push(
326
+ `<button type="button" class="bf-btn" id="copy-reference" disabled title="Select lines in the file first">copy reference</button>`,
327
+ );
328
+ }
329
+ return `<span class="bf-card__actions">${groups.join("")}${download}</span>`;
376
330
  }
377
331
 
378
332
  /** Render a file page (text, Markdown, image, binary, or over-size). */
@@ -384,14 +338,15 @@ export function renderFilePage(page: FilePage): string {
384
338
  // about the file; the language label, size, and date are the useful metadata.
385
339
  const meta = `${spec.label} · ${formatSize(page.file.size)} · ${formatDate(page.file.mtimeMs)}`;
386
340
  const download = `<a class="bf-btn" href="/raw/${encodePath(page.rel)}" download>download</a>`;
387
- 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>`;
388
342
 
389
343
  const isMarkdown = page.file.kind === "markdown";
390
344
  const rendered = isMarkdown && !page.markdownRaw;
391
- const wrappable = page.file.kind === "text" || (isMarkdown && page.markdownRaw);
345
+ const numbered = numberedLines(page);
392
346
  const keys = pageKeys(
393
347
  ...(isMarkdown ? [{ keys: "r", label: "Toggle rendered and raw" }] : []),
394
- ...(wrappable ? [{ keys: "u", label: "Toggle line wrapping" }] : []),
348
+ ...(numbered ? [{ keys: "u", label: "Toggle line wrapping" }] : []),
349
+ ...(numbered ? [{ keys: "y", label: "Copy a reference to the selected lines" }] : []),
395
350
  );
396
351
 
397
352
  let content: string;
@@ -403,7 +358,7 @@ export function renderFilePage(page: FilePage): string {
403
358
  });
404
359
  content = `<article class="bf-md">${html}</article>`;
405
360
  } else {
406
- content = `<pre class="bf-code bf-code--numbered"><code>${renderCodeLines(page.file.lines, language)}</code></pre>`;
361
+ content = `<pre class="bf-code bf-code--numbered" data-path="${escapeHtml(page.rel)}"><code>${renderCodeLines(page.file.lines, language)}</code></pre>`;
407
362
  }
408
363
  if (page.file.truncated)
409
364
  note = `<p class="bf-note">Showing the first ${page.file.lines.length} of ${page.file.totalLines} lines.</p>`;
@@ -447,87 +402,6 @@ function statusWord(letter: string): string {
447
402
  return STATUS_LABEL[letter] ?? "changed";
448
403
  }
449
404
 
450
- /**
451
- * The inner HTML per diff line. Hunk bodies are highlighted by rebuilding the
452
- * old side (context + deleted) and the new side (context + added), so the
453
- * tokenizer carries state within a hunk; meta, hunk, and note lines stay plain.
454
- * State does not cross hunks, since the unchanged lines between them are absent.
455
- */
456
- function highlightDiffText(lines: DiffLine[], language?: HighlightLanguage): string[] {
457
- if (!language) return lines.map((line) => escapeHtml(line.text));
458
- const out: string[] = new Array(lines.length);
459
- let index = 0;
460
- while (index < lines.length) {
461
- const kind = lines[index].kind;
462
- if (kind !== "ctx" && kind !== "add" && kind !== "del") {
463
- out[index] = escapeHtml(lines[index].text);
464
- index++;
465
- continue;
466
- }
467
- const start = index;
468
- const oldDoc: string[] = [];
469
- const newDoc: string[] = [];
470
- while (index < lines.length) {
471
- const body = lines[index].kind;
472
- if (body !== "ctx" && body !== "add" && body !== "del") break;
473
- if (body === "ctx") {
474
- oldDoc.push(lines[index].text);
475
- newDoc.push(lines[index].text);
476
- } else if (body === "add") {
477
- newDoc.push(lines[index].text);
478
- } else {
479
- oldDoc.push(lines[index].text);
480
- }
481
- index++;
482
- }
483
- const oldHtml = highlightLines(oldDoc, language);
484
- const newHtml = highlightLines(newDoc, language);
485
- let oldIndex = 0;
486
- let newIndex = 0;
487
- for (let cursor = start; cursor < index; cursor++) {
488
- const body = lines[cursor].kind;
489
- if (body === "ctx") {
490
- out[cursor] = newHtml[newIndex];
491
- newIndex++;
492
- oldIndex++;
493
- } else if (body === "add") {
494
- out[cursor] = newHtml[newIndex];
495
- newIndex++;
496
- } else {
497
- out[cursor] = oldHtml[oldIndex];
498
- oldIndex++;
499
- }
500
- }
501
- }
502
- return out;
503
- }
504
-
505
- /** Render unified-diff lines with a two-column gutter and per-kind styling. */
506
- export function renderDiffLines(lines: DiffLine[], language?: HighlightLanguage): string {
507
- const html = highlightDiffText(lines, language);
508
- // Every hunk is a section so its sticky header is bounded by its own body;
509
- // the section also gives the keyboard a stable, non-sticky scroll target.
510
- const out: string[] = [];
511
- let hunks = 0;
512
- let open = false;
513
- lines.forEach((line, index) => {
514
- if (line.kind === "hunk") {
515
- if (open) out.push("</span>");
516
- hunks += 1;
517
- out.push(`<span class="bf-diff-hunk" id="hunk-${hunks}" data-hunk="${hunks}">`);
518
- open = true;
519
- }
520
- const sign = line.kind === "add" ? "+" : line.kind === "del" ? "-" : line.kind === "ctx" ? " " : "";
521
- const gutter = `<span class="browse-diff-no" aria-hidden="true">${line.oldNo ?? ""}</span><span class="browse-diff-no" aria-hidden="true">${line.newNo ?? ""}</span>`;
522
- const marker = `<span class="browse-diff-sign" aria-hidden="true">${sign}</span>`;
523
- out.push(
524
- `<span class="bf-code__line bf-diff-line bf-diff-line--${line.kind}">${gutter}${marker}${html[index]}</span>`,
525
- );
526
- });
527
- if (open) out.push("</span>");
528
- return out.join("");
529
- }
530
-
531
405
  /** The scope label for a mode. */
532
406
  function modeScope(mode: DiffMode): string {
533
407
  if (mode === "staged") return "staged vs HEAD";
@@ -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
 
@@ -9,6 +9,12 @@
9
9
  *
10
10
  * It does nothing at all unless the server rendered the connection chip, which
11
11
  * is what makes `live: false` and JS-off leave the page exactly as it was.
12
+ *
13
+ * The folder check is the other half: the stream is shared by every open page, so
14
+ * it cannot say what *this* page's rows show. A page that lists a folder carries
15
+ * the revision of what it drew, and asks the server for the folder's current one
16
+ * once per interval; a difference reveals a reload control instead of touching
17
+ * the document, because a reload is the only honest way to add a row.
12
18
  */
13
19
 
14
20
  export const LIVE_CLIENT_JS = ` // Live status: one event stream feeds the header chips, the tree and listing
@@ -19,9 +25,14 @@ export const LIVE_CLIENT_JS = ` // Live status: one event stream feeds the heade
19
25
  var liveLine = document.getElementById("live-status");
20
26
  var liveGit = document.querySelector(".browse-git");
21
27
  var liveChanges = document.getElementById("browse-changes");
22
- var liveInterval = Number(liveChip.getAttribute("data-interval")) || 3000;
28
+ var liveInterval = Number(liveChip.getAttribute("data-interval")) || 5000;
23
29
  var liveStale = 0;
24
30
  var liveTone = "";
31
+ var liveRev = "";
32
+ var liveDir = liveChip.getAttribute("data-dir");
33
+ var liveDirRev = liveChip.getAttribute("data-rev");
34
+ var liveReload = document.getElementById("reload-hint");
35
+ var liveDirTimer = 0;
25
36
 
26
37
  function formatSpan(ms) {
27
38
  var seconds = Math.max(0, Math.round(ms / 1000));
@@ -88,7 +99,12 @@ export const LIVE_CLIENT_JS = ` // Live status: one event stream feeds the heade
88
99
  });
89
100
  }
90
101
 
102
+ // One revision covers the chip and the markers: a frame that did not move the
103
+ // change set is not painted at all, so a tick on an idle tree costs no DOM
104
+ // work however many rows the page drew.
91
105
  function paintLiveChanges(changes) {
106
+ if (changes && changes.rev === liveRev) return;
107
+ liveRev = changes && changes.rev ? changes.rev : "";
92
108
  var count = changes ? changes.count : 0;
93
109
  if (liveChanges) {
94
110
  liveChanges.hidden = count <= 0;
@@ -136,6 +152,32 @@ export const LIVE_CLIENT_JS = ` // Live status: one event stream feeds the heade
136
152
  paintLiveChanges(snapshot.changes);
137
153
  }
138
154
 
155
+ // The folder the page drew, checked against the disk on the frame cadence. It
156
+ // waits for the first frame, never runs in a hidden tab, and stops as soon as
157
+ // it has something to say: the control is the fix, and a reload ends the
158
+ // question.
159
+ function showLiveReload() {
160
+ window.clearInterval(liveDirTimer);
161
+ liveDirTimer = 0;
162
+ if (!liveReload || !liveReload.hidden) return;
163
+ liveReload.hidden = false;
164
+ liveReload.addEventListener("click", function () { window.location.reload(); });
165
+ }
166
+
167
+ function checkLiveDir() {
168
+ if (document.hidden || liveTone !== "success") return;
169
+ fetch("/api/dir-rev?path=" + encodeURIComponent(liveDir))
170
+ .then(function (response) {
171
+ if (!response.ok) throw new Error("check failed");
172
+ return response.json();
173
+ })
174
+ .then(function (answer) {
175
+ var rev = answer.rev;
176
+ if (typeof rev === "string" && rev !== liveDirRev) showLiveReload();
177
+ })
178
+ .catch(function () { /* the stream already reports a dead server */ });
179
+ }
180
+
139
181
  if (window.EventSource) {
140
182
  var liveStream = new EventSource("/events");
141
183
  liveStream.addEventListener("open", function () {
@@ -152,7 +194,11 @@ export const LIVE_CLIENT_JS = ` // Live status: one event stream feeds the heade
152
194
  window.addEventListener("pagehide", function () {
153
195
  liveStream.close();
154
196
  window.clearTimeout(liveStale);
197
+ window.clearInterval(liveDirTimer);
155
198
  });
199
+ if (liveDir !== null && liveDirRev && liveReload) {
200
+ liveDirTimer = window.setInterval(checkLiveDir, liveInterval);
201
+ }
156
202
  }
157
203
  }
158
204
  `;
@@ -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}
@@ -47,6 +46,8 @@ body.palette-open{overflow:hidden}
47
46
  swap only changes the colour, and the footer line reads as one muted clause. */
48
47
  .browse-live{font-family:var(--font-mono);border:1px solid currentColor;border-radius:var(--radius-pill);padding:2px 10px}
49
48
  .browse-live__glyph{font-size:9px;line-height:1}
49
+ .browse-reload{border:1px solid currentColor;border-radius:var(--radius-pill);padding:2px 10px;cursor:pointer;font-family:var(--font-mono)}
50
+ .browse-reload:hover{text-decoration:underline}
50
51
  .browse-live-line{color:var(--muted);font-variant-numeric:tabular-nums}
51
52
  .browse-live-line::after{content:" · "}
52
53
  .browse-diff-totals{font-family:var(--font-mono)}
@@ -91,8 +92,24 @@ body.palette-open{overflow:hidden}
91
92
  .browse-measure .bf-md>*{max-width:var(--browse-measure);margin-inline:auto}
92
93
  /* Tables start at the prose column and take only the gutter to its right, so a
93
94
  wide window widens them without a dead band on their left. Code stays inside
94
- the measure with the rest of the prose. */
95
- .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
+ }
96
113
  /* A table wider than the pane scrolls, but an overlay scrollbar is invisible
97
114
  until it moves; a gradient on the frame marks the columns still out of view.
98
115
  It has to be the frame: the scroller clips its own content, so a pseudo-element
@@ -114,10 +131,19 @@ body.palette-open{overflow:hidden}
114
131
  .browse-side .bf-tree{flex:1 1 auto;min-height:0;overflow:auto}
115
132
  }
116
133
  @media (max-width:820px){
117
- .browse-root{order:9;flex:1 1 100%;direction:ltr;text-align:left}
134
+ .browse-root{order:9;flex:1 1 100%}
118
135
  }
119
136
  @media (max-width:520px){
120
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}
121
147
  }
122
148
  /* A printed page scrolls nothing: the card and its body open up, the pinned hunk
123
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;
@@ -19,7 +19,8 @@
19
19
 
20
20
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
21
21
  import { isPursuedGoal } from "../../lib/goal-state.ts";
22
- import { PLAN_CONSENT, requestPlanMode } from "../../lib/plan-entry.ts";
22
+ import { isRemoteTurn, remoteLanguage } from "../../lib/interaction.ts";
23
+ import { PLAN_CONSENT, PLAN_CONSENT_SHORT, PLAN_CONSENT_SHORT_ZH, requestPlanMode } from "../../lib/plan-entry.ts";
23
24
  import { isPlanModeEnabled } from "../../lib/plan-state.ts";
24
25
  import { PLAN_ENTER_TOOL } from "../../lib/tool-names.ts";
25
26
  import type { Goal, GoalStatus } from "./types.ts";
@@ -45,7 +46,12 @@ export async function goalPhase(
45
46
  if (!options.ask || !ctx.hasUI) return "active";
46
47
  // Do not ask about a phase this session cannot enter.
47
48
  if (!pi.getAllTools().some((tool) => tool.name === PLAN_ENTER_TOOL)) return "active";
48
- const approved = await ctx.ui.confirm(PLAN_ASK, PLAN_CONSENT);
49
+ const consent = isRemoteTurn(ctx)
50
+ ? remoteLanguage(ctx) === "zh"
51
+ ? PLAN_CONSENT_SHORT_ZH
52
+ : PLAN_CONSENT_SHORT
53
+ : PLAN_CONSENT;
54
+ const approved = await ctx.ui.confirm(PLAN_ASK, consent);
49
55
  if (!approved) return "active";
50
56
  return (await requestPlanMode(pi, ctx)) ? "planning" : "active";
51
57
  }