@crouton-kit/crouter 0.3.27 → 0.3.29

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 (343) hide show
  1. package/dist/build-root.js +4 -2
  2. package/dist/builtin-memory/crouter-development/marketplaces.md +7 -6
  3. package/dist/builtin-memory/crouter-development/personas/base-prompt.md +1 -1
  4. package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +1 -1
  5. package/dist/builtin-memory/crouter-development/personas.md +1 -1
  6. package/dist/builtin-memory/crouter-development/plugins.md +20 -18
  7. package/dist/builtin-memory/design.md +1 -1
  8. package/dist/builtin-memory/development.md +1 -1
  9. package/dist/builtin-memory/internal/INDEX.md +1 -1
  10. package/dist/builtin-memory/internal/examples/INDEX.md +1 -1
  11. package/dist/builtin-memory/internal/examples/imessage-assistant.md +2 -2
  12. package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
  13. package/dist/builtin-memory/internal/storage-tiers.md +1 -1
  14. package/dist/builtin-memory/planning.md +1 -1
  15. package/dist/builtin-memory/spec.md +6 -4
  16. package/dist/builtin-personas/orchestration-kernel.md +4 -5
  17. package/dist/builtin-personas/runtime-base.md +8 -4
  18. package/dist/builtin-personas/spec/PERSONA.md +8 -4
  19. package/dist/builtin-personas/spec/orchestrator.md +5 -3
  20. package/dist/builtin-personas/spec/requirements/PERSONA.md +10 -0
  21. package/dist/builtin-views/canvas/core.mjs +586 -0
  22. package/dist/builtin-views/canvas/text.mjs +58 -0
  23. package/dist/builtin-views/canvas/tui.mjs +165 -0
  24. package/dist/builtin-views/canvas/web.jsx +120 -0
  25. package/dist/builtin-views/git-pr/core.mjs +673 -0
  26. package/dist/builtin-views/git-pr/text.mjs +84 -0
  27. package/dist/builtin-views/git-pr/tui.mjs +302 -0
  28. package/dist/builtin-views/git-pr/web.jsx +216 -0
  29. package/dist/builtin-views/inbox/_lib/render.mjs +1 -3
  30. package/dist/builtin-views/inbox/core.mjs +1273 -0
  31. package/dist/builtin-views/inbox/text.mjs +73 -0
  32. package/dist/builtin-views/inbox/tui.mjs +312 -0
  33. package/dist/builtin-views/inbox/web.jsx +188 -0
  34. package/dist/builtin-views/linkedin/core.mjs +906 -0
  35. package/dist/builtin-views/linkedin/text.mjs +69 -0
  36. package/dist/builtin-views/linkedin/tui.mjs +427 -0
  37. package/dist/builtin-views/linkedin/web.jsx +206 -0
  38. package/dist/builtin-views/workspace-sidebar/core.mjs +621 -0
  39. package/dist/builtin-views/workspace-sidebar/text.mjs +53 -0
  40. package/dist/builtin-views/workspace-sidebar/tui.mjs +142 -0
  41. package/dist/builtin-views/workspace-sidebar/web.jsx +109 -0
  42. package/dist/cli.js +3 -3
  43. package/dist/clients/attach/__tests__/action-parity.test.js +48 -0
  44. package/dist/clients/attach/__tests__/clipboard-image.test.js +76 -0
  45. package/dist/clients/attach/__tests__/editor-newline.test.js +57 -0
  46. package/dist/clients/attach/__tests__/git-info.test.js +31 -0
  47. package/dist/clients/attach/__tests__/onrequest-wired.test.js +20 -0
  48. package/dist/clients/attach/__tests__/reconnect-giveup.test.js +30 -0
  49. package/dist/clients/attach/__tests__/slash-quit-copy.test.js +42 -0
  50. package/dist/clients/attach/__tests__/titled-editor.test.d.ts +1 -0
  51. package/dist/clients/attach/__tests__/titled-editor.test.js +39 -0
  52. package/dist/clients/attach/attach-cmd.js +445 -41
  53. package/dist/clients/attach/auth-pickers.d.ts +9 -0
  54. package/dist/clients/attach/auth-pickers.js +194 -0
  55. package/dist/clients/attach/canvas-panels.d.ts +12 -0
  56. package/dist/clients/attach/canvas-panels.js +78 -0
  57. package/dist/clients/attach/chat-view.d.ts +27 -2
  58. package/dist/clients/attach/chat-view.js +98 -9
  59. package/dist/clients/attach/clipboard-image.d.ts +47 -8
  60. package/dist/clients/attach/clipboard-image.js +150 -26
  61. package/dist/clients/attach/clipboard-text.d.ts +3 -0
  62. package/dist/clients/attach/clipboard-text.js +39 -0
  63. package/dist/clients/attach/config-load.d.ts +54 -7
  64. package/dist/clients/attach/config-load.js +99 -3
  65. package/dist/clients/attach/context-message.d.ts +12 -0
  66. package/dist/clients/attach/context-message.js +72 -0
  67. package/dist/clients/attach/git-info.d.ts +20 -0
  68. package/dist/clients/attach/git-info.js +44 -0
  69. package/dist/clients/attach/graph-overlay.d.ts +43 -0
  70. package/dist/clients/attach/graph-overlay.js +306 -0
  71. package/dist/clients/attach/input-controller.d.ts +81 -13
  72. package/dist/clients/attach/input-controller.js +307 -67
  73. package/dist/clients/attach/pickers.d.ts +58 -0
  74. package/dist/clients/attach/pickers.js +171 -0
  75. package/dist/clients/attach/slash-commands.d.ts +44 -1
  76. package/dist/clients/attach/slash-commands.js +250 -27
  77. package/dist/clients/attach/titled-editor.d.ts +33 -0
  78. package/dist/clients/attach/titled-editor.js +84 -0
  79. package/dist/clients/attach/view-socket.d.ts +37 -2
  80. package/dist/clients/attach/view-socket.js +131 -7
  81. package/dist/clients/web/dev-server.d.ts +7 -0
  82. package/dist/clients/web/dev-server.js +59 -0
  83. package/dist/clients/web/events.d.ts +14 -0
  84. package/dist/clients/web/events.js +151 -0
  85. package/dist/clients/web/server.d.ts +18 -0
  86. package/dist/clients/web/server.js +450 -0
  87. package/dist/clients/web/web-cmd.d.ts +2 -0
  88. package/dist/clients/web/web-cmd.js +120 -0
  89. package/dist/commands/canvas.js +1 -2
  90. package/dist/commands/chord.js +1 -1
  91. package/dist/commands/dashboard.js +5 -1
  92. package/dist/commands/human/prompts.js +3 -3
  93. package/dist/commands/human/queue.d.ts +17 -0
  94. package/dist/commands/human/queue.js +111 -4
  95. package/dist/commands/memory/__tests__/lint-schema.test.js +27 -4
  96. package/dist/commands/memory/lint.d.ts +5 -4
  97. package/dist/commands/memory/lint.js +23 -7
  98. package/dist/commands/memory/read.js +2 -2
  99. package/dist/commands/memory/shared.d.ts +7 -1
  100. package/dist/commands/memory/shared.js +18 -3
  101. package/dist/commands/memory/write.js +20 -7
  102. package/dist/commands/memory.js +4 -4
  103. package/dist/commands/node.d.ts +1 -1
  104. package/dist/commands/node.js +64 -65
  105. package/dist/commands/pkg.js +1 -2
  106. package/dist/commands/revive.js +11 -3
  107. package/dist/commands/sys/feedback.d.ts +1 -0
  108. package/dist/commands/sys/feedback.js +163 -0
  109. package/dist/commands/sys/sync.d.ts +1 -0
  110. package/dist/commands/sys/sync.js +187 -0
  111. package/dist/commands/sys.js +4 -2
  112. package/dist/commands/view-cycle.js +2 -2
  113. package/dist/commands/view-list.js +8 -8
  114. package/dist/commands/view-new.js +21 -17
  115. package/dist/commands/view-pick.js +1 -1
  116. package/dist/commands/view-run.js +35 -14
  117. package/dist/commands/view.js +8 -6
  118. package/dist/commands/workspace.d.ts +2 -0
  119. package/dist/commands/workspace.js +161 -0
  120. package/dist/core/__tests__/broker-double-spawn.test.d.ts +1 -0
  121. package/dist/core/__tests__/broker-double-spawn.test.js +143 -0
  122. package/dist/core/__tests__/broker-fork-seam.test.d.ts +1 -0
  123. package/dist/core/__tests__/broker-fork-seam.test.js +102 -0
  124. package/dist/core/__tests__/broker-sdk-wiring.test.js +2 -0
  125. package/dist/core/__tests__/broker-snapshot-history.test.d.ts +1 -0
  126. package/dist/core/__tests__/broker-snapshot-history.test.js +105 -0
  127. package/dist/core/__tests__/child-death-wake.test.js +56 -111
  128. package/dist/core/__tests__/close.test.js +14 -20
  129. package/dist/core/__tests__/context-intro.test.js +19 -19
  130. package/dist/core/__tests__/daemon-boot.test.js +30 -23
  131. package/dist/core/__tests__/draw-style.test.js +1 -1
  132. package/dist/core/__tests__/error-stall-recycle.test.d.ts +1 -0
  133. package/dist/core/__tests__/error-stall-recycle.test.js +141 -0
  134. package/dist/core/__tests__/fixtures/fake-engine.d.ts +24 -1
  135. package/dist/core/__tests__/fixtures/fake-engine.js +32 -2
  136. package/dist/core/__tests__/fixtures/fake-pi-host.js +2 -2
  137. package/dist/core/__tests__/focuses.test.js +25 -13
  138. package/dist/core/__tests__/fork.test.js +22 -5
  139. package/dist/core/__tests__/full/broker-control-preempt.test.d.ts +1 -0
  140. package/dist/core/__tests__/full/broker-control-preempt.test.js +61 -0
  141. package/dist/core/__tests__/full/broker-model-changed-broadcast.test.d.ts +1 -0
  142. package/dist/core/__tests__/full/broker-model-changed-broadcast.test.js +57 -0
  143. package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.d.ts +1 -0
  144. package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.js +80 -0
  145. package/dist/core/__tests__/full/broker-pane-resolution.test.js +90 -0
  146. package/dist/core/__tests__/full/cascade-close.test.js +22 -8
  147. package/dist/core/__tests__/full/detach-focus.test.js +66 -169
  148. package/dist/core/__tests__/helpers/harness.d.ts +6 -9
  149. package/dist/core/__tests__/helpers/harness.js +19 -36
  150. package/dist/core/__tests__/human-node-not-supervised.test.d.ts +1 -0
  151. package/dist/core/__tests__/human-node-not-supervised.test.js +83 -0
  152. package/dist/core/__tests__/human-stranded-deliver.test.d.ts +1 -0
  153. package/dist/core/__tests__/human-stranded-deliver.test.js +108 -0
  154. package/dist/core/__tests__/lifecycle.test.js +1 -19
  155. package/dist/core/__tests__/live-mutation-verbs.test.js +5 -5
  156. package/dist/core/__tests__/memory-resolver.test.js +1 -1
  157. package/dist/core/__tests__/on-read-dedup-resume.test.d.ts +1 -0
  158. package/dist/core/__tests__/on-read-dedup-resume.test.js +81 -0
  159. package/dist/core/__tests__/refresh-stall-recycle.test.d.ts +1 -0
  160. package/dist/core/__tests__/refresh-stall-recycle.test.js +88 -0
  161. package/dist/core/__tests__/relaunch-root.test.d.ts +1 -0
  162. package/dist/core/__tests__/relaunch-root.test.js +114 -0
  163. package/dist/core/__tests__/reset.test.js +19 -79
  164. package/dist/core/__tests__/steer-note.test.js +9 -7
  165. package/dist/core/__tests__/tmux-surface.test.js +13 -7
  166. package/dist/core/bootstrap.d.ts +0 -3
  167. package/dist/core/bootstrap.js +1 -143
  168. package/dist/core/canvas/browse/__tests__/model.test.js +86 -1
  169. package/dist/core/canvas/browse/__tests__/render.test.js +2 -1
  170. package/dist/core/canvas/browse/app.js +131 -8
  171. package/dist/core/canvas/browse/model.d.ts +23 -5
  172. package/dist/core/canvas/browse/model.js +68 -13
  173. package/dist/core/canvas/browse/render.d.ts +7 -1
  174. package/dist/core/canvas/browse/render.js +178 -47
  175. package/dist/core/canvas/canvas.js +1 -1
  176. package/dist/core/canvas/focuses.d.ts +4 -4
  177. package/dist/core/canvas/focuses.js +7 -7
  178. package/dist/core/canvas/nav-model.d.ts +162 -0
  179. package/dist/core/canvas/nav-model.js +487 -0
  180. package/dist/core/canvas/paths.d.ts +7 -0
  181. package/dist/core/canvas/paths.js +9 -0
  182. package/dist/core/canvas/render.d.ts +68 -9
  183. package/dist/core/canvas/render.js +195 -61
  184. package/dist/core/canvas/types.d.ts +16 -23
  185. package/dist/core/config.js +1 -4
  186. package/dist/core/memory-resolver.js +43 -5
  187. package/dist/core/runtime/bearings.d.ts +1 -1
  188. package/dist/core/runtime/bearings.js +8 -8
  189. package/dist/core/runtime/branded-host.d.ts +16 -0
  190. package/dist/core/runtime/branded-host.js +127 -0
  191. package/dist/core/runtime/broker-protocol.d.ts +212 -6
  192. package/dist/core/runtime/broker-sdk.d.ts +0 -12
  193. package/dist/core/runtime/broker-sdk.js +77 -6
  194. package/dist/core/runtime/broker.d.ts +5 -1
  195. package/dist/core/runtime/broker.js +520 -59
  196. package/dist/core/runtime/close.d.ts +12 -2
  197. package/dist/core/runtime/close.js +37 -16
  198. package/dist/core/runtime/front-door.js +14 -7
  199. package/dist/core/runtime/host.d.ts +14 -34
  200. package/dist/core/runtime/host.js +14 -50
  201. package/dist/core/runtime/launch.d.ts +12 -9
  202. package/dist/core/runtime/launch.js +27 -15
  203. package/dist/core/runtime/lifecycle.d.ts +1 -1
  204. package/dist/core/runtime/lifecycle.js +15 -19
  205. package/dist/core/runtime/nodes.d.ts +0 -55
  206. package/dist/core/runtime/nodes.js +6 -74
  207. package/dist/core/runtime/placement.d.ts +91 -334
  208. package/dist/core/runtime/placement.js +262 -828
  209. package/dist/core/runtime/promote.d.ts +2 -0
  210. package/dist/core/runtime/promote.js +24 -6
  211. package/dist/core/runtime/recap.d.ts +8 -0
  212. package/dist/core/runtime/recap.js +107 -0
  213. package/dist/core/runtime/recycle.js +25 -61
  214. package/dist/core/runtime/reset.d.ts +43 -37
  215. package/dist/core/runtime/reset.js +131 -218
  216. package/dist/core/runtime/revive.d.ts +9 -29
  217. package/dist/core/runtime/revive.js +45 -135
  218. package/dist/core/runtime/spawn.d.ts +15 -11
  219. package/dist/core/runtime/spawn.js +168 -136
  220. package/dist/core/runtime/surface-bg.d.ts +11 -0
  221. package/dist/core/runtime/surface-bg.js +68 -0
  222. package/dist/core/runtime/tmux.d.ts +15 -43
  223. package/dist/core/runtime/tmux.js +38 -80
  224. package/dist/core/scope.d.ts +1 -2
  225. package/dist/core/scope.js +1 -5
  226. package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.d.ts +1 -0
  227. package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.js +57 -0
  228. package/dist/core/skill-sync/builtins.d.ts +42 -0
  229. package/dist/core/skill-sync/builtins.js +112 -0
  230. package/dist/core/skill-sync/claude-plugins.d.ts +23 -0
  231. package/dist/core/skill-sync/claude-plugins.js +71 -0
  232. package/dist/core/skill-sync/engine.d.ts +42 -0
  233. package/dist/core/skill-sync/engine.js +633 -0
  234. package/dist/core/skill-sync/export.d.ts +23 -0
  235. package/dist/core/skill-sync/export.js +86 -0
  236. package/dist/core/skill-sync/manifest.d.ts +64 -0
  237. package/dist/core/skill-sync/manifest.js +181 -0
  238. package/dist/core/skill-sync/profile.d.ts +76 -0
  239. package/dist/core/skill-sync/profile.js +173 -0
  240. package/dist/core/skill-sync/snapshot.d.ts +57 -0
  241. package/dist/core/skill-sync/snapshot.js +120 -0
  242. package/dist/core/spawn.d.ts +14 -0
  243. package/dist/core/spawn.js +29 -9
  244. package/dist/core/substrate/index.d.ts +2 -2
  245. package/dist/core/substrate/index.js +7 -7
  246. package/dist/core/substrate/injected-store.d.ts +10 -0
  247. package/dist/core/substrate/injected-store.js +55 -0
  248. package/dist/core/substrate/on-read.js +68 -6
  249. package/dist/core/substrate/render.d.ts +8 -11
  250. package/dist/core/substrate/render.js +29 -43
  251. package/dist/core/substrate/schema.d.ts +16 -11
  252. package/dist/core/substrate/schema.js +32 -31
  253. package/dist/core/tui/host.d.ts +12 -4
  254. package/dist/core/tui/host.js +280 -149
  255. package/dist/core/view/bridge.d.ts +10 -0
  256. package/dist/core/view/bridge.js +31 -0
  257. package/dist/core/view/chrome.d.ts +9 -0
  258. package/dist/core/view/chrome.js +22 -0
  259. package/dist/core/view/contract.d.ts +171 -0
  260. package/dist/core/view/contract.js +23 -0
  261. package/dist/core/view/loader.d.ts +31 -0
  262. package/dist/core/view/loader.js +188 -0
  263. package/dist/core/view/transport-local.d.ts +7 -0
  264. package/dist/core/view/transport-local.js +70 -0
  265. package/dist/core/view/transport.d.ts +4 -0
  266. package/dist/core/view/transport.js +15 -0
  267. package/dist/daemon/crtrd.d.ts +38 -27
  268. package/dist/daemon/crtrd.js +303 -376
  269. package/dist/daemon/manage.js +6 -1
  270. package/dist/index.js +1 -1
  271. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +28 -20
  272. package/dist/pi-extensions/canvas-context-intro.js +4 -4
  273. package/dist/pi-extensions/canvas-doc-substrate.js +29 -20
  274. package/dist/pi-extensions/canvas-nav.js +35 -385
  275. package/dist/pi-extensions/canvas-recap.d.ts +37 -0
  276. package/dist/pi-extensions/canvas-recap.js +236 -0
  277. package/dist/pi-extensions/canvas-resume.js +2 -1
  278. package/dist/pi-extensions/canvas-stophook.d.ts +15 -13
  279. package/dist/pi-extensions/canvas-stophook.js +119 -103
  280. package/dist/pi-extensions/canvas-view.js +2 -1
  281. package/dist/pi-extensions/widget-order-bus.d.ts +6 -0
  282. package/dist/pi-extensions/widget-order-bus.js +34 -0
  283. package/dist/prompts/view.d.ts +2 -2
  284. package/dist/prompts/view.js +145 -73
  285. package/dist/types.d.ts +1 -6
  286. package/dist/types.js +1 -3
  287. package/dist/web/ViewChrome.d.ts +7 -0
  288. package/dist/web/ViewChrome.js +28 -0
  289. package/dist/web/ViewPane.d.ts +39 -0
  290. package/dist/web/ViewPane.js +48 -0
  291. package/dist/web/index.d.ts +6 -0
  292. package/dist/web/index.js +16 -0
  293. package/dist/web/runtime.d.ts +39 -0
  294. package/dist/web/runtime.js +133 -0
  295. package/dist/web/states.d.ts +24 -0
  296. package/dist/web/states.js +24 -0
  297. package/dist/web/transport-http.d.ts +5 -0
  298. package/dist/web/transport-http.js +28 -0
  299. package/dist/web-client/assets/index-BUvQb4hR.css +2 -0
  300. package/dist/web-client/assets/index-ClLQXYAE.js +10 -0
  301. package/dist/web-client/index.html +13 -0
  302. package/package.json +20 -6
  303. package/dist/builtin-views/canvas/client.mjs +0 -303
  304. package/dist/builtin-views/canvas/view.mjs +0 -576
  305. package/dist/builtin-views/git-pr/client.mjs +0 -440
  306. package/dist/builtin-views/git-pr/view.mjs +0 -675
  307. package/dist/builtin-views/inbox/sources/gmail.mjs +0 -965
  308. package/dist/builtin-views/inbox/sources/linkedin.mjs +0 -427
  309. package/dist/builtin-views/inbox/view.mjs +0 -889
  310. package/dist/builtin-views/linkedin/client.mjs +0 -610
  311. package/dist/builtin-views/linkedin/view.mjs +0 -1171
  312. package/dist/commands/pkg/bridge.d.ts +0 -1
  313. package/dist/commands/pkg/bridge.js +0 -137
  314. package/dist/commands/skill/author.d.ts +0 -3
  315. package/dist/commands/skill/author.js +0 -140
  316. package/dist/commands/skill/shared.d.ts +0 -3
  317. package/dist/commands/skill/shared.js +0 -19
  318. package/dist/commands/skill.d.ts +0 -2
  319. package/dist/commands/skill.js +0 -21
  320. package/dist/commands/tmux-spread.d.ts +0 -2
  321. package/dist/commands/tmux-spread.js +0 -144
  322. package/dist/core/__tests__/full/daemon-liveness-pane.full.test.js +0 -292
  323. package/dist/core/__tests__/full/placement-focus.test.js +0 -309
  324. package/dist/core/__tests__/full/placement-reconcile.test.js +0 -212
  325. package/dist/core/__tests__/full/placement-revive.test.js +0 -238
  326. package/dist/core/__tests__/full/placement-teardown.test.js +0 -221
  327. package/dist/core/__tests__/home-session.test.js +0 -186
  328. package/dist/core/__tests__/relaunch.test.js +0 -335
  329. package/dist/core/bridge-map.d.ts +0 -19
  330. package/dist/core/bridge-map.js +0 -73
  331. package/dist/core/tui/contract.d.ts +0 -83
  332. package/dist/core/tui/contract.js +0 -8
  333. package/dist/core/tui/loader.d.ts +0 -16
  334. package/dist/core/tui/loader.js +0 -94
  335. package/dist/prompts/skill.d.ts +0 -2
  336. package/dist/prompts/skill.js +0 -650
  337. /package/dist/{core/__tests__/full/daemon-liveness-pane.full.test.d.ts → clients/attach/__tests__/action-parity.test.d.ts} +0 -0
  338. /package/dist/{core/__tests__/full/placement-focus.test.d.ts → clients/attach/__tests__/clipboard-image.test.d.ts} +0 -0
  339. /package/dist/{core/__tests__/full/placement-reconcile.test.d.ts → clients/attach/__tests__/editor-newline.test.d.ts} +0 -0
  340. /package/dist/{core/__tests__/full/placement-revive.test.d.ts → clients/attach/__tests__/git-info.test.d.ts} +0 -0
  341. /package/dist/{core/__tests__/full/placement-teardown.test.d.ts → clients/attach/__tests__/onrequest-wired.test.d.ts} +0 -0
  342. /package/dist/{core/__tests__/home-session.test.d.ts → clients/attach/__tests__/reconnect-giveup.test.d.ts} +0 -0
  343. /package/dist/{core/__tests__/relaunch.test.d.ts → clients/attach/__tests__/slash-quit-copy.test.d.ts} +0 -0
@@ -0,0 +1,487 @@
1
+ // nav-model.ts — the pure model/render layer of the canvas graph-nav chrome.
2
+ //
3
+ // Extracted from src/pi-extensions/canvas-nav.ts so a SINGLE source feeds both
4
+ // the legacy pi extension (canvas-nav) and the headless `crtr attach` viewer.
5
+ // Everything here is pi-API-free: it reads the canvas (sqlite + meta.json + node
6
+ // telemetry) and produces ANSI-styled strings + a flattened graph model. The
7
+ // extension-host wiring (widgets, key taps, the ask-poll timer, run/focus
8
+ // shelling) stays in canvas-nav.ts and drives these functions.
9
+ //
10
+ // Mutable interaction state that the legacy extension held at module scope —
11
+ // the per-node ask counts and the manual fold overrides — is threaded through
12
+ // parameters here (see `asks` and `FoldState`) so this layer owns no singletons
13
+ // and each consumer keeps its own state.
14
+ import { execFileSync } from 'node:child_process';
15
+ import { existsSync, readFileSync } from 'node:fs';
16
+ import { join } from 'node:path';
17
+ import { getNode, subscribersOf, subscriptionsOf, jobDir, listFocuses } from './index.js';
18
+ // ---------------------------------------------------------------------------
19
+ // Tuning constants
20
+ // ---------------------------------------------------------------------------
21
+ /** pi's InteractiveMode.MAX_WIDGET_LINES — the hard cap on lines in a string
22
+ * array widget; anything beyond it pi truncates with its own "... (widget
23
+ * truncated)". Our GRAPH viewport stays at/under this and scrolls internally. */
24
+ export const PI_MAX_WIDGET_LINES = 10;
25
+ export const VIEWPORT_FALLBACK_ROWS = 30;
26
+ // ---------------------------------------------------------------------------
27
+ // ANSI styling. pi renders embedded escapes in widget lines and measures width
28
+ // ANSI-aware, so raw escapes are safe and need no pi-tui dependency. The cursor
29
+ // (selected row) uses a theme-agnostic ATTRIBUTE (reverse), so it reads under
30
+ // NO_COLOR; the attached-row tint is a background COLOUR, while the dot glyph
31
+ // (●/○/✓/✗) carries the running signal independently, even where colour is
32
+ // stripped.
33
+ // ---------------------------------------------------------------------------
34
+ export const ESC = '\x1b[';
35
+ export const RESET = `${ESC}0m`;
36
+ export const BOLD = `${ESC}1m`;
37
+ export const DIM = `${ESC}2m`;
38
+ export const REVERSE = `${ESC}7m`;
39
+ /** Dark-green background bar marking an ATTACHED node (a human is currently
40
+ * watching it) — distinct from the cursor's reverse-video bar; chosen so
41
+ * default-fg text stays readable. */
42
+ export const BG_ATTACHED = `${ESC}48;5;22m`;
43
+ export const GREEN = `${ESC}32m`;
44
+ export const RED = `${ESC}31m`;
45
+ export const YELLOW = `${ESC}33m`;
46
+ export const CYAN = `${ESC}36m`;
47
+ export const GRAY = `${ESC}90m`;
48
+ /** Status glyph colored by state: active green, idle dim, done cyan, dead red. */
49
+ export function coloredGlyph(node) {
50
+ if (node === null)
51
+ return '?';
52
+ switch (node.status) {
53
+ case 'active': return `${GREEN}●${RESET}`;
54
+ case 'idle': return `${GRAY}○${RESET}`;
55
+ case 'done': return `${CYAN}✓${RESET}`;
56
+ case 'dead': return `${RED}✗${RESET}`;
57
+ case 'canceled': return `${YELLOW}⊘${RESET}`;
58
+ default: return '?';
59
+ }
60
+ }
61
+ const ANSI_RE = /\x1b\[[0-9;]*m/g;
62
+ /** Visible width, ignoring ANSI escapes. */
63
+ export function visibleWidth(s) {
64
+ return s.replace(ANSI_RE, '').length;
65
+ }
66
+ /** Truncate to `max` VISIBLE columns: escape sequences are copied through
67
+ * verbatim (so a cut never lands mid-escape) and the result always ends in
68
+ * RESET, so a clipped style can't bleed into the editor below. */
69
+ export function truncate(s, max = fillWidth()) {
70
+ if (visibleWidth(s) <= max)
71
+ return s;
72
+ let out = '';
73
+ let w = 0;
74
+ let i = 0;
75
+ while (i < s.length && w < max - 1) {
76
+ if (s[i] === '\x1b') {
77
+ const m = /^\x1b\[[0-9;]*m/.exec(s.slice(i));
78
+ if (m) {
79
+ out += m[0];
80
+ i += m[0].length;
81
+ continue;
82
+ }
83
+ }
84
+ out += s[i];
85
+ w++;
86
+ i++;
87
+ }
88
+ return `${out}…${RESET}`;
89
+ }
90
+ /** Visible columns available to ONE widget line — the cap for every line we
91
+ * emit, and the width a full-width reverse-video SELF bar fills to.
92
+ *
93
+ * pi does NOT clip widget lines; it WRAPS them. Each string line is wrapped in
94
+ * a `Text(paddingX = 1)` inside a full-terminal-width container, so the usable
95
+ * content width is `columns - 2` (a 1-col margin on each side). A line wider
96
+ * than that wraps, and the overflow spills onto a second row as a stray
97
+ * reverse-video block (the bug this guards against). Clamp to `columns - 2`. */
98
+ export function fillWidth() {
99
+ return Math.max(20, Math.min((process.stdout.columns ?? 80) - 2, 180));
100
+ }
101
+ /** Wrap `content` in a full-width background bar opened by `open` (REVERSE for
102
+ * the cursor, BG_ATTACHED for a human-watched node). `open` is re-asserted after every
103
+ * embedded RESET so a coloured cell (the status dot) can't punch a hole in the
104
+ * bar; the visible width is padded out to `width`; the line closes with a real
105
+ * RESET so the style never bleeds into the editor below. */
106
+ export function fillBar(content, width, open) {
107
+ const clipped = truncate(content, width);
108
+ const reasserted = clipped.replace(/\x1b\[0m/g, `${RESET}${open}`);
109
+ const pad = Math.max(0, width - visibleWidth(clipped));
110
+ return `${open}${reasserted}${' '.repeat(pad)}${RESET}`;
111
+ }
112
+ // ---------------------------------------------------------------------------
113
+ // Per-frame read cache
114
+ //
115
+ // One render fans the SAME handful of nodes out 5-10× each: computeSubtreeActivity
116
+ // walks the whole tree (getNode + subscriptions per node), buildGraphModel walks it
117
+ // again (climbRoot, sortedChildIds), then every visible row re-reads getNode +
118
+ // telemetry + subscriptions for its badges. getNode alone is an fs.readFileSync of
119
+ // meta.json plus a sqlite row read, and readTelemetry another readFileSync — so on a
120
+ // modest graph a single 'j' keystroke fanned out to DOZENS of redundant disk reads.
121
+ // That redundancy is the GRAPH view's open/scroll lag.
122
+ //
123
+ // Each render is a synchronous snapshot, so we memoize the read primitives for the
124
+ // duration of one frame and clear at the top of render(): the next keystroke (or the
125
+ // ask-poll / inbox render) re-reads fresh, so a killed node, a new spawn, or a status
126
+ // flip all surface on the very next frame. Nothing here writes, so a stale entry can
127
+ // at worst be one frame old — and render() always reruns immediately after any action.
128
+ // ---------------------------------------------------------------------------
129
+ const frameNodes = new Map();
130
+ const frameTelem = new Map();
131
+ const frameSubs = new Map();
132
+ const frameMgrs = new Map();
133
+ export function beginFrame() {
134
+ frameNodes.clear();
135
+ frameTelem.clear();
136
+ frameSubs.clear();
137
+ frameMgrs.clear();
138
+ }
139
+ /** getNode memoized for the current frame (meta.json read + sqlite row). */
140
+ export function cNode(id) {
141
+ if (frameNodes.has(id))
142
+ return frameNodes.get(id) ?? null;
143
+ const v = getNode(id);
144
+ frameNodes.set(id, v);
145
+ return v;
146
+ }
147
+ /** subscriptionsOf (a node's reports) memoized for the current frame. */
148
+ export function cSubscriptions(id) {
149
+ const hit = frameSubs.get(id);
150
+ if (hit !== undefined)
151
+ return hit;
152
+ let v;
153
+ try {
154
+ v = subscriptionsOf(id);
155
+ }
156
+ catch {
157
+ v = [];
158
+ }
159
+ frameSubs.set(id, v);
160
+ return v;
161
+ }
162
+ /** subscribersOf (a node's managers) memoized for the current frame. */
163
+ export function cSubscribers(id) {
164
+ const hit = frameMgrs.get(id);
165
+ if (hit !== undefined)
166
+ return hit;
167
+ let v;
168
+ try {
169
+ v = subscribersOf(id);
170
+ }
171
+ catch {
172
+ v = [];
173
+ }
174
+ frameMgrs.set(id, v);
175
+ return v;
176
+ }
177
+ export function readTelemetry(nodeId) {
178
+ const hit = frameTelem.get(nodeId);
179
+ if (hit !== undefined)
180
+ return hit;
181
+ let v;
182
+ try {
183
+ const p = join(jobDir(nodeId), 'telemetry.json');
184
+ v = existsSync(p) ? JSON.parse(readFileSync(p, 'utf8')) : {};
185
+ }
186
+ catch {
187
+ v = {};
188
+ }
189
+ frameTelem.set(nodeId, v);
190
+ return v;
191
+ }
192
+ export function fmtTokens(n) {
193
+ return n < 1_000 ? `${n}` : `${Math.round(n / 1_000)}k`;
194
+ }
195
+ /** The context-window cell — live window fill rounded to the nearest 1k (the same
196
+ * figure pi's footer shows), NOT a per-turn token delta. */
197
+ export function tokensCell(id) {
198
+ return fmtTokens(readTelemetry(id).context_tokens ?? 0);
199
+ }
200
+ /** Dimmed live "what is it doing" cue for an ACTIVE node — the last tool it ran.
201
+ * Empty for non-active rows (stale once a node parks) and when none is recorded. */
202
+ export function activityCell(id, node) {
203
+ if (node?.status !== 'active')
204
+ return '';
205
+ const a = (readTelemetry(id).last_activity ?? '').trim();
206
+ return a === '' ? '' : ` ${DIM}· ${a}${RESET}`;
207
+ }
208
+ /** Revive-count badge (meta.cycles). Hidden on the first cycle (0) to cut noise. */
209
+ export function cycleBadge(node) {
210
+ const c = node?.cycles ?? 0;
211
+ return c > 0 ? ` ${DIM}⟳${c}${RESET}` : '';
212
+ }
213
+ /** Short on-screen label: the explicit handle when set, else the pi-generated
214
+ * description, else the bare name. fullName joins BOTH (`handle description`);
215
+ * the nav chrome shows just the first so rows stay compact. */
216
+ export function navLabel(node, id) {
217
+ if (node === null)
218
+ return shortId(id);
219
+ const handle = node.name && node.name !== node.kind ? node.name : '';
220
+ if (handle !== '')
221
+ return handle;
222
+ const desc = (node.description ?? '').trim();
223
+ return desc !== '' ? desc : node.name;
224
+ }
225
+ export function shortId(id) {
226
+ return id.slice(0, 8);
227
+ }
228
+ // ---------------------------------------------------------------------------
229
+ // Attachment — is a human currently WATCHING a node? A separate axis from
230
+ // running (status 'active' = the engine is live on its detached broker, watched
231
+ // or not). Two signals:
232
+ // viewer pane — a `focuses` row points at the node (one cheap sqlite read per
233
+ // render pass; pane-existence alone is NOT the signal).
234
+ // broker — the broker persists its helloed-viewer count (tmux panes AND
235
+ // web tabs) to job/attach.json on every viewer change
236
+ // (src/core/runtime/broker.ts). Trusted only while the node is
237
+ // 'active': a broker crash can leave a stale file.
238
+ // ---------------------------------------------------------------------------
239
+ /** Node ids currently shown in a tmux focus viewport. Built once per render. */
240
+ export function focusedNodeIds() {
241
+ try {
242
+ return new Set(listFocuses().map((f) => f.node_id));
243
+ }
244
+ catch {
245
+ return new Set();
246
+ }
247
+ }
248
+ /** True when a human is watching `id` right now (tmux focus or broker viewer). */
249
+ export function isAttached(id, node, focused) {
250
+ if (focused.has(id))
251
+ return true;
252
+ if (node?.status !== 'active')
253
+ return false; // stale attach.json from a crash
254
+ try {
255
+ const p = join(jobDir(id), 'attach.json');
256
+ if (!existsSync(p))
257
+ return false;
258
+ const rec = JSON.parse(readFileSync(p, 'utf8'));
259
+ return typeof rec.viewers === 'number' && rec.viewers > 0;
260
+ }
261
+ catch {
262
+ return false;
263
+ }
264
+ }
265
+ // ---------------------------------------------------------------------------
266
+ // Per-node ask counts — ONE shell-out per poll. `crtr canvas attention map`
267
+ // buckets a whole sub-DAG's pending asks by node in a single process, so the
268
+ // timer stays cheap (< 2 s) regardless of how many nodes are visible. --json
269
+ // gives a parseable {counts} blob (the default render is XML chrome).
270
+ // ---------------------------------------------------------------------------
271
+ export function fetchAsksMap(rootId) {
272
+ try {
273
+ const raw = execFileSync('crtr', ['canvas', 'attention', 'map', '--view', rootId, '--json'], {
274
+ timeout: 2_500,
275
+ encoding: 'utf8',
276
+ });
277
+ const parsed = JSON.parse(raw.trim());
278
+ return parsed.counts ?? {};
279
+ }
280
+ catch {
281
+ return {};
282
+ }
283
+ }
284
+ // ---------------------------------------------------------------------------
285
+ // Graph queries (dependency-free, straight off the canvas db)
286
+ // ---------------------------------------------------------------------------
287
+ /** First manager (by created) — the UP step for the ancestry spine. */
288
+ export function managerOf(id) {
289
+ return cSubscribers(id)[0]?.node_id;
290
+ }
291
+ /** A kind:'human' node is a control-plane ASK (a humanloop deck on the human's
292
+ * screen), NOT a pi conversation — it has no session, so focusing/reviving it
293
+ * boots a confused blank "you have been revived" pi. Its pending-ask signal
294
+ * already rides the ⚑ badge on the ASKING node (attention.ts attributes asks by
295
+ * source.nodeId, never to the human node), so the row carries no signal of its
296
+ * own. Drop it from every navigable list (the tree, BASE reports, child counts,
297
+ * subtree expansion) so it can never be selected. */
298
+ export function isHumanAsk(id) {
299
+ return cNode(id)?.kind === 'human';
300
+ }
301
+ /** A node's direct children that are navigable conversations — human-ask nodes
302
+ * dropped. The one place the nav chrome enumerates children. */
303
+ export function convoChildIds(id) {
304
+ return cSubscriptions(id).map((s) => s.node_id).filter((cid) => !isHumanAsk(cid));
305
+ }
306
+ /** Live reports (active|idle) of a node — the DOWN set in BASE. */
307
+ export function liveReports(id) {
308
+ return convoChildIds(id).filter((cid) => {
309
+ const st = cNode(cid)?.status;
310
+ return st === 'active' || st === 'idle';
311
+ });
312
+ }
313
+ /** Direct navigable children — used for the ⤳ badge and fold counts (human-ask
314
+ * nodes excluded, so the count matches what the tree actually shows). */
315
+ export function childCount(id) {
316
+ return convoChildIds(id).length;
317
+ }
318
+ /** Climb first-manager edges from `self` to the ancestry root (cycle-guarded). */
319
+ export function climbRoot(self) {
320
+ let cur = self;
321
+ const seen = new Set([cur]);
322
+ for (;;) {
323
+ const mgr = managerOf(cur);
324
+ if (mgr === undefined || seen.has(mgr))
325
+ break;
326
+ seen.add(mgr);
327
+ cur = mgr;
328
+ }
329
+ return cur;
330
+ }
331
+ /** Space-joined ids of a node's subtree (cursor-relative {subtree} var). */
332
+ export function subtreeIds(root) {
333
+ const out = [];
334
+ const seen = new Set([root]);
335
+ const q = convoChildIds(root);
336
+ while (q.length > 0) {
337
+ const id = q.shift();
338
+ if (seen.has(id))
339
+ continue;
340
+ seen.add(id);
341
+ out.push(id);
342
+ for (const cid of convoChildIds(id))
343
+ if (!seen.has(cid))
344
+ q.push(cid);
345
+ }
346
+ return out;
347
+ }
348
+ // ---------------------------------------------------------------------------
349
+ // Shared cell builders
350
+ // ---------------------------------------------------------------------------
351
+ /** ⤳M direct-children badge — only on orchestrator rows. */
352
+ export function childBadge(node) {
353
+ if (node === null || node.mode !== 'orchestrator')
354
+ return '';
355
+ const m = childCount(node.node_id);
356
+ return m > 0 ? ` ${DIM}⤳${m}${RESET}` : '';
357
+ }
358
+ /** ⚑K pending-asks badge for a node, read from the supplied per-node ask map. */
359
+ export function askBadge(id, asks) {
360
+ const k = asks[id] ?? 0;
361
+ return k > 0 ? ` ${YELLOW}⚑${k}${RESET}` : '';
362
+ }
363
+ /** "Live work below" badge — green ⇣N when this node is NOT itself active but has
364
+ * N active descendants. The one signal that an idle/parked node isn't dead: real
365
+ * work is still running beneath it. Count comes from the single subtree-activity
366
+ * pass (no per-row walk). Suppressed on active rows (their own ● already says so). */
367
+ export function liveBelowBadge(node, activeBelow) {
368
+ if (node === null || node.status === 'active')
369
+ return '';
370
+ const n = activeBelow.get(node.node_id) ?? 0;
371
+ return n > 0 ? ` ${GREEN}⇣${n}${RESET}` : '';
372
+ }
373
+ /** Sort rank for sibling ordering — live nodes (active, then idle) ahead of
374
+ * terminal ones, so sessions still running surface at the TOP of each child
375
+ * group instead of being buried under finished/failed ones. */
376
+ export function statusRank(id) {
377
+ switch (cNode(id)?.status) {
378
+ case 'active': return 0;
379
+ case 'idle': return 1;
380
+ case 'done': return 2;
381
+ case 'canceled': return 3;
382
+ case 'dead': return 4;
383
+ default: return 5;
384
+ }
385
+ }
386
+ /** Direct children, live-first — the sibling order used both when flattening
387
+ * the tree and when stepping into a subtree (`l`). Array.sort is stable, so
388
+ * equal-status siblings keep their creation order. */
389
+ export function sortedChildIds(id) {
390
+ return convoChildIds(id).sort((a, b) => statusRank(a) - statusRank(b));
391
+ }
392
+ export function computeSubtreeActivity(root, self) {
393
+ const expand = new Set();
394
+ const activeBelow = new Map();
395
+ const seen = new Set();
396
+ // Returns the count of active nodes in subtree(id) INCLUDING id, and whether
397
+ // that subtree is worth revealing (holds an active node or self).
398
+ const visit = (id) => {
399
+ const selfActive = cNode(id)?.status === 'active';
400
+ if (seen.has(id))
401
+ return { active: selfActive ? 1 : 0, reveal: selfActive || id === self };
402
+ seen.add(id);
403
+ let below = 0;
404
+ let childReveal = false;
405
+ for (const c of convoChildIds(id)) {
406
+ const r = visit(c);
407
+ below += r.active;
408
+ if (r.reveal)
409
+ childReveal = true;
410
+ }
411
+ activeBelow.set(id, below);
412
+ if (childReveal)
413
+ expand.add(id); // a descendant is worth revealing → unfold id
414
+ return { active: below + (selfActive ? 1 : 0), reveal: childReveal || selfActive || id === self };
415
+ };
416
+ visit(root);
417
+ return { expand, activeBelow };
418
+ }
419
+ /** Fold policy alone — thin wrapper over computeSubtreeActivity for the keypress
420
+ * path (buildGraphModel without a precomputed set). The render path computes the
421
+ * activity once and threads `expand` in, so it never recomputes here. */
422
+ export function computeDefaultExpanded(root, self) {
423
+ return computeSubtreeActivity(root, self).expand;
424
+ }
425
+ export function buildGraphModel(self, folds, expand) {
426
+ const rootId = climbRoot(self);
427
+ const defaultExpanded = expand ?? computeDefaultExpanded(rootId, self);
428
+ // userExpanded / userCollapsed override the auto policy; absent → policy decides.
429
+ const isFolded = (id) => folds.userExpanded.has(id) ? false : folds.userCollapsed.has(id) ? true : !defaultExpanded.has(id);
430
+ const rows = [];
431
+ const visited = new Set();
432
+ const walk = (id, prefix, isRoot, isLast) => {
433
+ if (visited.has(id)) {
434
+ const connector = isRoot ? '' : isLast ? '└─ ' : '├─ ';
435
+ rows.push({ id, hasKids: false, isSelf: id === self, branch: prefix + connector, cycle: true, collapsed: false });
436
+ return;
437
+ }
438
+ visited.add(id);
439
+ const kids = sortedChildIds(id);
440
+ const folded = isFolded(id);
441
+ const connector = isRoot ? '' : isLast ? '└─ ' : '├─ ';
442
+ rows.push({ id, hasKids: kids.length > 0, isSelf: id === self, branch: prefix + connector, cycle: false, collapsed: folded });
443
+ if (folded)
444
+ return; // folded — don't descend
445
+ const childPrefix = isRoot ? '' : prefix + (isLast ? ' ' : '│ ');
446
+ for (let i = 0; i < kids.length; i++)
447
+ walk(kids[i], childPrefix, false, i === kids.length - 1);
448
+ };
449
+ walk(rootId, '', true, true);
450
+ return rows;
451
+ }
452
+ /** Render one GRAPH row. CURSOR (selected) → reverse-video bar; an ATTACHED
453
+ * (human-watched) node → a coloured background bar; SELF → bold name. The
454
+ * cursor outranks the attached tint when both land on the same row. Running
455
+ * is signaled by the dot glyph alone (● green = active engine). */
456
+ export function renderGraphRow(r, isCursor, focused, activeBelow, asks) {
457
+ const wrap = (line, attached) => isCursor ? fillBar(line, fillWidth(), REVERSE)
458
+ : attached ? fillBar(line, fillWidth(), BG_ATTACHED)
459
+ : truncate(line);
460
+ if (r.cycle) {
461
+ const line = `${r.branch} ${DIM}↺ ${shortId(r.id)}${RESET}`;
462
+ return wrap(line, false);
463
+ }
464
+ const node = cNode(r.id);
465
+ const dot = coloredGlyph(node);
466
+ const rawName = navLabel(node, r.id);
467
+ const name = r.isSelf ? `${BOLD}${rawName}${RESET}` : rawName;
468
+ const kind = `${DIM}${node?.kind ?? ''}${RESET}`;
469
+ const tokens = `${DIM}${tokensCell(r.id)}${RESET}`;
470
+ // ▸ marks an expandable (collapsed-with-kids) row. The cursor row gets no
471
+ // caret — its reverse-video bar already distinguishes it — so the triangle
472
+ // reads purely as "this unfolds".
473
+ const expandable = r.hasKids && r.collapsed;
474
+ const caret = !isCursor && expandable ? `${DIM}▸${RESET} ` : ' ';
475
+ const fold = expandable ? ` ${DIM}[+${childCount(r.id)}]${RESET}` : '';
476
+ const line = `${r.branch}${caret}${dot} ${name} ${kind} ${tokens}${cycleBadge(node)}${childBadge(node)}${liveBelowBadge(node, activeBelow)}${fold}${askBadge(r.id, asks)}${activityCell(r.id, node)}`;
477
+ return wrap(line, isAttached(r.id, node, focused));
478
+ }
479
+ /** Total lines the GRAPH widget may emit. pi hard-caps extension widgets at
480
+ * MAX_WIDGET_LINES — anything past that pi truncates itself, eating our own
481
+ * scroll chrome — so never exceed it (and shrink on a very short terminal).
482
+ * The viewport scrolls WITHIN this cap as the cursor moves. */
483
+ export function graphWidgetBudget() {
484
+ const rows = process.stdout.rows ?? VIEWPORT_FALLBACK_ROWS;
485
+ return Math.max(4, Math.min(PI_MAX_WIDGET_LINES, rows - 4));
486
+ }
487
+ export const GRAPH_HINT = `${DIM}jk move · hl fold · ↵ focus · e expand · x kill · m mgr · esc${RESET}`;
@@ -17,6 +17,13 @@ export declare function inboxPath(nodeId: string): string;
17
17
  export declare function passivePath(nodeId: string): string;
18
18
  export declare function transcriptPath(nodeId: string): string;
19
19
  export declare function sessionPtrPath(nodeId: string): string;
20
+ /** On-read injection dedup set — the doc realpaths already surfaced on-read in
21
+ * this node's CURRENT conversation transcript. Persisted so the once-per-
22
+ * transcript dedup survives a dormancy → revive(resume) cycle: a resume reuses
23
+ * the same .jsonl transcript in a NEW pi process, so the in-memory set would
24
+ * otherwise start empty and re-inject docs already present in the transcript.
25
+ * Deleted by the launch paths that start a FRESH transcript. */
26
+ export declare function injectedDocsPath(nodeId: string): string;
20
27
  /** Create the full directory skeleton for a node. Idempotent. */
21
28
  export declare function ensureNodeDirs(nodeId: string): void;
22
29
  /** Ensure the canvas home exists. Idempotent. */
@@ -62,6 +62,15 @@ export function transcriptPath(nodeId) {
62
62
  export function sessionPtrPath(nodeId) {
63
63
  return join(nodeDir(nodeId), 'session.ptr');
64
64
  }
65
+ /** On-read injection dedup set — the doc realpaths already surfaced on-read in
66
+ * this node's CURRENT conversation transcript. Persisted so the once-per-
67
+ * transcript dedup survives a dormancy → revive(resume) cycle: a resume reuses
68
+ * the same .jsonl transcript in a NEW pi process, so the in-memory set would
69
+ * otherwise start empty and re-inject docs already present in the transcript.
70
+ * Deleted by the launch paths that start a FRESH transcript. */
71
+ export function injectedDocsPath(nodeId) {
72
+ return join(nodeDir(nodeId), 'injected-docs.json');
73
+ }
65
74
  /** Create the full directory skeleton for a node. Idempotent. */
66
75
  export function ensureNodeDirs(nodeId) {
67
76
  for (const d of [contextDir(nodeId), jobDir(nodeId), reportsDir(nodeId)]) {
@@ -32,18 +32,77 @@ export interface DashboardRow {
32
32
  * agent you come back to. Drives the browser's resident-only lifecycle filter.
33
33
  * Only populated by dashboardRowsAll (the browser snapshot). */
34
34
  lifecycle?: Lifecycle;
35
- /** The node's spawn prompt (context/initial-prompt.md), trimmed + capped. Only
36
- * populated by dashboardRowsAll (the browser snapshot) the dashboard leaf
37
- * leaves it undefined to avoid a file read per node. Indexed by super-search
38
- * and shown in the preview panel when there is no live query. */
35
+ /** "Most recent activity" sort key epoch ms. The cheap boot stats the node's
36
+ * job/telemetry.json (rewritten on every turn_end at a DETERMINISTIC path, so
37
+ * no meta read), falling back to `created`'s epoch when a node never ran a turn.
38
+ * This is the cheap proxy for last-message recency the attention sort tie-breaks
39
+ * on. Always set by dashboardRowsAll; optional only so synthetic test rows + the
40
+ * scoped dashboardRows can omit it. (NB: the pi session-file mtime would be truer
41
+ * but its path lives in meta.json at a non-deterministic location — reading it
42
+ * per node would defeat the cheap boot, so telemetry mtime is used instead.) */
43
+ mtimeMs?: number;
44
+ /** The node's spawn prompt (context/initial-prompt.md), trimmed + capped. Populated
45
+ * LAZILY by loadPreview (selected-row preview) — NOT on the cheap boot path.
46
+ * Indexed by super-search and shown in the preview panel when there is no query. */
39
47
  goal?: string;
40
- /** EVERY user prompt across the node's pi session — the whole conversation, not
41
- * just the spawn prompt — joined + capped. Populated by dashboardRowsAll, undefined
42
- * for never-revived nodes (no session file). Powers whole-conversation super-search
43
- * + the windowed match snippet in the preview. */
48
+ /** EVERY user prompt across the node's pi session — the whole conversation, not just
49
+ * the spawn prompt — joined + capped. Populated LAZILY by loadPreview (ONE session
50
+ * read, folded with lastAssistant); undefined for never-revived nodes (no session
51
+ * file). Powers whole-conversation super-search + the windowed preview snippet. */
44
52
  prompts?: string;
53
+ /** The node's LAST assistant message (text only), trimmed + capped. Populated LAZILY
54
+ * by loadPreview (same single session read as `prompts`); undefined for a node whose
55
+ * session has no assistant reply yet. Shown in the preview's reply block. */
56
+ lastAssistant?: string;
57
+ /** True when the node is GENUINELY mid-turn right now — its `busy` marker exists
58
+ * AND its broker pid is alive (not merely an `active`, between-turns node). The
59
+ * live "is it generating?" cue. Computed on the cheap boot path for LIVE nodes
60
+ * only (dormant rows are always false). */
61
+ streaming?: boolean;
62
+ /** True when a viewer (focus row) is attached to the node — i.e. someone has it
63
+ * open on screen. Set on the cheap boot path from the one listFocuses() query. */
64
+ viewed?: boolean;
65
+ /** Lazy-enrichment guard (internal): true once enrichRow/enrichRows has folded in
66
+ * the full label (meta), ctx tokens (telemetry), and ⚑ asks. Idempotency marker so
67
+ * progressive paint can re-call enrich for a viewport every keystroke for free. */
68
+ enriched?: boolean;
69
+ /** Lazy-enrichment guard (internal): true once loadPreview has read goal + the
70
+ * session (prompts + lastAssistant). Idempotency marker for the selected row. */
71
+ previewLoaded?: boolean;
45
72
  }
46
73
  /** One row per node visible in the sub-DAG of `rootId` (including root). */
47
74
  export declare function dashboardRows(rootId: string): DashboardRow[];
48
- /** One row per node across the entire canvas. */
75
+ /**
76
+ * CHEAP boot builder — one row per node across the entire canvas, with only the
77
+ * fields that need no per-node meta/telemetry/session read. Two db queries total:
78
+ * `listNodes()` + `listFocuses()` (the focus set replaces 1473 `getFocusByNode`
79
+ * calls). For each node:
80
+ * - identity/status/kind/mode/cwd/created/lifecycle straight off the db row;
81
+ * - `name` is the db HANDLE only (the informative label needs meta.description —
82
+ * fold it in lazily via {@link enrichRow}/{@link enrichRows});
83
+ * - `viewed` from the one focus-set;
84
+ * - `streaming` only for LIVE nodes (active|idle — the only ones that can be
85
+ * mid-turn); dormant rows are always false (skips an isPidAlive + statSync);
86
+ * - `mtimeMs` via a cheap telemetry statSync (no read) for the attention sort;
87
+ * - `ctx_tokens`/`asks` left 0 and `goal`/`prompts`/`lastAssistant` undefined —
88
+ * all deferred to the lazy enrichment API below.
89
+ *
90
+ * This is the first-frame path: paint from it immediately, then enrich the visible
91
+ * viewport on demand. See {@link enrichRow}, {@link enrichRows}, {@link loadPreview}.
92
+ */
49
93
  export declare function dashboardRowsAll(): DashboardRow[];
94
+ /** Fold the cheap-boot row's deferred fields in for ONE row: the full label
95
+ * (meta.description → fullName), ctx tokens (telemetry.json), and ⚑ asks. One
96
+ * meta read + one telemetry read + one ask scan. Idempotent (no-op once enriched).
97
+ * For a batch, prefer {@link enrichRows} — it scans each cwd's ask inbox once. */
98
+ export declare function enrichRow(row: DashboardRow): DashboardRow;
99
+ /** Batch {@link enrichRow}: enrich every not-yet-enriched row, scanning each
100
+ * distinct cwd's ask inbox exactly ONCE (via `asksForNodes`) instead of per row.
101
+ * Use this for a whole viewport / the full forest; mutates each row in place. */
102
+ export declare function enrichRows(rows: DashboardRow[]): void;
103
+ /** Load the SELECTED row's preview text: the spawn `goal` (initial-prompt.md) plus
104
+ * the whole-conversation `prompts` and the `lastAssistant` reply — the latter two
105
+ * folded into ONE session-file read (see {@link readSessionParts}). Mutates the row
106
+ * in place; idempotent. Call only for the cursor row (and lazily/in-background to
107
+ * warm the prompt super-search corpus), never on the boot path. */
108
+ export declare function loadPreview(row: DashboardRow): DashboardRow;