@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,621 @@
1
+ // @ts-check
2
+ /**
3
+ * Workspace Sidebar — the PORTABLE CORE of the `workspace-sidebar` view
4
+ * (manifest · init · sources · commands · intents). The left rail of
5
+ * `crtr workspace`: a narrow, cwd-scoped navigator with two sections —
6
+ *
7
+ * ⌗ this graph the subtree of the node the chat pane is attached to
8
+ * (its forest root), rendered as an ASCII tree.
9
+ * ↪ elsewhere the OTHER top-level nodes (forest roots) started in this
10
+ * cwd — root rows only, ⚑ inbox flags.
11
+ *
12
+ * Runs in BOTH Node (the tmux TUI, via `tui.mjs`) and the browser (the
13
+ * React+Tailwind page, via `web.jsx`), so it imports NOTHING — no `node:*`, no
14
+ * crtr. The data layer is expressed as transport-agnostic `Source`/`Command`
15
+ * descriptors: the core describes WHAT to run (`request()` → a SourceRequest for
16
+ * `crtr`/`tmux`), the host's Transport runs it (local `execFile` for the TUI,
17
+ * the HTTP bridge for web), and the pure `parse()` turns bytes → typed data. The
18
+ * forest model, cwd scoping, attention rollup, and cursor logic are pure string
19
+ * work that runs anywhere.
20
+ *
21
+ * The rail is a CONTROLLER in the TUI: the `open` intent shells
22
+ * `crtr node focus <id> --pane <chatPane>` (a `Command`), swapping the selected
23
+ * node into the OTHER pane. The chat pane + the attached node are resolved each
24
+ * refresh from the window's tmux panes (the `@crtr_node` tag attach self-sets,
25
+ * which survives the swap). On the web target there is no chat pane — the same
26
+ * rail renders as a plain styled list; `open` no-ops with a banner.
27
+ *
28
+ * NOTHING throws. Sources return a `Result<T>` (typed `SourceError` on a hard
29
+ * failure); `refresh` keeps the last-known rail on a transient failure.
30
+ *
31
+ * @module workspace-sidebar/core
32
+ */
33
+
34
+ /**
35
+ * @typedef {import('../../core/view/contract.js').SourceError} SourceError
36
+ * @typedef {import('../../core/view/contract.js').IntentCtx<SidebarState>} Ctx
37
+ */
38
+
39
+ // ── Status vocabulary (single source: core/canvas/browse/render.ts) ───────────
40
+
41
+ const LIVE_STATUS = new Set(['active', 'idle']);
42
+
43
+ // ── Types ─────────────────────────────────────────────────────────────────────
44
+
45
+ /**
46
+ * One canvas node, flattened from `node inspect list --json`.
47
+ * @typedef {Object} CanvasNode
48
+ * @property {string} nodeId
49
+ * @property {string} name
50
+ * @property {string} kind
51
+ * @property {string} mode
52
+ * @property {string} lifecycle
53
+ * @property {string} status active | idle | done | dead | canceled.
54
+ * @property {string} cwd Originating cwd — the workspace scope key.
55
+ * @property {string|null} parent Spawn/subscription parent id (null ⇒ a root).
56
+ * @property {string} created ISO 8601 birth timestamp (drives ordering).
57
+ */
58
+
59
+ /**
60
+ * One LOGICAL rail row (each presenter paints it in its own idiom). A node row
61
+ * is selectable; header/chrome rows are inert chrome.
62
+ * @typedef {{kind:'header', text:string}
63
+ * | {kind:'chrome', text:string}
64
+ * | {kind:'node', nodeId:string, name:string, status:string, prefix:string, attached:boolean, asks:number}} RailRow
65
+ */
66
+
67
+ /**
68
+ * The view's immutable state (the core owns it; intents replace it via ctx.set).
69
+ * @typedef {Object} SidebarState
70
+ * @property {string} cwd Workspace scope key (process.cwd()).
71
+ * @property {string} targetOverride Optional explicit chat pane id (options.target); '' if unset.
72
+ * @property {string} chatPane tmux pane id of the chat pane, resolved each refresh.
73
+ * @property {string|null} attachedNode Node the chat pane currently views (@crtr_node), or null.
74
+ * @property {string|null} currentRoot Forest root of `this graph`.
75
+ * @property {RailRow[]} rows Flattened render list (chrome + node rows).
76
+ * @property {number} cursor Index into `rows` (always lands on a node row).
77
+ * @property {number} scroll draw.list scroll, stored back each frame.
78
+ * @property {number} graphsHere Live forest roots in this cwd.
79
+ * @property {number} nodesHere Total nodes in this cwd.
80
+ * @property {number} asksHere Total pending asks across the rendered set.
81
+ * @property {number} lastFetch Epoch ms of the last successful refresh.
82
+ * @property {SourceError|null} srcError Non-null ⇒ the node source failed (typed).
83
+ */
84
+
85
+ // ── Result helpers (inlined — the core imports nothing) ───────────────────────
86
+
87
+ /** @template T @param {T} data @returns {{ok:true, data:T}} */
88
+ function ok(data) {
89
+ return { ok: true, data };
90
+ }
91
+ /** @param {SourceError} error @returns {{ok:false, error:SourceError}} */
92
+ function fail(error) {
93
+ return { ok: false, error };
94
+ }
95
+
96
+ /** @param {string} s @returns {string} */
97
+ function firstLine(s) {
98
+ const lines = String(s || '').split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
99
+ return lines.length ? lines[0] : '';
100
+ }
101
+
102
+ /** @param {unknown} v @returns {string} */
103
+ function str(v) {
104
+ return v == null ? '' : String(v);
105
+ }
106
+
107
+ /** @param {number} n @param {string} w @returns {string} */
108
+ export function plural(n, w) {
109
+ return `${n} ${w}${n === 1 ? '' : 's'}`;
110
+ }
111
+
112
+ /** @param {any} n @returns {CanvasNode} */
113
+ function toNode(n) {
114
+ const o = n || {};
115
+ return {
116
+ nodeId: str(o.node_id),
117
+ name: str(o.name) || '(unnamed)',
118
+ kind: str(o.kind) || '?',
119
+ mode: str(o.mode) || '?',
120
+ lifecycle: str(o.lifecycle) || '?',
121
+ status: str(o.status) || '?',
122
+ cwd: str(o.cwd),
123
+ parent: o.parent ? str(o.parent) : null,
124
+ created: str(o.created),
125
+ };
126
+ }
127
+
128
+ // ── Typed SourceError displays (presenters render `display` verbatim) ─────────
129
+
130
+ /** @type {SourceError} */
131
+ const CRTR_MISSING = {
132
+ kind: 'crtr-missing',
133
+ display: {
134
+ headline: 'crtr unavailable',
135
+ explanation: 'The crtr binary was not found on PATH.',
136
+ nextStep: 'Install crtr (or set it on PATH), then press g.',
137
+ level: 'error',
138
+ blocking: true,
139
+ },
140
+ };
141
+ /** @param {string} msg @returns {SourceError} */
142
+ function crtrFailed(msg) {
143
+ return {
144
+ kind: 'crtr-failed',
145
+ display: {
146
+ headline: 'Canvas unavailable',
147
+ explanation: msg || 'crtr could not read the graph.',
148
+ nextStep: 'Press g to retry.',
149
+ level: 'error',
150
+ blocking: false,
151
+ },
152
+ };
153
+ }
154
+
155
+ // ── Sources (reads): a request descriptor + a pure parse ──────────────────────
156
+
157
+ /**
158
+ * Every node on the canvas (all statuses). The view filters to the workspace cwd
159
+ * and rebuilds the forest from `parent` edges itself. The PRIMARY instrument —
160
+ * its parse owns the two hard failures (crtr missing / a failing crtr command).
161
+ * @type {import('../../core/view/contract.js').Source<CanvasNode[]>}
162
+ */
163
+ export const nodesSource = {
164
+ id: 'nodes',
165
+ request: () => ({ kind: 'exec', bin: 'crtr', args: ['node', 'inspect', 'list', '--json'] }),
166
+ parse: (raw) => {
167
+ if (!raw.ok) return fail(CRTR_MISSING);
168
+ if (raw.exitCode !== 0) return fail(crtrFailed(firstLine(raw.stderr || raw.stdout)));
169
+ const out = String(raw.stdout || '').trim();
170
+ let data;
171
+ try {
172
+ data = out === '' ? {} : JSON.parse(out);
173
+ } catch {
174
+ return fail(crtrFailed('could not parse `crtr node inspect list` output as JSON'));
175
+ }
176
+ const arr = data && Array.isArray(data.nodes) ? data.nodes : [];
177
+ return ok(arr.map(toNode));
178
+ },
179
+ };
180
+
181
+ /**
182
+ * Per-node pending-ask counts for a visible set, in ONE pass: the current
183
+ * graph's whole sub-DAG (`--view <root>`) unioned with explicit `nodeIds` (the
184
+ * other roots in this cwd). BEST-EFFORT — any failure yields {} so the rail
185
+ * still renders without ⚑ flags.
186
+ * @type {import('../../core/view/contract.js').Source<Record<string,number>, {viewRoot:string|null, nodeIds:string[]}>}
187
+ */
188
+ export const attentionSource = {
189
+ id: 'attention',
190
+ request: ({ viewRoot, nodeIds }) => {
191
+ const args = ['canvas', 'attention', 'map', '--json'];
192
+ if (viewRoot) args.push('--view', viewRoot);
193
+ const extra = (nodeIds || []).filter(Boolean);
194
+ if (extra.length) args.push('--nodes', extra.join(','));
195
+ return { kind: 'exec', bin: 'crtr', args };
196
+ },
197
+ parse: (raw) => {
198
+ if (!raw.ok || raw.exitCode !== 0) return ok({});
199
+ let data;
200
+ try {
201
+ data = JSON.parse(String(raw.stdout || '').trim() || '{}');
202
+ } catch {
203
+ return ok({});
204
+ }
205
+ const counts = data && data.counts && typeof data.counts === 'object' ? data.counts : {};
206
+ /** @type {Record<string, number>} */
207
+ const out = {};
208
+ for (const [k, v] of Object.entries(counts)) out[k] = typeof v === 'number' ? v : 0;
209
+ return ok(out);
210
+ },
211
+ };
212
+
213
+ /**
214
+ * The host's working directory — the workspace scope key the rail filters on.
215
+ * Resolved through the transport (which runs exec in the host's cwd) so it is
216
+ * uniform across BOTH targets: `process.cwd()` is a Node-only facility the
217
+ * browser can't read, so the web target would otherwise scope to '' and show an
218
+ * empty rail. BEST-EFFORT — a failure falls back to the init-time cwd.
219
+ * @type {import('../../core/view/contract.js').Source<string>}
220
+ */
221
+ export const cwdSource = {
222
+ id: 'cwd',
223
+ request: () => ({ kind: 'exec', bin: 'pwd', args: [] }),
224
+ parse: (raw) => ok(raw.ok && raw.exitCode === 0 ? String(raw.stdout || '').trim() : ''),
225
+ };
226
+
227
+ /**
228
+ * The tmux panes in the rail's window, each with its `@crtr_node` tag. The chat
229
+ * pane the rail drives is the one attach has tagged (the tag survives the
230
+ * swap-pane `crtr node focus` does). BEST-EFFORT — no tmux / a failure yields []
231
+ * so the rail still renders (just without a chat-pane controller; the web target
232
+ * always lands here). The self-pane never carries `@crtr_node` (the rail tags
233
+ * `@crtr_view`), so "first pane with a node tag" is the chat pane without any
234
+ * self-exclusion.
235
+ * @type {import('../../core/view/contract.js').Source<Array<{pane:string, node:string}>>}
236
+ */
237
+ export const panesSource = {
238
+ id: 'tmux-panes',
239
+ request: () => ({ kind: 'exec', bin: 'tmux', args: ['list-panes', '-F', '#{pane_id}\t#{@crtr_node}'] }),
240
+ parse: (raw) => {
241
+ if (!raw.ok || raw.exitCode !== 0) return ok([]);
242
+ /** @type {Array<{pane:string, node:string}>} */
243
+ const rows = [];
244
+ for (const line of String(raw.stdout || '').split(/\r?\n/)) {
245
+ if (!line) continue;
246
+ const tab = line.indexOf('\t');
247
+ const pane = (tab < 0 ? line : line.slice(0, tab)).trim();
248
+ const node = tab < 0 ? '' : line.slice(tab + 1).trim();
249
+ if (pane) rows.push({ pane, node });
250
+ }
251
+ return ok(rows);
252
+ },
253
+ };
254
+
255
+ // ── Commands (writes): invoked by an intent ───────────────────────────────────
256
+
257
+ /**
258
+ * Focus a node into the chat pane (`crtr node focus <id> --pane <pane>`) — the
259
+ * swap-pane call the Alt+G graph overlay uses. The swap lands in the OTHER pane,
260
+ * so the rail neither waits on nor renders its result.
261
+ * @type {import('../../core/view/contract.js').Command<boolean, {nodeId:string, pane:string}>}
262
+ */
263
+ export const focusCommand = {
264
+ id: 'focus',
265
+ request: ({ nodeId, pane }) => ({ kind: 'exec', bin: 'crtr', args: ['node', 'focus', nodeId, '--pane', pane] }),
266
+ parse: (raw) => ok(raw.ok && raw.exitCode === 0),
267
+ };
268
+
269
+ // ── Forest model (within the cwd-scoped node set; pure) ────────────────────────
270
+
271
+ /** @param {CanvasNode} a @param {CanvasNode} b @returns {number} */
272
+ function byBirth(a, b) {
273
+ return a.created < b.created ? -1 : a.created > b.created ? 1 : 0;
274
+ }
275
+
276
+ /**
277
+ * Build id→node, parent→children, and the root list for the cwd-scoped set. A
278
+ * node is a ROOT when it has no parent inside the set.
279
+ * @param {CanvasNode[]} mine
280
+ */
281
+ function buildForest(mine) {
282
+ /** @type {Map<string, CanvasNode>} */
283
+ const byId = new Map();
284
+ for (const n of mine) byId.set(n.nodeId, n);
285
+ /** @type {Map<string, CanvasNode[]>} */
286
+ const children = new Map();
287
+ /** @type {CanvasNode[]} */
288
+ const roots = [];
289
+ for (const n of mine) {
290
+ const hasParent = n.parent && byId.has(n.parent);
291
+ if (hasParent) {
292
+ const arr = children.get(/** @type {string} */ (n.parent));
293
+ if (arr) arr.push(n);
294
+ else children.set(/** @type {string} */ (n.parent), [n]);
295
+ } else {
296
+ roots.push(n);
297
+ }
298
+ }
299
+ for (const arr of children.values()) arr.sort(byBirth);
300
+ roots.sort(byBirth);
301
+ return { byId, children, roots };
302
+ }
303
+
304
+ /**
305
+ * Climb from `id` to its forest root within the set. Cycle-guarded.
306
+ * @param {string} id @param {Map<string, CanvasNode>} byId @returns {string}
307
+ */
308
+ function climbRoot(id, byId) {
309
+ const seen = new Set();
310
+ let cur = id;
311
+ while (true) {
312
+ if (seen.has(cur)) return cur;
313
+ seen.add(cur);
314
+ const n = byId.get(cur);
315
+ if (!n || !n.parent || !byId.has(n.parent)) return cur;
316
+ cur = n.parent;
317
+ }
318
+ }
319
+
320
+ /**
321
+ * @param {string} id @param {Map<string,CanvasNode[]>} children
322
+ * @param {Map<string,CanvasNode>} byId @param {Set<string>} [guard] @returns {boolean}
323
+ */
324
+ function subtreeLive(id, children, byId, guard = new Set()) {
325
+ if (guard.has(id)) return false;
326
+ guard.add(id);
327
+ const n = byId.get(id);
328
+ let res = !!n && LIVE_STATUS.has(n.status);
329
+ if (!res) {
330
+ for (const c of children.get(id) || []) {
331
+ if (subtreeLive(c.nodeId, children, byId, guard)) {
332
+ res = true;
333
+ break;
334
+ }
335
+ }
336
+ }
337
+ guard.delete(id);
338
+ return res;
339
+ }
340
+
341
+ // ── Row builders (pure → logical RailRow[]) ───────────────────────────────────
342
+
343
+ /** @param {CanvasNode} n @param {string} prefix @param {boolean} attached @param {number} asks @returns {RailRow} */
344
+ function nodeRow(n, prefix, attached, asks) {
345
+ return { kind: 'node', nodeId: n.nodeId, name: n.name, status: n.status, prefix, attached, asks };
346
+ }
347
+ /** @param {string} text @returns {RailRow} */
348
+ function headerRow(text) {
349
+ return { kind: 'header', text };
350
+ }
351
+ /** @param {string} [text] @returns {RailRow} */
352
+ function chromeRow(text) {
353
+ return { kind: 'chrome', text: text || '' };
354
+ }
355
+
356
+ /**
357
+ * Build the cwd-scoped forest + pick `this graph`'s root. Pure. The caller
358
+ * (`refresh`) folds attention counts in and flattens the two sections, since
359
+ * those need the (async) attention source resolved first.
360
+ * @param {CanvasNode[]} mine
361
+ * @param {string|null} attachedNode
362
+ */
363
+ function buildRail(mine, attachedNode) {
364
+ const { byId, children, roots } = buildForest(mine);
365
+
366
+ // `this graph` root: the attached node's root if known & present, else the
367
+ // newest LIVE root here, else the newest root, else none.
368
+ /** @type {string|null} */
369
+ let currentRoot = null;
370
+ if (attachedNode && byId.has(attachedNode)) {
371
+ currentRoot = climbRoot(attachedNode, byId);
372
+ } else {
373
+ const live = roots.filter((r) => subtreeLive(r.nodeId, children, byId));
374
+ const pool = live.length ? live : roots;
375
+ if (pool.length) currentRoot = pool[pool.length - 1].nodeId; // newest
376
+ }
377
+
378
+ return {
379
+ byId, children, roots, currentRoot,
380
+ liveRoots: roots.filter((r) => subtreeLive(r.nodeId, children, byId)),
381
+ };
382
+ }
383
+
384
+ // ── Cursor model (skips chrome rows) ───────────────────────────────────────────
385
+
386
+ /** @param {RailRow[]} rows @returns {number[]} */
387
+ function selectableIdx(rows) {
388
+ /** @type {number[]} */
389
+ const out = [];
390
+ for (let i = 0; i < rows.length; i++) if (rows[i].kind === 'node') out.push(i);
391
+ return out;
392
+ }
393
+
394
+ /**
395
+ * Re-home the cursor after a rebuild: same nodeId if still present, else the
396
+ * attached node, else the first selectable row.
397
+ * @param {RailRow[]} rows @param {string|null} prevId @param {string|null} attachedNode @returns {number}
398
+ */
399
+ function reanchor(rows, prevId, attachedNode) {
400
+ const sel = selectableIdx(rows);
401
+ if (sel.length === 0) return 0;
402
+ const want = prevId || attachedNode;
403
+ const found = want ? rows.findIndex((r) => r.kind === 'node' && r.nodeId === want) : -1;
404
+ return found >= 0 ? found : sel[0];
405
+ }
406
+
407
+ /** @param {SidebarState} s @param {1|-1} dir @returns {SidebarState} */
408
+ function moveCursor(s, dir) {
409
+ const sel = selectableIdx(s.rows);
410
+ if (sel.length === 0) return s;
411
+ const pos = sel.indexOf(s.cursor);
412
+ if (pos < 0) return { ...s, cursor: sel[0] };
413
+ const next = Math.max(0, Math.min(sel.length - 1, pos + dir));
414
+ return { ...s, cursor: sel[next] };
415
+ }
416
+
417
+ // ── The portable core ──────────────────────────────────────────────────────────
418
+
419
+ /** @type {import('../../core/view/contract.js').ViewCore<SidebarState>} */
420
+ const core = {
421
+ manifest: {
422
+ id: 'workspace-sidebar',
423
+ title: 'Workspace',
424
+ description: 'Left rail for `crtr workspace` — this graph + other agents in this cwd',
425
+ refreshMs: 2500,
426
+ },
427
+
428
+ /** Cheap + synchronous initial state — NO fetch. @param {Readonly<Record<string,string>>} opts @returns {SidebarState} */
429
+ init(opts) {
430
+ return {
431
+ cwd: typeof process !== 'undefined' && process.cwd ? process.cwd() : '',
432
+ targetOverride: String((opts && opts.target) || ''),
433
+ chatPane: '',
434
+ attachedNode: null,
435
+ currentRoot: null,
436
+ rows: [],
437
+ cursor: 0,
438
+ scroll: 0,
439
+ graphsHere: 0,
440
+ nodesHere: 0,
441
+ asksHere: 0,
442
+ lastFetch: 0,
443
+ srcError: null,
444
+ };
445
+ },
446
+
447
+ sources: { cwdSource, nodesSource, attentionSource, panesSource },
448
+ commands: { focusCommand },
449
+
450
+ intents: {
451
+ /**
452
+ * Resolve the chat pane + attached node from tmux, pull the node graph,
453
+ * scope to this cwd, and rebuild the two-section rail. Runs in the host's
454
+ * single-flight lane. A blocking node-source failure with no prior rail
455
+ * drops to a guided takeover; a transient failure KEEPS the last-known rail.
456
+ * @param {Ctx} ctx
457
+ */
458
+ async refresh(ctx) {
459
+ ctx.signal.setStatus('Loading…');
460
+
461
+ // Resolve the chat pane (the @crtr_node-tagged sibling; the launch-time
462
+ // --target is the fallback when no pane is tagged yet) + its attached node.
463
+ const panes = await ctx.resolve(panesSource);
464
+ let chatPane = '';
465
+ /** @type {string|null} */
466
+ let attachedNode = null;
467
+ if (panes.ok) {
468
+ const tagged = panes.data.find((p) => p.node);
469
+ if (tagged) {
470
+ chatPane = tagged.pane;
471
+ attachedNode = tagged.node;
472
+ }
473
+ }
474
+ if (!chatPane && ctx.state.targetOverride) chatPane = ctx.state.targetOverride;
475
+
476
+ // The workspace scope key — resolved through the transport (uniform across
477
+ // TUI + web), falling back to the init-time cwd.
478
+ const cwdR = await ctx.resolve(cwdSource);
479
+ const cwd = cwdR.ok && cwdR.data ? cwdR.data : ctx.state.cwd;
480
+
481
+ const rn = await ctx.resolve(nodesSource);
482
+ if (!rn.ok) {
483
+ const err = rn.error;
484
+ const hadRail = ctx.state.rows.length > 0;
485
+ const keep = !err.display.blocking && hadRail;
486
+ ctx.set((s) => {
487
+ /** @type {SidebarState} */
488
+ const next = { ...s, cwd, chatPane, attachedNode, srcError: err };
489
+ if (!keep) {
490
+ next.rows = [];
491
+ next.currentRoot = null;
492
+ next.graphsHere = 0;
493
+ next.nodesHere = 0;
494
+ next.asksHere = 0;
495
+ }
496
+ return next;
497
+ });
498
+ // A takeover (empty rail) owns the rect + names the cause; a kept rail
499
+ // raises the cause as a banner.
500
+ if (keep) ctx.signal.setBanner(err.display.explanation, err.display.level);
501
+ else ctx.signal.clearBanner();
502
+ ctx.signal.setStatus(null);
503
+ ctx.signal.setSubtitle(null);
504
+ return;
505
+ }
506
+
507
+ const mine = rn.data.filter((n) => n.cwd === cwd);
508
+ const { byId, children, roots, currentRoot, liveRoots } = buildRail(mine, attachedNode);
509
+
510
+ // Attention in one pass: the current sub-DAG + EVERY root here (so a
511
+ // blocked-but-not-live root still surfaces its ⚑). Best-effort.
512
+ const rootIds = roots.map((r) => r.nodeId);
513
+ let asks = /** @type {Record<string,number>} */ ({});
514
+ if (currentRoot || rootIds.length) {
515
+ const a = await ctx.resolve(attentionSource, { viewRoot: currentRoot, nodeIds: rootIds });
516
+ if (a.ok) asks = a.data;
517
+ }
518
+ let asksHere = 0;
519
+ for (const v of Object.values(asks)) asksHere += v;
520
+
521
+ // `elsewhere` = other roots still LIVE OR with a pending ask. Live first,
522
+ // then newest.
523
+ const others = roots.filter(
524
+ (r) => r.nodeId !== currentRoot && (subtreeLive(r.nodeId, children, byId) || (asks[r.nodeId] || 0) > 0),
525
+ );
526
+ others.sort((a, b) => {
527
+ const la = subtreeLive(a.nodeId, children, byId) ? 1 : 0;
528
+ const lb = subtreeLive(b.nodeId, children, byId) ? 1 : 0;
529
+ if (la !== lb) return lb - la;
530
+ return a.created < b.created ? 1 : -1; // newest first
531
+ });
532
+
533
+ // ── Build the flat render list ───────────────────────────────────────
534
+ /** @type {RailRow[]} */
535
+ const rows = [];
536
+ rows.push(headerRow('⌗ this graph'));
537
+ if (currentRoot && byId.has(currentRoot)) {
538
+ /** @type {Set<string>} */
539
+ const visited = new Set();
540
+ /** @param {CanvasNode} node @param {string} indent @param {boolean} isLast @param {boolean} isRoot */
541
+ const walk = (node, indent, isLast, isRoot) => {
542
+ if (visited.has(node.nodeId)) return;
543
+ visited.add(node.nodeId);
544
+ const prefix = isRoot ? '' : indent + (isLast ? '└─ ' : '├─ ');
545
+ rows.push(nodeRow(node, prefix, node.nodeId === attachedNode, asks[node.nodeId] || 0));
546
+ const kids = children.get(node.nodeId) || [];
547
+ const childIndent = isRoot ? '' : indent + (isLast ? ' ' : '│ ');
548
+ for (let i = 0; i < kids.length; i++) walk(kids[i], childIndent, i === kids.length - 1, false);
549
+ };
550
+ walk(/** @type {CanvasNode} */ (byId.get(currentRoot)), '', true, true);
551
+ } else {
552
+ rows.push(chromeRow(' no agent here yet'));
553
+ }
554
+
555
+ rows.push(chromeRow('')); // section gap
556
+ rows.push(headerRow(`↪ elsewhere${others.length ? ` · ${others.length}` : ''}`));
557
+ if (others.length) {
558
+ for (const r of others) rows.push(nodeRow(r, '', false, asks[r.nodeId] || 0));
559
+ } else {
560
+ rows.push(chromeRow(' —'));
561
+ }
562
+
563
+ const prevRow = ctx.state.rows[ctx.state.cursor];
564
+ const cursor = reanchor(rows, prevRow && prevRow.kind === 'node' ? prevRow.nodeId : null, attachedNode);
565
+
566
+ ctx.set((s) => ({
567
+ ...s,
568
+ cwd,
569
+ chatPane,
570
+ attachedNode,
571
+ currentRoot,
572
+ rows,
573
+ cursor,
574
+ graphsHere: liveRoots.length,
575
+ nodesHere: mine.length,
576
+ asksHere,
577
+ lastFetch: Date.now(),
578
+ srcError: null,
579
+ }));
580
+
581
+ ctx.signal.setSubtitle(liveRoots.length > 0 ? `${plural(liveRoots.length, 'live graph')}` : null);
582
+ if (asksHere > 0) ctx.signal.setBanner(`${plural(asksHere, 'ask')} waiting on a human — see the ⚑ rows`, 'action');
583
+ else ctx.signal.clearBanner();
584
+ ctx.signal.setStatus(null);
585
+ },
586
+
587
+ /** @param {Ctx} ctx */
588
+ cursorDown: (ctx) => ctx.set((s) => moveCursor(s, 1)),
589
+ /** @param {Ctx} ctx */
590
+ cursorUp: (ctx) => ctx.set((s) => moveCursor(s, -1)),
591
+ /** @param {Ctx} ctx @param {number} [i] */
592
+ select: (ctx, i) =>
593
+ ctx.set((s) => {
594
+ if (typeof i !== 'number') return s;
595
+ return selectableIdx(s.rows).includes(i) ? { ...s, cursor: i } : s;
596
+ }),
597
+
598
+ /**
599
+ * Swap the cursor's node into the chat pane (TUI ↵). No chat pane wired (the
600
+ * web target, or a rail not launched via `crtr workspace`) ⇒ a guidance
601
+ * banner, no-op.
602
+ * @param {Ctx} ctx
603
+ */
604
+ async open(ctx) {
605
+ const s = ctx.state;
606
+ const row = s.rows[s.cursor];
607
+ if (!row || row.kind !== 'node') return;
608
+ if (!s.chatPane) {
609
+ ctx.signal.setBanner('No chat pane wired — run this rail via `crtr workspace`.', 'error');
610
+ return;
611
+ }
612
+ await ctx.execute(focusCommand, { nodeId: row.nodeId, pane: s.chatPane });
613
+ ctx.signal.setStatus(`→ ${row.name}`);
614
+ },
615
+
616
+ /** @param {Ctx} ctx */
617
+ quit: (ctx) => ctx.signal.quit(),
618
+ },
619
+ };
620
+
621
+ export default core;
@@ -0,0 +1,53 @@
1
+ // @ts-check
2
+ /**
3
+ * Workspace Sidebar — the text presenter (`dump`) for the non-TTY / piped path
4
+ * (`crtr view run workspace-sidebar | cat`). Node-only, no ANSI. A plain
5
+ * snapshot of the current state; the host threads its current banner via `ctx`.
6
+ *
7
+ * `dump` reads the SAME logical `RailRow[]` the TUI/web presenters do. The pure
8
+ * `plural` helper is imported from `core.mjs`.
9
+ *
10
+ * @module workspace-sidebar/text
11
+ */
12
+
13
+ import { plural } from './core.mjs';
14
+
15
+ /** @typedef {import('./core.mjs').SidebarState} SidebarState */
16
+ /** @typedef {import('./core.mjs').RailRow} RailRow */
17
+
18
+ const STATUS_GLYPH = { active: '●', idle: '○', done: '✓', dead: '✗', canceled: '⊘' };
19
+
20
+ /**
21
+ * Plain-text snapshot for the non-TTY / piped path.
22
+ * @param {SidebarState} state
23
+ * @param {import('../../core/view/contract.js').DumpContext} [ctx]
24
+ * @returns {string}
25
+ */
26
+ export function dump(state, ctx) {
27
+ /** @type {string[]} */
28
+ const lines = ['Workspace sidebar'];
29
+ const banner = ctx && ctx.banner ? ctx.banner : null;
30
+ if (banner) lines.push('', `[${banner.level}] ${banner.msg}`);
31
+ else if (state.srcError) lines.push('', `[${state.srcError.display.level}] ${state.srcError.display.explanation}`);
32
+
33
+ lines.push('', `${plural(state.graphsHere, 'graph')} · ${state.nodesHere} nodes · ${plural(state.asksHere, 'ask')}`, '');
34
+
35
+ if (state.rows.length === 0) {
36
+ lines.push(state.srcError ? '(canvas unavailable)' : state.lastFetch === 0 ? '(loading…)' : '(no agents in this cwd)');
37
+ return lines.join('\n');
38
+ }
39
+
40
+ for (const r of state.rows) {
41
+ if (r.kind === 'header') {
42
+ lines.push(r.text);
43
+ } else if (r.kind === 'chrome') {
44
+ lines.push(r.text);
45
+ } else {
46
+ const mark = r.attached ? '▸ ' : ' ';
47
+ const glyph = STATUS_GLYPH[r.status] || '?';
48
+ const flag = r.asks > 0 ? ` ⚑${r.asks}` : '';
49
+ lines.push(`${mark}${r.prefix}${glyph} ${r.name}${flag}`);
50
+ }
51
+ }
52
+ return lines.join('\n');
53
+ }