@gavin-hu/my-pi-agent 0.20.0 → 0.22.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 (298) hide show
  1. package/README.md +46 -30
  2. package/README.zh-CN.md +40 -25
  3. package/assets/screens/board.png +0 -0
  4. package/assets/screens/chrome.png +0 -0
  5. package/assets/screens/file-browse.png +0 -0
  6. package/assets/screens/plan-review.png +0 -0
  7. package/assets/screens/plan.png +0 -0
  8. package/docs/extensions/README.md +22 -23
  9. package/docs/extensions/board.md +397 -168
  10. package/docs/extensions/bytefly.md +234 -197
  11. package/docs/extensions/check.md +15 -17
  12. package/docs/extensions/context.md +35 -15
  13. package/docs/extensions/file-browse.md +57 -63
  14. package/docs/extensions/file-open.md +9 -11
  15. package/docs/extensions/file-read.md +12 -14
  16. package/docs/extensions/goal.md +15 -17
  17. package/docs/extensions/job.md +54 -42
  18. package/docs/extensions/memory.md +17 -19
  19. package/docs/extensions/plan.md +197 -140
  20. package/docs/extensions/question.md +16 -18
  21. package/docs/extensions/rewind.md +17 -19
  22. package/docs/extensions/serve.md +145 -0
  23. package/docs/extensions/session-bus.md +38 -16
  24. package/docs/extensions/subagent.md +23 -25
  25. package/docs/extensions/team.md +83 -44
  26. package/docs/extensions/todo.md +18 -18
  27. package/docs/extensions/watch.md +31 -31
  28. package/docs/extensions/web-access.md +2 -4
  29. package/docs/extensions/wechat.md +34 -35
  30. package/docs/extensions/worktree.md +56 -50
  31. package/extensions/board/commands.ts +79 -25
  32. package/extensions/board/format.ts +43 -24
  33. package/extensions/board/index.ts +17 -4
  34. package/extensions/board/input.ts +4 -9
  35. package/extensions/board/join.ts +5 -20
  36. package/extensions/board/lazy-page.ts +40 -0
  37. package/extensions/board/page.ts +65 -93
  38. package/extensions/board/result.ts +30 -39
  39. package/extensions/board/rows.ts +62 -25
  40. package/extensions/board/schema.ts +49 -11
  41. package/extensions/board/snapshot.ts +8 -6
  42. package/extensions/board/tone.ts +22 -0
  43. package/extensions/board/tools.ts +154 -55
  44. package/extensions/board/transport/router.ts +3 -5
  45. package/extensions/board/transport/server.ts +59 -28
  46. package/extensions/board/types.ts +36 -42
  47. package/extensions/board/view/card.ts +229 -0
  48. package/extensions/board/view/client.ts +35 -16
  49. package/extensions/board/view/dialog.ts +169 -6
  50. package/extensions/board/view/empty.ts +47 -0
  51. package/extensions/board/view/keys.ts +13 -2
  52. package/extensions/board/view/live.ts +90 -28
  53. package/extensions/board/view/names.ts +13 -4
  54. package/extensions/board/view/page.ts +38 -13
  55. package/extensions/board/view/render.ts +219 -277
  56. package/extensions/board/view/styles.ts +130 -32
  57. package/extensions/board/view/team.ts +297 -76
  58. package/extensions/bytefly/block/fallback.ts +1 -1
  59. package/extensions/bytefly/doctor/runtime.ts +5 -2
  60. package/extensions/bytefly/footer/collect/label.ts +60 -0
  61. package/extensions/bytefly/footer/collect/snapshot.ts +8 -1
  62. package/extensions/bytefly/footer/config.ts +7 -8
  63. package/extensions/bytefly/footer/register.ts +33 -4
  64. package/extensions/bytefly/footer/render/footer.ts +18 -4
  65. package/extensions/bytefly/footer/render/key.ts +46 -0
  66. package/extensions/bytefly/footer/render/layout.ts +13 -3
  67. package/extensions/bytefly/footer/render/lines.ts +10 -7
  68. package/extensions/bytefly/footer/render/segments.ts +41 -38
  69. package/extensions/bytefly/footer/render/status.ts +0 -11
  70. package/extensions/bytefly/footer/types.ts +7 -0
  71. package/extensions/bytefly/header/component.ts +4 -20
  72. package/extensions/bytefly/header/config.ts +1 -1
  73. package/extensions/bytefly/index.ts +32 -6
  74. package/extensions/bytefly/mode/editor.ts +115 -0
  75. package/extensions/bytefly/panel/commands.ts +210 -120
  76. package/extensions/bytefly/panel/format.ts +4 -4
  77. package/extensions/bytefly/panel/settings.ts +73 -6
  78. package/extensions/bytefly/panel/tui/keys.ts +2 -6
  79. package/extensions/bytefly/update/cache.ts +1 -1
  80. package/extensions/bytefly/update/register.ts +4 -4
  81. package/extensions/bytefly/working/config.ts +5 -7
  82. package/extensions/bytefly/working/format.ts +10 -13
  83. package/extensions/bytefly/working/register.ts +22 -27
  84. package/extensions/check/commands.ts +3 -2
  85. package/extensions/check/render.ts +10 -1
  86. package/extensions/check/{tui.ts → view/check-screens.ts} +14 -19
  87. package/extensions/context/commands.ts +2 -2
  88. package/extensions/context/{tui.ts → view/context-report.ts} +51 -19
  89. package/extensions/file-browse/config/config.ts +4 -19
  90. package/extensions/file-browse/index.ts +89 -79
  91. package/extensions/file-browse/live/live.ts +1 -1
  92. package/extensions/file-browse/transport/router.ts +12 -4
  93. package/extensions/file-browse/transport/server.ts +61 -28
  94. package/extensions/file-browse/view/chrome.ts +4 -4
  95. package/extensions/file-browse/view/client.ts +3 -1
  96. package/extensions/file-browse/view/html.ts +73 -40
  97. package/extensions/file-browse/view/live-client.ts +2 -2
  98. package/extensions/file-browse/view/markdown.ts +13 -14
  99. package/extensions/file-browse/view/palette.ts +2 -2
  100. package/extensions/file-browse/view/raw-html.ts +9 -9
  101. package/extensions/file-browse/view/tree.ts +1 -1
  102. package/extensions/file-browse/view/urls.ts +20 -6
  103. package/extensions/file-read/render.ts +4 -4
  104. package/extensions/goal/plan-route.ts +2 -2
  105. package/extensions/goal/tui.ts +12 -2
  106. package/extensions/job/child.ts +527 -0
  107. package/extensions/job/clock.ts +142 -0
  108. package/extensions/job/commands.ts +4 -2
  109. package/extensions/job/config.ts +8 -1
  110. package/extensions/job/format.ts +22 -1
  111. package/extensions/job/log-writer.ts +13 -2
  112. package/extensions/job/registry.ts +63 -7
  113. package/extensions/job/render.ts +34 -0
  114. package/extensions/job/runtime.ts +296 -655
  115. package/extensions/job/session.ts +132 -19
  116. package/extensions/job/store.ts +7 -7
  117. package/extensions/job/tools.ts +21 -9
  118. package/extensions/job/types.ts +47 -0
  119. package/extensions/job/{tui.ts → view/jobs-screen.ts} +19 -99
  120. package/extensions/job/waiters.ts +44 -1
  121. package/extensions/memory/commands.ts +4 -2
  122. package/extensions/memory/{tui.ts → view/memory-picker.ts} +12 -12
  123. package/extensions/plan/commands.ts +12 -9
  124. package/extensions/plan/feedback.ts +62 -16
  125. package/extensions/plan/format.ts +8 -2
  126. package/extensions/plan/git-bash.ts +115 -16
  127. package/extensions/plan/index.ts +58 -51
  128. package/extensions/plan/plan-index.ts +4 -2
  129. package/extensions/plan/plan-text.ts +114 -0
  130. package/extensions/plan/plans.ts +5 -54
  131. package/extensions/plan/policy.ts +8 -10
  132. package/extensions/plan/review-lazy.ts +85 -0
  133. package/extensions/plan/review.ts +2 -2
  134. package/extensions/plan/runtime.ts +61 -41
  135. package/extensions/plan/sections.ts +1 -1
  136. package/extensions/plan/steps.ts +3 -3
  137. package/extensions/plan/todos.ts +1 -1
  138. package/extensions/plan/tools/cancel.ts +27 -62
  139. package/extensions/plan/tools/enter.ts +55 -81
  140. package/extensions/plan/tools/exit.ts +221 -238
  141. package/extensions/plan/tools/refusal.ts +18 -0
  142. package/extensions/plan/tools/steps.ts +75 -92
  143. package/extensions/plan/tools/write.ts +77 -120
  144. package/extensions/plan/tools.ts +188 -30
  145. package/extensions/plan/tui.ts +5 -5
  146. package/extensions/plan/types.ts +48 -5
  147. package/extensions/plan/web-blocks.ts +76 -5
  148. package/extensions/plan/web-chrome.ts +4 -0
  149. package/extensions/plan/web-client.ts +102 -2
  150. package/extensions/plan/web-collab.ts +4 -4
  151. package/extensions/plan/web-comments.ts +39 -8
  152. package/extensions/plan/web-composer.ts +14 -3
  153. package/extensions/plan/web-keys.ts +149 -25
  154. package/extensions/plan/web-lazy.ts +53 -0
  155. package/extensions/plan/web-markdown.ts +10 -68
  156. package/extensions/plan/web-outline.ts +22 -2
  157. package/extensions/plan/web-page.ts +56 -86
  158. package/extensions/plan/web-payload.ts +188 -0
  159. package/extensions/plan/web-plan.ts +23 -13
  160. package/extensions/plan/web-router.ts +18 -9
  161. package/extensions/plan/web-settle.ts +14 -7
  162. package/extensions/plan/web-share-session.ts +24 -55
  163. package/extensions/plan/web-share.ts +19 -37
  164. package/extensions/plan/web-write.ts +112 -0
  165. package/extensions/plan/web.ts +67 -131
  166. package/extensions/question/tools.ts +6 -2
  167. package/extensions/question/{tui.ts → view/question-screen.ts} +11 -11
  168. package/extensions/rewind/commands/commands.ts +6 -3
  169. package/extensions/rewind/state/runtime.ts +1 -1
  170. package/extensions/rewind/{view/status.ts → status.ts} +4 -4
  171. package/extensions/rewind/view/review-tui.ts +4 -4
  172. package/extensions/serve/config.ts +39 -0
  173. package/extensions/serve/index.ts +85 -0
  174. package/extensions/serve/registry.ts +260 -0
  175. package/extensions/session-bus/commands.ts +11 -6
  176. package/extensions/session-bus/join-request.ts +17 -9
  177. package/extensions/session-bus/mail-watch.ts +26 -17
  178. package/extensions/session-bus/protocol.ts +15 -3
  179. package/extensions/session-bus/resolve.ts +5 -7
  180. package/extensions/session-bus/result.ts +1 -27
  181. package/extensions/session-bus/runtime.ts +40 -12
  182. package/extensions/session-bus/store.ts +101 -19
  183. package/extensions/session-bus/tools.ts +5 -4
  184. package/extensions/session-bus/types.ts +19 -0
  185. package/extensions/session-bus/{tui.ts → view/session-list.ts} +16 -16
  186. package/extensions/subagent/agents.ts +1 -1
  187. package/extensions/subagent/commands.ts +4 -2
  188. package/extensions/subagent/format.ts +15 -15
  189. package/extensions/subagent/index.ts +1 -1
  190. package/extensions/subagent/render.ts +3 -3
  191. package/extensions/subagent/{background-tui.ts → view/agents-board.ts} +11 -11
  192. package/extensions/team/commands.ts +24 -5
  193. package/extensions/team/dispatch.ts +18 -4
  194. package/extensions/team/format.ts +17 -2
  195. package/extensions/team/index.ts +9 -8
  196. package/extensions/team/schema.ts +6 -5
  197. package/extensions/team/tools.ts +107 -45
  198. package/extensions/team/types.ts +3 -2
  199. package/extensions/todo/commands.ts +4 -2
  200. package/extensions/todo/format.ts +11 -0
  201. package/extensions/todo/render.ts +57 -0
  202. package/extensions/todo/runtime.ts +1 -1
  203. package/extensions/todo/tools.ts +1 -1
  204. package/extensions/todo/{tui.ts → view/todo-screen.ts} +18 -90
  205. package/extensions/todo/widget.ts +47 -0
  206. package/extensions/watch/commands.ts +4 -2
  207. package/extensions/watch/format.ts +36 -7
  208. package/extensions/watch/render.ts +57 -0
  209. package/extensions/watch/runtime.ts +27 -22
  210. package/extensions/watch/tools.ts +4 -4
  211. package/extensions/watch/{tui.ts → view/watch-screen.ts} +18 -80
  212. package/extensions/web-access/fetch/tool.ts +12 -4
  213. package/extensions/wechat/commands/commands.ts +18 -14
  214. package/extensions/wechat/index.ts +72 -37
  215. package/extensions/wechat/state/credentials.ts +1 -1
  216. package/extensions/wechat/state/state.ts +1 -1
  217. package/extensions/wechat/text/status.ts +112 -0
  218. package/extensions/wechat/types.ts +2 -2
  219. package/extensions/wechat/view/status-screen.ts +4 -103
  220. package/extensions/worktree/commands.ts +11 -9
  221. package/extensions/worktree/enter.ts +2 -2
  222. package/extensions/worktree/exit.ts +2 -2
  223. package/extensions/worktree/index.ts +5 -5
  224. package/extensions/worktree/screen.ts +3 -3
  225. package/extensions/worktree/status.ts +2 -2
  226. package/extensions/worktree/tools.ts +113 -116
  227. package/lib/README.md +28 -13
  228. package/lib/board/actions.ts +111 -0
  229. package/lib/board/artifact.ts +149 -0
  230. package/lib/board/card.ts +163 -52
  231. package/lib/board/config.ts +58 -0
  232. package/lib/board/contract.ts +113 -0
  233. package/lib/board/join.ts +38 -0
  234. package/lib/board/lanes.ts +182 -0
  235. package/lib/board/lead.ts +130 -0
  236. package/lib/board/limits.ts +78 -0
  237. package/lib/board/links.ts +31 -10
  238. package/lib/board/page-pointer.ts +30 -5
  239. package/lib/board/read.ts +83 -0
  240. package/lib/board/reason.ts +102 -0
  241. package/lib/board/round-text.ts +68 -6
  242. package/lib/board/round.ts +31 -15
  243. package/lib/board/rules.ts +151 -307
  244. package/lib/board/seat.ts +35 -0
  245. package/lib/board/session.ts +199 -51
  246. package/lib/board/skipped.ts +22 -0
  247. package/lib/board/steps.ts +487 -0
  248. package/lib/board/store.ts +119 -36
  249. package/lib/board/tone.ts +92 -0
  250. package/lib/board/view.ts +81 -63
  251. package/lib/bus-request.ts +83 -0
  252. package/{extensions/watch → lib}/child.ts +40 -21
  253. package/lib/env.ts +65 -7
  254. package/lib/format.ts +13 -0
  255. package/lib/http/index.ts +17 -11
  256. package/lib/http/mount.ts +142 -0
  257. package/lib/http/port.ts +30 -0
  258. package/lib/http/serve.ts +170 -46
  259. package/lib/http/server.ts +3 -3
  260. package/lib/lazy.ts +45 -0
  261. package/lib/plan-entry.ts +46 -21
  262. package/lib/plans/paths.ts +82 -0
  263. package/lib/policy.ts +9 -6
  264. package/lib/report.ts +3 -2
  265. package/lib/requirements.ts +10 -10
  266. package/lib/session-label.ts +129 -7
  267. package/lib/session-roster.ts +43 -1
  268. package/lib/session-tool.ts +39 -22
  269. package/lib/stray-fields.ts +7 -2
  270. package/lib/tool-names.ts +5 -10
  271. package/lib/tui/README.md +29 -11
  272. package/lib/tui/chrome.ts +16 -15
  273. package/lib/tui/cursor.ts +18 -3
  274. package/lib/tui/glyphs.ts +11 -11
  275. package/lib/tui/render-cache.ts +29 -0
  276. package/lib/tui/tokens.ts +33 -2
  277. package/lib/tui/tool.ts +33 -4
  278. package/lib/web/README.md +21 -0
  279. package/lib/web/client-runtime.ts +27 -1
  280. package/lib/web/components.ts +5 -1
  281. package/lib/web/index.ts +1 -0
  282. package/lib/web/markdown.ts +83 -0
  283. package/lib/web/markup.ts +20 -2
  284. package/lib/web/runtime.ts +9 -10
  285. package/package.json +8 -2
  286. package/skills/init/SKILL.md +5 -5
  287. package/skills/next/SKILL.md +3 -5
  288. package/extensions/board/config.ts +0 -36
  289. package/extensions/board/seat.ts +0 -28
  290. package/lib/tui/index.ts +0 -34
  291. package/skills/project-docs/SKILL.md +0 -133
  292. package/skills/project-docs/references/audit.md +0 -131
  293. package/skills/project-docs/references/changelog.md +0 -105
  294. package/skills/project-docs/references/discovery.md +0 -127
  295. package/skills/project-docs/references/readme.md +0 -80
  296. package/skills/project-docs/references/release.md +0 -84
  297. package/skills/project-docs/references/roadmap.md +0 -93
  298. package/skills/project-docs/scripts/state.ts +0 -316
@@ -16,7 +16,7 @@ import { lastTodos } from "../../lib/todo-state.ts";
16
16
  import { DEFAULT_TODO_CONFIG, type TodoConfig } from "./config.ts";
17
17
  import { shouldNudge } from "./nudge.ts";
18
18
  import { hasOpenTodos } from "./state.ts";
19
- import { TodoWidget, WIDGET_KEY } from "./tui.ts";
19
+ import { TodoWidget, WIDGET_KEY } from "./widget.ts";
20
20
  import type { Todo } from "./types.ts";
21
21
 
22
22
  export interface TodoRuntime {
@@ -22,7 +22,7 @@ import { paint } from "../../lib/tui/tokens.ts";
22
22
  import { compareByActivity, formatCallText, formatTodoText, type TodoRailInput } from "./format.ts";
23
23
  import type { TodoRuntime } from "./runtime.ts";
24
24
  import { TodoParams, TodoResult, type TodoArgs } from "./schema.ts";
25
- import { TodoResult as TodoResultView } from "./tui.ts";
25
+ import { TodoResult as TodoResultView } from "./render.ts";
26
26
  import type { Todo, TodoDetails } from "./types.ts";
27
27
 
28
28
  export const TOOL_NAME = TODO_TOOL;
@@ -1,102 +1,30 @@
1
1
  /**
2
- * Terminal rendering for the todo list.
2
+ * The `/todos` screen: a scrollable, dismissible list with its own frame.
3
3
  *
4
- * `todoRow` is the shared item layout used by the persistent widget and the
5
- * `/todos` screen. Two tiny components wrap it: a non-interactive widget for
6
- * `ctx.ui.setWidget()`, and a scrollable, dismissible list for `ctx.ui.custom()`.
4
+ * The frame, the hints, the range line and the empty state come from the
5
+ * `lib/tui/` kit (`dockListFrame`, `screenHint`, `formatRange`, `emptyState`),
6
+ * and the rows from `todoRow` in `format.ts`.
7
+ *
8
+ * Full-screen surfaces live under `view/` and load on first use: the `/todos`
9
+ * command imports this module behind its TUI branch, so registration never pays
10
+ * for it. The widget and the transcript block, which Pi draws synchronously,
11
+ * stay outside this directory on purpose.
7
12
  */
8
13
 
9
14
  import type { Theme } from "@earendil-works/pi-coding-agent";
10
- import { truncateToWidth, type Component, type TuiMouseEvent, type TuiMouseEventResult } from "@earendil-works/pi-tui";
11
- import { clampScroll, fitRows, formatRange, isCloseKey, navigateScroll, wheelDelta } from "../../lib/tui/cursor.ts";
12
- import { dockListFrame, screenHint, type ViewportRowsSource } from "../../lib/tui/chrome.ts";
13
- import { clip } from "../../lib/tui/columns.ts";
14
- import { emptyState, listRow } from "../../lib/tui/rows.ts";
15
- import { SEPARATORS } from "../../lib/tui/glyphs.ts";
16
- import { titleRule } from "../../lib/tui/rules.ts";
17
- import { paint } from "../../lib/tui/tokens.ts";
18
- import {
19
- progressCount,
20
- progressSummary,
21
- todoGlyphChar,
22
- todoGlyphRole,
23
- todoLabel,
24
- todoLabelRole,
25
- todoLabelText,
26
- todoRailLines,
27
- type TodoRailInput,
28
- } from "./format.ts";
29
- import { currentTodo } from "./state.ts";
30
- import type { Todo } from "./types.ts";
31
-
32
- /** Widget key used with `ctx.ui.setWidget()`. */
33
- export const WIDGET_KEY = "todo-widget";
15
+ import { type Component, type TuiMouseEvent, type TuiMouseEventResult } from "@earendil-works/pi-tui";
16
+ import { dockListFrame, screenHint, type ViewportRowsSource } from "../../../lib/tui/chrome.ts";
17
+ import { clip } from "../../../lib/tui/columns.ts";
18
+ import { clampScroll, fitRows, formatRange, isCloseKey, navigateScroll, wheelDelta } from "../../../lib/tui/cursor.ts";
19
+ import { emptyState } from "../../../lib/tui/rows.ts";
20
+ import { titleRule } from "../../../lib/tui/rules.ts";
21
+ import { paint } from "../../../lib/tui/tokens.ts";
22
+ import { progressSummary, todoRow } from "../format.ts";
23
+ import type { Todo } from "../types.ts";
34
24
 
35
25
  /** Items the `/todos` screen shows when the terminal height is unknown. */
36
26
  const SCREEN_DEFAULT_ITEMS = 12;
37
27
 
38
- /** One indented item line, clipped to `width`. */
39
- function todoRow(todo: Todo, theme: Theme, width: number): string {
40
- return listRow(theme, width, {
41
- glyph: todoGlyphChar(todo),
42
- glyphRole: todoGlyphRole(todo),
43
- label: todoLabelText(todo),
44
- labelRole: todoLabelRole(todo),
45
- });
46
- }
47
-
48
- /**
49
- * One-line widget summary: progress plus the current item, or a completion note
50
- * when nothing is open. Label-first and glyph-free, matching the `goal` and
51
- * `jobs` widgets. Callers clip it to the available width.
52
- */
53
- function todoLine(todos: Todo[], theme: Theme): string {
54
- const head = `${paint(theme, "accent", "Todos")}${paint(theme, "faint", SEPARATORS.item)}${paint(theme, "faint", progressCount(todos))}`;
55
- const current = currentTodo(todos);
56
- if (!current) return `${head} ${paint(theme, "faint", "completed")}`;
57
- return `${head}${paint(theme, "faint", SEPARATORS.item)}${todoLabel(current, theme)}`;
58
- }
59
-
60
- /** Transcript result block: the indented glyph rail, wrapping at the render width. */
61
- export class TodoResult implements Component {
62
- private input: TodoRailInput;
63
- private theme: Theme;
64
-
65
- constructor(input: TodoRailInput, theme: Theme) {
66
- this.input = input;
67
- this.theme = theme;
68
- }
69
-
70
- /** Update in place so the transcript can reuse this component across renders. */
71
- setInput(input: TodoRailInput, theme: Theme): void {
72
- this.input = input;
73
- this.theme = theme;
74
- }
75
-
76
- invalidate(): void {}
77
-
78
- render(width: number): string[] {
79
- // The leading blank line separates the rail from the call header, matching
80
- // the built-ins' header/output gap.
81
- return ["", ...todoRailLines(this.input, this.theme, Math.max(1, width))];
82
- }
83
- }
84
-
85
- /** Persistent one-line widget shown above the editor while the list has work. */
86
- export class TodoWidget implements Component {
87
- constructor(
88
- private readonly todos: Todo[],
89
- private readonly theme: Theme,
90
- ) {}
91
-
92
- invalidate(): void {}
93
-
94
- render(width: number): string[] {
95
- if (this.todos.length === 0) return [];
96
- return [truncateToWidth(todoLine(this.todos, this.theme), Math.max(1, width), "…")];
97
- }
98
- }
99
-
100
28
  /** Dismissible, scrollable list opened by `/todos`; reads the live list on each render. */
101
29
  export class TodoListComponent implements Component {
102
30
  private scrollTop = 0;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The persistent one-line todo widget, shown above the editor while the list
3
+ * has work.
4
+ *
5
+ * It is a rail, not a screen: `runtime.syncWidget` sets it whenever the list
6
+ * changes, through a synchronous factory Pi calls at paint time, so this module
7
+ * stays on the registration path. `WIDGET_KEY` lives here for the same reason —
8
+ * the runtime and the tests read it without touching a component.
9
+ */
10
+
11
+ import type { Theme } from "@earendil-works/pi-coding-agent";
12
+ import { truncateToWidth, type Component } from "@earendil-works/pi-tui";
13
+ import { SEPARATORS } from "../../lib/tui/glyphs.ts";
14
+ import { paint } from "../../lib/tui/tokens.ts";
15
+ import { progressCount, todoLabel } from "./format.ts";
16
+ import { currentTodo } from "./state.ts";
17
+ import type { Todo } from "./types.ts";
18
+
19
+ /** Widget key used with `ctx.ui.setWidget()`. */
20
+ export const WIDGET_KEY = "todo-widget";
21
+
22
+ /**
23
+ * One-line widget summary: progress plus the current item, or a completion note
24
+ * when nothing is open. Label-first and glyph-free, matching the `goal` and
25
+ * `jobs` widgets. Callers clip it to the available width.
26
+ */
27
+ function todoLine(todos: Todo[], theme: Theme): string {
28
+ const head = `${paint(theme, "accent", "Todos")}${paint(theme, "faint", SEPARATORS.item)}${paint(theme, "faint", progressCount(todos))}`;
29
+ const current = currentTodo(todos);
30
+ if (!current) return `${head} ${paint(theme, "faint", "completed")}`;
31
+ return `${head}${paint(theme, "faint", SEPARATORS.item)}${todoLabel(current, theme)}`;
32
+ }
33
+
34
+ /** Persistent one-line widget shown above the editor while the list has work. */
35
+ export class TodoWidget implements Component {
36
+ constructor(
37
+ private readonly todos: Todo[],
38
+ private readonly theme: Theme,
39
+ ) {}
40
+
41
+ invalidate(): void {}
42
+
43
+ render(width: number): string[] {
44
+ if (this.todos.length === 0) return [];
45
+ return [truncateToWidth(todoLine(this.todos, this.theme), Math.max(1, width), "…")];
46
+ }
47
+ }
@@ -8,10 +8,8 @@
8
8
  */
9
9
 
10
10
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
11
- import { withPolledScreen } from "../../lib/screen-session.ts";
12
11
  import { formatWatchList, shortLabel } from "./format.ts";
13
12
  import type { WatchRuntime } from "./runtime.ts";
14
- import { WatchListComponent } from "./tui.ts";
15
13
 
16
14
  /** How often the open screen re-reads live state. */
17
15
  const REFRESH_MS = 500;
@@ -92,6 +90,10 @@ export function registerCommands(pi: ExtensionAPI, runtime: WatchRuntime): void
92
90
  return;
93
91
  }
94
92
 
93
+ // The dock screen and its poll loop are the TUI's: the listing above and
94
+ // registration load neither.
95
+ const { withPolledScreen } = await import("../../lib/screen-session.ts");
96
+ const { WatchListComponent } = await import("./view/watch-screen.ts");
95
97
  await withPolledScreen(
96
98
  pi,
97
99
  ctx,
@@ -14,7 +14,7 @@ import type { Theme } from "@earendil-works/pi-coding-agent";
14
14
  import { boundedTail, sanitizeLine } from "../../lib/format.ts";
15
15
  import { clip, clipLabel, labelLine } from "../../lib/tui/columns.ts";
16
16
  import { GLYPHS, SEPARATORS } from "../../lib/tui/glyphs.ts";
17
- import { byRunningFirst } from "../../lib/tui/rows.ts";
17
+ import { byRunningFirst, statusRow, type StatusRow } from "../../lib/tui/rows.ts";
18
18
  import { paint, type TuiRole } from "../../lib/tui/tokens.ts";
19
19
  import type { PendingMatch, WatchRecord, WatchStatus } from "./types.ts";
20
20
 
@@ -23,12 +23,39 @@ export function compileRegex(pattern: string, caseSensitive: boolean): RegExp {
23
23
  return new RegExp(pattern, caseSensitive ? "" : "i");
24
24
  }
25
25
 
26
+ /**
27
+ * A watcher's pattern as `/<pattern>/<flags>`, with the wake badge appended:
28
+ * `/pat/`, `/pat/i`, `/pat/ · wake`, `/pat/i · wake`.
29
+ */
30
+ export function formatPattern(watch: Pick<WatchRecord, "pattern" | "caseSensitive" | "wake">): string {
31
+ const flags = watch.caseSensitive ? "" : "i";
32
+ const badge = watch.wake ? `${SEPARATORS.item}wake` : "";
33
+ return `/${sanitizeLine(watch.pattern)}/${flags}${badge}`;
34
+ }
35
+
26
36
  /** What a watcher reads: a path or a command, clipped for display. */
27
37
  export function sourceLabel(watch: WatchRecord): string {
28
38
  if (watch.source.kind === "file") return clipLabel(watch.source.path ?? "");
29
39
  return clipLabel(`$ ${watch.source.command ?? ""}`);
30
40
  }
31
41
 
42
+ /** One watcher row: marker, status glyph, id, label, and a right-aligned count. */
43
+ export function watchRow(
44
+ watch: WatchRecord,
45
+ theme: Theme,
46
+ width: number,
47
+ options: Pick<StatusRow, "selected" | "marker"> = {},
48
+ ): string {
49
+ return statusRow(theme, width, {
50
+ ...options,
51
+ glyph: statusGlyphChar(watch.status),
52
+ glyphRole: statusRole(watch.status),
53
+ label: `${watch.id} ${shortLabel(watch)}`,
54
+ labelRole: options.selected ? "accent" : "label",
55
+ trailing: `${watch.matchCount}`,
56
+ });
57
+ }
58
+
32
59
  /** A watcher's label, falling back to its source. */
33
60
  export function shortLabel(watch: WatchRecord): string {
34
61
  return clipLabel(watch.label || sourceLabel(watch));
@@ -62,11 +89,14 @@ export function statusGlyphChar(status: WatchStatus): string {
62
89
  }
63
90
  }
64
91
 
65
- /** Short status word, including an exit code or error when present. */
92
+ /** Short status word, including an exit code, the signal that ended it, or an error when present. */
66
93
  export function statusWord(watch: WatchRecord): string {
67
94
  if (watch.status === "error" && watch.error) return `error (${watch.error})`;
68
- if (watch.status === "exited" && watch.exitCode !== undefined && watch.exitCode !== null) {
69
- return `exited (${watch.exitCode})`;
95
+ if (watch.status === "exited") {
96
+ if (typeof watch.exitCode === "number") return `exited (${watch.exitCode})`;
97
+ // Node reports a signalled child with a null code and the signal that ended it, so the
98
+ // code alone would leave a killed command looking like a clean exit.
99
+ if (watch.signal) return `exited (${watch.signal})`;
70
100
  }
71
101
  return watch.status;
72
102
  }
@@ -96,7 +126,7 @@ export function formatWatchStatus(watch: WatchRecord, now = Date.now()): string
96
126
  const lines = [
97
127
  `${watch.id}: ${statusWord(watch)}`,
98
128
  `source: ${sourceLabel(watch)}`,
99
- `pattern: /${sanitizeLine(watch.pattern)}/${watch.caseSensitive ? "" : "i"}`,
129
+ `pattern: ${formatPattern(watch)}`,
100
130
  ];
101
131
  if (watch.exclude.length > 0)
102
132
  lines.push(`exclude: ${watch.exclude.map((item) => `/${sanitizeLine(item)}/`).join(" ")}`);
@@ -174,9 +204,8 @@ export function formatWatchOutcomeLine(watch: WatchRecord, theme: Theme, now = D
174
204
  /** Unstyled, sanitized detail lines for the `/watch` focus pane. */
175
205
  export function formatWatchDetail(watch: WatchRecord, now = Date.now()): string[] {
176
206
  const age = Math.max(0, (watch.endedAt ?? now) - watch.startedAt);
177
- const flags = `${watch.caseSensitive ? "" : "i"}${watch.wake ? " · wake" : ""}`;
178
207
  return [
179
- sanitizeLine(`${sourceLabel(watch)} · /${watch.pattern}/${flags}`),
208
+ sanitizeLine(`${sourceLabel(watch)} · ${formatPattern(watch)}`),
180
209
  sanitizeLine(
181
210
  `${statusWord(watch)} · ${watch.linesRead} lines · ${watch.matchCount} matches · ${Math.round(age / 1000)}s`,
182
211
  ),
@@ -0,0 +1,57 @@
1
+ /**
2
+ * The `WatchResult` transcript block: the `watch` tool's list-or-matches result.
3
+ *
4
+ * It wraps the shared `TailResult` kit over the `/watch`-style row rail, so the
5
+ * tool's `renderResult` stays a few lines and every row is drawn by `watchRow`
6
+ * in `format.ts`. The `/watch` screen draws the same rows through that helper;
7
+ * this module is on the registration path, and the screen is not.
8
+ */
9
+
10
+ import type { Theme } from "@earendil-works/pi-coding-agent";
11
+ import type { Component } from "@earendil-works/pi-tui";
12
+ import { TailResult, type TailResultInput } from "../../lib/tui/tool.ts";
13
+ import { watchRow } from "./format.ts";
14
+ import type { WatchRecord } from "./types.ts";
15
+
16
+ /** Input for the transcript result: a themed watcher list or a matched-line tail. */
17
+ export type WatchResultInput =
18
+ | { kind: "list"; watches: WatchRecord[]; more: number }
19
+ | { kind: "lines"; label: string; lines: string[]; earlier: number };
20
+
21
+ /** Map the watch vocabulary onto the shared transcript tail block. */
22
+ function watchTail(input: WatchResultInput): TailResultInput<WatchRecord> {
23
+ if (input.kind === "lines") {
24
+ return { kind: "lines", lines: input.lines, earlier: input.earlier, earlierLabel: input.label };
25
+ }
26
+ return {
27
+ kind: "list",
28
+ rows: input.watches,
29
+ more: input.more,
30
+ rowLine: (watch, width, theme) => watchRow(watch, theme, width, { marker: false }),
31
+ };
32
+ }
33
+
34
+ /**
35
+ * Transcript result block for the `watch` tool: the shared tail block over the
36
+ * `/watch`-style row rail or the themed matched-line tail. Reused across renders.
37
+ */
38
+ export class WatchResult implements Component {
39
+ private readonly view: TailResult<WatchRecord>;
40
+
41
+ constructor(input: WatchResultInput, theme: Theme) {
42
+ this.view = new TailResult(watchTail(input), theme);
43
+ }
44
+
45
+ /** Update in place so the transcript can reuse this component across renders. */
46
+ setInput(input: WatchResultInput, theme: Theme): void {
47
+ this.view.setInput(watchTail(input), theme);
48
+ }
49
+
50
+ invalidate(): void {
51
+ this.view.invalidate();
52
+ }
53
+
54
+ render(width: number): string[] {
55
+ return this.view.render(width);
56
+ }
57
+ }
@@ -14,7 +14,7 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
14
14
  import { resolveEffectiveCwd } from "../../lib/env.ts";
15
15
  import { LineSplitter } from "../../lib/lines.ts";
16
16
  import type { KillTreeFn, SpawnFn, SpawnedProcess } from "../../lib/spawn.ts";
17
- import { createChildSupervisor } from "./child.ts";
17
+ import { createChildSupervisor, KILL_GRACE_MS } from "../../lib/child.ts";
18
18
  import { DEFAULT_CONFIG, loadConfig, type WatchConfig } from "./config.ts";
19
19
  import { sanitizeUntrustedLine } from "../../lib/format.ts";
20
20
  import { defaultInterval } from "../../lib/timer.ts";
@@ -24,8 +24,6 @@ import type { PendingMatch, WatchMatch, WatchRecord, WatchSource } from "./types
24
24
 
25
25
  /** Largest slice read from a file per tick. */
26
26
  const MAX_CHUNK = 1024 * 1024;
27
- /** Grace before a stopped command escalates to SIGKILL; `child.ts` defaults the same. */
28
- const KILL_GRACE_MS = 2000;
29
27
 
30
28
  /** Internal watcher state: the public record plus non-serializable fields. */
31
29
  interface Internal {
@@ -277,24 +275,29 @@ export function createWatchRuntime(options: WatchRuntimeOptions = {}): WatchRunt
277
275
  };
278
276
 
279
277
  const startCommand = (w: Internal): void => {
280
- const error = children.start(w, w.source.command ?? "", w.source.cwd ?? process.cwd(), {
281
- onData: (chunk) => consume(w, chunk, now()),
282
- onSettled: (outcome) => {
283
- if (w.status !== "running") return;
284
- if (outcome.kind === "closed") {
285
- flushPending(w, now());
286
- w.status = "exited";
287
- w.exitCode = outcome.code;
288
- w.signal = outcome.signal;
289
- } else {
290
- w.status = "error";
291
- w.error = outcome.message;
292
- }
293
- w.endedAt = now();
294
- ui.paint();
295
- if (runningCount() === 0) stopClock();
278
+ const error = children.start(
279
+ w,
280
+ w.source.command ?? "",
281
+ { cwd: w.source.cwd ?? process.cwd() },
282
+ {
283
+ onData: (chunk) => consume(w, chunk, now()),
284
+ onSettled: (outcome) => {
285
+ if (w.status !== "running") return;
286
+ if (outcome.kind === "closed") {
287
+ flushPending(w, now());
288
+ w.status = "exited";
289
+ w.exitCode = outcome.code;
290
+ w.signal = outcome.signal;
291
+ } else {
292
+ w.status = "error";
293
+ w.error = outcome.message;
294
+ }
295
+ w.endedAt = now();
296
+ ui.paint();
297
+ if (runningCount() === 0) stopClock();
298
+ },
296
299
  },
297
- });
300
+ );
298
301
  if (error === undefined) return;
299
302
  w.status = "error";
300
303
  w.error = error;
@@ -313,8 +316,10 @@ export function createWatchRuntime(options: WatchRuntimeOptions = {}): WatchRunt
313
316
 
314
317
  const add = (opts: AddWatchOptions, ctx: ExtensionContext): WatchRecord => {
315
318
  if (!config.enabled) throw new Error("watch is disabled.");
316
- if (watches.size >= config.maxWatchers) {
317
- throw new Error(`at most ${config.maxWatchers} watchers; stop one first.`);
319
+ if (runningCount() >= config.maxWatchers) {
320
+ throw new Error(
321
+ `at most ${config.maxWatchers} live watcher${config.maxWatchers === 1 ? "" : "s"}; stop one first.`,
322
+ );
318
323
  }
319
324
  const base = resolveEffectiveCwd(ctx.cwd);
320
325
  const source: WatchSource = opts.path
@@ -8,7 +8,6 @@
8
8
  */
9
9
 
10
10
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
11
- import { sanitizeLine } from "../../lib/format.ts";
12
11
  import { errorResult, textResult } from "../../lib/tool-result.ts";
13
12
  import { reuseText, toolDetails } from "../../lib/tui/chrome.ts";
14
13
  import { callLine, detailsOrLine, rejectionLine } from "../../lib/tui/tool.ts";
@@ -17,6 +16,7 @@ import { paint } from "../../lib/tui/tokens.ts";
17
16
  import {
18
17
  compareWatches,
19
18
  formatCallText,
19
+ formatPattern,
20
20
  formatTail,
21
21
  formatWatchList,
22
22
  formatWatchOutcomeLine,
@@ -25,7 +25,7 @@ import {
25
25
  } from "./format.ts";
26
26
  import type { WatchRuntime } from "./runtime.ts";
27
27
  import { WatchParams, normalizeCall, type WatchArgs, type WatchCall } from "./schema.ts";
28
- import { WatchResult, type WatchResultInput } from "./tui.ts";
28
+ import { WatchResult, type WatchResultInput } from "./render.ts";
29
29
  import type { WatchDetails } from "./types.ts";
30
30
 
31
31
  export const TOOL_NAME = "watch";
@@ -81,9 +81,9 @@ export function registerTools(pi: ExtensionAPI, runtime: WatchRuntime): void {
81
81
  },
82
82
  ctx as ExtensionContext,
83
83
  );
84
- const flags = `${call.caseSensitive ? "" : "i"}${watch.wake ? ", wake" : ""}`;
84
+ const pattern = formatPattern(watch);
85
85
  return textResult(
86
- `Watching ${watch.id}: ${sourceLabel(watch)} → /${sanitizeLine(watch.pattern)}/${flags}\nRead with \`watch matches ${watch.id}\` or \`watch logs ${watch.id}\`.`,
86
+ `Watching ${watch.id}: ${sourceLabel(watch)} → ${pattern}\nRead with \`watch matches ${watch.id}\` or \`watch logs ${watch.id}\`.`,
87
87
  { action: "add", watch } satisfies WatchDetails,
88
88
  );
89
89
  }
@@ -1,87 +1,25 @@
1
1
  /**
2
- * Terminal rendering for the watch extension's `/watch` screen.
2
+ * The `/watch` screen: a dismissible list with a focused detail pane and a
3
+ * matches view, drawn as a `DockListScreen`.
3
4
  *
4
- * `WatchListComponent` is the dismissible `/watch` screen with a focused detail
5
- * pane and a matches view. It reads live state through injected closures and
6
- * tracks the selection by id so a live re-sort cannot move the cursor, through
7
- * the shared `IdCursor` and `LinePane`.
5
+ * It reads live state through injected closures and tracks the selection by id
6
+ * so a live re-sort cannot move the cursor, through the shared `IdCursor` and
7
+ * `LinePane`. The frame, the navigation keys, the y/N confirmation, and the
8
+ * matches pane come from the shared `DockListScreen`, so this module owns only
9
+ * the watch vocabulary: the rows, the focus block, the counts, the hints, and
10
+ * what the verbs do. Matched lines arrive already sanitized by the runtime.
8
11
  *
9
- * The frame, the navigation keys, the y/N confirmation, and the matches pane come
10
- * from the shared `DockListScreen`, so this module owns only the watch
11
- * vocabulary: the row, the focus block, the counts, the hints, and what the
12
- * verbs do. Matched lines arrive already sanitized by the runtime, so no
13
- * untrusted escape sequence reaches the terminal here.
12
+ * Full-screen surfaces live under `view/` and load on first use: the `/watch`
13
+ * command imports this module behind its TUI branch, so registration never
14
+ * pays for it.
14
15
  */
15
16
 
16
17
  import type { Theme } from "@earendil-works/pi-coding-agent";
17
- import type { Component } from "@earendil-works/pi-tui";
18
- import type { ScreenHint, ViewportRowsSource } from "../../lib/tui/chrome.ts";
19
- import { DockListScreen, type DetailContents } from "../../lib/tui/dock-screen.ts";
20
- import { statusRow, type StatusRow } from "../../lib/tui/rows.ts";
21
- import { TailResult, type TailResultInput } from "../../lib/tui/tool.ts";
22
- import { paint } from "../../lib/tui/tokens.ts";
23
- import { compareWatches, formatWatchDetail, shortLabel, statusGlyphChar, statusRole } from "./format.ts";
24
- import type { WatchRecord } from "./types.ts";
25
-
26
- /** One watcher row: marker, status glyph, id, label, and a right-aligned count. */
27
- function watchRow(
28
- watch: WatchRecord,
29
- theme: Theme,
30
- width: number,
31
- options: Pick<StatusRow, "selected" | "marker"> = {},
32
- ): string {
33
- return statusRow(theme, width, {
34
- ...options,
35
- glyph: statusGlyphChar(watch.status),
36
- glyphRole: statusRole(watch.status),
37
- label: `${watch.id} ${shortLabel(watch)}`,
38
- labelRole: options.selected ? "accent" : "label",
39
- trailing: `${watch.matchCount}`,
40
- });
41
- }
42
-
43
- /** Input for the transcript result: a themed watcher list or a matched-line tail. */
44
- export type WatchResultInput =
45
- | { kind: "list"; watches: WatchRecord[]; more: number }
46
- | { kind: "lines"; label: string; lines: string[]; earlier: number };
47
-
48
- /** Map the watch vocabulary onto the shared transcript tail block. */
49
- function watchTail(input: WatchResultInput): TailResultInput<WatchRecord> {
50
- if (input.kind === "lines") {
51
- return { kind: "lines", lines: input.lines, earlier: input.earlier, earlierLabel: input.label };
52
- }
53
- return {
54
- kind: "list",
55
- rows: input.watches,
56
- more: input.more,
57
- rowLine: (watch, width, theme) => watchRow(watch, theme, width, { marker: false }),
58
- };
59
- }
60
-
61
- /**
62
- * Transcript result block for the `watch` tool: the shared tail block over the
63
- * `/watch`-style row rail or the themed matched-line tail. Reused across renders.
64
- */
65
- export class WatchResult implements Component {
66
- private readonly view: TailResult<WatchRecord>;
67
-
68
- constructor(input: WatchResultInput, theme: Theme) {
69
- this.view = new TailResult(watchTail(input), theme);
70
- }
71
-
72
- /** Update in place so the transcript can reuse this component across renders. */
73
- setInput(input: WatchResultInput, theme: Theme): void {
74
- this.view.setInput(watchTail(input), theme);
75
- }
76
-
77
- invalidate(): void {
78
- this.view.invalidate();
79
- }
80
-
81
- render(width: number): string[] {
82
- return this.view.render(width);
83
- }
84
- }
18
+ import type { ScreenHint, ViewportRowsSource } from "../../../lib/tui/chrome.ts";
19
+ import { DockListScreen, type DetailContents } from "../../../lib/tui/dock-screen.ts";
20
+ import { paint } from "../../../lib/tui/tokens.ts";
21
+ import { compareWatches, formatWatchDetail, shortLabel, watchRow } from "../format.ts";
22
+ import type { WatchRecord } from "../types.ts";
85
23
 
86
24
  export interface WatchListCallbacks {
87
25
  /** Current matched lines for a watcher, or undefined when unknown. */
@@ -157,8 +95,8 @@ export class WatchListComponent extends DockListScreen<WatchRecord> {
157
95
  "Esc close",
158
96
  "↑/↓ or j/k select",
159
97
  { text: "Enter or l matches", color: "accent" },
160
- { text: "x or K stop", color: "warning" },
161
- { text: "d clear stopped", color: "warning" },
98
+ { text: "x or K stop", color: "warn" },
99
+ { text: "d clear stopped", color: "warn" },
162
100
  "PgUp/PgDn",
163
101
  ];
164
102
  }
@@ -28,9 +28,9 @@ import { charLength } from "../text.ts";
28
28
  import { oneLine } from "../transcript.ts";
29
29
  import { createPageCache, type PageCache } from "./cache.ts";
30
30
  import type { WebFetchConfig } from "./config.ts";
31
- import { findPassages, type Passage } from "./find.ts";
31
+ import type { Passage } from "./find.ts";
32
32
  import { formatMatches, formatPage } from "./format.ts";
33
- import { runFetch, type FetchDeps, type PageResult } from "./page.ts";
33
+ import type { FetchDeps, PageResult } from "./page.ts";
34
34
  import { resolveRequest, WebFetchOutput, WebFetchParams, type FetchResponse, type WebFetchArgs } from "./schema.ts";
35
35
  import type { FetchRequest } from "./types.ts";
36
36
  import { loadFetchConfig } from "../config.ts";
@@ -66,6 +66,10 @@ async function loadPage(
66
66
  deps: FetchDeps,
67
67
  cache: PageCache,
68
68
  ): Promise<LoadedPage> {
69
+ // The page loader is the call's, not registration's: the schema and the
70
+ // renderers above never need it, so a listing loads neither it nor its
71
+ // extract/ssrf/PDF subtree.
72
+ const { runFetch } = await import("./page.ts");
69
73
  const cacheable = config.cacheEnabled && request.cacheable && !request.refresh;
70
74
  const cachedEntry = cacheable ? cache.get(url, config.cacheTtlMs) : undefined;
71
75
  if (cachedEntry) return { page: cachedEntry, cached: true, fetchedAt: cachedEntry.fetchedAt };
@@ -83,7 +87,10 @@ async function loadPage(
83
87
  }
84
88
 
85
89
  /** Build the response entry and model-facing text for one fetched page. */
86
- function pageResult(loaded: LoadedPage, request: FetchRequest): { response: FetchResponse; text: string } {
90
+ async function pageResult(
91
+ loaded: LoadedPage,
92
+ request: FetchRequest,
93
+ ): Promise<{ response: FetchResponse; text: string }> {
87
94
  const { page, cached, fetchedAt } = loaded;
88
95
  const finding = request.find.length > 0;
89
96
 
@@ -93,6 +100,7 @@ function pageResult(loaded: LoadedPage, request: FetchRequest): { response: Fetc
93
100
  let matchesTruncated = false;
94
101
 
95
102
  if (finding) {
103
+ const { findPassages } = await import("./find.ts");
96
104
  // Request one extra so a full list proves more hits exist; only the cap is shown.
97
105
  const found = findPassages(page.text, request.find, {
98
106
  mode: request.mode,
@@ -211,7 +219,7 @@ export function registerFetchTool(pi: ExtensionAPI, deps: FetchToolDeps = {}): v
211
219
  let text: string;
212
220
  try {
213
221
  const loaded = await loadPage(request.url, request, config, signal, fetchDeps, cache);
214
- ({ response: page, text } = pageResult(loaded, request));
222
+ ({ response: page, text } = await pageResult(loaded, request));
215
223
  } catch (error) {
216
224
  const message = error instanceof Error ? error.message : String(error);
217
225
  ({ response: page, text } = fetchErrorResult(request.url, request, message));