@crouton-kit/crouter 0.3.28 → 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 (322) 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 +5 -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__/slash-quit-copy.test.js +42 -0
  49. package/dist/clients/attach/__tests__/titled-editor.test.js +39 -0
  50. package/dist/clients/attach/attach-cmd.js +282 -46
  51. package/dist/clients/attach/auth-pickers.d.ts +9 -0
  52. package/dist/clients/attach/auth-pickers.js +194 -0
  53. package/dist/clients/attach/canvas-panels.d.ts +4 -2
  54. package/dist/clients/attach/canvas-panels.js +45 -17
  55. package/dist/clients/attach/chat-view.d.ts +27 -2
  56. package/dist/clients/attach/chat-view.js +98 -9
  57. package/dist/clients/attach/clipboard-image.d.ts +47 -8
  58. package/dist/clients/attach/clipboard-image.js +150 -26
  59. package/dist/clients/attach/clipboard-text.d.ts +3 -0
  60. package/dist/clients/attach/clipboard-text.js +39 -0
  61. package/dist/clients/attach/config-load.d.ts +54 -7
  62. package/dist/clients/attach/config-load.js +99 -3
  63. package/dist/clients/attach/context-message.d.ts +12 -0
  64. package/dist/clients/attach/context-message.js +72 -0
  65. package/dist/clients/attach/git-info.d.ts +20 -0
  66. package/dist/clients/attach/git-info.js +44 -0
  67. package/dist/clients/attach/graph-overlay.d.ts +13 -4
  68. package/dist/clients/attach/graph-overlay.js +59 -19
  69. package/dist/clients/attach/input-controller.d.ts +75 -13
  70. package/dist/clients/attach/input-controller.js +305 -67
  71. package/dist/clients/attach/pickers.d.ts +58 -0
  72. package/dist/clients/attach/pickers.js +171 -0
  73. package/dist/clients/attach/slash-commands.d.ts +22 -0
  74. package/dist/clients/attach/slash-commands.js +91 -25
  75. package/dist/clients/attach/titled-editor.d.ts +33 -0
  76. package/dist/clients/attach/titled-editor.js +84 -0
  77. package/dist/clients/attach/view-socket.d.ts +18 -1
  78. package/dist/clients/attach/view-socket.js +70 -1
  79. package/dist/clients/web/dev-server.d.ts +7 -0
  80. package/dist/clients/web/dev-server.js +59 -0
  81. package/dist/clients/web/events.d.ts +14 -0
  82. package/dist/clients/web/events.js +151 -0
  83. package/dist/clients/web/server.d.ts +18 -0
  84. package/dist/clients/web/server.js +450 -0
  85. package/dist/clients/web/web-cmd.d.ts +2 -0
  86. package/dist/clients/web/web-cmd.js +120 -0
  87. package/dist/commands/canvas.js +1 -2
  88. package/dist/commands/chord.js +1 -1
  89. package/dist/commands/dashboard.js +5 -1
  90. package/dist/commands/memory/__tests__/lint-schema.test.js +6 -6
  91. package/dist/commands/memory/lint.js +14 -2
  92. package/dist/commands/memory/read.js +2 -2
  93. package/dist/commands/memory/shared.d.ts +7 -1
  94. package/dist/commands/memory/shared.js +18 -3
  95. package/dist/commands/memory/write.js +8 -4
  96. package/dist/commands/memory.js +4 -4
  97. package/dist/commands/node.d.ts +1 -1
  98. package/dist/commands/node.js +64 -65
  99. package/dist/commands/pkg.js +1 -2
  100. package/dist/commands/revive.js +11 -3
  101. package/dist/commands/sys/sync.d.ts +1 -0
  102. package/dist/commands/sys/sync.js +187 -0
  103. package/dist/commands/sys.js +3 -2
  104. package/dist/commands/view-cycle.js +2 -2
  105. package/dist/commands/view-list.js +8 -8
  106. package/dist/commands/view-new.js +21 -17
  107. package/dist/commands/view-pick.js +1 -1
  108. package/dist/commands/view-run.js +35 -14
  109. package/dist/commands/view.js +8 -6
  110. package/dist/commands/workspace.d.ts +2 -0
  111. package/dist/commands/workspace.js +161 -0
  112. package/dist/core/__tests__/broker-double-spawn.test.d.ts +1 -0
  113. package/dist/core/__tests__/broker-double-spawn.test.js +143 -0
  114. package/dist/core/__tests__/broker-fork-seam.test.d.ts +1 -0
  115. package/dist/core/__tests__/broker-fork-seam.test.js +102 -0
  116. package/dist/core/__tests__/broker-sdk-wiring.test.js +2 -0
  117. package/dist/core/__tests__/child-death-wake.test.js +56 -111
  118. package/dist/core/__tests__/close.test.js +14 -20
  119. package/dist/core/__tests__/context-intro.test.js +19 -19
  120. package/dist/core/__tests__/daemon-boot.test.js +30 -23
  121. package/dist/core/__tests__/draw-style.test.js +1 -1
  122. package/dist/core/__tests__/error-stall-recycle.test.d.ts +1 -0
  123. package/dist/core/__tests__/error-stall-recycle.test.js +141 -0
  124. package/dist/core/__tests__/fixtures/fake-engine.d.ts +17 -1
  125. package/dist/core/__tests__/fixtures/fake-engine.js +22 -2
  126. package/dist/core/__tests__/fixtures/fake-pi-host.js +2 -2
  127. package/dist/core/__tests__/focuses.test.js +25 -13
  128. package/dist/core/__tests__/fork.test.js +22 -5
  129. package/dist/core/__tests__/full/broker-control-preempt.test.d.ts +1 -0
  130. package/dist/core/__tests__/full/broker-control-preempt.test.js +61 -0
  131. package/dist/core/__tests__/full/broker-model-changed-broadcast.test.d.ts +1 -0
  132. package/dist/core/__tests__/full/broker-model-changed-broadcast.test.js +57 -0
  133. package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.d.ts +1 -0
  134. package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.js +80 -0
  135. package/dist/core/__tests__/full/broker-pane-resolution.test.js +90 -0
  136. package/dist/core/__tests__/full/cascade-close.test.js +22 -8
  137. package/dist/core/__tests__/full/detach-focus.test.js +66 -169
  138. package/dist/core/__tests__/helpers/harness.d.ts +6 -9
  139. package/dist/core/__tests__/helpers/harness.js +19 -36
  140. package/dist/core/__tests__/human-node-not-supervised.test.d.ts +1 -0
  141. package/dist/core/__tests__/human-node-not-supervised.test.js +83 -0
  142. package/dist/core/__tests__/lifecycle.test.js +1 -19
  143. package/dist/core/__tests__/live-mutation-verbs.test.js +5 -5
  144. package/dist/core/__tests__/memory-resolver.test.js +1 -1
  145. package/dist/core/__tests__/on-read-dedup-resume.test.js +2 -2
  146. package/dist/core/__tests__/refresh-stall-recycle.test.d.ts +1 -0
  147. package/dist/core/__tests__/refresh-stall-recycle.test.js +88 -0
  148. package/dist/core/__tests__/relaunch-root.test.d.ts +1 -0
  149. package/dist/core/__tests__/relaunch-root.test.js +114 -0
  150. package/dist/core/__tests__/reset.test.js +19 -79
  151. package/dist/core/__tests__/steer-note.test.js +9 -7
  152. package/dist/core/__tests__/tmux-surface.test.js +13 -7
  153. package/dist/core/bootstrap.d.ts +0 -3
  154. package/dist/core/bootstrap.js +1 -143
  155. package/dist/core/canvas/browse/__tests__/model.test.js +86 -1
  156. package/dist/core/canvas/browse/__tests__/render.test.js +2 -1
  157. package/dist/core/canvas/browse/app.js +131 -8
  158. package/dist/core/canvas/browse/model.d.ts +23 -5
  159. package/dist/core/canvas/browse/model.js +68 -13
  160. package/dist/core/canvas/browse/render.d.ts +7 -1
  161. package/dist/core/canvas/browse/render.js +178 -47
  162. package/dist/core/canvas/canvas.js +1 -1
  163. package/dist/core/canvas/focuses.d.ts +4 -4
  164. package/dist/core/canvas/focuses.js +7 -7
  165. package/dist/core/canvas/nav-model.js +8 -7
  166. package/dist/core/canvas/render.d.ts +68 -9
  167. package/dist/core/canvas/render.js +195 -61
  168. package/dist/core/canvas/types.d.ts +16 -23
  169. package/dist/core/config.js +1 -4
  170. package/dist/core/memory-resolver.js +43 -5
  171. package/dist/core/runtime/bearings.d.ts +1 -1
  172. package/dist/core/runtime/bearings.js +8 -8
  173. package/dist/core/runtime/branded-host.d.ts +16 -0
  174. package/dist/core/runtime/branded-host.js +127 -0
  175. package/dist/core/runtime/broker-protocol.d.ts +212 -6
  176. package/dist/core/runtime/broker.d.ts +4 -1
  177. package/dist/core/runtime/broker.js +494 -58
  178. package/dist/core/runtime/close.d.ts +12 -2
  179. package/dist/core/runtime/close.js +37 -16
  180. package/dist/core/runtime/front-door.js +6 -14
  181. package/dist/core/runtime/host.d.ts +14 -34
  182. package/dist/core/runtime/host.js +14 -50
  183. package/dist/core/runtime/launch.d.ts +12 -9
  184. package/dist/core/runtime/launch.js +27 -15
  185. package/dist/core/runtime/lifecycle.d.ts +1 -1
  186. package/dist/core/runtime/lifecycle.js +15 -19
  187. package/dist/core/runtime/nodes.d.ts +0 -55
  188. package/dist/core/runtime/nodes.js +6 -74
  189. package/dist/core/runtime/placement.d.ts +91 -335
  190. package/dist/core/runtime/placement.js +262 -840
  191. package/dist/core/runtime/promote.d.ts +2 -0
  192. package/dist/core/runtime/promote.js +24 -6
  193. package/dist/core/runtime/recap.d.ts +8 -0
  194. package/dist/core/runtime/recap.js +107 -0
  195. package/dist/core/runtime/recycle.js +25 -61
  196. package/dist/core/runtime/reset.d.ts +43 -37
  197. package/dist/core/runtime/reset.js +131 -218
  198. package/dist/core/runtime/revive.d.ts +9 -29
  199. package/dist/core/runtime/revive.js +40 -139
  200. package/dist/core/runtime/spawn.d.ts +15 -16
  201. package/dist/core/runtime/spawn.js +165 -194
  202. package/dist/core/runtime/surface-bg.d.ts +11 -0
  203. package/dist/core/runtime/surface-bg.js +68 -0
  204. package/dist/core/runtime/tmux.d.ts +13 -50
  205. package/dist/core/runtime/tmux.js +33 -87
  206. package/dist/core/scope.d.ts +1 -2
  207. package/dist/core/scope.js +1 -5
  208. package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.d.ts +1 -0
  209. package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.js +57 -0
  210. package/dist/core/skill-sync/builtins.d.ts +42 -0
  211. package/dist/core/skill-sync/builtins.js +112 -0
  212. package/dist/core/skill-sync/claude-plugins.d.ts +23 -0
  213. package/dist/core/skill-sync/claude-plugins.js +71 -0
  214. package/dist/core/skill-sync/engine.d.ts +42 -0
  215. package/dist/core/skill-sync/engine.js +633 -0
  216. package/dist/core/skill-sync/export.d.ts +23 -0
  217. package/dist/core/skill-sync/export.js +86 -0
  218. package/dist/core/skill-sync/manifest.d.ts +64 -0
  219. package/dist/core/skill-sync/manifest.js +181 -0
  220. package/dist/core/skill-sync/profile.d.ts +76 -0
  221. package/dist/core/skill-sync/profile.js +173 -0
  222. package/dist/core/skill-sync/snapshot.d.ts +57 -0
  223. package/dist/core/skill-sync/snapshot.js +120 -0
  224. package/dist/core/substrate/index.d.ts +1 -1
  225. package/dist/core/substrate/index.js +1 -1
  226. package/dist/core/substrate/injected-store.js +3 -3
  227. package/dist/core/substrate/on-read.js +68 -6
  228. package/dist/core/substrate/render.d.ts +8 -11
  229. package/dist/core/substrate/render.js +29 -43
  230. package/dist/core/substrate/schema.d.ts +10 -3
  231. package/dist/core/substrate/schema.js +6 -3
  232. package/dist/core/tui/host.d.ts +12 -4
  233. package/dist/core/tui/host.js +280 -149
  234. package/dist/core/view/bridge.d.ts +10 -0
  235. package/dist/core/view/bridge.js +31 -0
  236. package/dist/core/view/chrome.d.ts +9 -0
  237. package/dist/core/view/chrome.js +22 -0
  238. package/dist/core/view/contract.d.ts +171 -0
  239. package/dist/core/view/contract.js +23 -0
  240. package/dist/core/view/loader.d.ts +31 -0
  241. package/dist/core/view/loader.js +188 -0
  242. package/dist/core/view/transport-local.d.ts +7 -0
  243. package/dist/core/view/transport-local.js +70 -0
  244. package/dist/core/view/transport.d.ts +4 -0
  245. package/dist/core/view/transport.js +15 -0
  246. package/dist/daemon/crtrd.d.ts +38 -27
  247. package/dist/daemon/crtrd.js +303 -376
  248. package/dist/daemon/manage.js +6 -1
  249. package/dist/index.js +1 -1
  250. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +28 -20
  251. package/dist/pi-extensions/canvas-context-intro.js +4 -4
  252. package/dist/pi-extensions/canvas-doc-substrate.js +16 -16
  253. package/dist/pi-extensions/canvas-nav.js +5 -0
  254. package/dist/pi-extensions/canvas-recap.d.ts +37 -0
  255. package/dist/pi-extensions/canvas-recap.js +236 -0
  256. package/dist/pi-extensions/canvas-resume.js +2 -1
  257. package/dist/pi-extensions/canvas-stophook.d.ts +14 -12
  258. package/dist/pi-extensions/canvas-stophook.js +87 -101
  259. package/dist/pi-extensions/canvas-view.js +2 -1
  260. package/dist/pi-extensions/widget-order-bus.d.ts +6 -0
  261. package/dist/pi-extensions/widget-order-bus.js +34 -0
  262. package/dist/prompts/view.d.ts +2 -2
  263. package/dist/prompts/view.js +145 -73
  264. package/dist/types.d.ts +1 -6
  265. package/dist/types.js +1 -3
  266. package/dist/web/ViewChrome.d.ts +7 -0
  267. package/dist/web/ViewChrome.js +28 -0
  268. package/dist/web/ViewPane.d.ts +39 -0
  269. package/dist/web/ViewPane.js +48 -0
  270. package/dist/web/index.d.ts +6 -0
  271. package/dist/web/index.js +16 -0
  272. package/dist/web/runtime.d.ts +39 -0
  273. package/dist/web/runtime.js +133 -0
  274. package/dist/web/states.d.ts +24 -0
  275. package/dist/web/states.js +24 -0
  276. package/dist/web/transport-http.d.ts +5 -0
  277. package/dist/web/transport-http.js +28 -0
  278. package/dist/web-client/assets/index-BUvQb4hR.css +2 -0
  279. package/dist/web-client/assets/index-ClLQXYAE.js +10 -0
  280. package/dist/web-client/index.html +13 -0
  281. package/package.json +20 -6
  282. package/dist/builtin-views/canvas/client.mjs +0 -303
  283. package/dist/builtin-views/canvas/view.mjs +0 -576
  284. package/dist/builtin-views/git-pr/client.mjs +0 -440
  285. package/dist/builtin-views/git-pr/view.mjs +0 -675
  286. package/dist/builtin-views/inbox/sources/gmail.mjs +0 -965
  287. package/dist/builtin-views/inbox/sources/linkedin.mjs +0 -427
  288. package/dist/builtin-views/inbox/view.mjs +0 -889
  289. package/dist/builtin-views/linkedin/client.mjs +0 -610
  290. package/dist/builtin-views/linkedin/view.mjs +0 -1171
  291. package/dist/commands/pkg/bridge.d.ts +0 -1
  292. package/dist/commands/pkg/bridge.js +0 -137
  293. package/dist/commands/skill/author.d.ts +0 -3
  294. package/dist/commands/skill/author.js +0 -140
  295. package/dist/commands/skill/shared.d.ts +0 -3
  296. package/dist/commands/skill/shared.js +0 -19
  297. package/dist/commands/skill.d.ts +0 -2
  298. package/dist/commands/skill.js +0 -21
  299. package/dist/commands/tmux-spread.d.ts +0 -2
  300. package/dist/commands/tmux-spread.js +0 -144
  301. package/dist/core/__tests__/full/daemon-liveness-pane.full.test.js +0 -292
  302. package/dist/core/__tests__/full/placement-focus.test.js +0 -309
  303. package/dist/core/__tests__/full/placement-reconcile.test.js +0 -212
  304. package/dist/core/__tests__/full/placement-revive.test.js +0 -238
  305. package/dist/core/__tests__/full/placement-teardown.test.js +0 -297
  306. package/dist/core/__tests__/home-session.test.js +0 -186
  307. package/dist/core/__tests__/relaunch.test.js +0 -335
  308. package/dist/core/bridge-map.d.ts +0 -19
  309. package/dist/core/bridge-map.js +0 -73
  310. package/dist/core/tui/contract.d.ts +0 -83
  311. package/dist/core/tui/contract.js +0 -8
  312. package/dist/core/tui/loader.d.ts +0 -16
  313. package/dist/core/tui/loader.js +0 -94
  314. package/dist/prompts/skill.d.ts +0 -2
  315. package/dist/prompts/skill.js +0 -650
  316. /package/dist/{core/__tests__/full/daemon-liveness-pane.full.test.d.ts → clients/attach/__tests__/action-parity.test.d.ts} +0 -0
  317. /package/dist/{core/__tests__/full/placement-focus.test.d.ts → clients/attach/__tests__/clipboard-image.test.d.ts} +0 -0
  318. /package/dist/{core/__tests__/full/placement-reconcile.test.d.ts → clients/attach/__tests__/editor-newline.test.d.ts} +0 -0
  319. /package/dist/{core/__tests__/full/placement-revive.test.d.ts → clients/attach/__tests__/git-info.test.d.ts} +0 -0
  320. /package/dist/{core/__tests__/full/placement-teardown.test.d.ts → clients/attach/__tests__/onrequest-wired.test.d.ts} +0 -0
  321. /package/dist/{core/__tests__/home-session.test.d.ts → clients/attach/__tests__/slash-quit-copy.test.d.ts} +0 -0
  322. /package/dist/{core/__tests__/relaunch.test.d.ts → clients/attach/__tests__/titled-editor.test.d.ts} +0 -0
@@ -20,12 +20,28 @@ export { detectColorCaps };
20
20
  // Fixed chrome heights, shared with app.ts so its viewport math never drifts
21
21
  // from what renderFrame actually draws.
22
22
  // header = title + tab bar + status line + separator (+ search input when searching)
23
- // preview = separator + meta line + PREVIEW_BODY prompt lines
24
- export const PREVIEW_BODY = 5;
23
+ // preview = separator + meta line + PROMPT_LINES (spawn/match) + REPLY_LINES (last reply)
24
+ export const PROMPT_LINES = 3;
25
+ export const REPLY_LINES = 3;
26
+ export const PREVIEW_BODY = PROMPT_LINES + REPLY_LINES;
25
27
  export const PREVIEW_HEIGHT = PREVIEW_BODY + 2;
28
+ // header = title + tab bar + status line + column header + separator (+ search input)
26
29
  export function headerHeight(search) {
27
- return 4 + (search ? 1 : 0);
30
+ return 5 + (search ? 1 : 0);
28
31
  }
32
+ // ── Row table layout ──────────────────────────────────────────────────────────
33
+ // A row is three zones: a fixed-width STATUS rail (glyph + word) on the left, the
34
+ // flexible tree-indented NAME in the middle, and a flush-right METADATA cluster
35
+ // (kind/mode · ctx · age · project) whose fixed-width segments align into clean
36
+ // vertical columns down the right edge. Wide terminals widen the name; narrow ones
37
+ // clip the name (and drop the cluster) first. The status WORD — not a lone glyph —
38
+ // is the legible, NO_COLOR-safe state signal.
39
+ const STATUS_W = 10; // glyph + ' ' + longest word ('active'/'cancel') + trailing gap
40
+ const KM_W = 22; // kind/mode column
41
+ const CTX_W = 6; // ctx tokens column
42
+ const AGE_W = 5; // age column
43
+ const PROJ_W = 16; // project (~basename) column, only across All dirs
44
+ const COL_GAP = ' ';
29
45
  // ── ANSI ────────────────────────────────────────────────────────────────────
30
46
  const ESC = '\x1b[';
31
47
  const RESET = `${ESC}0m`;
@@ -40,6 +56,7 @@ const FG_RED = '31';
40
56
  const FG_CYAN = '36';
41
57
  const FG_GRAY = '90'; // bright-black
42
58
  const FG_BRIGHT_YELLOW = '93';
59
+ const FG_BRIGHT_GREEN = '92'; // streaming pulse (brighter than the active-status green)
43
60
  const FG_BRIGHT_CYAN = '96'; // query-match highlight (ties to the cyan search accent)
44
61
  const STATUS_GLYPH = {
45
62
  active: '●',
@@ -165,8 +182,67 @@ function nameSpans(name, query, style) {
165
182
  flush();
166
183
  return out;
167
184
  }
168
- /** One row line: `<indent><collapse> <glyph> <name> [kind/mode] ctx Nk age [~dir] [⚑n]`.
169
- * `showCwd` adds the project-name cue (All-dirs view); `now` drives the age. */
185
+ /** Visible (cell) width of a span group ANSI-free, surrogate-safe. */
186
+ function spansWidth(spans) {
187
+ let n = 0;
188
+ for (const s of spans)
189
+ n += [...s.text].length;
190
+ return n;
191
+ }
192
+ /** The status rail: a status WORD next to its glyph, both in the status hue, padded
193
+ * to STATUS_W so the rail forms a clean left column. `live` (bright green) when the
194
+ * node is genuinely mid-turn; otherwise the lifecycle word. The word is what makes
195
+ * state legible at a glance and survives NO_COLOR (the glyph is the second cue). */
196
+ function statusRail(r) {
197
+ const streaming = r.streaming === true;
198
+ const word = streaming
199
+ ? 'live'
200
+ : ({ active: 'active', idle: 'idle', done: 'done', dead: 'dead', canceled: 'cancel' }[r.status] ?? r.status);
201
+ const fg = streaming ? FG_BRIGHT_GREEN : STATUS_COLOR[r.status];
202
+ const glyph = streaming ? '⟳' : (STATUS_GLYPH[r.status] ?? '?');
203
+ const text = `${glyph} ${word}`;
204
+ const pad = Math.max(1, STATUS_W - [...text].length);
205
+ return [
206
+ { text, style: { fg, bold: streaming } },
207
+ { text: ' '.repeat(pad) },
208
+ ];
209
+ }
210
+ /** The flush-right metadata cluster as fixed-width segments, so kind/mode · ctx ·
211
+ * age · project align into vertical columns down the right edge. `showCwd` adds the
212
+ * project column (only meaningful across All dirs). */
213
+ function metaCluster(r, now, showCwd) {
214
+ const ctxFg = ctxColorCode(r.ctx_tokens);
215
+ const out = [
216
+ { text: clipPad(`${r.kind}/${r.mode}`, KM_W), style: { fg: FG_GRAY } },
217
+ { text: COL_GAP },
218
+ { text: fmtCtx(r.ctx_tokens).padStart(CTX_W), style: { fg: ctxFg, dim: ctxFg === undefined } },
219
+ { text: COL_GAP },
220
+ { text: relAge(r.created, now).padStart(AGE_W), style: { dim: true } },
221
+ ];
222
+ if (showCwd) {
223
+ out.push({ text: COL_GAP }, { text: clipPad(`~${baseDir(r.cwd)}`, PROJ_W), style: { fg: FG_GRAY } });
224
+ }
225
+ return out;
226
+ }
227
+ /** Left-clip-pad: clip `s` to `w` cells then pad-end with spaces to exactly `w`. */
228
+ function clipPad(s, w) {
229
+ return clip(s, w).padEnd(w);
230
+ }
231
+ /** Compose a row from a flexible LEFT zone and a fixed flush-right RIGHT cluster:
232
+ * pad fills the gap so RIGHT hugs the edge; when the terminal is too narrow the
233
+ * cluster is dropped rather than colliding with the name. The caller has already
234
+ * clipped the name to fit. */
235
+ function assembleRow(left, right, width, color, lineBase, fill) {
236
+ const leftW = spansWidth(left);
237
+ const rightW = spansWidth(right);
238
+ if (right.length === 0 || leftW + rightW + 1 > width) {
239
+ return assemble(left, width, color, lineBase, fill);
240
+ }
241
+ const pad = width - leftW - rightW;
242
+ return assemble([...left, { text: ' '.repeat(pad) }, ...right], width, color, lineBase, fill);
243
+ }
244
+ /** One table row: `<status rail> <indent><collapse> <name> <flags>` ........ `<meta>`.
245
+ * `showCwd` adds the project column (All-dirs view); `now` drives the age. */
170
246
  function rowLine(row, tree, width, isCursor, query, caps, showCwd, now) {
171
247
  const node = tree.nodes.get(row.id);
172
248
  if (node === undefined)
@@ -174,30 +250,35 @@ function rowLine(row, tree, width, isCursor, query, caps, showCwd, now) {
174
250
  const r = node.row;
175
251
  const indent = ' '.repeat(row.depth);
176
252
  const collapse = !row.hasChildren ? ' ' : row.collapsed ? '▸' : '▾';
177
- const glyph = STATUS_GLYPH[r.status] ?? '?';
178
253
  const terminal = r.status === 'done' || r.status === 'dead' || r.status === 'canceled';
179
- const ctxStr = fmtCtx(r.ctx_tokens);
180
- const ctxFg = ctxColorCode(r.ctx_tokens);
181
- const age = relAge(r.created, now);
182
- // Name: bold on the cursor row; dim for terminal status (live names stay default).
183
- const nameStyle = { dim: !isCursor && terminal, bold: isCursor };
184
- const spans = [
185
- { text: `${indent}${collapse} ` },
186
- { text: glyph, style: { fg: STATUS_COLOR[r.status] } }, // load-bearing status hue
187
- { text: ' ' },
188
- ...nameSpans(r.name, query, nameStyle),
189
- { text: ` [${r.kind}/${r.mode}]`, style: { fg: FG_GRAY } }, // recedes
190
- { text: ' ctx ', style: { dim: true } },
191
- { text: ctxStr, style: { fg: ctxFg, dim: ctxFg === undefined } }, // tiered budget cue
192
- ];
193
- if (age !== '')
194
- spans.push({ text: ` ${age}`, style: { dim: true } }); // recency cue
195
- if (showCwd)
196
- spans.push({ text: ` ~${baseDir(r.cwd)}`, style: { fg: FG_GRAY } }); // project cue (All dirs)
254
+ // Left gutter (fixed slot): an eye when a viewer is ATTACHED — a scannable rail
255
+ // down the left edge of "what's on a screen right now". Blank otherwise.
256
+ const gutter = r.viewed === true
257
+ ? { text: '◉ ', style: { fg: FG_BRIGHT_CYAN, bold: true } }
258
+ : { text: ' ' };
259
+ // Inline attention flag that travels with the name (streaming lives in the rail,
260
+ // attached lives in the gutter).
261
+ const flags = [];
197
262
  if (r.asks > 0)
198
- spans.push({ text: ` ⚑${r.asks}`, style: { fg: FG_BRIGHT_YELLOW, bold: true } }); // attention
199
- // Row base: cursor → subtle bg (256) or reverse fallback (also covers !color);
200
- // non-matched ancestor → whole-row dim for tree context (keep prior behavior).
263
+ flags.push({ text: ` ⚑${r.asks}`, style: { fg: FG_BRIGHT_YELLOW, bold: true } }); // pending asks
264
+ const status = statusRail(r);
265
+ const right = metaCluster(r, now, showCwd);
266
+ const treeLead = `${indent}${collapse} `;
267
+ // Clip the NAME (only) so gutter + status + tree + name + flags + meta all fit;
268
+ // the cluster is dropped first (assembleRow) when even a 4-col name won't fit it.
269
+ const fixed = spansWidth([gutter]) + spansWidth(status) + [...treeLead].length + spansWidth(flags);
270
+ const rightW = spansWidth(right);
271
+ const nameMax = Math.max(4, width - fixed - rightW - 1);
272
+ const nameStyle = { dim: !isCursor && terminal, bold: isCursor }; // dim terminal names; bold the cursor row
273
+ const left = [
274
+ gutter,
275
+ ...status,
276
+ { text: treeLead, style: { dim: true } },
277
+ ...nameSpans(clip(r.name, nameMax), query, nameStyle),
278
+ ...flags,
279
+ ];
280
+ // Row base: cursor → subtle bg (256) or reverse fallback (also !color); non-matched
281
+ // ancestor → whole-row dim for tree context.
201
282
  let lineBase = '';
202
283
  let fill = false;
203
284
  if (isCursor) {
@@ -207,7 +288,26 @@ function rowLine(row, tree, width, isCursor, query, caps, showCwd, now) {
207
288
  else if (!row.matched) {
208
289
  lineBase = DIM;
209
290
  }
210
- return assemble(spans, width, caps.color, lineBase, fill);
291
+ return assembleRow(left, right, width, caps.color, lineBase, fill);
292
+ }
293
+ /** The column-header row — dim labels aligned to the same zones the rows use, so
294
+ * the metadata columns read as a table. */
295
+ function columnHeaderLine(width, showCwd, caps) {
296
+ const left = [
297
+ { text: ' ' }, // viewer gutter (no label)
298
+ { text: 'STATUS'.padEnd(STATUS_W), style: { dim: true, bold: true } },
299
+ { text: 'NAME', style: { dim: true, bold: true } },
300
+ ];
301
+ const right = [
302
+ { text: 'KIND/MODE'.padEnd(KM_W), style: { dim: true, bold: true } },
303
+ { text: COL_GAP },
304
+ { text: 'CTX'.padStart(CTX_W), style: { dim: true, bold: true } },
305
+ { text: COL_GAP },
306
+ { text: 'AGE'.padStart(AGE_W), style: { dim: true, bold: true } },
307
+ ];
308
+ if (showCwd)
309
+ right.push({ text: COL_GAP }, { text: 'PROJECT'.padEnd(PROJ_W), style: { dim: true, bold: true } });
310
+ return assembleRow(left, right, width, caps.color, '', false);
211
311
  }
212
312
  /** The status line (always present): active cwd scope · sort mode · committed
213
313
  * filter. The committed-filter indicator lives here now (not its own line) so a
@@ -248,11 +348,13 @@ function snippetLine(ln, width, caps) {
248
348
  return assemble(spans, width, caps.color, '', false);
249
349
  }
250
350
  /** The bottom preview panel — exactly PREVIEW_HEIGHT lines: a separator, a meta
251
- * line (status · kind/mode · project · age · ctx · asks), then the selected node's
252
- * prompt wrapped to PREVIEW_BODY lines. Under a live query the body is WINDOWED to
253
- * the matching prompt (anywhere in the conversation) with the match highlighted;
254
- * with no query it shows the spawn prompt from the start. The "which one was this?"
255
- * answer paired with super-search. Always full height so viewport math holds. */
351
+ * line (status · kind/mode · project · age · ctx · asks · streaming/viewing), then
352
+ * TWO blocks the spawn prompt / query-match (PROMPT_LINES) and the node's LAST
353
+ * assistant reply (REPLY_LINES, prefixed `↩`). Under a live query the prompt block
354
+ * is WINDOWED to the matching prompt (anywhere in the conversation) with the match
355
+ * highlighted; with no query it shows the spawn prompt from the start. Seeing both
356
+ * ends — what it was asked and where it left off — answers "which one was this?".
357
+ * Always full height so viewport math holds. */
256
358
  function previewPanel(r, width, caps, now, query) {
257
359
  const out = [`${DIM}${'─'.repeat(width)}${RESET}`];
258
360
  if (r === undefined) {
@@ -266,23 +368,43 @@ function previewPanel(r, width, caps, now, query) {
266
368
  const metaPieces = [`${r.status} ${r.kind}/${r.mode}`, baseDir(r.cwd), relAge(r.created, now), `ctx ${fmtCtx(r.ctx_tokens)}`];
267
369
  if (r.asks > 0)
268
370
  metaPieces.push(`⚑${r.asks}`);
371
+ if (r.streaming === true)
372
+ metaPieces.push('⟳ streaming');
373
+ if (r.viewed === true)
374
+ metaPieces.push('◉ viewing');
269
375
  const metaText = clip(metaPieces.filter((p) => p !== '').join(' · '), Math.max(0, width - 2));
270
376
  out.push(caps.color ? `${glyph} ${DIM}${metaText}${RESET}` : `${glyph} ${metaText}`);
271
- // With a query, window+highlight the matching prompt from the WHOLE conversation;
272
- // otherwise show the spawn prompt from the start.
377
+ // Block 1 — spawn prompt / query-match. With a query, window+highlight the
378
+ // matching prompt from the WHOLE conversation; otherwise the spawn prompt.
273
379
  const sourceText = query !== '' ? promptText(r) : (r.goal ?? '');
274
- const snippet = previewSnippet(query, sourceText, width, PREVIEW_BODY);
275
- if (snippet.length === 0) {
380
+ const promptSnippet = previewSnippet(query, sourceText, width, PROMPT_LINES);
381
+ if (promptSnippet.length === 0) {
276
382
  out.push(`${DIM}(no spawn prompt)${RESET}`);
277
- for (let i = 1; i < PREVIEW_BODY; i++)
383
+ for (let i = 1; i < PROMPT_LINES; i++)
278
384
  out.push('');
279
385
  }
280
386
  else {
281
- for (let i = 0; i < PREVIEW_BODY; i++) {
282
- const ln = snippet[i];
387
+ for (let i = 0; i < PROMPT_LINES; i++) {
388
+ const ln = promptSnippet[i];
283
389
  out.push(ln === undefined ? '' : snippetLine(ln, width, caps));
284
390
  }
285
391
  }
392
+ // Block 2 — the node's LAST assistant reply, prefixed `↩` on its first line so
393
+ // the two blocks read apart. Wrapped to width-2 to leave room for the marker.
394
+ const replyMark = caps.color ? `${DIM}↩ ${RESET}` : '↩ ';
395
+ const replySnippet = previewSnippet('', r.lastAssistant ?? '', Math.max(1, width - 2), REPLY_LINES);
396
+ if (replySnippet.length === 0) {
397
+ out.push(`${replyMark}${DIM}(no reply yet)${RESET}`);
398
+ for (let i = 1; i < REPLY_LINES; i++)
399
+ out.push('');
400
+ }
401
+ else {
402
+ for (let i = 0; i < REPLY_LINES; i++) {
403
+ const ln = replySnippet[i];
404
+ const prefix = i === 0 ? replyMark : ' ';
405
+ out.push(ln === undefined ? (i === 0 ? `${replyMark}` : '') : `${prefix}${snippetLine(ln, Math.max(1, width - 2), caps)}`);
406
+ }
407
+ }
286
408
  return out;
287
409
  }
288
410
  /**
@@ -315,11 +437,12 @@ export function renderFrame(state, size, caps = { color: false, color256: false
315
437
  const slash = caps.color ? `${ESC}${FG_CYAN}m/${RESET}` : '/';
316
438
  lines.push(`${slash} ${state.query}▎`);
317
439
  }
318
- // separator.
440
+ // column header (table labels) + separator.
441
+ const showCwd = state.cwdScope === null; // project column only matters across dirs
442
+ lines.push(columnHeaderLine(width, showCwd, caps));
319
443
  lines.push(`${DIM}${'─'.repeat(width)}${RESET}`);
320
444
  // body — windowed visible rows, leaving room for the (optional) preview panel.
321
445
  const head = lines.length; // === headerHeight(state.search)
322
- const showCwd = state.cwdScope === null; // project cue only matters across dirs
323
446
  const previewOn = state.preview && state.visible.length > 0;
324
447
  const previewH = previewOn ? PREVIEW_HEIGHT : 0;
325
448
  const viewport = Math.max(1, rows - head - 1 /* footer */ - previewH);
@@ -344,11 +467,19 @@ export function renderFrame(state, size, caps = { color: false, color256: false
344
467
  for (const l of previewPanel(r, width, caps, now, state.query))
345
468
  lines.push(l);
346
469
  }
347
- // footer.
348
- const footer = state.search
349
- ? '⏎ commit Esc cancel ⌫ delete'
350
- : '↑↓ move →/← tree ⏎ resume Tab tabs / search s sort c cwd r residents p preview q quit';
351
- lines.push(`${DIM}${clip(footer, width)}${RESET}`);
470
+ // footer — the y/n close-out confirm takes over when pending; else search / nav.
471
+ if (state.pendingClose !== null && state.pendingClose !== undefined) {
472
+ const node = state.tree.nodes.get(state.pendingClose);
473
+ const who = node !== undefined ? node.row.name : state.pendingClose;
474
+ const warn = `⚠ ${who} is actively streaming — close it (and its subtree) anyway? y / n`;
475
+ lines.push(caps.color ? `${BOLD}${ESC}${FG_BRIGHT_YELLOW}m${clip(warn, width)}${RESET}` : `${REVERSE}${clip(warn, width)}${RESET}`);
476
+ }
477
+ else {
478
+ const footer = state.search
479
+ ? '⏎ commit Esc cancel ⌫ delete'
480
+ : '↑↓ move →/← tree ⏎ resume x close Tab tabs / search s sort c cwd r residents p preview q quit';
481
+ lines.push(`${DIM}${clip(footer, width)}${RESET}`);
482
+ }
352
483
  // Assemble: home, each line cleared to EOL, then clear below.
353
484
  const body = lines.map((l) => `${l}${ESC}K`).join('\r\n');
354
485
  return `${ESC}H${body}${ESC}J`;
@@ -22,7 +22,7 @@ import { ensureHome, ensureNodeDirs, nodeMetaPath, nodeDir, nodesRoot, } from '.
22
22
  const IDENTITY_KEYS = [
23
23
  'node_id', 'name', 'description', 'cycles', 'created', 'cwd', 'host_kind', 'kind', 'mode',
24
24
  'lifecycle', 'persona_ack', 'parent', 'spawned_by', 'fork_from', 'passive_default',
25
- 'home_session', 'pi_session_id', 'pi_session_file', 'model_override', 'launch',
25
+ 'pi_session_id', 'pi_session_file', 'model_override', 'launch',
26
26
  ];
27
27
  /** Project any node object down to its durable-identity subset. */
28
28
  function toIdentity(m) {
@@ -4,10 +4,10 @@ import type { FocusRow } from './types.js';
4
4
  export declare function openFocusRow(focus_id: string, pane: string | null, session: string | null, node_id: string): void;
5
5
  /** Hot-swap a focus's occupant — single-statement UPDATE. Respects
6
6
  * UNIQUE(node_id): if `node_id` already occupies ANOTHER focus this throws
7
- * (correct — vacate-first is retargetFocus's job, Step 6, not this setter's). */
7
+ * (correct — vacate-first is placement.focus()'s job, not this setter's). */
8
8
  export declare function setFocusOccupant(focus_id: string, node_id: string): void;
9
- /** Re-point a focus's durable pane + its derived session cache for
10
- * reconcileFocus / the daemon (Step 6). Single-statement UPDATE. */
9
+ /** Re-point a viewer row's durable pane + its derived session cache after the
10
+ * pane moves. Single-statement UPDATE. */
11
11
  export declare function setFocusPane(focus_id: string, pane: string | null, session: string | null): void;
12
12
  /** DELETE a viewport. */
13
13
  export declare function closeFocusRow(focus_id: string): void;
@@ -16,7 +16,7 @@ export declare function getFocusByNode(node_id: string): FocusRow | null;
16
16
  /** The focus realized by a given pane (`%id`), or null. */
17
17
  export declare function getFocusByPane(pane: string): FocusRow | null;
18
18
  /** A focus by its stable id, or null. Used by placement to read a row back by id
19
- * (handFocusToManager / retargetFocus / registerRootFocus). */
19
+ * after openFocusRow registers a viewer. */
20
20
  export declare function getFocusById(focus_id: string): FocusRow | null;
21
21
  /** Every focus row, ordered by id. */
22
22
  export declare function listFocuses(): FocusRow[];
@@ -11,9 +11,9 @@
11
11
  // setPresence) — it never runs raw focus SQL itself.
12
12
  //
13
13
  // Each setter is a single atomic statement. UNIQUE(node_id) upholds "a node
14
- // occupies at most one focus" (Q5): a second focus row (or an occupant UPDATE)
15
- // for an already-focused node throws — that is correct, the Q5 vacate-first
16
- // orchestration is retargetFocus's job (Step 6), not these setters'.
14
+ // occupies at most one viewer" (Q5): a second focus row (or an occupant UPDATE)
15
+ // for an already-viewed node throws — that is correct, the vacate-first
16
+ // orchestration is placement.focus()'s job, not these setters'.
17
17
  import { openDb } from './db.js';
18
18
  function focusFrom(r) {
19
19
  return {
@@ -35,12 +35,12 @@ export function openFocusRow(focus_id, pane, session, node_id) {
35
35
  }
36
36
  /** Hot-swap a focus's occupant — single-statement UPDATE. Respects
37
37
  * UNIQUE(node_id): if `node_id` already occupies ANOTHER focus this throws
38
- * (correct — vacate-first is retargetFocus's job, Step 6, not this setter's). */
38
+ * (correct — vacate-first is placement.focus()'s job, not this setter's). */
39
39
  export function setFocusOccupant(focus_id, node_id) {
40
40
  openDb().prepare('UPDATE focuses SET node_id = ? WHERE focus_id = ?').run(node_id, focus_id);
41
41
  }
42
- /** Re-point a focus's durable pane + its derived session cache for
43
- * reconcileFocus / the daemon (Step 6). Single-statement UPDATE. */
42
+ /** Re-point a viewer row's durable pane + its derived session cache after the
43
+ * pane moves. Single-statement UPDATE. */
44
44
  export function setFocusPane(focus_id, pane, session) {
45
45
  openDb()
46
46
  .prepare('UPDATE focuses SET pane = ?, session = ? WHERE focus_id = ?')
@@ -68,7 +68,7 @@ export function getFocusByPane(pane) {
68
68
  return r ? focusFrom(r) : null;
69
69
  }
70
70
  /** A focus by its stable id, or null. Used by placement to read a row back by id
71
- * (handFocusToManager / retargetFocus / registerRootFocus). */
71
+ * after openFocusRow registers a viewer. */
72
72
  export function getFocusById(focus_id) {
73
73
  const r = openDb()
74
74
  .prepare('SELECT * FROM focuses WHERE focus_id = ?')
@@ -227,13 +227,14 @@ export function shortId(id) {
227
227
  }
228
228
  // ---------------------------------------------------------------------------
229
229
  // Attachment — is a human currently WATCHING a node? A separate axis from
230
- // running (status 'active' = the engine is live on its host, which may be an
231
- // unwatched backstage pane or a paneless broker). Two hosts, two signals:
232
- // tmux — 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 to job/attach.json
235
- // on every viewer change (src/core/runtime/broker.ts). Trusted only
236
- // while the node is 'active': a broker crash can leave a stale file.
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.
237
238
  // ---------------------------------------------------------------------------
238
239
  /** Node ids currently shown in a tmux focus viewport. Built once per render. */
239
240
  export function focusedNodeIds() {
@@ -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;