@gavin-hu/my-pi-agent 0.16.0 → 0.18.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 (200) hide show
  1. package/README.md +29 -15
  2. package/README.zh-CN.md +27 -15
  3. package/extensions/bytefly/README.md +195 -115
  4. package/extensions/bytefly/footer/collect/entries.ts +1 -6
  5. package/extensions/bytefly/footer/collect/snapshot.ts +23 -9
  6. package/extensions/bytefly/footer/price/price.ts +12 -8
  7. package/extensions/bytefly/footer/register.ts +13 -1
  8. package/extensions/bytefly/footer/types.ts +11 -0
  9. package/extensions/bytefly/footer/unpriced.ts +36 -0
  10. package/extensions/bytefly/header/component.ts +15 -29
  11. package/extensions/bytefly/header/identity.ts +71 -0
  12. package/extensions/bytefly/header/register.ts +9 -3
  13. package/extensions/bytefly/index.ts +13 -5
  14. package/extensions/bytefly/message/format.ts +20 -13
  15. package/extensions/bytefly/message/register.ts +8 -7
  16. package/extensions/bytefly/notify/types.ts +1 -1
  17. package/extensions/bytefly/panel/commands.ts +46 -25
  18. package/extensions/bytefly/panel/format.ts +49 -14
  19. package/extensions/bytefly/panel/join.ts +35 -9
  20. package/extensions/bytefly/panel/settings.ts +29 -39
  21. package/extensions/bytefly/panel/skills.ts +51 -0
  22. package/extensions/bytefly/panel/tui/extensions-view.ts +7 -61
  23. package/extensions/bytefly/panel/tui/frame.ts +9 -1
  24. package/extensions/bytefly/panel/tui/list-view.ts +72 -0
  25. package/extensions/bytefly/panel/tui/nav.ts +71 -3
  26. package/extensions/bytefly/panel/tui/settings-section.ts +250 -0
  27. package/extensions/bytefly/panel/tui/skills-view.ts +50 -0
  28. package/extensions/bytefly/panel/tui.ts +223 -331
  29. package/extensions/bytefly/panel/types.ts +26 -8
  30. package/extensions/bytefly/picker/discovery.ts +32 -27
  31. package/extensions/bytefly/picker/resources.ts +16 -8
  32. package/extensions/bytefly/picker/runtime.ts +62 -34
  33. package/extensions/bytefly/picker/toggle.ts +16 -10
  34. package/extensions/bytefly/picker/types.ts +31 -13
  35. package/extensions/bytefly/update/format.ts +66 -8
  36. package/extensions/bytefly/update/patch.ts +66 -0
  37. package/extensions/bytefly/update/register.ts +4 -11
  38. package/extensions/bytefly/update/release.ts +65 -0
  39. package/extensions/bytefly/update/state.ts +14 -0
  40. package/extensions/bytefly/update/suppress.ts +47 -29
  41. package/extensions/bytefly/update/types.ts +15 -5
  42. package/extensions/check/tui.ts +2 -0
  43. package/extensions/context/README.md +21 -11
  44. package/extensions/context/format.ts +10 -8
  45. package/extensions/context/report.ts +21 -8
  46. package/extensions/context/tui.ts +26 -10
  47. package/extensions/context/types.ts +5 -3
  48. package/extensions/file-browse/README.md +89 -22
  49. package/extensions/file-browse/chrome.ts +83 -0
  50. package/extensions/file-browse/client.ts +74 -459
  51. package/extensions/file-browse/config.ts +12 -3
  52. package/extensions/file-browse/diff-html.ts +94 -0
  53. package/extensions/file-browse/html.ts +26 -152
  54. package/extensions/file-browse/live-client.ts +47 -1
  55. package/extensions/file-browse/live.ts +35 -14
  56. package/extensions/file-browse/palette.ts +224 -0
  57. package/extensions/file-browse/rev.ts +42 -0
  58. package/extensions/file-browse/router.ts +27 -6
  59. package/extensions/file-browse/server.ts +7 -0
  60. package/extensions/file-browse/styles.ts +10 -3
  61. package/extensions/file-browse/tree.ts +381 -0
  62. package/extensions/file-read/README.md +4 -1
  63. package/extensions/file-read/extract/odt.ts +7 -5
  64. package/extensions/goal/plan-route.ts +8 -2
  65. package/extensions/goal/tools.ts +6 -11
  66. package/extensions/job/README.md +7 -5
  67. package/extensions/job/index.ts +35 -52
  68. package/extensions/job/tools.ts +51 -77
  69. package/extensions/memory/README.md +3 -3
  70. package/extensions/memory/tools.ts +4 -6
  71. package/extensions/memory/tui.ts +5 -1
  72. package/extensions/plan/README.md +333 -84
  73. package/extensions/plan/commands.ts +73 -7
  74. package/extensions/plan/feedback.ts +300 -63
  75. package/extensions/plan/index.ts +1 -1
  76. package/extensions/plan/plan-index.ts +4 -2
  77. package/extensions/plan/plan-text.ts +18 -1
  78. package/extensions/plan/plans.ts +184 -58
  79. package/extensions/plan/review-prompt.ts +93 -0
  80. package/extensions/plan/review.ts +95 -95
  81. package/extensions/plan/runtime.ts +4 -2
  82. package/extensions/plan/split.ts +87 -0
  83. package/extensions/plan/steps.ts +123 -107
  84. package/extensions/plan/tools/cancel.ts +79 -0
  85. package/extensions/plan/tools/enter.ts +17 -12
  86. package/extensions/plan/tools/exit.ts +148 -59
  87. package/extensions/plan/tools/steps.ts +7 -19
  88. package/extensions/plan/tools/write.ts +29 -31
  89. package/extensions/plan/tools.ts +6 -3
  90. package/extensions/plan/tui.ts +60 -15
  91. package/extensions/plan/types.ts +45 -3
  92. package/extensions/plan/web-chrome.ts +102 -0
  93. package/extensions/plan/web-client.ts +177 -446
  94. package/extensions/plan/web-collab.ts +207 -0
  95. package/extensions/plan/web-comments.ts +269 -0
  96. package/extensions/plan/web-markdown.ts +222 -0
  97. package/extensions/plan/web-page.ts +131 -19
  98. package/extensions/plan/web-router.ts +148 -32
  99. package/extensions/plan/web-share-session.ts +197 -0
  100. package/extensions/plan/web-share.ts +169 -0
  101. package/extensions/plan/web-streams.ts +124 -0
  102. package/extensions/plan/web-tokens.ts +63 -0
  103. package/extensions/plan/web.ts +193 -45
  104. package/extensions/question/README.md +3 -0
  105. package/extensions/question/dialogs.ts +91 -1
  106. package/extensions/question/tools.ts +2 -1
  107. package/extensions/rewind/checklist.ts +4 -0
  108. package/extensions/rewind/review-tui.ts +3 -1
  109. package/extensions/rewind/rewind-tui.ts +4 -0
  110. package/extensions/session-bus/README.md +247 -0
  111. package/extensions/session-bus/commands.ts +165 -0
  112. package/extensions/session-bus/format.ts +124 -0
  113. package/extensions/session-bus/index.ts +69 -0
  114. package/extensions/session-bus/notice.ts +58 -0
  115. package/extensions/session-bus/protocol.ts +134 -0
  116. package/extensions/session-bus/result.ts +243 -0
  117. package/extensions/session-bus/rows.ts +169 -0
  118. package/extensions/session-bus/runtime.ts +575 -0
  119. package/extensions/session-bus/store.ts +329 -0
  120. package/extensions/session-bus/team.ts +102 -0
  121. package/extensions/session-bus/tools.ts +227 -0
  122. package/extensions/session-bus/tui.ts +394 -0
  123. package/extensions/session-bus/types.ts +98 -0
  124. package/extensions/subagent/README.md +26 -13
  125. package/extensions/subagent/format.ts +72 -25
  126. package/extensions/subagent/index.ts +21 -47
  127. package/extensions/subagent/orchestrate.ts +18 -41
  128. package/extensions/subagent/render.ts +29 -42
  129. package/extensions/subagent/stream.ts +24 -18
  130. package/extensions/subagent/types.ts +7 -0
  131. package/extensions/todo/tools.ts +5 -6
  132. package/extensions/todo/tui.ts +5 -1
  133. package/extensions/watch/README.md +9 -5
  134. package/extensions/watch/index.ts +25 -44
  135. package/extensions/watch/tools.ts +31 -59
  136. package/extensions/wechat/README.md +45 -36
  137. package/extensions/wechat/bridge.ts +46 -48
  138. package/extensions/wechat/client.ts +1 -4
  139. package/extensions/wechat/commands.ts +3 -3
  140. package/extensions/wechat/deliver.ts +57 -37
  141. package/extensions/wechat/interaction.ts +54 -18
  142. package/extensions/wechat/login-screen.ts +2 -3
  143. package/extensions/wechat/notices.ts +9 -7
  144. package/extensions/wechat/peer-worker.ts +19 -34
  145. package/extensions/wechat/presence.ts +3 -4
  146. package/extensions/wechat/prompt.ts +53 -23
  147. package/extensions/wechat/reply-pipeline.ts +12 -4
  148. package/extensions/wechat/slash.ts +14 -0
  149. package/extensions/wechat/status-screen.ts +2 -2
  150. package/extensions/wechat/tools.ts +3 -11
  151. package/extensions/worktree/README.md +48 -12
  152. package/extensions/worktree/commands.ts +109 -65
  153. package/extensions/worktree/enter.ts +1 -1
  154. package/extensions/worktree/exit.ts +11 -1
  155. package/extensions/worktree/git.ts +5 -0
  156. package/extensions/worktree/lifecycle.ts +9 -1
  157. package/extensions/worktree/registry.ts +11 -5
  158. package/extensions/worktree/screen.ts +263 -0
  159. package/extensions/worktree/status.ts +84 -40
  160. package/lib/README.md +12 -7
  161. package/lib/claims/overlap.ts +43 -0
  162. package/lib/config.ts +12 -0
  163. package/lib/format.ts +6 -2
  164. package/lib/http/router.ts +40 -13
  165. package/lib/http/server.ts +27 -13
  166. package/lib/interaction.ts +28 -1
  167. package/lib/notice.ts +29 -2
  168. package/lib/plan-entry.ts +7 -2
  169. package/lib/plans/sections.ts +172 -0
  170. package/lib/plans/setting.ts +52 -0
  171. package/lib/plans/split.ts +215 -0
  172. package/lib/plans/store.ts +19 -0
  173. package/lib/plans/team.ts +151 -0
  174. package/lib/qr.ts +64 -0
  175. package/lib/repo-key.ts +74 -0
  176. package/lib/report.ts +117 -25
  177. package/lib/requirements.ts +1 -2
  178. package/lib/tool-result.ts +54 -4
  179. package/lib/tui/README.md +30 -8
  180. package/lib/tui/chrome.ts +27 -6
  181. package/lib/tui/dock-screen.ts +91 -19
  182. package/lib/tui/gauge.ts +9 -5
  183. package/lib/tui/line-pane.ts +15 -4
  184. package/lib/tui/tool.ts +23 -4
  185. package/lib/usage.ts +26 -0
  186. package/lib/web/README.md +131 -0
  187. package/lib/web/base.ts +19 -2
  188. package/lib/web/components.ts +10 -6
  189. package/lib/web/layout.ts +2 -0
  190. package/lib/web/markup.ts +2 -1
  191. package/lib/web/runtime.ts +103 -25
  192. package/lib/web/tokens.ts +23 -7
  193. package/package.json +5 -2
  194. package/skills/changelog/SKILL.md +57 -45
  195. package/skills/init/SKILL.md +80 -45
  196. package/skills/init/references/agents-md-quality.md +26 -2
  197. package/skills/init/references/interview-questions.md +36 -14
  198. package/skills/next/SKILL.md +209 -0
  199. package/skills/parallel-dev/SKILL.md +150 -0
  200. package/extensions/wechat/qrcode.ts +0 -46
@@ -0,0 +1,224 @@
1
+ /**
2
+ * The quick-open palette: a fuzzy path search over `/api/files`.
3
+ *
4
+ * It is a fragment of the same IIFE `client.ts` ships (`PALETTE_CLIENT_JS` is
5
+ * interpolated into `CLIENT_JS`). The page's keymap opens it through
6
+ * `window.bfPalette`, the same contract shape as `window.bfKeys`.
7
+ */
8
+
9
+ export const PALETTE_CLIENT_JS = ` // Quick open: a fuzzy path palette backed by /api/files.
10
+ var quickOpenButton = document.getElementById("quick-open");
11
+ var paletteBackdrop = null;
12
+ var paletteInput = null;
13
+ var paletteList = null;
14
+ var paletteStatus = null;
15
+ var paletteResults = [];
16
+ var paletteActive = -1;
17
+ var paletteTimer = null;
18
+
19
+ function paletteOpen() {
20
+ return paletteBackdrop !== null && paletteBackdrop.hidden === false;
21
+ }
22
+
23
+ function encodeSegments(path) {
24
+ return path.split("/").map(encodeURIComponent).join("/");
25
+ }
26
+
27
+ function closestRole(node, role) {
28
+ while (node && node !== paletteList) {
29
+ if (node.getAttribute && node.getAttribute("role") === role) return node;
30
+ node = node.parentNode;
31
+ }
32
+ return null;
33
+ }
34
+
35
+ function buildPalette() {
36
+ if (paletteBackdrop) return;
37
+ paletteBackdrop = document.createElement("div");
38
+ paletteBackdrop.className = "bf-dialog";
39
+ paletteBackdrop.hidden = true;
40
+ var dialog = document.createElement("div");
41
+ dialog.className = "bf-dialog__panel";
42
+ dialog.setAttribute("role", "dialog");
43
+ dialog.setAttribute("aria-modal", "true");
44
+ dialog.setAttribute("aria-label", "Quick open a file");
45
+ paletteInput = document.createElement("input");
46
+ paletteInput.type = "text";
47
+ paletteInput.className = "bf-dialog__input";
48
+ paletteInput.id = "palette-input";
49
+ paletteInput.setAttribute("role", "combobox");
50
+ paletteInput.setAttribute("aria-expanded", "true");
51
+ paletteInput.setAttribute("aria-controls", "palette-list");
52
+ paletteInput.setAttribute("aria-autocomplete", "list");
53
+ paletteInput.setAttribute("autocomplete", "off");
54
+ paletteInput.setAttribute("placeholder", "Type a file path…");
55
+ paletteInput.setAttribute("aria-label", "Quick open a file");
56
+ paletteList = document.createElement("ul");
57
+ paletteList.className = "bf-dialog__list";
58
+ paletteList.id = "palette-list";
59
+ paletteList.setAttribute("role", "listbox");
60
+ paletteStatus = document.createElement("p");
61
+ paletteStatus.className = "bf-dialog__status";
62
+ paletteStatus.setAttribute("role", "status");
63
+ dialog.appendChild(paletteInput);
64
+ dialog.appendChild(paletteList);
65
+ dialog.appendChild(paletteStatus);
66
+ paletteBackdrop.appendChild(dialog);
67
+ document.body.appendChild(paletteBackdrop);
68
+
69
+ paletteBackdrop.addEventListener("mousedown", function (event) {
70
+ if (event.target === paletteBackdrop) closePalette();
71
+ });
72
+ paletteInput.addEventListener("input", function () {
73
+ if (paletteTimer) window.clearTimeout(paletteTimer);
74
+ paletteTimer = window.setTimeout(runSearch, 80);
75
+ });
76
+ paletteInput.addEventListener("keydown", function (event) {
77
+ if (event.key === "Tab") {
78
+ event.preventDefault();
79
+ } else if (event.key === "ArrowDown" || (event.ctrlKey && event.key === "n")) {
80
+ event.preventDefault();
81
+ moveActive(1);
82
+ } else if (event.key === "ArrowUp" || (event.ctrlKey && event.key === "p")) {
83
+ event.preventDefault();
84
+ moveActive(-1);
85
+ } else if (event.key === "Enter") {
86
+ event.preventDefault();
87
+ openActive();
88
+ } else if (event.key === "Escape") {
89
+ event.preventDefault();
90
+ closePalette();
91
+ }
92
+ });
93
+ paletteList.addEventListener("mousemove", function (event) {
94
+ var option = closestRole(event.target, "option");
95
+ if (option) setActive(Number(option.getAttribute("data-index")));
96
+ });
97
+ paletteList.addEventListener("click", function (event) {
98
+ var option = closestRole(event.target, "option");
99
+ if (option) {
100
+ setActive(Number(option.getAttribute("data-index")));
101
+ openActive();
102
+ }
103
+ });
104
+ }
105
+
106
+ function openPalette() {
107
+ buildPalette();
108
+ paletteBackdrop.hidden = false;
109
+ document.body.classList.add("palette-open");
110
+ paletteInput.value = "";
111
+ renderResults([], "Searching…");
112
+ paletteInput.focus();
113
+ runSearch();
114
+ }
115
+
116
+ function closePalette() {
117
+ if (!paletteBackdrop) return;
118
+ paletteBackdrop.hidden = true;
119
+ document.body.classList.remove("palette-open");
120
+ if (quickOpenButton) quickOpenButton.focus();
121
+ }
122
+
123
+ function runSearch() {
124
+ var query = paletteInput.value.trim();
125
+ fetch("/api/files?q=" + encodeURIComponent(query))
126
+ .then(function (response) {
127
+ if (!response.ok) throw new Error("search failed");
128
+ return response.json();
129
+ })
130
+ .then(function (data) {
131
+ if (paletteInput.value.trim() !== query) return;
132
+ if (query === "") {
133
+ renderResults([], data.total + " files indexed" + (data.truncated ? " (truncated)" : ""));
134
+ } else {
135
+ renderResults(data.results || [], null);
136
+ }
137
+ })
138
+ .catch(function () {
139
+ renderResults([], "Search failed");
140
+ });
141
+ }
142
+
143
+ function appendHighlighted(target, path, positions) {
144
+ var marks = {};
145
+ (positions || []).forEach(function (index) { marks[index] = true; });
146
+ var buffer = "";
147
+ var marked = false;
148
+ function flush() {
149
+ if (buffer === "") return;
150
+ if (marked) {
151
+ var mark = document.createElement("mark");
152
+ mark.textContent = buffer;
153
+ target.appendChild(mark);
154
+ } else {
155
+ target.appendChild(document.createTextNode(buffer));
156
+ }
157
+ buffer = "";
158
+ }
159
+ for (var i = 0; i < path.length; i++) {
160
+ var isMatch = marks[i] === true;
161
+ if (isMatch !== marked) { flush(); marked = isMatch; }
162
+ buffer += path.charAt(i);
163
+ }
164
+ flush();
165
+ }
166
+
167
+ function renderResults(results, hint) {
168
+ paletteResults = results;
169
+ paletteActive = results.length > 0 ? 0 : -1;
170
+ paletteList.textContent = "";
171
+ results.forEach(function (result, index) {
172
+ var option = document.createElement("li");
173
+ option.className = "bf-dialog__option";
174
+ option.id = "palette-option-" + index;
175
+ option.setAttribute("role", "option");
176
+ option.setAttribute("data-index", String(index));
177
+ appendHighlighted(option, result.path, result.positions);
178
+ paletteList.appendChild(option);
179
+ });
180
+ if (hint) paletteStatus.textContent = hint;
181
+ else if (results.length === 0) paletteStatus.textContent = "No matches";
182
+ else paletteStatus.textContent = results.length + (results.length === 1 ? " match" : " matches");
183
+ updateActive();
184
+ }
185
+
186
+ function updateActive() {
187
+ Array.prototype.forEach.call(paletteList.children, function (option, index) {
188
+ option.setAttribute("aria-selected", index === paletteActive ? "true" : "false");
189
+ });
190
+ var active = paletteList.children[paletteActive];
191
+ if (paletteActive >= 0 && active) {
192
+ paletteInput.setAttribute("aria-activedescendant", "palette-option-" + paletteActive);
193
+ if (active.scrollIntoView) active.scrollIntoView({ block: "nearest" });
194
+ } else {
195
+ paletteInput.removeAttribute("aria-activedescendant");
196
+ }
197
+ }
198
+
199
+ function setActive(index) {
200
+ if (index < 0 || index >= paletteResults.length) return;
201
+ paletteActive = index;
202
+ updateActive();
203
+ }
204
+
205
+ function moveActive(delta) {
206
+ if (paletteResults.length === 0) return;
207
+ var next = paletteActive + delta;
208
+ if (next < 0) next = paletteResults.length - 1;
209
+ if (next >= paletteResults.length) next = 0;
210
+ setActive(next);
211
+ }
212
+
213
+ function openActive() {
214
+ var result = paletteResults[paletteActive];
215
+ if (result) window.location.assign("/view/" + encodeSegments(result.path));
216
+ }
217
+ var quickOpenButton = document.getElementById("quick-open");
218
+ if (quickOpenButton) quickOpenButton.addEventListener("click", openPalette);
219
+
220
+ window.bfPalette = {
221
+ open: openPalette,
222
+ close: closePalette,
223
+ isOpen: paletteOpen
224
+ };`;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Revisions for the browse live status.
3
+ *
4
+ * A revision is a short digest of something a page already drew — the change set
5
+ * a frame carries, or the names a listing holds — so a client can tell "nothing
6
+ * moved" from "paint again" without comparing the payload it was sent. A
7
+ * collision costs one skipped repaint and is corrected by the next frame or a
8
+ * reload, which is the trade a 12-character digest makes for a small frame.
9
+ */
10
+
11
+ import { createHash } from "node:crypto";
12
+
13
+ /**
14
+ * A digest of the parts, cut to 12 hex characters. The parts are joined with NUL,
15
+ * which no filename can contain, so two different lists cannot run together into
16
+ * the same text.
17
+ */
18
+ function digest(parts: readonly string[]): string {
19
+ return createHash("sha1").update(parts.join("\u0000")).digest("hex").slice(0, 12);
20
+ }
21
+
22
+ /** The change set a frame carries: its totals and paths, in a stable order. */
23
+ export interface RevisionInput {
24
+ count: number;
25
+ insertions: number;
26
+ deletions: number;
27
+ files: readonly string[];
28
+ dirs: readonly string[];
29
+ }
30
+
31
+ /**
32
+ * The revision of a change set. The totals are part of it, so an edit that moves
33
+ * no path still moves the revision and the chip's `+A −D` stays honest.
34
+ */
35
+ export function changeRevision(changes: RevisionInput): string {
36
+ return digest([`${changes.count}:${changes.insertions}:${changes.deletions}`, ...changes.files, ...changes.dirs]);
37
+ }
38
+
39
+ /** The revision of a listing's names, independent of the order they arrived in. */
40
+ export function listingRevision(names: readonly string[]): string {
41
+ return digest([...names].sort());
42
+ }
@@ -20,8 +20,9 @@ import {
20
20
  scriptHeaders,
21
21
  type ServeResponse,
22
22
  } from "../../lib/http/index.ts";
23
- import { THEME_JS } from "../../lib/web/index.ts";
23
+ import { THEME_JS, THEME_JS_PATH } from "../../lib/web/index.ts";
24
24
  import { CLIENT_JS } from "./client.ts";
25
+ import type { LivePage } from "./chrome.ts";
25
26
  import type { ServeConfig } from "./config.ts";
26
27
  import { isDiffMode, type DiffMode } from "./diff.ts";
27
28
  import type { DiffList, GitDiffProvider, GitStatus, GitStatusProvider } from "./git.ts";
@@ -43,6 +44,7 @@ import { fuzzySearch } from "./fuzzy.ts";
43
44
  import { renderDirectoryPage, renderDiffPage, renderErrorPage, renderFileDiffPage, renderFilePage } from "./html.ts";
44
45
  import type { ChangeSummary } from "./html.ts";
45
46
  import { encodePath, HttpError, relativePath, resolveDecodedPath, resolveRequestPath } from "./paths.ts";
47
+ import { listingRevision } from "./rev.ts";
46
48
 
47
49
  export interface ServeContext {
48
50
  root: string;
@@ -108,9 +110,13 @@ function summaryOf(list: DiffList): ChangeSummary {
108
110
  };
109
111
  }
110
112
 
111
- /** The live chrome a page renders, or `undefined` when live status is off. */
112
- function liveOf(ctx: ServeContext): { intervalMs: number } | undefined {
113
- return ctx.config.live && ctx.live ? { intervalMs: ctx.config.liveIntervalMs } : undefined;
113
+ /**
114
+ * The live chrome a page renders, or `undefined` when live status is off. A
115
+ * listing page also passes the revision of what it drew, which is what its
116
+ * reload control compares against; every other page has nothing to compare.
117
+ */
118
+ function liveOf(ctx: ServeContext, folder?: { dir: string; rev: string }): LivePage | undefined {
119
+ return ctx.config.live && ctx.live ? { intervalMs: ctx.config.liveIntervalMs, ...folder } : undefined;
114
120
  }
115
121
 
116
122
  /** The `head` change summary, for the header chip, tree markers, and file action. */
@@ -184,7 +190,10 @@ async function directoryResponse(ctx: ServeContext, rel: string): Promise<ServeR
184
190
  maxThumbBytes: ctx.config.maxThumbBytes,
185
191
  git,
186
192
  summary: changes,
187
- live: liveOf(ctx),
193
+ live: liveOf(
194
+ ctx,
195
+ listing.truncated ? undefined : { dir: rel, rev: listingRevision(listing.entries.map((entry) => entry.name)) },
196
+ ),
188
197
  }),
189
198
  );
190
199
  }
@@ -312,6 +321,17 @@ async function treeApi(ctx: ServeContext, decodedPath: string): Promise<ServeRes
312
321
  return jsonResponse(200, { path: rel, entries: listing.entries.map(treeApiEntry) });
313
322
  }
314
323
 
324
+ /** The revision of one folder's listing: what a page's reload control compares against. */
325
+ async function dirRevApi(ctx: ServeContext, decodedPath: string): Promise<ServeResponse> {
326
+ const abs = await resolveDecodedPath(ctx.root, decodedPath);
327
+ const rel = relativePath(ctx.root, abs);
328
+ const listing = await listDirectory(ctx.root, rel, ctx.config.maxDirEntries);
329
+ // A truncated listing is a prefix whose boundary moves, so its revision is not
330
+ // comparable with what the page drew: `null` says "cannot tell".
331
+ const names = listing.entries.map((entry) => entry.name);
332
+ return jsonResponse(200, { path: rel, rev: listing.truncated ? null : listingRevision(names) });
333
+ }
334
+
315
335
  /** Fuzzy file search for the quick-open palette, over the cached index. */
316
336
  async function filesApi(ctx: ServeContext, query: string): Promise<ServeResponse> {
317
337
  const index = ctx.index ? await ctx.index() : await buildFileIndex(ctx.root, ctx.config.maxIndexEntries);
@@ -352,9 +372,10 @@ export async function handleRequest(
352
372
 
353
373
  try {
354
374
  if (pathname === "/app.js") return { status: 200, headers: scriptHeaders(), body: CLIENT_JS };
355
- if (pathname === "/theme.js") return { status: 200, headers: scriptHeaders(), body: THEME_JS };
375
+ if (pathname === THEME_JS_PATH) return { status: 200, headers: scriptHeaders(), body: THEME_JS };
356
376
  if (pathname === "/favicon.ico") return { status: 204, headers: pageHeaders(), body: "" };
357
377
  if (pathname === "/api/tree") return await treeApi(ctx, search.get("path") ?? "");
378
+ if (pathname === "/api/dir-rev") return await dirRevApi(ctx, search.get("path") ?? "");
358
379
  if (pathname === "/api/files") return await filesApi(ctx, search.get("q") ?? "");
359
380
  if (pathname === "/events") return liveEvents(ctx);
360
381
  const rawMode = search.get("mode");
@@ -103,6 +103,13 @@ export async function createFileServer(options: FileServerOptions): Promise<File
103
103
  respond(request, response, result);
104
104
  return;
105
105
  }
106
+ // Checked before the head is written: a refusal must not leave a stream the
107
+ // client would wait on. `204` is the one status an event source does not
108
+ // reconnect after, so a page beyond the cap goes quiet instead of retrying.
109
+ if (live.atCapacity()) {
110
+ respond(request, response, { status: 204, headers: { "Cache-Control": "no-store" }, body: "" });
111
+ return;
112
+ }
106
113
  openEventStream(response, result);
107
114
  const detach = live.attach({
108
115
  write: (frame) => {
@@ -19,9 +19,6 @@ export const BROWSE_CSS = `${WEB_BASE_CSS}
19
19
  ${WEB_THEME_CSS}
20
20
  ${WEB_LAYOUT_CSS}
21
21
  ${WEB_COMPONENTS_CSS}
22
- ${WEB_THEME_CSS}
23
- ${WEB_LAYOUT_CSS}
24
- ${WEB_COMPONENTS_CSS}
25
22
  .browse-side{padding:var(--space-3)}
26
23
  .browse-root{flex:1 1 auto;direction:rtl;text-align:left}
27
24
  .browse-git{font-family:var(--font-mono)}
@@ -50,6 +47,8 @@ body.palette-open{overflow:hidden}
50
47
  swap only changes the colour, and the footer line reads as one muted clause. */
51
48
  .browse-live{font-family:var(--font-mono);border:1px solid currentColor;border-radius:var(--radius-pill);padding:2px 10px}
52
49
  .browse-live__glyph{font-size:9px;line-height:1}
50
+ .browse-reload{border:1px solid currentColor;border-radius:var(--radius-pill);padding:2px 10px;cursor:pointer;font-family:var(--font-mono)}
51
+ .browse-reload:hover{text-decoration:underline}
53
52
  .browse-live-line{color:var(--muted);font-variant-numeric:tabular-nums}
54
53
  .browse-live-line::after{content:" · "}
55
54
  .browse-diff-totals{font-family:var(--font-mono)}
@@ -121,4 +120,12 @@ body.palette-open{overflow:hidden}
121
120
  }
122
121
  @media (max-width:520px){
123
122
  .bf-toolbar .bf-theme-toggle{margin-left:auto}
123
+ }
124
+ /* A printed page scrolls nothing: the card and its body open up, the pinned hunk
125
+ header stops pinning, and the progress hairline belongs to the screen. */
126
+ @media print{
127
+ .bf-card{border:0}
128
+ .bf-card,.bf-app__main,.bf-code,.bf-md,.bf-diff,.browse-listing{height:auto!important;max-height:none!important;overflow:visible!important}
129
+ .bf-diff .bf-diff-line--hunk{position:static}
130
+ .browse-progress{display:none}
124
131
  }`;