@crouton-kit/crouter 0.3.28 → 0.3.30

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 (324) hide show
  1. package/dist/build-root.d.ts +15 -7
  2. package/dist/build-root.js +48 -34
  3. package/dist/builtin-memory/crouter-development/marketplaces.md +7 -6
  4. package/dist/builtin-memory/crouter-development/personas/base-prompt.md +1 -1
  5. package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +1 -1
  6. package/dist/builtin-memory/crouter-development/personas.md +1 -1
  7. package/dist/builtin-memory/crouter-development/plugins.md +20 -18
  8. package/dist/builtin-memory/design.md +1 -1
  9. package/dist/builtin-memory/development.md +1 -1
  10. package/dist/builtin-memory/internal/INDEX.md +1 -1
  11. package/dist/builtin-memory/internal/examples/INDEX.md +1 -1
  12. package/dist/builtin-memory/internal/examples/imessage-assistant.md +2 -2
  13. package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
  14. package/dist/builtin-memory/internal/storage-tiers.md +1 -1
  15. package/dist/builtin-memory/planning.md +1 -1
  16. package/dist/builtin-memory/spec.md +6 -4
  17. package/dist/builtin-personas/orchestration-kernel.md +4 -5
  18. package/dist/builtin-personas/runtime-base.md +5 -4
  19. package/dist/builtin-personas/spec/PERSONA.md +8 -4
  20. package/dist/builtin-personas/spec/orchestrator.md +5 -3
  21. package/dist/builtin-personas/spec/requirements/PERSONA.md +10 -0
  22. package/dist/builtin-views/canvas/core.mjs +586 -0
  23. package/dist/builtin-views/canvas/text.mjs +58 -0
  24. package/dist/builtin-views/canvas/tui.mjs +165 -0
  25. package/dist/builtin-views/canvas/web.jsx +120 -0
  26. package/dist/builtin-views/git-pr/core.mjs +673 -0
  27. package/dist/builtin-views/git-pr/text.mjs +84 -0
  28. package/dist/builtin-views/git-pr/tui.mjs +302 -0
  29. package/dist/builtin-views/git-pr/web.jsx +216 -0
  30. package/dist/builtin-views/inbox/_lib/render.mjs +1 -3
  31. package/dist/builtin-views/inbox/core.mjs +1273 -0
  32. package/dist/builtin-views/inbox/text.mjs +73 -0
  33. package/dist/builtin-views/inbox/tui.mjs +312 -0
  34. package/dist/builtin-views/inbox/web.jsx +188 -0
  35. package/dist/builtin-views/linkedin/core.mjs +906 -0
  36. package/dist/builtin-views/linkedin/text.mjs +69 -0
  37. package/dist/builtin-views/linkedin/tui.mjs +427 -0
  38. package/dist/builtin-views/linkedin/web.jsx +206 -0
  39. package/dist/builtin-views/workspace-sidebar/core.mjs +621 -0
  40. package/dist/builtin-views/workspace-sidebar/text.mjs +53 -0
  41. package/dist/builtin-views/workspace-sidebar/tui.mjs +142 -0
  42. package/dist/builtin-views/workspace-sidebar/web.jsx +109 -0
  43. package/dist/cli.js +22 -17
  44. package/dist/clients/attach/__tests__/action-parity.test.js +48 -0
  45. package/dist/clients/attach/__tests__/clipboard-image.test.js +76 -0
  46. package/dist/clients/attach/__tests__/editor-newline.test.js +57 -0
  47. package/dist/clients/attach/__tests__/git-info.test.js +31 -0
  48. package/dist/clients/attach/__tests__/onrequest-wired.test.js +20 -0
  49. package/dist/clients/attach/__tests__/slash-quit-copy.test.js +42 -0
  50. package/dist/clients/attach/__tests__/titled-editor.test.js +39 -0
  51. package/dist/clients/attach/attach-cmd.js +282 -46
  52. package/dist/clients/attach/auth-pickers.d.ts +9 -0
  53. package/dist/clients/attach/auth-pickers.js +194 -0
  54. package/dist/clients/attach/canvas-panels.d.ts +4 -2
  55. package/dist/clients/attach/canvas-panels.js +45 -17
  56. package/dist/clients/attach/chat-view.d.ts +27 -2
  57. package/dist/clients/attach/chat-view.js +98 -9
  58. package/dist/clients/attach/clipboard-image.d.ts +47 -8
  59. package/dist/clients/attach/clipboard-image.js +150 -26
  60. package/dist/clients/attach/clipboard-text.d.ts +3 -0
  61. package/dist/clients/attach/clipboard-text.js +39 -0
  62. package/dist/clients/attach/config-load.d.ts +54 -7
  63. package/dist/clients/attach/config-load.js +99 -3
  64. package/dist/clients/attach/context-message.d.ts +12 -0
  65. package/dist/clients/attach/context-message.js +72 -0
  66. package/dist/clients/attach/git-info.d.ts +20 -0
  67. package/dist/clients/attach/git-info.js +44 -0
  68. package/dist/clients/attach/graph-overlay.d.ts +13 -4
  69. package/dist/clients/attach/graph-overlay.js +59 -19
  70. package/dist/clients/attach/input-controller.d.ts +75 -13
  71. package/dist/clients/attach/input-controller.js +305 -67
  72. package/dist/clients/attach/pickers.d.ts +58 -0
  73. package/dist/clients/attach/pickers.js +171 -0
  74. package/dist/clients/attach/slash-commands.d.ts +22 -0
  75. package/dist/clients/attach/slash-commands.js +91 -25
  76. package/dist/clients/attach/titled-editor.d.ts +33 -0
  77. package/dist/clients/attach/titled-editor.js +84 -0
  78. package/dist/clients/attach/view-socket.d.ts +18 -1
  79. package/dist/clients/attach/view-socket.js +70 -1
  80. package/dist/clients/web/dev-server.d.ts +7 -0
  81. package/dist/clients/web/dev-server.js +59 -0
  82. package/dist/clients/web/events.d.ts +14 -0
  83. package/dist/clients/web/events.js +151 -0
  84. package/dist/clients/web/server.d.ts +18 -0
  85. package/dist/clients/web/server.js +450 -0
  86. package/dist/clients/web/web-cmd.d.ts +2 -0
  87. package/dist/clients/web/web-cmd.js +120 -0
  88. package/dist/commands/canvas.js +1 -2
  89. package/dist/commands/chord.js +4 -25
  90. package/dist/commands/dashboard.js +5 -1
  91. package/dist/commands/memory/__tests__/lint-schema.test.js +6 -6
  92. package/dist/commands/memory/lint.js +14 -2
  93. package/dist/commands/memory/read.js +2 -2
  94. package/dist/commands/memory/shared.d.ts +7 -1
  95. package/dist/commands/memory/shared.js +18 -3
  96. package/dist/commands/memory/write.js +8 -4
  97. package/dist/commands/memory.js +4 -4
  98. package/dist/commands/node.d.ts +1 -1
  99. package/dist/commands/node.js +64 -65
  100. package/dist/commands/pkg.js +1 -2
  101. package/dist/commands/revive.js +11 -3
  102. package/dist/commands/sys/sync.d.ts +1 -0
  103. package/dist/commands/sys/sync.js +187 -0
  104. package/dist/commands/sys.js +3 -2
  105. package/dist/commands/view-cycle.js +2 -2
  106. package/dist/commands/view-list.js +8 -8
  107. package/dist/commands/view-new.js +21 -17
  108. package/dist/commands/view-pick.js +1 -1
  109. package/dist/commands/view-run.js +35 -14
  110. package/dist/commands/view.js +8 -6
  111. package/dist/commands/workspace.d.ts +2 -0
  112. package/dist/commands/workspace.js +161 -0
  113. package/dist/core/__tests__/broker-double-spawn.test.d.ts +1 -0
  114. package/dist/core/__tests__/broker-double-spawn.test.js +143 -0
  115. package/dist/core/__tests__/broker-fork-seam.test.d.ts +1 -0
  116. package/dist/core/__tests__/broker-fork-seam.test.js +102 -0
  117. package/dist/core/__tests__/broker-sdk-wiring.test.js +2 -0
  118. package/dist/core/__tests__/child-death-wake.test.js +56 -111
  119. package/dist/core/__tests__/close.test.js +14 -20
  120. package/dist/core/__tests__/context-intro.test.js +19 -19
  121. package/dist/core/__tests__/daemon-boot.test.js +30 -23
  122. package/dist/core/__tests__/draw-style.test.js +1 -1
  123. package/dist/core/__tests__/error-stall-recycle.test.d.ts +1 -0
  124. package/dist/core/__tests__/error-stall-recycle.test.js +141 -0
  125. package/dist/core/__tests__/fixtures/fake-engine.d.ts +17 -1
  126. package/dist/core/__tests__/fixtures/fake-engine.js +22 -2
  127. package/dist/core/__tests__/fixtures/fake-pi-host.js +2 -2
  128. package/dist/core/__tests__/focuses.test.js +25 -13
  129. package/dist/core/__tests__/fork.test.js +22 -5
  130. package/dist/core/__tests__/full/broker-control-preempt.test.d.ts +1 -0
  131. package/dist/core/__tests__/full/broker-control-preempt.test.js +61 -0
  132. package/dist/core/__tests__/full/broker-model-changed-broadcast.test.d.ts +1 -0
  133. package/dist/core/__tests__/full/broker-model-changed-broadcast.test.js +57 -0
  134. package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.d.ts +1 -0
  135. package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.js +80 -0
  136. package/dist/core/__tests__/full/broker-pane-resolution.test.js +90 -0
  137. package/dist/core/__tests__/full/cascade-close.test.js +22 -8
  138. package/dist/core/__tests__/full/detach-focus.test.js +66 -169
  139. package/dist/core/__tests__/helpers/harness.d.ts +6 -9
  140. package/dist/core/__tests__/helpers/harness.js +19 -36
  141. package/dist/core/__tests__/human-node-not-supervised.test.d.ts +1 -0
  142. package/dist/core/__tests__/human-node-not-supervised.test.js +83 -0
  143. package/dist/core/__tests__/lifecycle.test.js +1 -19
  144. package/dist/core/__tests__/listing-completeness.test.js +2 -2
  145. package/dist/core/__tests__/live-mutation-verbs.test.js +5 -5
  146. package/dist/core/__tests__/memory-resolver.test.js +1 -1
  147. package/dist/core/__tests__/on-read-dedup-resume.test.js +2 -2
  148. package/dist/core/__tests__/refresh-stall-recycle.test.d.ts +1 -0
  149. package/dist/core/__tests__/refresh-stall-recycle.test.js +88 -0
  150. package/dist/core/__tests__/relaunch-root.test.d.ts +1 -0
  151. package/dist/core/__tests__/relaunch-root.test.js +114 -0
  152. package/dist/core/__tests__/reset.test.js +19 -79
  153. package/dist/core/__tests__/steer-note.test.js +9 -7
  154. package/dist/core/__tests__/tmux-surface.test.js +13 -7
  155. package/dist/core/bootstrap.d.ts +0 -3
  156. package/dist/core/bootstrap.js +1 -143
  157. package/dist/core/canvas/browse/__tests__/model.test.js +86 -1
  158. package/dist/core/canvas/browse/__tests__/render.test.js +2 -1
  159. package/dist/core/canvas/browse/app.js +131 -8
  160. package/dist/core/canvas/browse/model.d.ts +23 -5
  161. package/dist/core/canvas/browse/model.js +68 -13
  162. package/dist/core/canvas/browse/render.d.ts +7 -1
  163. package/dist/core/canvas/browse/render.js +178 -47
  164. package/dist/core/canvas/canvas.js +1 -1
  165. package/dist/core/canvas/focuses.d.ts +4 -4
  166. package/dist/core/canvas/focuses.js +7 -7
  167. package/dist/core/canvas/nav-model.js +8 -7
  168. package/dist/core/canvas/render.d.ts +68 -9
  169. package/dist/core/canvas/render.js +195 -61
  170. package/dist/core/canvas/types.d.ts +16 -23
  171. package/dist/core/config.js +1 -4
  172. package/dist/core/memory-resolver.js +43 -5
  173. package/dist/core/runtime/bearings.d.ts +1 -1
  174. package/dist/core/runtime/bearings.js +8 -8
  175. package/dist/core/runtime/branded-host.d.ts +16 -0
  176. package/dist/core/runtime/branded-host.js +127 -0
  177. package/dist/core/runtime/broker-protocol.d.ts +212 -6
  178. package/dist/core/runtime/broker.d.ts +4 -1
  179. package/dist/core/runtime/broker.js +494 -58
  180. package/dist/core/runtime/close.d.ts +12 -2
  181. package/dist/core/runtime/close.js +37 -16
  182. package/dist/core/runtime/front-door.js +6 -14
  183. package/dist/core/runtime/host.d.ts +14 -34
  184. package/dist/core/runtime/host.js +14 -50
  185. package/dist/core/runtime/launch.d.ts +12 -9
  186. package/dist/core/runtime/launch.js +27 -15
  187. package/dist/core/runtime/lifecycle.d.ts +1 -1
  188. package/dist/core/runtime/lifecycle.js +15 -19
  189. package/dist/core/runtime/nodes.d.ts +0 -55
  190. package/dist/core/runtime/nodes.js +6 -74
  191. package/dist/core/runtime/placement.d.ts +91 -335
  192. package/dist/core/runtime/placement.js +291 -858
  193. package/dist/core/runtime/promote.d.ts +2 -0
  194. package/dist/core/runtime/promote.js +24 -6
  195. package/dist/core/runtime/recap.d.ts +8 -0
  196. package/dist/core/runtime/recap.js +107 -0
  197. package/dist/core/runtime/recycle.js +25 -61
  198. package/dist/core/runtime/reset.d.ts +43 -37
  199. package/dist/core/runtime/reset.js +131 -218
  200. package/dist/core/runtime/revive.d.ts +9 -29
  201. package/dist/core/runtime/revive.js +40 -139
  202. package/dist/core/runtime/spawn.d.ts +15 -16
  203. package/dist/core/runtime/spawn.js +165 -194
  204. package/dist/core/runtime/surface-bg.d.ts +11 -0
  205. package/dist/core/runtime/surface-bg.js +68 -0
  206. package/dist/core/runtime/tmux.d.ts +15 -52
  207. package/dist/core/runtime/tmux.js +35 -99
  208. package/dist/core/scope.d.ts +1 -2
  209. package/dist/core/scope.js +1 -5
  210. package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.d.ts +1 -0
  211. package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.js +57 -0
  212. package/dist/core/skill-sync/builtins.d.ts +42 -0
  213. package/dist/core/skill-sync/builtins.js +112 -0
  214. package/dist/core/skill-sync/claude-plugins.d.ts +23 -0
  215. package/dist/core/skill-sync/claude-plugins.js +71 -0
  216. package/dist/core/skill-sync/engine.d.ts +42 -0
  217. package/dist/core/skill-sync/engine.js +633 -0
  218. package/dist/core/skill-sync/export.d.ts +23 -0
  219. package/dist/core/skill-sync/export.js +86 -0
  220. package/dist/core/skill-sync/manifest.d.ts +64 -0
  221. package/dist/core/skill-sync/manifest.js +181 -0
  222. package/dist/core/skill-sync/profile.d.ts +76 -0
  223. package/dist/core/skill-sync/profile.js +173 -0
  224. package/dist/core/skill-sync/snapshot.d.ts +57 -0
  225. package/dist/core/skill-sync/snapshot.js +120 -0
  226. package/dist/core/substrate/index.d.ts +1 -1
  227. package/dist/core/substrate/index.js +1 -1
  228. package/dist/core/substrate/injected-store.js +3 -3
  229. package/dist/core/substrate/on-read.js +68 -6
  230. package/dist/core/substrate/render.d.ts +8 -11
  231. package/dist/core/substrate/render.js +29 -43
  232. package/dist/core/substrate/schema.d.ts +10 -3
  233. package/dist/core/substrate/schema.js +6 -3
  234. package/dist/core/tui/host.d.ts +12 -4
  235. package/dist/core/tui/host.js +280 -149
  236. package/dist/core/view/bridge.d.ts +10 -0
  237. package/dist/core/view/bridge.js +31 -0
  238. package/dist/core/view/chrome.d.ts +9 -0
  239. package/dist/core/view/chrome.js +22 -0
  240. package/dist/core/view/contract.d.ts +171 -0
  241. package/dist/core/view/contract.js +23 -0
  242. package/dist/core/view/loader.d.ts +31 -0
  243. package/dist/core/view/loader.js +188 -0
  244. package/dist/core/view/transport-local.d.ts +7 -0
  245. package/dist/core/view/transport-local.js +70 -0
  246. package/dist/core/view/transport.d.ts +4 -0
  247. package/dist/core/view/transport.js +15 -0
  248. package/dist/daemon/crtrd.d.ts +38 -27
  249. package/dist/daemon/crtrd.js +303 -376
  250. package/dist/daemon/manage.js +6 -1
  251. package/dist/index.js +1 -1
  252. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +28 -20
  253. package/dist/pi-extensions/canvas-context-intro.js +4 -4
  254. package/dist/pi-extensions/canvas-doc-substrate.js +16 -16
  255. package/dist/pi-extensions/canvas-nav.js +5 -0
  256. package/dist/pi-extensions/canvas-recap.d.ts +37 -0
  257. package/dist/pi-extensions/canvas-recap.js +236 -0
  258. package/dist/pi-extensions/canvas-resume.js +2 -1
  259. package/dist/pi-extensions/canvas-stophook.d.ts +14 -12
  260. package/dist/pi-extensions/canvas-stophook.js +87 -101
  261. package/dist/pi-extensions/canvas-view.js +2 -1
  262. package/dist/pi-extensions/widget-order-bus.d.ts +6 -0
  263. package/dist/pi-extensions/widget-order-bus.js +34 -0
  264. package/dist/prompts/view.d.ts +2 -2
  265. package/dist/prompts/view.js +145 -73
  266. package/dist/types.d.ts +1 -6
  267. package/dist/types.js +1 -3
  268. package/dist/web/ViewChrome.d.ts +7 -0
  269. package/dist/web/ViewChrome.js +28 -0
  270. package/dist/web/ViewPane.d.ts +39 -0
  271. package/dist/web/ViewPane.js +48 -0
  272. package/dist/web/index.d.ts +6 -0
  273. package/dist/web/index.js +16 -0
  274. package/dist/web/runtime.d.ts +39 -0
  275. package/dist/web/runtime.js +133 -0
  276. package/dist/web/states.d.ts +24 -0
  277. package/dist/web/states.js +24 -0
  278. package/dist/web/transport-http.d.ts +5 -0
  279. package/dist/web/transport-http.js +28 -0
  280. package/dist/web-client/assets/index-BUvQb4hR.css +2 -0
  281. package/dist/web-client/assets/index-ClLQXYAE.js +10 -0
  282. package/dist/web-client/index.html +13 -0
  283. package/package.json +20 -6
  284. package/dist/builtin-views/canvas/client.mjs +0 -303
  285. package/dist/builtin-views/canvas/view.mjs +0 -576
  286. package/dist/builtin-views/git-pr/client.mjs +0 -440
  287. package/dist/builtin-views/git-pr/view.mjs +0 -675
  288. package/dist/builtin-views/inbox/sources/gmail.mjs +0 -965
  289. package/dist/builtin-views/inbox/sources/linkedin.mjs +0 -427
  290. package/dist/builtin-views/inbox/view.mjs +0 -889
  291. package/dist/builtin-views/linkedin/client.mjs +0 -610
  292. package/dist/builtin-views/linkedin/view.mjs +0 -1171
  293. package/dist/commands/pkg/bridge.d.ts +0 -1
  294. package/dist/commands/pkg/bridge.js +0 -137
  295. package/dist/commands/skill/author.d.ts +0 -3
  296. package/dist/commands/skill/author.js +0 -140
  297. package/dist/commands/skill/shared.d.ts +0 -3
  298. package/dist/commands/skill/shared.js +0 -19
  299. package/dist/commands/skill.d.ts +0 -2
  300. package/dist/commands/skill.js +0 -21
  301. package/dist/commands/tmux-spread.d.ts +0 -2
  302. package/dist/commands/tmux-spread.js +0 -144
  303. package/dist/core/__tests__/full/daemon-liveness-pane.full.test.js +0 -292
  304. package/dist/core/__tests__/full/placement-focus.test.js +0 -309
  305. package/dist/core/__tests__/full/placement-reconcile.test.js +0 -212
  306. package/dist/core/__tests__/full/placement-revive.test.js +0 -238
  307. package/dist/core/__tests__/full/placement-teardown.test.js +0 -297
  308. package/dist/core/__tests__/home-session.test.js +0 -186
  309. package/dist/core/__tests__/relaunch.test.js +0 -335
  310. package/dist/core/bridge-map.d.ts +0 -19
  311. package/dist/core/bridge-map.js +0 -73
  312. package/dist/core/tui/contract.d.ts +0 -83
  313. package/dist/core/tui/contract.js +0 -8
  314. package/dist/core/tui/loader.d.ts +0 -16
  315. package/dist/core/tui/loader.js +0 -94
  316. package/dist/prompts/skill.d.ts +0 -2
  317. package/dist/prompts/skill.js +0 -650
  318. /package/dist/{core/__tests__/full/daemon-liveness-pane.full.test.d.ts → clients/attach/__tests__/action-parity.test.d.ts} +0 -0
  319. /package/dist/{core/__tests__/full/placement-focus.test.d.ts → clients/attach/__tests__/clipboard-image.test.d.ts} +0 -0
  320. /package/dist/{core/__tests__/full/placement-reconcile.test.d.ts → clients/attach/__tests__/editor-newline.test.d.ts} +0 -0
  321. /package/dist/{core/__tests__/full/placement-revive.test.d.ts → clients/attach/__tests__/git-info.test.d.ts} +0 -0
  322. /package/dist/{core/__tests__/full/placement-teardown.test.d.ts → clients/attach/__tests__/onrequest-wired.test.d.ts} +0 -0
  323. /package/dist/{core/__tests__/home-session.test.d.ts → clients/attach/__tests__/slash-quit-copy.test.d.ts} +0 -0
  324. /package/dist/{core/__tests__/relaunch.test.d.ts → clients/attach/__tests__/titled-editor.test.d.ts} +0 -0
@@ -0,0 +1,586 @@
1
+ // @ts-check
2
+ /**
3
+ * Canvas Dashboard — the PORTABLE CORE of the crtr `canvas` view (manifest ·
4
+ * init · sources · intents). The READ-ONLY monitor of the live agent graph: one
5
+ * core renders in BOTH targets — the tmux TUI (`crtr view run canvas`, via
6
+ * `tui.mjs`) and the React+Tailwind web page (`crtr view serve canvas`, via
7
+ * `web.jsx`).
8
+ *
9
+ * Runs in BOTH Node and the browser, so it imports NOTHING — no `node:*`, no
10
+ * crtr. The data layer is expressed as transport-agnostic `Source` descriptors:
11
+ * the core describes WHAT to run (`request()` → a SourceRequest for
12
+ * `crtr … --json`), the host's Transport runs it (local `execFile` for the TUI,
13
+ * the HTTP bridge for web), and the pure `parse()` turns bytes → typed data | a
14
+ * typed `SourceError`. The forest building + relative-age / chrome-copy logic is
15
+ * pure string/data work that runs anywhere.
16
+ *
17
+ * It rebuilds the forest from each node's `parent` edge, keeps only trees that
18
+ * contain live (active/idle) or human-blocked work, and renders them as a
19
+ * scrollable ASCII tree (TUI) / DOM tree (web). It auto-polls (refreshMs) — no
20
+ * node-focus / close / swap (views are sessionless). j/k move a read cursor; g
21
+ * forces a refresh; q quits.
22
+ *
23
+ * NOTHING throws. Sources return a `Result<T>` (a typed `SourceError` on
24
+ * failure); the `refresh` intent KEEPS the last-known forest on a transient
25
+ * failure and raises the cause as a banner, dropping to a guided takeover only
26
+ * when there is nothing to keep (graceful partial failure).
27
+ *
28
+ * @module canvas/core
29
+ */
30
+
31
+ /**
32
+ * @typedef {import('../../core/view/contract.js').SourceError} SourceError
33
+ * @typedef {import('../../core/view/contract.js').IntentCtx<CanvasState>} Ctx
34
+ */
35
+
36
+ /**
37
+ * One canvas node, flattened from `node inspect list --json`.
38
+ * @typedef {Object} CanvasNode
39
+ * @property {string} nodeId Full node id (`<time>-<hash>`).
40
+ * @property {string} name Display name.
41
+ * @property {string} kind Node kind (general/developer/explore/human/…).
42
+ * @property {string} mode base | orchestrator.
43
+ * @property {string} lifecycle resident | terminal.
44
+ * @property {string} status active | idle | done | dead | canceled.
45
+ * @property {string|null} parent Spawn/subscription parent id (null ⇒ a forest root).
46
+ * @property {string} created ISO 8601 birth timestamp (drives child ordering).
47
+ */
48
+
49
+ /**
50
+ * One cwd with pending human asks, from `canvas attention list --json`.
51
+ * @typedef {Object} AttentionItem
52
+ * @property {string} nodeId
53
+ * @property {string} name
54
+ * @property {string} cwd
55
+ * @property {number} count Pending ask count.
56
+ */
57
+
58
+ /**
59
+ * One rendered tree line (a flattened forest node). Width-independent, so it is
60
+ * built once per refresh and re-rendered on resize without a re-fetch.
61
+ * @typedef {Object} TreeRow
62
+ * @property {string} nodeId
63
+ * @property {string} prefix Tree-branch art (e.g. "│ └─ "); '' for a root.
64
+ * @property {string} glyph Status glyph.
65
+ * @property {string} status
66
+ * @property {string} name
67
+ * @property {string} kind
68
+ * @property {string} mode
69
+ * @property {string} lifecycle
70
+ * @property {string} shortId
71
+ * @property {string} created ISO 8601 birth timestamp (drives the right-flush age).
72
+ * @property {boolean} blocked True ⇒ this node has pending human asks.
73
+ * @property {number} askCount
74
+ */
75
+
76
+ /**
77
+ * The view's immutable state (the core owns it; intents replace it via ctx.set).
78
+ * @typedef {Object} CanvasState
79
+ * @property {TreeRow[]} rows Flattened active-tree forest (render source).
80
+ * @property {number} cursor Read cursor into rows (j/k).
81
+ * @property {number} scroll draw.list scroll, stored back each frame.
82
+ * @property {number} shownRoots How many trees survived the active filter.
83
+ * @property {number} totalNodes Total nodes on the canvas (all statuses).
84
+ * @property {number} activeCount Nodes with status 'active' (canvas-wide).
85
+ * @property {number} attnTotal Total pending human asks (canvas-wide).
86
+ * @property {number} lastFetch Epoch ms of the last successful refresh.
87
+ * @property {SourceError|null} sourceError Non-null ⇒ the data source failed;
88
+ * presenters render its `display` VERBATIM (the not-ready takeover when there
89
+ * is no data, and the dump fallback). Cleared on a successful refresh.
90
+ */
91
+
92
+ // ── Result helpers (inlined — the core imports nothing) ───────────────────────
93
+
94
+ /** @template T @param {T} data @returns {{ok:true, data:T}} */
95
+ function ok(data) {
96
+ return { ok: true, data };
97
+ }
98
+ /** @param {SourceError} error @returns {{ok:false, error:SourceError}} */
99
+ function fail(error) {
100
+ return { ok: false, error };
101
+ }
102
+
103
+ // ── Field mappers ───────────────────────────────────────────────────
104
+
105
+ /** @param {unknown} v @returns {string} */
106
+ function str(v) {
107
+ return v == null ? '' : String(v);
108
+ }
109
+
110
+ /** @param {any} n @returns {CanvasNode} */
111
+ function toNode(n) {
112
+ const o = n || {};
113
+ return {
114
+ nodeId: str(o.node_id),
115
+ name: str(o.name) || '(unnamed)',
116
+ kind: str(o.kind) || '?',
117
+ mode: str(o.mode) || '?',
118
+ lifecycle: str(o.lifecycle) || '?',
119
+ status: str(o.status) || '?',
120
+ parent: o.parent ? str(o.parent) : null,
121
+ created: str(o.created),
122
+ };
123
+ }
124
+
125
+ /** @param {any} i @returns {AttentionItem} */
126
+ function toAttention(i) {
127
+ const o = i || {};
128
+ return {
129
+ nodeId: str(o.node_id),
130
+ name: str(o.name),
131
+ cwd: str(o.cwd),
132
+ count: typeof o.count === 'number' ? o.count : 0,
133
+ };
134
+ }
135
+
136
+ // ── Status vocabulary (mirrors core/canvas/browse/render.ts — single source) ──
137
+
138
+ const LIVE_STATUS = new Set(['active', 'idle']);
139
+
140
+ /** @type {Record<string,string>} */
141
+ export const STATUS_GLYPH = {
142
+ active: '●',
143
+ idle: '○',
144
+ done: '✓',
145
+ dead: '✗',
146
+ canceled: '⊘',
147
+ };
148
+
149
+ /** @param {string} lifecycle @returns {string} */
150
+ export function lifeAbbr(lifecycle) {
151
+ if (lifecycle === 'resident') return 'res';
152
+ if (lifecycle === 'terminal') return 'term';
153
+ return lifecycle || '?';
154
+ }
155
+
156
+ /** @param {string} id @returns {string} */
157
+ function shortId(id) {
158
+ const s = String(id || '');
159
+ const dash = s.indexOf('-');
160
+ return dash > 0 ? s.slice(0, dash) : s.slice(0, 8);
161
+ }
162
+
163
+ const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
164
+
165
+ /**
166
+ * Relative-age ladder (design §5): `now` (<60s), `{m}m` (<60m), `{h}h` (<24h),
167
+ * `{d}d` (<7d), else a calendar date `Mon D` (`Mar 4`), prior-year `Mon ʼYY`.
168
+ * Max ~5 cols. Used for the right-flushed per-row age (the node's birth time).
169
+ * Shared by the TUI render + the text dump (both import it from here).
170
+ * @param {string} createdIso @param {number} now @returns {string}
171
+ */
172
+ export function relAge(createdIso, now) {
173
+ const t = Date.parse(createdIso);
174
+ if (Number.isNaN(t)) return '';
175
+ const s = Math.max(0, Math.floor((now - t) / 1000));
176
+ if (s < 60) return 'now';
177
+ const m = Math.floor(s / 60);
178
+ if (m < 60) return `${m}m`;
179
+ const h = Math.floor(m / 60);
180
+ if (h < 24) return `${h}h`;
181
+ const d = Math.floor(h / 24);
182
+ if (d < 7) return `${d}d`;
183
+ const date = new Date(t);
184
+ const mon = MONTHS[date.getMonth()] || '?';
185
+ if (date.getFullYear() === new Date(now).getFullYear()) return `${mon} ${date.getDate()}`;
186
+ return `${mon} ʼ${String(date.getFullYear()).slice(-2)}`;
187
+ }
188
+
189
+ /** @param {number} n @param {string} w @returns {string} */
190
+ export function plural(n, w) {
191
+ return `${n} ${w}${n === 1 ? '' : 's'}`;
192
+ }
193
+
194
+ // ── Typed SourceError displays (the `display`/`kind` split; presenters render
195
+ // `display` verbatim and never branch on `kind`) ────────────────────────────
196
+
197
+ /** @type {SourceError} */
198
+ const CRTR_MISSING = {
199
+ kind: 'crtr-missing',
200
+ display: {
201
+ headline: 'Canvas unavailable',
202
+ explanation: 'crtr could not be found to read the canvas graph.',
203
+ nextStep: 'Install crtr (or set CRTR_BIN on PATH), then press g.',
204
+ level: 'error',
205
+ blocking: true,
206
+ },
207
+ };
208
+
209
+ /** @param {string} stderr @returns {SourceError} */
210
+ function crtrFailed(stderr) {
211
+ const s = String(stderr || '');
212
+ if (/help-gate:\s*blocked/i.test(s)) {
213
+ return {
214
+ kind: 'help-gate',
215
+ display: {
216
+ headline: 'Canvas unavailable',
217
+ explanation: 'crtr help-gate blocked the call (run the command with -h once).',
218
+ nextStep: 'Press g to retry.',
219
+ level: 'error',
220
+ blocking: true,
221
+ },
222
+ };
223
+ }
224
+ return {
225
+ kind: 'crtr-failed',
226
+ display: {
227
+ headline: 'Canvas unavailable',
228
+ explanation: 'crtr could not read the canvas graph: ' + (extractMessage(s) || 'unknown error'),
229
+ nextStep: 'Press g to retry.',
230
+ level: 'error',
231
+ blocking: true,
232
+ },
233
+ };
234
+ }
235
+
236
+ /** @param {string} cmd @returns {SourceError} */
237
+ function parseError(cmd) {
238
+ return {
239
+ kind: 'parse',
240
+ display: {
241
+ headline: 'Canvas unavailable',
242
+ explanation: `Could not parse \`crtr ${cmd}\` output as JSON.`,
243
+ nextStep: 'Press g to retry.',
244
+ level: 'error',
245
+ blocking: true,
246
+ },
247
+ };
248
+ }
249
+
250
+ /**
251
+ * Pull a human message out of crtr stderr: prefer the last `ERROR:` line, else
252
+ * the last non-empty line, else ''.
253
+ * @param {string} stderr @returns {string}
254
+ */
255
+ function extractMessage(stderr) {
256
+ const lines = String(stderr || '')
257
+ .split(/\r?\n/)
258
+ .map((l) => l.trim())
259
+ .filter(Boolean);
260
+ for (let i = lines.length - 1; i >= 0; i--) {
261
+ if (/^ERROR:/i.test(lines[i])) return lines[i].replace(/^ERROR:\s*/i, '').trim();
262
+ }
263
+ if (lines.length) return lines[lines.length - 1];
264
+ return '';
265
+ }
266
+
267
+ // ── Sources (reads): a request descriptor + a pure parse. The host's transport
268
+ // runs the request (local execFile for TUI, the HTTP bridge for web). The
269
+ // spawned `crtr` is a grandchild of the view host — not a pi tool call — so
270
+ // the help-gate never intercepts it. ────────────────────────────────────────
271
+
272
+ /**
273
+ * Every node on the canvas (all statuses). The view rebuilds the forest from the
274
+ * `parent` edges and filters to active trees itself. The PRIMARY instrument —
275
+ * its parse owns the hard failure (crtr missing / a failing crtr command).
276
+ * @type {import('../../core/view/contract.js').Source<CanvasNode[]>}
277
+ */
278
+ export const nodesSource = {
279
+ id: 'nodes',
280
+ request: () => ({ kind: 'exec', bin: 'crtr', args: ['node', 'inspect', 'list', '--json'] }),
281
+ parse: (raw) => {
282
+ if (!raw.ok) return fail(CRTR_MISSING);
283
+ if (raw.exitCode !== 0) return fail(crtrFailed(raw.stderr || raw.stdout));
284
+ const out = String(raw.stdout || '').trim();
285
+ let data;
286
+ try {
287
+ data = out === '' ? {} : JSON.parse(out);
288
+ } catch {
289
+ return fail(parseError('node inspect list'));
290
+ }
291
+ const arr = data && Array.isArray(data.nodes) ? data.nodes : [];
292
+ return ok(arr.map(toNode));
293
+ },
294
+ };
295
+
296
+ /**
297
+ * Pending human asks across the canvas (the "blocked on a human" signal).
298
+ * BEST-EFFORT: any failure degrades to no flags (the graph still renders), so a
299
+ * parse never blocks the monitor — it returns an empty set instead of an error.
300
+ * @type {import('../../core/view/contract.js').Source<{items:AttentionItem[], total:number}>}
301
+ */
302
+ export const attentionSource = {
303
+ id: 'attention',
304
+ request: () => ({ kind: 'exec', bin: 'crtr', args: ['canvas', 'attention', 'list', '--json'] }),
305
+ parse: (raw) => {
306
+ if (!raw.ok || raw.exitCode !== 0) return ok({ items: [], total: 0 });
307
+ const out = String(raw.stdout || '').trim();
308
+ let data;
309
+ try {
310
+ data = out === '' ? {} : JSON.parse(out);
311
+ } catch {
312
+ return ok({ items: [], total: 0 });
313
+ }
314
+ const items = data && Array.isArray(data.items) ? data.items : [];
315
+ const total = data && typeof data.total === 'number' ? data.total : 0;
316
+ return ok({ items: items.map(toAttention), total });
317
+ },
318
+ };
319
+
320
+ // ── Forest builder ─────────────────────────────────────────────
321
+
322
+ /**
323
+ * Build the flattened active-tree forest from a flat node list + the
324
+ * human-blocked id→count map.
325
+ *
326
+ * A tree is shown iff its subtree contains any LIVE node (status active|idle) OR
327
+ * any human-blocked node — so a fully-finished tree drops out, but a blocked one
328
+ * always surfaces. Shown trees are rendered IN FULL (finished children included)
329
+ * to preserve context, mirroring `crtr canvas dashboard`.
330
+ *
331
+ * @param {CanvasNode[]} nodes
332
+ * @param {Map<string,number>} blockedById
333
+ * @returns {{rows: TreeRow[], shownRoots: number}}
334
+ */
335
+ export function buildForest(nodes, blockedById) {
336
+ /** @type {Map<string, CanvasNode>} */
337
+ const byId = new Map();
338
+ for (const n of nodes) byId.set(n.nodeId, n);
339
+
340
+ /** @type {Map<string, CanvasNode[]>} */
341
+ const children = new Map();
342
+ /** @type {CanvasNode[]} */
343
+ const roots = [];
344
+ for (const n of nodes) {
345
+ const parent = n.parent && byId.has(n.parent) ? n.parent : null;
346
+ if (parent) {
347
+ const arr = children.get(parent);
348
+ if (arr) arr.push(n);
349
+ else children.set(parent, [n]);
350
+ } else {
351
+ roots.push(n);
352
+ }
353
+ }
354
+ // Order siblings + roots by birth time (matches spawn order).
355
+ const cmp = (/** @type {CanvasNode} */ a, /** @type {CanvasNode} */ b) =>
356
+ a.created < b.created ? -1 : a.created > b.created ? 1 : 0;
357
+ for (const arr of children.values()) arr.sort(cmp);
358
+ roots.sort(cmp);
359
+
360
+ // Subtree liveness, memoised + cycle-guarded (the graph is a forest, but the
361
+ // db is mutable — guard defensively).
362
+ /** @type {Map<string, boolean>} */
363
+ const liveMemo = new Map();
364
+ /** @param {string} id @param {Set<string>} guard @returns {boolean} */
365
+ function subtreeLive(id, guard) {
366
+ const memo = liveMemo.get(id);
367
+ if (memo !== undefined) return memo;
368
+ if (guard.has(id)) return false;
369
+ guard.add(id);
370
+ const node = byId.get(id);
371
+ let res = !!node && (LIVE_STATUS.has(node.status) || blockedById.has(id));
372
+ if (!res) {
373
+ for (const c of children.get(id) || []) {
374
+ if (subtreeLive(c.nodeId, guard)) {
375
+ res = true;
376
+ break;
377
+ }
378
+ }
379
+ }
380
+ guard.delete(id);
381
+ liveMemo.set(id, res);
382
+ return res;
383
+ }
384
+
385
+ /** @param {CanvasNode} node @param {string} prefix @returns {TreeRow} */
386
+ function makeRow(node, prefix) {
387
+ const askCount = blockedById.get(node.nodeId) || 0;
388
+ return {
389
+ nodeId: node.nodeId,
390
+ prefix,
391
+ glyph: STATUS_GLYPH[node.status] || '?',
392
+ status: node.status,
393
+ name: node.name,
394
+ kind: node.kind,
395
+ mode: node.mode,
396
+ lifecycle: node.lifecycle,
397
+ shortId: shortId(node.nodeId),
398
+ created: node.created,
399
+ blocked: askCount > 0,
400
+ askCount,
401
+ };
402
+ }
403
+
404
+ /** @type {TreeRow[]} */
405
+ const rows = [];
406
+ /** @type {Set<string>} */
407
+ const visited = new Set();
408
+ /** @param {CanvasNode} node @param {string} indent @param {boolean} isLast @param {boolean} isRoot */
409
+ function walk(node, indent, isLast, isRoot) {
410
+ if (visited.has(node.nodeId)) return; // cycle guard
411
+ visited.add(node.nodeId);
412
+ const prefix = isRoot ? '' : indent + (isLast ? '└─ ' : '├─ ');
413
+ rows.push(makeRow(node, prefix));
414
+ const kids = children.get(node.nodeId) || [];
415
+ const childIndent = isRoot ? '' : indent + (isLast ? ' ' : '│ ');
416
+ for (let i = 0; i < kids.length; i++) {
417
+ walk(kids[i], childIndent, i === kids.length - 1, false);
418
+ }
419
+ }
420
+
421
+ let shownRoots = 0;
422
+ for (const r of roots) {
423
+ if (!subtreeLive(r.nodeId, new Set())) continue;
424
+ shownRoots++;
425
+ walk(r, '', true, true);
426
+ }
427
+ return { rows, shownRoots };
428
+ }
429
+
430
+ // ── Chrome copy (subtitle / footer / dump; pure) ──────────────────────────────
431
+
432
+ /**
433
+ * Live title subtitle — canvas-wide health (design §3 "a dim ` · <subtitle>`").
434
+ * `null` ⇒ no subtitle (the title leads alone) on an empty canvas.
435
+ * @param {CanvasState} state @returns {string|null}
436
+ */
437
+ function subtitleFor(state) {
438
+ if (state.totalNodes === 0) return null;
439
+ return `${state.activeCount} active · ${plural(state.totalNodes, 'node')}`;
440
+ }
441
+
442
+ /**
443
+ * Footer status (left, transient) — the RENDERED forest scope, distinct from the
444
+ * canvas-wide subtitle. `null` ⇒ nothing (the empty/loading state speaks).
445
+ * @param {CanvasState} state @returns {string|null}
446
+ */
447
+ function footerSummary(state) {
448
+ if (state.rows.length === 0) return null;
449
+ return `${plural(state.shownRoots, 'live tree')} · ${state.rows.length} shown`;
450
+ }
451
+
452
+ /** @param {CanvasState} state @returns {string} */
453
+ export function dumpSummary(state) {
454
+ return (
455
+ `${plural(state.shownRoots, 'tree')} · ${state.rows.length} shown · ` +
456
+ `${state.activeCount} active · ${plural(state.attnTotal, 'ask')} · ` +
457
+ `${state.totalNodes} total`
458
+ );
459
+ }
460
+
461
+ // ── The portable core ──────────────────────────────────────────────────────────
462
+
463
+ /** @type {import('../../core/view/contract.js').ViewCore<CanvasState>} */
464
+ const core = {
465
+ manifest: {
466
+ id: 'canvas',
467
+ title: 'Canvas',
468
+ description: 'Live agent graph — who is working, who is blocked',
469
+ refreshMs: 3000,
470
+ },
471
+
472
+ /**
473
+ * Cheap + synchronous initial state — NO slow fetch (the host paints a loading
474
+ * frame, then dispatches the first 'refresh').
475
+ * @returns {CanvasState}
476
+ */
477
+ init() {
478
+ return {
479
+ rows: [],
480
+ cursor: 0,
481
+ scroll: 0,
482
+ shownRoots: 0,
483
+ totalNodes: 0,
484
+ activeCount: 0,
485
+ attnTotal: 0,
486
+ lastFetch: 0,
487
+ sourceError: null,
488
+ };
489
+ },
490
+
491
+ sources: { nodesSource, attentionSource },
492
+
493
+ intents: {
494
+ /**
495
+ * Pull the node graph + attention, rebuild the active-tree forest. Runs in
496
+ * the host's single-flight lane (launch, refreshMs, and the `refresh`
497
+ * keybind). A nodes failure KEEPS the last-known forest and raises the cause
498
+ * as a banner (the not-ready takeover only owns the screen when there is
499
+ * nothing to keep — see the presenters). Attention is best-effort: a failure
500
+ * still renders the graph, just without the blocked flags.
501
+ * @param {Ctx} ctx
502
+ */
503
+ async refresh(ctx) {
504
+ ctx.signal.setStatus('Loading the canvas…');
505
+
506
+ const rn = await ctx.resolve(nodesSource);
507
+ if (!rn.ok) {
508
+ // Data source down. KEEP the last-known forest; store the typed error
509
+ // (drives the BLOCKED chip + the not-ready takeover when rows are empty)
510
+ // and raise the cause as the error banner.
511
+ ctx.set((s) => ({ ...s, sourceError: rn.error }));
512
+ ctx.signal.setStatus(null);
513
+ ctx.signal.setBanner(rn.error.display.explanation, rn.error.display.level);
514
+ return;
515
+ }
516
+ const nodes = rn.data;
517
+
518
+ /** @type {Map<string,number>} */
519
+ const blockedById = new Map();
520
+ let attnTotal = 0;
521
+ const ra = await ctx.resolve(attentionSource);
522
+ if (ra.ok) {
523
+ for (const it of ra.data.items) {
524
+ if (it.nodeId) blockedById.set(it.nodeId, it.count);
525
+ }
526
+ attnTotal = ra.data.total;
527
+ }
528
+
529
+ const built = buildForest(nodes, blockedById);
530
+ ctx.set((s) => {
531
+ /** @type {CanvasState} */
532
+ const next = {
533
+ ...s,
534
+ sourceError: null,
535
+ rows: built.rows,
536
+ shownRoots: built.shownRoots,
537
+ totalNodes: nodes.length,
538
+ activeCount: nodes.filter((n) => n.status === 'active').length,
539
+ attnTotal,
540
+ lastFetch: Date.now(),
541
+ };
542
+ if (next.cursor >= next.rows.length) next.cursor = Math.max(0, next.rows.length - 1);
543
+ return next;
544
+ });
545
+
546
+ ctx.signal.setSubtitle(subtitleFor(ctx.state));
547
+
548
+ // Data-freshness → state chip. Pending human asks are the one thing that
549
+ // wants a human: raise an ACTION banner → the host derives the ATTENTION
550
+ // (yellow) chip. Otherwise clear → READY (green). (busy→working is
551
+ // automatic.)
552
+ if (attnTotal > 0) {
553
+ ctx.signal.setBanner(`${plural(attnTotal, 'ask')} waiting on a human — see the ⚑ rows`, 'action');
554
+ } else {
555
+ ctx.signal.clearBanner();
556
+ }
557
+ ctx.signal.setStatus(footerSummary(ctx.state));
558
+ },
559
+
560
+ /** @param {Ctx} ctx */
561
+ cursorDown: (ctx) => ctx.set((s) => ({ ...s, cursor: s.rows.length ? Math.min(s.rows.length - 1, s.cursor + 1) : 0 })),
562
+ /** @param {Ctx} ctx */
563
+ cursorUp: (ctx) => ctx.set((s) => ({ ...s, cursor: Math.max(0, s.cursor - 1) })),
564
+ /**
565
+ * Activate (open) a node. STANDALONE this just sets the read cursor to the
566
+ * node's row — the view stays a read-only monitor. In the WEB SHELL the
567
+ * `{nodeId}` payload is what the host's intent TAP observes to open that
568
+ * node's conversation pane (§5): the core never imports the shell and never
569
+ * knows whether anyone is listening; it always emits the intent, and the
570
+ * payload carries the referent. Unknown/missing id ⇒ no-op.
571
+ * @param {Ctx} ctx @param {{nodeId?: string}} [payload]
572
+ */
573
+ activate: (ctx, payload) => {
574
+ const id = payload && payload.nodeId;
575
+ if (!id) return;
576
+ ctx.set((s) => {
577
+ const i = s.rows.findIndex((r) => r.nodeId === id);
578
+ return i >= 0 ? { ...s, cursor: i } : s;
579
+ });
580
+ },
581
+ /** @param {Ctx} ctx */
582
+ quit: (ctx) => ctx.signal.quit(),
583
+ },
584
+ };
585
+
586
+ export default core;
@@ -0,0 +1,58 @@
1
+ // @ts-check
2
+ /**
3
+ * Canvas Dashboard — the text presenter (`dump`) for the non-TTY / piped path of
4
+ * the `canvas` view (`crtr view run canvas | cat`). Node-only, no ANSI. A plain
5
+ * snapshot of the current state; the host threads its current banner via `ctx`.
6
+ *
7
+ * A data-source failure is a typed `SourceError` (we render its
8
+ * `display.explanation` VERBATIM, never branching on `kind`). The pure helpers
9
+ * `dump` shares with the TUI (relAge, lifeAbbr, dumpSummary) are imported from
10
+ * `core.mjs`.
11
+ *
12
+ * @module canvas/text
13
+ */
14
+
15
+ import { relAge, lifeAbbr, dumpSummary } from './core.mjs';
16
+
17
+ /** @typedef {import('./core.mjs').CanvasState} CanvasState */
18
+
19
+ /**
20
+ * Plain-text snapshot for the non-TTY / piped path. No ANSI. The host threads
21
+ * its current banner via `ctx` so guidance (a source error / a pending-ask
22
+ * action) surfaces without the view mirroring it into state.
23
+ * @param {CanvasState} state
24
+ * @param {import('../../core/view/contract.js').DumpContext} [ctx]
25
+ * @returns {string}
26
+ */
27
+ export function dump(state, ctx) {
28
+ /** @type {string[]} */
29
+ const lines = ['Canvas — live agent graph'];
30
+ const banner = ctx && ctx.banner ? ctx.banner : null;
31
+ if (banner) lines.push('', `[${banner.level}] ${banner.msg}`);
32
+ else if (state.sourceError) lines.push('', `[${state.sourceError.display.level}] ${state.sourceError.display.explanation}`);
33
+
34
+ lines.push('', dumpSummary(state), '');
35
+
36
+ if (state.rows.length === 0) {
37
+ lines.push(
38
+ state.sourceError
39
+ ? '(canvas unavailable)'
40
+ : state.lastFetch === 0
41
+ ? '(loading…)'
42
+ : state.totalNodes === 0
43
+ ? '(no nodes on the canvas)'
44
+ : '(no active trees)',
45
+ );
46
+ return lines.join('\n');
47
+ }
48
+
49
+ const now = Date.now();
50
+ for (const r of state.rows) {
51
+ const blk = r.blocked ? ` ⚑${r.askCount}` : '';
52
+ const age = relAge(r.created, now);
53
+ lines.push(
54
+ `${r.prefix}${r.glyph} ${r.name} [${r.kind}/${r.mode}] ${lifeAbbr(r.lifecycle)} ${r.shortId} ${age}${blk}`,
55
+ );
56
+ }
57
+ return lines.join('\n');
58
+ }