@gavin-hu/my-pi-agent 0.16.0 → 0.17.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 (135) hide show
  1. package/README.md +14 -8
  2. package/README.zh-CN.md +14 -9
  3. package/extensions/bytefly/README.md +174 -101
  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/panel/commands.ts +46 -25
  17. package/extensions/bytefly/panel/format.ts +49 -14
  18. package/extensions/bytefly/panel/join.ts +35 -9
  19. package/extensions/bytefly/panel/settings.ts +10 -27
  20. package/extensions/bytefly/panel/skills.ts +51 -0
  21. package/extensions/bytefly/panel/tui/extensions-view.ts +7 -61
  22. package/extensions/bytefly/panel/tui/frame.ts +9 -1
  23. package/extensions/bytefly/panel/tui/list-view.ts +72 -0
  24. package/extensions/bytefly/panel/tui/nav.ts +71 -3
  25. package/extensions/bytefly/panel/tui/settings-section.ts +250 -0
  26. package/extensions/bytefly/panel/tui/skills-view.ts +50 -0
  27. package/extensions/bytefly/panel/tui.ts +223 -331
  28. package/extensions/bytefly/panel/types.ts +26 -8
  29. package/extensions/bytefly/picker/discovery.ts +32 -27
  30. package/extensions/bytefly/picker/resources.ts +16 -8
  31. package/extensions/bytefly/picker/runtime.ts +62 -34
  32. package/extensions/bytefly/picker/toggle.ts +16 -10
  33. package/extensions/bytefly/picker/types.ts +31 -13
  34. package/extensions/bytefly/update/format.ts +66 -8
  35. package/extensions/bytefly/update/patch.ts +66 -0
  36. package/extensions/bytefly/update/register.ts +4 -11
  37. package/extensions/bytefly/update/release.ts +65 -0
  38. package/extensions/bytefly/update/state.ts +14 -0
  39. package/extensions/bytefly/update/suppress.ts +47 -29
  40. package/extensions/bytefly/update/types.ts +15 -5
  41. package/extensions/check/tui.ts +2 -0
  42. package/extensions/context/README.md +21 -11
  43. package/extensions/context/format.ts +10 -8
  44. package/extensions/context/report.ts +21 -8
  45. package/extensions/context/tui.ts +26 -10
  46. package/extensions/context/types.ts +5 -3
  47. package/extensions/file-browse/README.md +29 -1
  48. package/extensions/file-browse/client.ts +16 -463
  49. package/extensions/file-browse/html.ts +7 -5
  50. package/extensions/file-browse/palette.ts +224 -0
  51. package/extensions/file-browse/router.ts +2 -2
  52. package/extensions/file-browse/styles.ts +8 -3
  53. package/extensions/file-browse/tree.ts +381 -0
  54. package/extensions/file-read/README.md +4 -1
  55. package/extensions/file-read/extract/odt.ts +7 -5
  56. package/extensions/goal/tools.ts +6 -11
  57. package/extensions/job/index.ts +20 -45
  58. package/extensions/job/tools.ts +51 -77
  59. package/extensions/memory/tools.ts +4 -6
  60. package/extensions/memory/tui.ts +5 -1
  61. package/extensions/plan/README.md +88 -33
  62. package/extensions/plan/commands.ts +19 -4
  63. package/extensions/plan/config.ts +27 -0
  64. package/extensions/plan/plan-index.ts +3 -1
  65. package/extensions/plan/plans.ts +31 -13
  66. package/extensions/plan/review.ts +34 -14
  67. package/extensions/plan/runtime.ts +3 -1
  68. package/extensions/plan/steps.ts +43 -14
  69. package/extensions/plan/tools/cancel.ts +79 -0
  70. package/extensions/plan/tools/enter.ts +10 -10
  71. package/extensions/plan/tools/exit.ts +54 -43
  72. package/extensions/plan/tools/steps.ts +7 -19
  73. package/extensions/plan/tools/write.ts +22 -29
  74. package/extensions/plan/tools.ts +5 -2
  75. package/extensions/plan/tui.ts +9 -7
  76. package/extensions/plan/types.ts +8 -0
  77. package/extensions/plan/web-chrome.ts +101 -0
  78. package/extensions/plan/web-client.ts +98 -311
  79. package/extensions/plan/web-markdown.ts +222 -0
  80. package/extensions/plan/web-page.ts +28 -2
  81. package/extensions/plan/web-router.ts +2 -2
  82. package/extensions/rewind/checklist.ts +4 -0
  83. package/extensions/rewind/review-tui.ts +3 -1
  84. package/extensions/rewind/rewind-tui.ts +4 -0
  85. package/extensions/subagent/README.md +25 -12
  86. package/extensions/subagent/format.ts +72 -25
  87. package/extensions/subagent/index.ts +21 -47
  88. package/extensions/subagent/orchestrate.ts +18 -41
  89. package/extensions/subagent/render.ts +29 -42
  90. package/extensions/subagent/stream.ts +24 -18
  91. package/extensions/subagent/types.ts +7 -0
  92. package/extensions/todo/tools.ts +5 -6
  93. package/extensions/todo/tui.ts +5 -1
  94. package/extensions/watch/index.ts +10 -35
  95. package/extensions/watch/tools.ts +31 -59
  96. package/extensions/wechat/README.md +40 -18
  97. package/extensions/wechat/bridge.ts +38 -40
  98. package/extensions/wechat/client.ts +1 -4
  99. package/extensions/wechat/commands.ts +1 -1
  100. package/extensions/wechat/interaction.ts +25 -4
  101. package/extensions/wechat/login-screen.ts +2 -3
  102. package/extensions/wechat/notices.ts +7 -5
  103. package/extensions/wechat/prompt.ts +19 -4
  104. package/extensions/wechat/qrcode.ts +9 -9
  105. package/extensions/wechat/slash.ts +14 -0
  106. package/extensions/wechat/status-screen.ts +2 -2
  107. package/extensions/wechat/tools.ts +3 -11
  108. package/extensions/worktree/README.md +48 -12
  109. package/extensions/worktree/commands.ts +109 -65
  110. package/extensions/worktree/enter.ts +1 -1
  111. package/extensions/worktree/exit.ts +11 -1
  112. package/extensions/worktree/lifecycle.ts +9 -1
  113. package/extensions/worktree/screen.ts +263 -0
  114. package/extensions/worktree/status.ts +84 -38
  115. package/lib/README.md +7 -6
  116. package/lib/config.ts +12 -0
  117. package/lib/format.ts +6 -2
  118. package/lib/interaction.ts +18 -1
  119. package/lib/report.ts +75 -2
  120. package/lib/tool-result.ts +54 -4
  121. package/lib/tui/README.md +16 -8
  122. package/lib/tui/chrome.ts +27 -6
  123. package/lib/tui/dock-screen.ts +6 -2
  124. package/lib/tui/gauge.ts +9 -5
  125. package/lib/usage.ts +26 -0
  126. package/lib/web/README.md +131 -0
  127. package/lib/web/base.ts +19 -2
  128. package/lib/web/components.ts +10 -6
  129. package/lib/web/layout.ts +2 -0
  130. package/lib/web/markup.ts +2 -1
  131. package/lib/web/runtime.ts +103 -25
  132. package/lib/web/tokens.ts +23 -7
  133. package/package.json +3 -1
  134. package/skills/changelog/SKILL.md +57 -45
  135. package/skills/next/SKILL.md +184 -0
@@ -2,245 +2,25 @@
2
2
  * The first-party browser script, served as `/app.js`.
3
3
  *
4
4
  * It is intentionally tiny and dependency-free: it only progressively enhances
5
- * what the server already rendered — filtering the tree and listing by path,
6
- * lazily expanding folders through `/api/tree` (with loading and error
7
- * feedback), collapsing the sidebar on narrow viewports, and swapping a broken
8
- * thumbnail for its category icon. Navigation works without it.
5
+ * what the server already rendered. The sidebar (filter, fold, lazy tree,
6
+ * keyboard) is `tree.ts` and the quick-open palette is `palette.ts`; both are
7
+ * fragments of this same IIFE, interpolated below. What stays here is the shell:
8
+ * sorting, wrapping, copying, the diff's hunk navigation, the reading progress,
9
+ * and the page's keymap. Navigation works without any of it.
9
10
  *
10
11
  * Kept as a string constant so the extension is a single self-contained module
11
12
  * and `bun build --no-bundle` needs no separate asset.
12
13
  */
13
14
 
14
- import { keysDialogScript, SIDEBAR_COLLAPSE_MAX, THEME_TOGGLE_JS } from "../../lib/web/index.ts";
15
+ import { keysDialogScript, THEME_TOGGLE_JS } from "../../lib/web/index.ts";
15
16
  import { LIVE_CLIENT_JS } from "./live-client.ts";
17
+ import { PALETTE_CLIENT_JS } from "./palette.ts";
18
+ import { TREE_CLIENT_JS } from "./tree.ts";
16
19
 
17
20
  export const CLIENT_JS = `(function () {
18
21
  "use strict";
19
- var tree = document.querySelector("nav.bf-tree");
20
- var filter = document.getElementById("filter");
21
- var foldAllButton = document.getElementById("fold-all");
22
22
  var rows = Array.prototype.slice.call(document.querySelectorAll("table.bf-table tbody tr"));
23
- var query = "";
24
-
25
- function matches(text) {
26
- return !query || text.toLowerCase().indexOf(query) !== -1;
27
- }
28
-
29
- var SVG_NS = "http://www.w3.org/2000/svg";
30
-
31
- function svgIcon(entry) {
32
- var svg = document.createElementNS(SVG_NS, "svg");
33
- svg.setAttribute("class", "browse-cat " + (entry.colorClass || ""));
34
- svg.setAttribute("aria-hidden", "true");
35
- svg.setAttribute("title", entry.label || "");
36
- var use = document.createElementNS(SVG_NS, "use");
37
- use.setAttribute("href", entry.iconHref);
38
- svg.appendChild(use);
39
- return svg;
40
- }
41
-
42
- function buildNode(entry) {
43
- var li = document.createElement("li");
44
- var row = document.createElement("div");
45
- row.className = "bf-tree__row";
46
- row.setAttribute("data-path", entry.path || "");
47
- var twisty = document.createElement(entry.isDir ? "button" : "span");
48
- twisty.className = "bf-tree__tw";
49
- row.appendChild(twisty);
50
- row.appendChild(svgIcon(entry));
51
- if (entry.isDir) {
52
- li.dataset.open = "false";
53
- twisty.classList.add("bf-tree__tw--toggle");
54
- twisty.type = "button";
55
- twisty.textContent = "\\u25B8";
56
- twisty.setAttribute("aria-expanded", "false");
57
- twisty.setAttribute("aria-label", entry.name);
58
- var dir = document.createElement("a");
59
- dir.href = entry.href;
60
- dir.textContent = entry.name + "/";
61
- row.appendChild(dir);
62
- } else if (entry.broken) {
63
- twisty.className = "bf-tree__tw bf-tree__tw--spacer";
64
- twisty.setAttribute("aria-hidden", "true");
65
- var broken = document.createElement("span");
66
- broken.className = "browse-broken";
67
- broken.title = "broken symbolic link";
68
- broken.textContent = entry.name;
69
- row.appendChild(broken);
70
- var mark = document.createElement("span");
71
- mark.className = "browse-link-mark";
72
- mark.textContent = "\\u21A9";
73
- row.appendChild(mark);
74
- } else {
75
- twisty.className = "bf-tree__tw bf-tree__tw--spacer";
76
- twisty.setAttribute("aria-hidden", "true");
77
- var file = document.createElement("a");
78
- file.href = entry.href;
79
- file.textContent = entry.name;
80
- row.appendChild(file);
81
- }
82
- li.appendChild(row);
83
- return li;
84
- }
85
-
86
- function filterNode(li) {
87
- var row = li.querySelector(":scope > .bf-tree__row");
88
- var own = matches(row ? row.getAttribute("data-path") || row.textContent : "");
89
- var childMatch = false;
90
- Array.prototype.forEach.call(li.querySelectorAll(":scope > ul > li"), function (child) {
91
- if (filterNode(child)) childMatch = true;
92
- });
93
- var visible = own || childMatch;
94
- li.hidden = !visible;
95
- return visible;
96
- }
97
-
98
- function applyFilter() {
99
- var empty = document.getElementById("tree-empty");
100
- var anyTree = false;
101
- if (tree) {
102
- Array.prototype.forEach.call(tree.querySelectorAll(":scope > ul > li"), function (li) {
103
- if (filterNode(li)) anyTree = true;
104
- });
105
- }
106
- if (empty) empty.hidden = !tree || anyTree;
107
- rows.forEach(function (row) {
108
- var path = row.getAttribute("data-path") || row.getAttribute("data-name") || "";
109
- row.hidden = !matches(path);
110
- });
111
- // Lists are repainted too: while a query is active every list is shown, and
112
- // clearing the query brings the folds back.
113
- if (tree) Array.prototype.forEach.call(tree.querySelectorAll("li[data-open]"), paint);
114
- }
115
-
116
- if (filter) {
117
- filter.addEventListener("input", function () {
118
- query = filter.value.trim().toLowerCase();
119
- applyFilter();
120
- });
121
- }
122
-
123
- // Visibility is derived: the intent in data-open, opened up while a filter is
124
- // active so a fold cannot hide a match.
125
- function isOpen(li) {
126
- return query !== "" || li.dataset.open === "true";
127
- }
128
-
129
- function paint(li) {
130
- var row = li.querySelector(":scope > .bf-tree__row");
131
- var twisty = row ? row.querySelector(":scope > .bf-tree__tw") : null;
132
- if (!twisty || twisty.tagName !== "BUTTON") return;
133
- var open = isOpen(li);
134
- // A pending load owns the glyph until it lands or fails.
135
- if (!li.dataset.loading && !li.dataset.error) {
136
- twisty.textContent = open ? "\\u25BE" : "\\u25B8";
137
- twisty.setAttribute("aria-expanded", open ? "true" : "false");
138
- }
139
- var list = li.querySelector(":scope > ul");
140
- if (list) list.hidden = !open;
141
- }
142
-
143
- function setOpen(li, open) {
144
- li.dataset.open = open ? "true" : "false";
145
- paint(li);
146
- }
147
-
148
- // Every dir with rendered children: what the fold-all control acts on.
149
- function foldableDirs() {
150
- var dirs = [];
151
- if (!tree) return dirs;
152
- Array.prototype.forEach.call(tree.querySelectorAll("li"), function (li) {
153
- if (li.querySelector(":scope > ul")) dirs.push(li);
154
- });
155
- return dirs;
156
- }
157
-
158
- function syncFoldButton() {
159
- if (!foldAllButton) return;
160
- var dirs = foldableDirs();
161
- var open = 0;
162
- dirs.forEach(function (li) {
163
- if (li.dataset.open === "true") open++;
164
- });
165
- foldAllButton.hidden = dirs.length === 0;
166
- foldAllButton.textContent = open > 0 ? "fold all" : "expand all";
167
- }
168
-
169
- function setAll(open) {
170
- foldableDirs().forEach(function (li) {
171
- setOpen(li, open);
172
- });
173
- syncFoldButton();
174
- }
175
-
176
- function loadChildren(li, twisty, path) {
177
- var name = twisty.getAttribute("aria-label") || "";
178
- li.dataset.loading = "true";
179
- twisty.textContent = "\\u2026";
180
- twisty.setAttribute("aria-busy", "true");
181
- twisty.setAttribute("aria-label", "Loading " + name);
182
- fetch("/api/tree?path=" + encodeURIComponent(path))
183
- .then(function (response) {
184
- if (!response.ok) throw new Error("load failed");
185
- return response.json();
186
- })
187
- .then(function (data) {
188
- var built = document.createElement("ul");
189
- (data.entries || []).forEach(function (entry) {
190
- built.appendChild(buildNode(entry));
191
- });
192
- li.appendChild(built);
193
- delete li.dataset.loading;
194
- delete li.dataset.error;
195
- setOpen(li, true);
196
- twisty.setAttribute("aria-busy", "false");
197
- twisty.setAttribute("aria-label", name);
198
- applyFilter();
199
- })
200
- .catch(function () {
201
- delete li.dataset.loading;
202
- li.dataset.error = "true";
203
- li.dataset.open = "false";
204
- twisty.textContent = "\\u26A0";
205
- twisty.setAttribute("aria-busy", "false");
206
- twisty.setAttribute("aria-expanded", "false");
207
- twisty.setAttribute("aria-label", "Could not load " + name + ". Press to retry.");
208
- });
209
- }
210
-
211
- if (tree) {
212
- tree.addEventListener("click", function (event) {
213
- var twisty = event.target.closest(".bf-tree__tw");
214
- if (!twisty || twisty.tagName !== "BUTTON" || !tree.contains(twisty)) return;
215
- event.preventDefault();
216
- var li = twisty.closest("li");
217
- if (!li) return;
218
- var list = li.querySelector(":scope > ul");
219
- if (li.dataset.open === "true") {
220
- setOpen(li, false);
221
- return;
222
- }
223
- if (list) {
224
- setOpen(li, true);
225
- applyFilter();
226
- return;
227
- }
228
- var row = li.querySelector(":scope > .bf-tree__row");
229
- var path = row ? row.getAttribute("data-path") : null;
230
- if (!path) return;
231
- loadChildren(li, twisty, path);
232
- });
233
- }
234
-
235
- var panel = document.querySelector("details.bf-app__sidebar");
236
- if (panel && window.matchMedia) {
237
- var mq = window.matchMedia("(max-width: ${SIDEBAR_COLLAPSE_MAX}px)");
238
- var syncSide = function () { panel.open = !mq.matches; };
239
- syncSide();
240
- if (mq.addEventListener) mq.addEventListener("change", syncSide);
241
- else if (mq.addListener) mq.addListener(syncSide);
242
- }
243
-
23
+ ${TREE_CLIENT_JS}
244
24
  Array.prototype.forEach.call(document.querySelectorAll("img.bf-table__thumb"), function (img) {
245
25
  img.addEventListener("error", function () {
246
26
  img.hidden = true;
@@ -366,214 +146,7 @@ export const CLIENT_JS = `(function () {
366
146
  });
367
147
  });
368
148
 
369
- // Quick open: a fuzzy path palette backed by /api/files.
370
- var quickOpenButton = document.getElementById("quick-open");
371
- var paletteBackdrop = null;
372
- var paletteInput = null;
373
- var paletteList = null;
374
- var paletteStatus = null;
375
- var paletteResults = [];
376
- var paletteActive = -1;
377
- var paletteTimer = null;
378
-
379
- function paletteOpen() {
380
- return paletteBackdrop !== null && paletteBackdrop.hidden === false;
381
- }
382
-
383
- function encodeSegments(path) {
384
- return path.split("/").map(encodeURIComponent).join("/");
385
- }
386
-
387
- function closestRole(node, role) {
388
- while (node && node !== paletteList) {
389
- if (node.getAttribute && node.getAttribute("role") === role) return node;
390
- node = node.parentNode;
391
- }
392
- return null;
393
- }
394
-
395
- function buildPalette() {
396
- if (paletteBackdrop) return;
397
- paletteBackdrop = document.createElement("div");
398
- paletteBackdrop.className = "bf-dialog";
399
- paletteBackdrop.hidden = true;
400
- var dialog = document.createElement("div");
401
- dialog.className = "bf-dialog__panel";
402
- dialog.setAttribute("role", "dialog");
403
- dialog.setAttribute("aria-modal", "true");
404
- dialog.setAttribute("aria-label", "Quick open a file");
405
- paletteInput = document.createElement("input");
406
- paletteInput.type = "text";
407
- paletteInput.className = "bf-dialog__input";
408
- paletteInput.id = "palette-input";
409
- paletteInput.setAttribute("role", "combobox");
410
- paletteInput.setAttribute("aria-expanded", "true");
411
- paletteInput.setAttribute("aria-controls", "palette-list");
412
- paletteInput.setAttribute("aria-autocomplete", "list");
413
- paletteInput.setAttribute("autocomplete", "off");
414
- paletteInput.setAttribute("placeholder", "Type a file path…");
415
- paletteInput.setAttribute("aria-label", "Quick open a file");
416
- paletteList = document.createElement("ul");
417
- paletteList.className = "bf-dialog__list";
418
- paletteList.id = "palette-list";
419
- paletteList.setAttribute("role", "listbox");
420
- paletteStatus = document.createElement("p");
421
- paletteStatus.className = "bf-dialog__status";
422
- paletteStatus.setAttribute("role", "status");
423
- dialog.appendChild(paletteInput);
424
- dialog.appendChild(paletteList);
425
- dialog.appendChild(paletteStatus);
426
- paletteBackdrop.appendChild(dialog);
427
- document.body.appendChild(paletteBackdrop);
428
-
429
- paletteBackdrop.addEventListener("mousedown", function (event) {
430
- if (event.target === paletteBackdrop) closePalette();
431
- });
432
- paletteInput.addEventListener("input", function () {
433
- if (paletteTimer) window.clearTimeout(paletteTimer);
434
- paletteTimer = window.setTimeout(runSearch, 80);
435
- });
436
- paletteInput.addEventListener("keydown", function (event) {
437
- if (event.key === "Tab") {
438
- event.preventDefault();
439
- } else if (event.key === "ArrowDown" || (event.ctrlKey && event.key === "n")) {
440
- event.preventDefault();
441
- moveActive(1);
442
- } else if (event.key === "ArrowUp" || (event.ctrlKey && event.key === "p")) {
443
- event.preventDefault();
444
- moveActive(-1);
445
- } else if (event.key === "Enter") {
446
- event.preventDefault();
447
- openActive();
448
- } else if (event.key === "Escape") {
449
- event.preventDefault();
450
- closePalette();
451
- }
452
- });
453
- paletteList.addEventListener("mousemove", function (event) {
454
- var option = closestRole(event.target, "option");
455
- if (option) setActive(Number(option.getAttribute("data-index")));
456
- });
457
- paletteList.addEventListener("click", function (event) {
458
- var option = closestRole(event.target, "option");
459
- if (option) {
460
- setActive(Number(option.getAttribute("data-index")));
461
- openActive();
462
- }
463
- });
464
- }
465
-
466
- function openPalette() {
467
- buildPalette();
468
- paletteBackdrop.hidden = false;
469
- document.body.classList.add("palette-open");
470
- paletteInput.value = "";
471
- renderResults([], "Searching…");
472
- paletteInput.focus();
473
- runSearch();
474
- }
475
-
476
- function closePalette() {
477
- if (!paletteBackdrop) return;
478
- paletteBackdrop.hidden = true;
479
- document.body.classList.remove("palette-open");
480
- if (quickOpenButton) quickOpenButton.focus();
481
- }
482
-
483
- function runSearch() {
484
- var query = paletteInput.value.trim();
485
- fetch("/api/files?q=" + encodeURIComponent(query))
486
- .then(function (response) {
487
- if (!response.ok) throw new Error("search failed");
488
- return response.json();
489
- })
490
- .then(function (data) {
491
- if (paletteInput.value.trim() !== query) return;
492
- if (query === "") {
493
- renderResults([], data.total + " files indexed" + (data.truncated ? " (truncated)" : ""));
494
- } else {
495
- renderResults(data.results || [], null);
496
- }
497
- })
498
- .catch(function () {
499
- renderResults([], "Search failed");
500
- });
501
- }
502
-
503
- function appendHighlighted(target, path, positions) {
504
- var marks = {};
505
- (positions || []).forEach(function (index) { marks[index] = true; });
506
- var buffer = "";
507
- var marked = false;
508
- function flush() {
509
- if (buffer === "") return;
510
- if (marked) {
511
- var mark = document.createElement("mark");
512
- mark.textContent = buffer;
513
- target.appendChild(mark);
514
- } else {
515
- target.appendChild(document.createTextNode(buffer));
516
- }
517
- buffer = "";
518
- }
519
- for (var i = 0; i < path.length; i++) {
520
- var isMatch = marks[i] === true;
521
- if (isMatch !== marked) { flush(); marked = isMatch; }
522
- buffer += path.charAt(i);
523
- }
524
- flush();
525
- }
526
-
527
- function renderResults(results, hint) {
528
- paletteResults = results;
529
- paletteActive = results.length > 0 ? 0 : -1;
530
- paletteList.textContent = "";
531
- results.forEach(function (result, index) {
532
- var option = document.createElement("li");
533
- option.className = "bf-dialog__option";
534
- option.id = "palette-option-" + index;
535
- option.setAttribute("role", "option");
536
- option.setAttribute("data-index", String(index));
537
- appendHighlighted(option, result.path, result.positions);
538
- paletteList.appendChild(option);
539
- });
540
- if (hint) paletteStatus.textContent = hint;
541
- else if (results.length === 0) paletteStatus.textContent = "No matches";
542
- else paletteStatus.textContent = results.length + (results.length === 1 ? " match" : " matches");
543
- updateActive();
544
- }
545
-
546
- function updateActive() {
547
- Array.prototype.forEach.call(paletteList.children, function (option, index) {
548
- option.setAttribute("aria-selected", index === paletteActive ? "true" : "false");
549
- });
550
- var active = paletteList.children[paletteActive];
551
- if (paletteActive >= 0 && active) {
552
- paletteInput.setAttribute("aria-activedescendant", "palette-option-" + paletteActive);
553
- if (active.scrollIntoView) active.scrollIntoView({ block: "nearest" });
554
- } else {
555
- paletteInput.removeAttribute("aria-activedescendant");
556
- }
557
- }
558
-
559
- function setActive(index) {
560
- if (index < 0 || index >= paletteResults.length) return;
561
- paletteActive = index;
562
- updateActive();
563
- }
564
-
565
- function moveActive(delta) {
566
- if (paletteResults.length === 0) return;
567
- var next = paletteActive + delta;
568
- if (next < 0) next = paletteResults.length - 1;
569
- if (next >= paletteResults.length) next = 0;
570
- setActive(next);
571
- }
572
-
573
- function openActive() {
574
- var result = paletteResults[paletteActive];
575
- if (result) window.location.assign("/view/" + encodeSegments(result.path));
576
- }
149
+ ${PALETTE_CLIENT_JS}
577
150
 
578
151
  // Keyboard: the shared shortcut sheet, the per-page actions the server
579
152
  // rendered, and hunk-to-hunk navigation through a diff.
@@ -657,15 +230,10 @@ export const CLIENT_JS = `(function () {
657
230
  markTableOverflow();
658
231
 
659
232
  document.addEventListener("keydown", function (event) {
660
- if (event.key === "Escape" && window.bfKeys.isOpen()) {
661
- event.preventDefault();
662
- window.bfKeys.close();
663
- return;
664
- }
665
- if (paletteOpen()) {
233
+ if (window.bfPalette && window.bfPalette.isOpen()) {
666
234
  if (event.key === "Escape") {
667
235
  event.preventDefault();
668
- closePalette();
236
+ window.bfPalette.close();
669
237
  }
670
238
  return;
671
239
  }
@@ -675,13 +243,13 @@ export const CLIENT_JS = `(function () {
675
243
  if (editable) return;
676
244
  if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "k") {
677
245
  event.preventDefault();
678
- openPalette();
246
+ window.bfPalette.open();
679
247
  return;
680
248
  }
681
249
  if (event.metaKey || event.ctrlKey || event.altKey) return;
682
250
  if (event.key === "/") {
683
251
  event.preventDefault();
684
- openPalette();
252
+ window.bfPalette.open();
685
253
  } else if (event.key === "?") {
686
254
  event.preventDefault();
687
255
  window.bfKeys.toggle();
@@ -697,26 +265,11 @@ export const CLIENT_JS = `(function () {
697
265
  } else if (event.key === "p" && hunkAnchors.length > 0) {
698
266
  event.preventDefault();
699
267
  moveHunk(-1);
700
- } else if (event.key === "-" && foldAllButton) {
268
+ } else if (event.key === "-" && document.getElementById("fold-all")) {
701
269
  event.preventDefault();
702
- foldAllButton.click();
270
+ document.getElementById("fold-all").click();
703
271
  }
704
272
  });
705
273
 
706
- if (quickOpenButton) quickOpenButton.addEventListener("click", openPalette);
707
- if (foldAllButton) {
708
- foldAllButton.addEventListener("click", function () {
709
- // The control folds when anything is open, and expands when nothing is.
710
- var open = false;
711
- foldableDirs().forEach(function (li) {
712
- if (li.dataset.open === "true") open = true;
713
- });
714
- setAll(!open);
715
- });
716
- }
717
-
718
- // The control starts in the state the rendered tree implies.
719
- syncFoldButton();
720
-
721
274
  ${LIVE_CLIENT_JS}})();
722
275
  `;
@@ -9,7 +9,7 @@
9
9
  */
10
10
 
11
11
  import { GLYPHS } from "../../lib/tui/glyphs.ts";
12
- import { brandLockup, docHead, type KeyHint, keysSheet } from "../../lib/web/index.ts";
12
+ import { brandLockup, docHead, type KeyHint, keysSheet, themeToggle } from "../../lib/web/index.ts";
13
13
  import { escapeHtml } from "../../lib/escape.ts";
14
14
  import { formatSize, type DirEntry, type FileView, type Listing, type TreeNode } from "./files.ts";
15
15
  import { DIFF_MODES, statusLetter, type ChangedFile, type DiffLine, type DiffMode } from "./diff.ts";
@@ -159,7 +159,7 @@ ${SPRITE}
159
159
  <span class="bf-chip bf-chip--accent">read-only</span>
160
160
  ${renderLiveChip(layout.live)}
161
161
  <button type="button" class="bf-btn bf-btn--icon browse-keys-btn" id="keys-open" aria-label="Keyboard shortcuts" title="Keyboard shortcuts">?</button>
162
- <button type="button" class="bf-btn bf-btn--icon bf-theme-toggle" id="theme-toggle" aria-label="Toggle light and dark theme" title="Toggle light and dark theme"><span class="theme-glyph" aria-hidden="true">☾</span></button>
162
+ ${themeToggle()}
163
163
  </div><div class="browse-progress" aria-hidden="true"><span class="browse-progress__fill" id="progress-fill"></span></div></header>
164
164
  <div class="bf-app__body">
165
165
  <details class="bf-app__sidebar browse-sidebar" open>
@@ -213,7 +213,7 @@ function renderTreeNode(node: TreeNode, currentRel: string, summary?: ChangeSumm
213
213
  const href = hrefFor(node);
214
214
  const current = node.rel === currentRel ? ` aria-current="page"` : "";
215
215
  const twisty = isDir
216
- ? `<button type="button" class="bf-tree__tw bf-tree__tw--toggle" aria-expanded="${expanded}" aria-label="${escapeHtml(node.name)}">${expanded ? "▾" : "▸"}</button>`
216
+ ? `<button type="button" class="bf-tree__tw bf-tree__tw--toggle" tabindex="-1" aria-hidden="true">${expanded ? "▾" : "▸"}</button>`
217
217
  : `<span class="bf-tree__tw bf-tree__tw--spacer" aria-hidden="true"></span>`;
218
218
  const dataPath = ` data-path="${escapeHtml(node.rel)}"`;
219
219
  const name = isDir ? `${escapeHtml(node.name)}/` : escapeHtml(node.name);
@@ -222,7 +222,9 @@ function renderTreeNode(node: TreeNode, currentRel: string, summary?: ChangeSumm
222
222
  : "";
223
223
  const label = node.broken
224
224
  ? `<span class="browse-broken" title="broken symbolic link">${name}</span><span class="browse-link-mark">↩</span>`
225
- : `<a href="${href}">${name}</a>`;
225
+ : isDir
226
+ ? `<a href="${href}" aria-expanded="${expanded}" title="${escapeHtml(node.rel)}">${name}</a>`
227
+ : `<a href="${href}" title="${escapeHtml(node.rel)}">${name}</a>`;
226
228
  return `<li data-open="${expanded}"><div class="bf-tree__row"${current}${dataPath}>${twisty}${iconMarkup(spec)}${label}${changeMark(summary, node.rel, isDir)}</div>${children}</li>`;
227
229
  }
228
230
 
@@ -238,7 +240,7 @@ function renderRow(entry: DirEntry, maxThumbBytes: number, thumbnails: boolean,
238
240
  .join(" ");
239
241
  const link = entry.broken
240
242
  ? `<span class="browse-broken" title="broken symbolic link">${name}</span>`
241
- : `<a href="${hrefFor(entry)}">${name}</a>`;
243
+ : `<a href="${hrefFor(entry)}" title="${escapeHtml(entry.rel)}">${name}</a>`;
242
244
  const mark = entry.isSymlink
243
245
  ? `<span class="browse-link-mark" title="${entry.broken ? "broken symbolic link" : "symbolic link"}">↩</span>`
244
246
  : "";