@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
@@ -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
+ };`;
@@ -20,7 +20,7 @@ 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
25
  import type { ServeConfig } from "./config.ts";
26
26
  import { isDiffMode, type DiffMode } from "./diff.ts";
@@ -352,7 +352,7 @@ export async function handleRequest(
352
352
 
353
353
  try {
354
354
  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 };
355
+ if (pathname === THEME_JS_PATH) return { status: 200, headers: scriptHeaders(), body: THEME_JS };
356
356
  if (pathname === "/favicon.ico") return { status: 204, headers: pageHeaders(), body: "" };
357
357
  if (pathname === "/api/tree") return await treeApi(ctx, search.get("path") ?? "");
358
358
  if (pathname === "/api/files") return await filesApi(ctx, search.get("q") ?? "");
@@ -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)}
@@ -121,4 +118,12 @@ body.palette-open{overflow:hidden}
121
118
  }
122
119
  @media (max-width:520px){
123
120
  .bf-toolbar .bf-theme-toggle{margin-left:auto}
121
+ }
122
+ /* A printed page scrolls nothing: the card and its body open up, the pinned hunk
123
+ header stops pinning, and the progress hairline belongs to the screen. */
124
+ @media print{
125
+ .bf-card{border:0}
126
+ .bf-card,.bf-app__main,.bf-code,.bf-md,.bf-diff,.browse-listing{height:auto!important;max-height:none!important;overflow:visible!important}
127
+ .bf-diff .bf-diff-line--hunk{position:static}
128
+ .browse-progress{display:none}
124
129
  }`;
@@ -0,0 +1,381 @@
1
+ /**
2
+ * The browse page's sidebar: the path filter, the fold-all control, the lazy
3
+ * tree, and the tree's keyboard model.
4
+ *
5
+ * It is a fragment of the same IIFE `client.ts` ships (`TREE_CLIENT_JS` is
6
+ * interpolated into `CLIENT_JS`), kept in its own module because the sidebar is
7
+ * one concern with its own state: the filter query, each folder's `data-open`
8
+ * intent, and the one row the keyboard is on.
9
+ */
10
+
11
+ import { SIDEBAR_COLLAPSE_MAX } from "../../lib/web/index.ts";
12
+
13
+ export const TREE_CLIENT_JS = ` var tree = document.querySelector("nav.bf-tree");
14
+ var filter = document.getElementById("filter");
15
+ var foldAllButton = document.getElementById("fold-all");
16
+ var query = "";
17
+
18
+ function matches(text) {
19
+ return !query || text.toLowerCase().indexOf(query) !== -1;
20
+ }
21
+
22
+ // A row is found from inside it (an event target) or from its own list item:
23
+ // closest() only looks upwards, so paint() cannot start from the li.
24
+ function rowOf(node) {
25
+ var row = node && node.closest ? node.closest(".bf-tree__row") : null;
26
+ return row && tree && tree.contains(row) ? row : null;
27
+ }
28
+
29
+ function rowFor(li) {
30
+ return li ? li.querySelector(":scope > .bf-tree__row") : null;
31
+ }
32
+
33
+ function rowLink(row) {
34
+ return row ? row.querySelector("a") : null;
35
+ }
36
+
37
+ var SVG_NS = "http://www.w3.org/2000/svg";
38
+
39
+ function svgIcon(entry) {
40
+ var svg = document.createElementNS(SVG_NS, "svg");
41
+ svg.setAttribute("class", "browse-cat " + (entry.colorClass || ""));
42
+ svg.setAttribute("aria-hidden", "true");
43
+ svg.setAttribute("title", entry.label || "");
44
+ var use = document.createElementNS(SVG_NS, "use");
45
+ use.setAttribute("href", entry.iconHref);
46
+ svg.appendChild(use);
47
+ return svg;
48
+ }
49
+
50
+ function buildNode(entry) {
51
+ var li = document.createElement("li");
52
+ var row = document.createElement("div");
53
+ row.className = "bf-tree__row";
54
+ row.setAttribute("data-path", entry.path || "");
55
+ var twisty = document.createElement(entry.isDir ? "button" : "span");
56
+ twisty.className = "bf-tree__tw";
57
+ row.appendChild(twisty);
58
+ row.appendChild(svgIcon(entry));
59
+ if (entry.isDir) {
60
+ li.dataset.open = "false";
61
+ twisty.classList.add("bf-tree__tw--toggle");
62
+ twisty.type = "button";
63
+ twisty.tabIndex = -1;
64
+ twisty.setAttribute("aria-hidden", "true");
65
+ twisty.textContent = "\\u25B8";
66
+ var dir = document.createElement("a");
67
+ dir.href = entry.href;
68
+ dir.tabIndex = -1;
69
+ dir.setAttribute("aria-expanded", "false");
70
+ dir.textContent = entry.name + "/";
71
+ row.appendChild(dir);
72
+ } else if (entry.broken) {
73
+ twisty.className = "bf-tree__tw bf-tree__tw--spacer";
74
+ twisty.setAttribute("aria-hidden", "true");
75
+ var broken = document.createElement("span");
76
+ broken.className = "browse-broken";
77
+ broken.title = "broken symbolic link";
78
+ broken.textContent = entry.name;
79
+ row.appendChild(broken);
80
+ var mark = document.createElement("span");
81
+ mark.className = "browse-link-mark";
82
+ mark.textContent = "\\u21A9";
83
+ row.appendChild(mark);
84
+ } else {
85
+ twisty.className = "bf-tree__tw bf-tree__tw--spacer";
86
+ twisty.setAttribute("aria-hidden", "true");
87
+ var file = document.createElement("a");
88
+ file.href = entry.href;
89
+ file.tabIndex = -1;
90
+ file.textContent = entry.name;
91
+ row.appendChild(file);
92
+ }
93
+ li.appendChild(row);
94
+ return li;
95
+ }
96
+
97
+ function filterNode(li) {
98
+ var row = li.querySelector(":scope > .bf-tree__row");
99
+ var own = matches(row ? row.getAttribute("data-path") || row.textContent : "");
100
+ var childMatch = false;
101
+ Array.prototype.forEach.call(li.querySelectorAll(":scope > ul > li"), function (child) {
102
+ if (filterNode(child)) childMatch = true;
103
+ });
104
+ var visible = own || childMatch;
105
+ li.hidden = !visible;
106
+ return visible;
107
+ }
108
+
109
+ function applyFilter() {
110
+ var empty = document.getElementById("tree-empty");
111
+ var anyTree = false;
112
+ if (tree) {
113
+ Array.prototype.forEach.call(tree.querySelectorAll(":scope > ul > li"), function (li) {
114
+ if (filterNode(li)) anyTree = true;
115
+ });
116
+ }
117
+ if (empty) empty.hidden = !tree || anyTree;
118
+ Array.prototype.forEach.call(document.querySelectorAll("table.bf-table tbody tr"), function (row) {
119
+ var path = row.getAttribute("data-path") || row.getAttribute("data-name") || "";
120
+ row.hidden = !matches(path);
121
+ });
122
+ // Lists are repainted too: while a query is active every list is shown, and
123
+ // clearing the query brings the folds back.
124
+ if (tree) Array.prototype.forEach.call(tree.querySelectorAll("li[data-open]"), paint);
125
+ }
126
+
127
+ if (filter) {
128
+ filter.addEventListener("input", function () {
129
+ query = filter.value.trim().toLowerCase();
130
+ applyFilter();
131
+ });
132
+ }
133
+
134
+ // Visibility is derived: the intent in data-open, opened up while a filter is
135
+ // active so a fold cannot hide a match.
136
+ function isOpen(li) {
137
+ return query !== "" || li.dataset.open === "true";
138
+ }
139
+
140
+ function paint(li) {
141
+ var row = rowFor(li);
142
+ var twisty = row ? row.querySelector(":scope > .bf-tree__tw") : null;
143
+ if (!twisty || twisty.tagName !== "BUTTON") return;
144
+ var open = isOpen(li);
145
+ // A pending load owns the glyph until it lands or fails.
146
+ if (!li.dataset.loading && !li.dataset.error) twisty.textContent = open ? "\\u25BE" : "\\u25B8";
147
+ var link = rowLink(row);
148
+ if (link) link.setAttribute("aria-expanded", open ? "true" : "false");
149
+ var list = li.querySelector(":scope > ul");
150
+ if (list) list.hidden = !open;
151
+ }
152
+
153
+ function setOpen(li, open) {
154
+ li.dataset.open = open ? "true" : "false";
155
+ paint(li);
156
+ }
157
+
158
+ // Every dir with rendered children: what the fold-all control acts on.
159
+ function foldableDirs() {
160
+ var dirs = [];
161
+ if (!tree) return dirs;
162
+ Array.prototype.forEach.call(tree.querySelectorAll("li"), function (li) {
163
+ if (li.querySelector(":scope > ul")) dirs.push(li);
164
+ });
165
+ return dirs;
166
+ }
167
+
168
+ function syncFoldButton() {
169
+ if (!foldAllButton) return;
170
+ var dirs = foldableDirs();
171
+ var open = 0;
172
+ dirs.forEach(function (li) {
173
+ if (li.dataset.open === "true") open++;
174
+ });
175
+ foldAllButton.hidden = dirs.length === 0;
176
+ foldAllButton.textContent = open > 0 ? "fold all" : "expand all";
177
+ }
178
+
179
+ function setAll(open) {
180
+ foldableDirs().forEach(function (li) {
181
+ setOpen(li, open);
182
+ });
183
+ syncFoldButton();
184
+ }
185
+
186
+ function loadChildren(li, twisty, path) {
187
+ var link = rowLink(rowFor(li));
188
+ var name = link ? link.textContent.slice(0, -1) : path;
189
+ li.dataset.loading = "true";
190
+ twisty.textContent = "\\u2026";
191
+ if (link) link.setAttribute("aria-busy", "true");
192
+ fetch("/api/tree?path=" + encodeURIComponent(path))
193
+ .then(function (response) {
194
+ if (!response.ok) throw new Error("load failed");
195
+ return response.json();
196
+ })
197
+ .then(function (data) {
198
+ var built = document.createElement("ul");
199
+ (data.entries || []).forEach(function (entry) {
200
+ built.appendChild(buildNode(entry));
201
+ });
202
+ li.appendChild(built);
203
+ delete li.dataset.loading;
204
+ delete li.dataset.error;
205
+ setOpen(li, true);
206
+ if (link) link.setAttribute("aria-busy", "false");
207
+ applyFilter();
208
+ })
209
+ .catch(function () {
210
+ delete li.dataset.loading;
211
+ li.dataset.error = "true";
212
+ li.dataset.open = "false";
213
+ twisty.textContent = "\\u26A0";
214
+ twisty.title = "Could not load " + name + ". Press to retry.";
215
+ if (link) {
216
+ link.setAttribute("aria-busy", "false");
217
+ link.setAttribute("aria-expanded", "false");
218
+ }
219
+ });
220
+ }
221
+
222
+ if (tree) {
223
+ tree.addEventListener("click", function (event) {
224
+ var twisty = event.target.closest(".bf-tree__tw");
225
+ if (!twisty || twisty.tagName !== "BUTTON" || !tree.contains(twisty)) return;
226
+ event.preventDefault();
227
+ var li = twisty.closest("li");
228
+ if (!li) return;
229
+ var list = li.querySelector(":scope > ul");
230
+ if (li.dataset.open === "true") {
231
+ setOpen(li, false);
232
+ return;
233
+ }
234
+ if (list) {
235
+ setOpen(li, true);
236
+ applyFilter();
237
+ return;
238
+ }
239
+ var row = li.querySelector(":scope > .bf-tree__row");
240
+ var path = row ? row.getAttribute("data-path") : null;
241
+ if (!path) return;
242
+ loadChildren(li, twisty, path);
243
+ });
244
+ }
245
+
246
+ // The tree is one tab stop. Arrows move between the rows a reader can see,
247
+ // Right opens a folder (reading it first when it has never been loaded),
248
+ // Left closes it or steps out to the parent, Space toggles, and Enter follows
249
+ // the focused link itself — the row is a link, so the browser owns activation.
250
+ function treeRows() {
251
+ return tree ? Array.prototype.slice.call(tree.querySelectorAll(".bf-tree__row")) : [];
252
+ }
253
+
254
+ function visibleRows() {
255
+ return treeRows().filter(function (row) { return row.getClientRects().length > 0; });
256
+ }
257
+
258
+ function setTabStop(row) {
259
+ treeRows().forEach(function (other) {
260
+ var link = rowLink(other);
261
+ if (link) link.tabIndex = other === row ? 0 : -1;
262
+ });
263
+ }
264
+
265
+ function focusRow(row) {
266
+ if (!row) return;
267
+ setTabStop(row);
268
+ var link = rowLink(row);
269
+ if (link) link.focus();
270
+ }
271
+
272
+ function moveFocus(row, delta) {
273
+ var rows = visibleRows();
274
+ var at = rows.indexOf(row);
275
+ if (at >= 0) focusRow(rows[at + delta]);
276
+ }
277
+
278
+ function parentRowOf(row) {
279
+ var li = row.parentNode;
280
+ var list = li ? li.parentNode : null;
281
+ var parentLi = list && list.closest ? list.closest("li") : null;
282
+ return parentLi ? parentLi.querySelector(":scope > .bf-tree__row") : null;
283
+ }
284
+
285
+ // A folder that has never been expanded has no child list yet; opening it is
286
+ // the same request the twisty makes, so the keyboard reuses it.
287
+ function openFolder(row) {
288
+ var li = row.parentNode;
289
+ var twisty = row.querySelector(".bf-tree__tw--toggle");
290
+ if (!li || !twisty) return null;
291
+ var list = li.querySelector(":scope > ul");
292
+ if (list) return list;
293
+ var path = row.getAttribute("data-path");
294
+ if (path) loadChildren(li, twisty, path);
295
+ return null;
296
+ }
297
+
298
+ function expandRow(row) {
299
+ var li = row.parentNode;
300
+ if (!li || !row.querySelector(".bf-tree__tw--toggle")) return;
301
+ var list = openFolder(row);
302
+ if (!list) return;
303
+ if (!isOpen(li)) {
304
+ setOpen(li, true);
305
+ applyFilter();
306
+ return;
307
+ }
308
+ var rows = visibleRows();
309
+ var next = rows[rows.indexOf(row) + 1];
310
+ if (next && parentRowOf(next) === row) focusRow(next);
311
+ }
312
+
313
+ function collapseRow(row) {
314
+ var li = row.parentNode;
315
+ if (li && li.querySelector(":scope > ul") && isOpen(li)) {
316
+ setOpen(li, false);
317
+ applyFilter();
318
+ return;
319
+ }
320
+ focusRow(parentRowOf(row));
321
+ }
322
+
323
+ function toggleRow(row) {
324
+ var li = row.parentNode;
325
+ if (!li || !row.querySelector(".bf-tree__tw--toggle")) return;
326
+ if (!openFolder(row)) return;
327
+ setOpen(li, li.dataset.open !== "true");
328
+ applyFilter();
329
+ }
330
+
331
+ if (tree) {
332
+ tree.addEventListener("keydown", function (event) {
333
+ var row = rowOf(event.target);
334
+ if (!row || event.metaKey || event.ctrlKey || event.altKey) return;
335
+ var rows = visibleRows();
336
+ if (event.key === "ArrowDown") moveFocus(row, 1);
337
+ else if (event.key === "ArrowUp") moveFocus(row, -1);
338
+ else if (event.key === "Home") focusRow(rows[0]);
339
+ else if (event.key === "End") focusRow(rows[rows.length - 1]);
340
+ else if (event.key === "ArrowRight") expandRow(row);
341
+ else if (event.key === "ArrowLeft") collapseRow(row);
342
+ else if (event.key === " " && row.querySelector(".bf-tree__tw--toggle")) toggleRow(row);
343
+ else return;
344
+ // The page keymap treats a bare key as its own; the tree keeps its own.
345
+ event.preventDefault();
346
+ event.stopPropagation();
347
+ });
348
+ // A pointer click lands on one row, so that row becomes the tab stop the
349
+ // next Tab returns to.
350
+ tree.addEventListener("focusin", function (event) {
351
+ var row = rowOf(event.target);
352
+ if (row) setTabStop(row);
353
+ });
354
+ }
355
+
356
+ // The current page's row, else the first one, is the way into the tree.
357
+ setTabStop(tree && tree.querySelector(".bf-tree__row[aria-current]") || (tree ? treeRows()[0] : null));
358
+
359
+ var panel = document.querySelector("details.bf-app__sidebar");
360
+ if (panel && window.matchMedia) {
361
+ var mq = window.matchMedia("(max-width: ${SIDEBAR_COLLAPSE_MAX}px)");
362
+ var syncSide = function () { panel.open = !mq.matches; };
363
+ syncSide();
364
+ if (mq.addEventListener) mq.addEventListener("change", syncSide);
365
+ else if (mq.addListener) mq.addListener(syncSide);
366
+ }
367
+
368
+ if (foldAllButton) {
369
+ foldAllButton.addEventListener("click", function () {
370
+ // The control folds when anything is open, and expands when nothing is.
371
+ var open = false;
372
+ foldableDirs().forEach(function (li) {
373
+ if (li.dataset.open === "true") open = true;
374
+ });
375
+ setAll(!open);
376
+ });
377
+ }
378
+
379
+ // The control starts in the state the rendered tree implies.
380
+ syncFoldButton();
381
+ `;
@@ -201,7 +201,10 @@ config defaults, and error listing all derive from it.
201
201
  2. Add `extensions/file-read/extract/<fmt>.ts` exporting a `FormatExtractor` that
202
202
  `await import(...)`s its library, reads the bytes with `readFormatBytes`, and
203
203
  normalizes the output. Keep the parsing in a pure, exported helper so it is
204
- testable without a real file.
204
+ testable without a real file. **The import goes inside the function**, not at
205
+ module scope: `formats.ts` imports every extractor when the tool registers, so
206
+ a module-scope import loads that library on every start — `test/dependencies.test.ts`
207
+ fails on exactly that.
205
208
  3. Add a `ReadFormat` entry to `FORMATS` and widen the `id` union.
206
209
  4. Add the new extractor to `extract/extract.test.ts` with a real minimal
207
210
  fixture, and extend `tool.test.ts` for format selection and config.
@@ -5,11 +5,11 @@
5
5
  * unzips it with `fflate`, parses the XML with `fast-xml-parser`, and walks the
6
6
  * body for text: paragraphs and headings become lines, table cells are
7
7
  * tab-separated, and `text:s` / `text:tab` / `text:line-break` become their
8
- * whitespace. The walk over the parsed tree is pure and tested directly.
8
+ * whitespace. Both libraries load on the first ODT read, never at registration,
9
+ * so a session that never opens one pays nothing for them; the walk over the
10
+ * parsed tree is pure and tested directly.
9
11
  */
10
12
 
11
- import { strFromU8, unzipSync } from "fflate";
12
- import { XMLParser } from "fast-xml-parser";
13
13
  import { normalizeBodyText, readFormatBytes, type FormatExtractor } from "./file.ts";
14
14
 
15
15
  /** Elements that end a line. */
@@ -68,12 +68,13 @@ function collect(nodes: unknown, out: string[], inText: boolean): void {
68
68
  }
69
69
 
70
70
  /**
71
- * Extract plain text from the body of an ODT `content.xml`. Pure.
71
+ * Extract plain text from the body of an ODT `content.xml`.
72
72
  *
73
73
  * Trailing tabs and spaces at line ends, runs of blank lines, and the final
74
74
  * newline are stripped so the tool's paging sees ordinary text.
75
75
  */
76
- export function odtXmlToText(xml: string): string {
76
+ export async function odtXmlToText(xml: string): Promise<string> {
77
+ const { XMLParser } = await import("fast-xml-parser");
77
78
  const parser = new XMLParser({
78
79
  preserveOrder: true,
79
80
  ignoreAttributes: false,
@@ -94,6 +95,7 @@ export function odtXmlToText(xml: string): string {
94
95
 
95
96
  /** Extract the body text from an ODT file. */
96
97
  export const extractOdtText: FormatExtractor = async (file) => {
98
+ const { strFromU8, unzipSync } = await import("fflate");
97
99
  const archive = unzipSync(await readFormatBytes(file));
98
100
  const content = archive["content.xml"];
99
101
  if (!content) throw new Error("ODT archive has no content.xml.");