@crouton-kit/crouter 0.3.27 → 0.3.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (343) hide show
  1. package/dist/build-root.js +4 -2
  2. package/dist/builtin-memory/crouter-development/marketplaces.md +7 -6
  3. package/dist/builtin-memory/crouter-development/personas/base-prompt.md +1 -1
  4. package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +1 -1
  5. package/dist/builtin-memory/crouter-development/personas.md +1 -1
  6. package/dist/builtin-memory/crouter-development/plugins.md +20 -18
  7. package/dist/builtin-memory/design.md +1 -1
  8. package/dist/builtin-memory/development.md +1 -1
  9. package/dist/builtin-memory/internal/INDEX.md +1 -1
  10. package/dist/builtin-memory/internal/examples/INDEX.md +1 -1
  11. package/dist/builtin-memory/internal/examples/imessage-assistant.md +2 -2
  12. package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
  13. package/dist/builtin-memory/internal/storage-tiers.md +1 -1
  14. package/dist/builtin-memory/planning.md +1 -1
  15. package/dist/builtin-memory/spec.md +6 -4
  16. package/dist/builtin-personas/orchestration-kernel.md +4 -5
  17. package/dist/builtin-personas/runtime-base.md +8 -4
  18. package/dist/builtin-personas/spec/PERSONA.md +8 -4
  19. package/dist/builtin-personas/spec/orchestrator.md +5 -3
  20. package/dist/builtin-personas/spec/requirements/PERSONA.md +10 -0
  21. package/dist/builtin-views/canvas/core.mjs +586 -0
  22. package/dist/builtin-views/canvas/text.mjs +58 -0
  23. package/dist/builtin-views/canvas/tui.mjs +165 -0
  24. package/dist/builtin-views/canvas/web.jsx +120 -0
  25. package/dist/builtin-views/git-pr/core.mjs +673 -0
  26. package/dist/builtin-views/git-pr/text.mjs +84 -0
  27. package/dist/builtin-views/git-pr/tui.mjs +302 -0
  28. package/dist/builtin-views/git-pr/web.jsx +216 -0
  29. package/dist/builtin-views/inbox/_lib/render.mjs +1 -3
  30. package/dist/builtin-views/inbox/core.mjs +1273 -0
  31. package/dist/builtin-views/inbox/text.mjs +73 -0
  32. package/dist/builtin-views/inbox/tui.mjs +312 -0
  33. package/dist/builtin-views/inbox/web.jsx +188 -0
  34. package/dist/builtin-views/linkedin/core.mjs +906 -0
  35. package/dist/builtin-views/linkedin/text.mjs +69 -0
  36. package/dist/builtin-views/linkedin/tui.mjs +427 -0
  37. package/dist/builtin-views/linkedin/web.jsx +206 -0
  38. package/dist/builtin-views/workspace-sidebar/core.mjs +621 -0
  39. package/dist/builtin-views/workspace-sidebar/text.mjs +53 -0
  40. package/dist/builtin-views/workspace-sidebar/tui.mjs +142 -0
  41. package/dist/builtin-views/workspace-sidebar/web.jsx +109 -0
  42. package/dist/cli.js +3 -3
  43. package/dist/clients/attach/__tests__/action-parity.test.js +48 -0
  44. package/dist/clients/attach/__tests__/clipboard-image.test.js +76 -0
  45. package/dist/clients/attach/__tests__/editor-newline.test.js +57 -0
  46. package/dist/clients/attach/__tests__/git-info.test.js +31 -0
  47. package/dist/clients/attach/__tests__/onrequest-wired.test.js +20 -0
  48. package/dist/clients/attach/__tests__/reconnect-giveup.test.js +30 -0
  49. package/dist/clients/attach/__tests__/slash-quit-copy.test.js +42 -0
  50. package/dist/clients/attach/__tests__/titled-editor.test.d.ts +1 -0
  51. package/dist/clients/attach/__tests__/titled-editor.test.js +39 -0
  52. package/dist/clients/attach/attach-cmd.js +445 -41
  53. package/dist/clients/attach/auth-pickers.d.ts +9 -0
  54. package/dist/clients/attach/auth-pickers.js +194 -0
  55. package/dist/clients/attach/canvas-panels.d.ts +12 -0
  56. package/dist/clients/attach/canvas-panels.js +78 -0
  57. package/dist/clients/attach/chat-view.d.ts +27 -2
  58. package/dist/clients/attach/chat-view.js +98 -9
  59. package/dist/clients/attach/clipboard-image.d.ts +47 -8
  60. package/dist/clients/attach/clipboard-image.js +150 -26
  61. package/dist/clients/attach/clipboard-text.d.ts +3 -0
  62. package/dist/clients/attach/clipboard-text.js +39 -0
  63. package/dist/clients/attach/config-load.d.ts +54 -7
  64. package/dist/clients/attach/config-load.js +99 -3
  65. package/dist/clients/attach/context-message.d.ts +12 -0
  66. package/dist/clients/attach/context-message.js +72 -0
  67. package/dist/clients/attach/git-info.d.ts +20 -0
  68. package/dist/clients/attach/git-info.js +44 -0
  69. package/dist/clients/attach/graph-overlay.d.ts +43 -0
  70. package/dist/clients/attach/graph-overlay.js +306 -0
  71. package/dist/clients/attach/input-controller.d.ts +81 -13
  72. package/dist/clients/attach/input-controller.js +307 -67
  73. package/dist/clients/attach/pickers.d.ts +58 -0
  74. package/dist/clients/attach/pickers.js +171 -0
  75. package/dist/clients/attach/slash-commands.d.ts +44 -1
  76. package/dist/clients/attach/slash-commands.js +250 -27
  77. package/dist/clients/attach/titled-editor.d.ts +33 -0
  78. package/dist/clients/attach/titled-editor.js +84 -0
  79. package/dist/clients/attach/view-socket.d.ts +37 -2
  80. package/dist/clients/attach/view-socket.js +131 -7
  81. package/dist/clients/web/dev-server.d.ts +7 -0
  82. package/dist/clients/web/dev-server.js +59 -0
  83. package/dist/clients/web/events.d.ts +14 -0
  84. package/dist/clients/web/events.js +151 -0
  85. package/dist/clients/web/server.d.ts +18 -0
  86. package/dist/clients/web/server.js +450 -0
  87. package/dist/clients/web/web-cmd.d.ts +2 -0
  88. package/dist/clients/web/web-cmd.js +120 -0
  89. package/dist/commands/canvas.js +1 -2
  90. package/dist/commands/chord.js +1 -1
  91. package/dist/commands/dashboard.js +5 -1
  92. package/dist/commands/human/prompts.js +3 -3
  93. package/dist/commands/human/queue.d.ts +17 -0
  94. package/dist/commands/human/queue.js +111 -4
  95. package/dist/commands/memory/__tests__/lint-schema.test.js +27 -4
  96. package/dist/commands/memory/lint.d.ts +5 -4
  97. package/dist/commands/memory/lint.js +23 -7
  98. package/dist/commands/memory/read.js +2 -2
  99. package/dist/commands/memory/shared.d.ts +7 -1
  100. package/dist/commands/memory/shared.js +18 -3
  101. package/dist/commands/memory/write.js +20 -7
  102. package/dist/commands/memory.js +4 -4
  103. package/dist/commands/node.d.ts +1 -1
  104. package/dist/commands/node.js +64 -65
  105. package/dist/commands/pkg.js +1 -2
  106. package/dist/commands/revive.js +11 -3
  107. package/dist/commands/sys/feedback.d.ts +1 -0
  108. package/dist/commands/sys/feedback.js +163 -0
  109. package/dist/commands/sys/sync.d.ts +1 -0
  110. package/dist/commands/sys/sync.js +187 -0
  111. package/dist/commands/sys.js +4 -2
  112. package/dist/commands/view-cycle.js +2 -2
  113. package/dist/commands/view-list.js +8 -8
  114. package/dist/commands/view-new.js +21 -17
  115. package/dist/commands/view-pick.js +1 -1
  116. package/dist/commands/view-run.js +35 -14
  117. package/dist/commands/view.js +8 -6
  118. package/dist/commands/workspace.d.ts +2 -0
  119. package/dist/commands/workspace.js +161 -0
  120. package/dist/core/__tests__/broker-double-spawn.test.d.ts +1 -0
  121. package/dist/core/__tests__/broker-double-spawn.test.js +143 -0
  122. package/dist/core/__tests__/broker-fork-seam.test.d.ts +1 -0
  123. package/dist/core/__tests__/broker-fork-seam.test.js +102 -0
  124. package/dist/core/__tests__/broker-sdk-wiring.test.js +2 -0
  125. package/dist/core/__tests__/broker-snapshot-history.test.d.ts +1 -0
  126. package/dist/core/__tests__/broker-snapshot-history.test.js +105 -0
  127. package/dist/core/__tests__/child-death-wake.test.js +56 -111
  128. package/dist/core/__tests__/close.test.js +14 -20
  129. package/dist/core/__tests__/context-intro.test.js +19 -19
  130. package/dist/core/__tests__/daemon-boot.test.js +30 -23
  131. package/dist/core/__tests__/draw-style.test.js +1 -1
  132. package/dist/core/__tests__/error-stall-recycle.test.d.ts +1 -0
  133. package/dist/core/__tests__/error-stall-recycle.test.js +141 -0
  134. package/dist/core/__tests__/fixtures/fake-engine.d.ts +24 -1
  135. package/dist/core/__tests__/fixtures/fake-engine.js +32 -2
  136. package/dist/core/__tests__/fixtures/fake-pi-host.js +2 -2
  137. package/dist/core/__tests__/focuses.test.js +25 -13
  138. package/dist/core/__tests__/fork.test.js +22 -5
  139. package/dist/core/__tests__/full/broker-control-preempt.test.d.ts +1 -0
  140. package/dist/core/__tests__/full/broker-control-preempt.test.js +61 -0
  141. package/dist/core/__tests__/full/broker-model-changed-broadcast.test.d.ts +1 -0
  142. package/dist/core/__tests__/full/broker-model-changed-broadcast.test.js +57 -0
  143. package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.d.ts +1 -0
  144. package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.js +80 -0
  145. package/dist/core/__tests__/full/broker-pane-resolution.test.js +90 -0
  146. package/dist/core/__tests__/full/cascade-close.test.js +22 -8
  147. package/dist/core/__tests__/full/detach-focus.test.js +66 -169
  148. package/dist/core/__tests__/helpers/harness.d.ts +6 -9
  149. package/dist/core/__tests__/helpers/harness.js +19 -36
  150. package/dist/core/__tests__/human-node-not-supervised.test.d.ts +1 -0
  151. package/dist/core/__tests__/human-node-not-supervised.test.js +83 -0
  152. package/dist/core/__tests__/human-stranded-deliver.test.d.ts +1 -0
  153. package/dist/core/__tests__/human-stranded-deliver.test.js +108 -0
  154. package/dist/core/__tests__/lifecycle.test.js +1 -19
  155. package/dist/core/__tests__/live-mutation-verbs.test.js +5 -5
  156. package/dist/core/__tests__/memory-resolver.test.js +1 -1
  157. package/dist/core/__tests__/on-read-dedup-resume.test.d.ts +1 -0
  158. package/dist/core/__tests__/on-read-dedup-resume.test.js +81 -0
  159. package/dist/core/__tests__/refresh-stall-recycle.test.d.ts +1 -0
  160. package/dist/core/__tests__/refresh-stall-recycle.test.js +88 -0
  161. package/dist/core/__tests__/relaunch-root.test.d.ts +1 -0
  162. package/dist/core/__tests__/relaunch-root.test.js +114 -0
  163. package/dist/core/__tests__/reset.test.js +19 -79
  164. package/dist/core/__tests__/steer-note.test.js +9 -7
  165. package/dist/core/__tests__/tmux-surface.test.js +13 -7
  166. package/dist/core/bootstrap.d.ts +0 -3
  167. package/dist/core/bootstrap.js +1 -143
  168. package/dist/core/canvas/browse/__tests__/model.test.js +86 -1
  169. package/dist/core/canvas/browse/__tests__/render.test.js +2 -1
  170. package/dist/core/canvas/browse/app.js +131 -8
  171. package/dist/core/canvas/browse/model.d.ts +23 -5
  172. package/dist/core/canvas/browse/model.js +68 -13
  173. package/dist/core/canvas/browse/render.d.ts +7 -1
  174. package/dist/core/canvas/browse/render.js +178 -47
  175. package/dist/core/canvas/canvas.js +1 -1
  176. package/dist/core/canvas/focuses.d.ts +4 -4
  177. package/dist/core/canvas/focuses.js +7 -7
  178. package/dist/core/canvas/nav-model.d.ts +162 -0
  179. package/dist/core/canvas/nav-model.js +487 -0
  180. package/dist/core/canvas/paths.d.ts +7 -0
  181. package/dist/core/canvas/paths.js +9 -0
  182. package/dist/core/canvas/render.d.ts +68 -9
  183. package/dist/core/canvas/render.js +195 -61
  184. package/dist/core/canvas/types.d.ts +16 -23
  185. package/dist/core/config.js +1 -4
  186. package/dist/core/memory-resolver.js +43 -5
  187. package/dist/core/runtime/bearings.d.ts +1 -1
  188. package/dist/core/runtime/bearings.js +8 -8
  189. package/dist/core/runtime/branded-host.d.ts +16 -0
  190. package/dist/core/runtime/branded-host.js +127 -0
  191. package/dist/core/runtime/broker-protocol.d.ts +212 -6
  192. package/dist/core/runtime/broker-sdk.d.ts +0 -12
  193. package/dist/core/runtime/broker-sdk.js +77 -6
  194. package/dist/core/runtime/broker.d.ts +5 -1
  195. package/dist/core/runtime/broker.js +520 -59
  196. package/dist/core/runtime/close.d.ts +12 -2
  197. package/dist/core/runtime/close.js +37 -16
  198. package/dist/core/runtime/front-door.js +14 -7
  199. package/dist/core/runtime/host.d.ts +14 -34
  200. package/dist/core/runtime/host.js +14 -50
  201. package/dist/core/runtime/launch.d.ts +12 -9
  202. package/dist/core/runtime/launch.js +27 -15
  203. package/dist/core/runtime/lifecycle.d.ts +1 -1
  204. package/dist/core/runtime/lifecycle.js +15 -19
  205. package/dist/core/runtime/nodes.d.ts +0 -55
  206. package/dist/core/runtime/nodes.js +6 -74
  207. package/dist/core/runtime/placement.d.ts +91 -334
  208. package/dist/core/runtime/placement.js +262 -828
  209. package/dist/core/runtime/promote.d.ts +2 -0
  210. package/dist/core/runtime/promote.js +24 -6
  211. package/dist/core/runtime/recap.d.ts +8 -0
  212. package/dist/core/runtime/recap.js +107 -0
  213. package/dist/core/runtime/recycle.js +25 -61
  214. package/dist/core/runtime/reset.d.ts +43 -37
  215. package/dist/core/runtime/reset.js +131 -218
  216. package/dist/core/runtime/revive.d.ts +9 -29
  217. package/dist/core/runtime/revive.js +45 -135
  218. package/dist/core/runtime/spawn.d.ts +15 -11
  219. package/dist/core/runtime/spawn.js +168 -136
  220. package/dist/core/runtime/surface-bg.d.ts +11 -0
  221. package/dist/core/runtime/surface-bg.js +68 -0
  222. package/dist/core/runtime/tmux.d.ts +15 -43
  223. package/dist/core/runtime/tmux.js +38 -80
  224. package/dist/core/scope.d.ts +1 -2
  225. package/dist/core/scope.js +1 -5
  226. package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.d.ts +1 -0
  227. package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.js +57 -0
  228. package/dist/core/skill-sync/builtins.d.ts +42 -0
  229. package/dist/core/skill-sync/builtins.js +112 -0
  230. package/dist/core/skill-sync/claude-plugins.d.ts +23 -0
  231. package/dist/core/skill-sync/claude-plugins.js +71 -0
  232. package/dist/core/skill-sync/engine.d.ts +42 -0
  233. package/dist/core/skill-sync/engine.js +633 -0
  234. package/dist/core/skill-sync/export.d.ts +23 -0
  235. package/dist/core/skill-sync/export.js +86 -0
  236. package/dist/core/skill-sync/manifest.d.ts +64 -0
  237. package/dist/core/skill-sync/manifest.js +181 -0
  238. package/dist/core/skill-sync/profile.d.ts +76 -0
  239. package/dist/core/skill-sync/profile.js +173 -0
  240. package/dist/core/skill-sync/snapshot.d.ts +57 -0
  241. package/dist/core/skill-sync/snapshot.js +120 -0
  242. package/dist/core/spawn.d.ts +14 -0
  243. package/dist/core/spawn.js +29 -9
  244. package/dist/core/substrate/index.d.ts +2 -2
  245. package/dist/core/substrate/index.js +7 -7
  246. package/dist/core/substrate/injected-store.d.ts +10 -0
  247. package/dist/core/substrate/injected-store.js +55 -0
  248. package/dist/core/substrate/on-read.js +68 -6
  249. package/dist/core/substrate/render.d.ts +8 -11
  250. package/dist/core/substrate/render.js +29 -43
  251. package/dist/core/substrate/schema.d.ts +16 -11
  252. package/dist/core/substrate/schema.js +32 -31
  253. package/dist/core/tui/host.d.ts +12 -4
  254. package/dist/core/tui/host.js +280 -149
  255. package/dist/core/view/bridge.d.ts +10 -0
  256. package/dist/core/view/bridge.js +31 -0
  257. package/dist/core/view/chrome.d.ts +9 -0
  258. package/dist/core/view/chrome.js +22 -0
  259. package/dist/core/view/contract.d.ts +171 -0
  260. package/dist/core/view/contract.js +23 -0
  261. package/dist/core/view/loader.d.ts +31 -0
  262. package/dist/core/view/loader.js +188 -0
  263. package/dist/core/view/transport-local.d.ts +7 -0
  264. package/dist/core/view/transport-local.js +70 -0
  265. package/dist/core/view/transport.d.ts +4 -0
  266. package/dist/core/view/transport.js +15 -0
  267. package/dist/daemon/crtrd.d.ts +38 -27
  268. package/dist/daemon/crtrd.js +303 -376
  269. package/dist/daemon/manage.js +6 -1
  270. package/dist/index.js +1 -1
  271. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +28 -20
  272. package/dist/pi-extensions/canvas-context-intro.js +4 -4
  273. package/dist/pi-extensions/canvas-doc-substrate.js +29 -20
  274. package/dist/pi-extensions/canvas-nav.js +35 -385
  275. package/dist/pi-extensions/canvas-recap.d.ts +37 -0
  276. package/dist/pi-extensions/canvas-recap.js +236 -0
  277. package/dist/pi-extensions/canvas-resume.js +2 -1
  278. package/dist/pi-extensions/canvas-stophook.d.ts +15 -13
  279. package/dist/pi-extensions/canvas-stophook.js +119 -103
  280. package/dist/pi-extensions/canvas-view.js +2 -1
  281. package/dist/pi-extensions/widget-order-bus.d.ts +6 -0
  282. package/dist/pi-extensions/widget-order-bus.js +34 -0
  283. package/dist/prompts/view.d.ts +2 -2
  284. package/dist/prompts/view.js +145 -73
  285. package/dist/types.d.ts +1 -6
  286. package/dist/types.js +1 -3
  287. package/dist/web/ViewChrome.d.ts +7 -0
  288. package/dist/web/ViewChrome.js +28 -0
  289. package/dist/web/ViewPane.d.ts +39 -0
  290. package/dist/web/ViewPane.js +48 -0
  291. package/dist/web/index.d.ts +6 -0
  292. package/dist/web/index.js +16 -0
  293. package/dist/web/runtime.d.ts +39 -0
  294. package/dist/web/runtime.js +133 -0
  295. package/dist/web/states.d.ts +24 -0
  296. package/dist/web/states.js +24 -0
  297. package/dist/web/transport-http.d.ts +5 -0
  298. package/dist/web/transport-http.js +28 -0
  299. package/dist/web-client/assets/index-BUvQb4hR.css +2 -0
  300. package/dist/web-client/assets/index-ClLQXYAE.js +10 -0
  301. package/dist/web-client/index.html +13 -0
  302. package/package.json +20 -6
  303. package/dist/builtin-views/canvas/client.mjs +0 -303
  304. package/dist/builtin-views/canvas/view.mjs +0 -576
  305. package/dist/builtin-views/git-pr/client.mjs +0 -440
  306. package/dist/builtin-views/git-pr/view.mjs +0 -675
  307. package/dist/builtin-views/inbox/sources/gmail.mjs +0 -965
  308. package/dist/builtin-views/inbox/sources/linkedin.mjs +0 -427
  309. package/dist/builtin-views/inbox/view.mjs +0 -889
  310. package/dist/builtin-views/linkedin/client.mjs +0 -610
  311. package/dist/builtin-views/linkedin/view.mjs +0 -1171
  312. package/dist/commands/pkg/bridge.d.ts +0 -1
  313. package/dist/commands/pkg/bridge.js +0 -137
  314. package/dist/commands/skill/author.d.ts +0 -3
  315. package/dist/commands/skill/author.js +0 -140
  316. package/dist/commands/skill/shared.d.ts +0 -3
  317. package/dist/commands/skill/shared.js +0 -19
  318. package/dist/commands/skill.d.ts +0 -2
  319. package/dist/commands/skill.js +0 -21
  320. package/dist/commands/tmux-spread.d.ts +0 -2
  321. package/dist/commands/tmux-spread.js +0 -144
  322. package/dist/core/__tests__/full/daemon-liveness-pane.full.test.js +0 -292
  323. package/dist/core/__tests__/full/placement-focus.test.js +0 -309
  324. package/dist/core/__tests__/full/placement-reconcile.test.js +0 -212
  325. package/dist/core/__tests__/full/placement-revive.test.js +0 -238
  326. package/dist/core/__tests__/full/placement-teardown.test.js +0 -221
  327. package/dist/core/__tests__/home-session.test.js +0 -186
  328. package/dist/core/__tests__/relaunch.test.js +0 -335
  329. package/dist/core/bridge-map.d.ts +0 -19
  330. package/dist/core/bridge-map.js +0 -73
  331. package/dist/core/tui/contract.d.ts +0 -83
  332. package/dist/core/tui/contract.js +0 -8
  333. package/dist/core/tui/loader.d.ts +0 -16
  334. package/dist/core/tui/loader.js +0 -94
  335. package/dist/prompts/skill.d.ts +0 -2
  336. package/dist/prompts/skill.js +0 -650
  337. /package/dist/{core/__tests__/full/daemon-liveness-pane.full.test.d.ts → clients/attach/__tests__/action-parity.test.d.ts} +0 -0
  338. /package/dist/{core/__tests__/full/placement-focus.test.d.ts → clients/attach/__tests__/clipboard-image.test.d.ts} +0 -0
  339. /package/dist/{core/__tests__/full/placement-reconcile.test.d.ts → clients/attach/__tests__/editor-newline.test.d.ts} +0 -0
  340. /package/dist/{core/__tests__/full/placement-revive.test.d.ts → clients/attach/__tests__/git-info.test.d.ts} +0 -0
  341. /package/dist/{core/__tests__/full/placement-teardown.test.d.ts → clients/attach/__tests__/onrequest-wired.test.d.ts} +0 -0
  342. /package/dist/{core/__tests__/home-session.test.d.ts → clients/attach/__tests__/reconnect-giveup.test.d.ts} +0 -0
  343. /package/dist/{core/__tests__/relaunch.test.d.ts → clients/attach/__tests__/slash-quit-copy.test.d.ts} +0 -0
@@ -15,18 +15,44 @@
15
15
  // `crtr attach <node>` form is not expressible without a framework change. attach
16
16
  // is therefore a BRANCH wrapping a single leaf: `crtr attach to <node>`.
17
17
  import { randomUUID } from 'node:crypto';
18
- import { Container, ProcessTerminal, Text, TUI, matchesKey, } from '@earendil-works/pi-tui';
19
- import { CustomEditor, getSelectListTheme, } from '@earendil-works/pi-coding-agent';
18
+ import { execFile } from 'node:child_process';
19
+ import { CombinedAutocompleteProvider, Container, ProcessTerminal, Text, TUI, matchesKey, truncateToWidth, } from '@earendil-works/pi-tui';
20
+ import { DynamicBorder, getSelectListTheme, } from '@earendil-works/pi-coding-agent';
20
21
  import { defineBranch, defineLeaf } from '../../core/command.js';
21
22
  import { InputError } from '../../core/io.js';
22
- import { getNode } from '../../core/canvas/index.js';
23
+ import { getNode, getRow } from '../../core/canvas/index.js';
23
24
  // tmux driver verbs only through placement (the §5.1 model-over-driver seam) —
24
25
  // never `core/runtime/tmux.js` directly.
25
- import { setPaneOption, currentTmux } from '../../core/runtime/placement.js';
26
+ import { setPaneOption, currentTmux, inTmux, installMenuBinding } from '../../core/runtime/placement.js';
27
+ import { climbRoot, visibleWidth } from '../../core/canvas/nav-model.js';
26
28
  import { ChatView } from './chat-view.js';
27
29
  import { InputController } from './input-controller.js';
28
- import { applyTheme, createKeybindingsManager } from './config-load.js';
29
- import { BrokerUnavailableError, ViewSocketClient } from './view-socket.js';
30
+ import { writeClipboardText } from './clipboard-text.js';
31
+ import { buildCanvasPanelLines } from './canvas-panels.js';
32
+ import { GraphOverlay } from './graph-overlay.js';
33
+ import { slashCommandList } from './slash-commands.js';
34
+ import { applyTheme, attachPalette, createKeybindingsManager, defaultAgentDir, mirrorKeybindingsToEditor, mirrorKittyProtocolToEditor, } from './config-load.js';
35
+ import { buildLoginPicker, buildLogoutPicker } from './auth-pickers.js';
36
+ import { TitledEditor, thinkingBorderColor, thinkingTitleStyle, defaultTitleStyle } from './titled-editor.js';
37
+ import { fetchGitInfo } from './git-info.js';
38
+ import { BrokerUnavailableError, ViewSocketClient, reconnectShouldGiveUp } from './view-socket.js';
39
+ /** Async per-node ask-map fetch (NON-blocking — the viewer must never block its
40
+ * input pump on a shell-out, unlike canvas-nav's execFileSync). Buckets a whole
41
+ * sub-DAG's pending asks in one `crtr` process; on any error the callback is not
42
+ * invoked, so the caller keeps its last good map. */
43
+ function fetchAsksAsync(rootId, cb) {
44
+ execFile('crtr', ['canvas', 'attention', 'map', '--view', rootId, '--json'], { timeout: 2_500, maxBuffer: 4 * 1024 * 1024 }, (err, stdout) => {
45
+ if (err)
46
+ return;
47
+ try {
48
+ const parsed = JSON.parse(stdout.trim());
49
+ cb(parsed.counts ?? {});
50
+ }
51
+ catch {
52
+ /* malformed — keep last map */
53
+ }
54
+ });
55
+ }
30
56
  /** Run the interactive attach session against `nodeId`. Resolves when the
31
57
  * viewer detaches (ctrl+c/ctrl+d), the broker goes away, or a signal lands —
32
58
  * always restoring the terminal first. */
@@ -100,49 +126,241 @@ async function runAttach(nodeId, observer) {
100
126
  // 1. Theme + keybindings FIRST — pi's components throw "Theme not initialized"
101
127
  // otherwise (T5 contract). One KeybindingsManager feeds BOTH editor + input.
102
128
  applyTheme({ cwd: meta.cwd });
129
+ const pal = attachPalette();
103
130
  const km = createKeybindingsManager();
104
- // 2. TUI + chat container + editor.
105
- const tui = new TUI(new ProcessTerminal());
131
+ // pi-coding-agent's CustomEditor may resolve a SEPARATE pi-tui instance whose
132
+ // global keybindings we'd otherwise never set — mirror the manager onto it so
133
+ // the editor honors the user's bindings (e.g. alt+enter → newline). No-op on a
134
+ // deduped install. Awaited before tui.start so the editor never handles a key
135
+ // against stale defaults.
136
+ await mirrorKeybindingsToEditor(km);
137
+ // The front door installs the Alt+C menu binding before it ever loads a theme,
138
+ // so it lands unstyled. Now that this viewer process HAS themed (applyTheme
139
+ // above populated the live-theme global), re-install it so the menu picks up
140
+ // the distinct-surface background. Idempotent re-bind; tmux-only, best-effort.
141
+ if (inTmux()) {
142
+ try {
143
+ installMenuBinding();
144
+ }
145
+ catch { /* best-effort */ }
146
+ }
147
+ // 2. TUI + layout containers + editor. Top-to-bottom render order (set at
148
+ // step 8): chat · rule · badge · managers (↑ subscriber) · editor (themed
149
+ // border) · reports (↓ subscriptions) · status bar. A themed DynamicBorder
150
+ // rule divides the scrolling chat from the fixed chrome below it. The badge
151
+ // sits in the BOTTOM-anchored chrome
152
+ // stack (directly above managers), NOT as the topmost child: pi-tui anchors
153
+ // the viewport to the cursor at the bottom, so a topmost badge scrolls off
154
+ // into scrollback the instant the chat exceeds one screen. The badge + the
155
+ // two canvas panels reproduce the pi-extension chrome natively (Unit Q).
156
+ const procTerminal = new ProcessTerminal();
157
+ const tui = new TUI(procTerminal);
106
158
  const chatContainer = new Container();
159
+ const chrome = new Container(); // a themed rule dividing scrolling chat from the fixed chrome below
160
+ const managers = new Container();
161
+ const reports = new Container();
162
+ const queued = new Container(); // pending steering/follow-up messages, above the editor
163
+ const pickerPanel = new Container(); // native pickers (/model etc.), inline directly under the editor
107
164
  const footer = new Container();
108
- const editorTheme = { borderColor: (s) => s, selectList: getSelectListTheme() };
109
- const editor = new CustomEditor(tui, editorTheme, km, { paddingX: 1 });
165
+ chrome.addChild(new DynamicBorder(pal.border));
166
+ const editorTheme = { borderColor: pal.border, selectList: getSelectListTheme() };
167
+ // TitledEditor paints the session name into its top border (solid-background
168
+ // chip) and tracks a thinking-level border color, both set from live state below.
169
+ const editor = new TitledEditor(tui, editorTheme, km, { paddingX: 1 });
170
+ // Slash-command autocomplete: builtins + native canvas commands now; enriched
171
+ // with the broker's engine/extension/skill commands on the get_commands ack.
172
+ const setCommands = (commands) => {
173
+ editor.setAutocompleteProvider(new CombinedAutocompleteProvider(slashCommandList(commands), meta.cwd));
174
+ };
175
+ setCommands();
110
176
  // 3. Render layer (T5). ChatView owns the chat scroll + activity spinner; the
111
177
  // footer/status line below is ours, so onFooterEvent is left unset (footer
112
178
  // updates flow through handleBrokerFrame's single update path instead).
113
- const chatView = new ChatView(tui, chatContainer, { cwd: meta.cwd });
179
+ const chatView = new ChatView(tui, chatContainer, { cwd: meta.cwd, palette: pal });
114
180
  // 4. Footer/status line — surfaces role + live state + transient notices.
115
181
  const clientId = randomUUID();
116
182
  let role = observer ? 'observer' : 'controller';
183
+ let brokerAgentDir;
117
184
  let liveState;
118
185
  let notice = '';
186
+ // Live context occupancy for the footer: `contextTokens` is the prompt-token
187
+ // count of the most recent assistant turn (input + cacheRead + cacheWrite =
188
+ // everything sent to the model, since with prompt caching bare `input` is just
189
+ // the uncached slice and badly understates the context), read off the relayed
190
+ // message_end stream. It rides data the viewer already receives, so the
191
+ // indicator needs no broker/protocol change.
192
+ let contextTokens;
193
+ const formatTokens = (n) => n >= 1000 ? `${(n / 1000).toFixed(n >= 10000 ? 0 : 1)}k` : `${n}`;
194
+ // A themed, aligned status bar: live state on the LEFT (role · model · streaming
195
+ // · context · queued), the transient notice pushed to the RIGHT edge — not jammed inline.
119
196
  const renderFooter = () => {
120
- const bits = [role === 'controller' ? 'drive' : 'read-only'];
197
+ const segs = [
198
+ role === 'controller' ? pal.active('● drive') : pal.muted('○ read-only'),
199
+ ];
121
200
  if (liveState?.model)
122
- bits.push(liveState.model);
201
+ segs.push(pal.info(liveState.model));
123
202
  if (liveState?.isStreaming)
124
- bits.push('streaming');
125
- if (liveState?.sessionName)
126
- bits.push(liveState.sessionName);
127
- if (liveState && liveState.pendingMessageCount > 0)
128
- bits.push(`queued ${liveState.pendingMessageCount}`);
129
- let line = `\x1b[2m${bits.join(' · ')}\x1b[22m`;
130
- if (notice)
131
- line += ` \x1b[33m${notice}\x1b[39m`;
203
+ segs.push(pal.active('streaming'));
204
+ if (contextTokens !== undefined) {
205
+ segs.push(pal.muted(`${formatTokens(contextTokens)} ctx`));
206
+ }
207
+ if (liveState && liveState.pendingMessageCount > 0) {
208
+ segs.push(pal.muted(`queued ${liveState.pendingMessageCount}`));
209
+ }
210
+ const left = segs.join(pal.muted(' · '));
211
+ const right = notice ? pal.warning(`⚠ ${notice}`) : '';
212
+ // Text(_, 1, 0) pads 1 col each side, so the usable span is columns - 2.
213
+ const span = Math.max(1, (process.stdout.columns ?? 80) - 2);
214
+ const rightW = visibleWidth(right);
215
+ // Truncate the left segment (ANSI-aware) so a long model/notice never wraps;
216
+ // the right-aligned notice keeps its slot.
217
+ const maxLeft = right ? Math.max(0, span - rightW - 1) : span;
218
+ const leftFit = visibleWidth(left) > maxLeft ? truncateToWidth(left, maxLeft, '…') : left;
219
+ const gap = Math.max(right ? 1 : 0, span - visibleWidth(leftFit) - rightW);
220
+ const line = right ? leftFit + ' '.repeat(gap) + right : leftFit;
132
221
  footer.clear();
133
222
  footer.addChild(new Text(line, 1, 0));
134
223
  tui.requestRender();
135
224
  };
136
- const setNotice = (msg) => {
225
+ // Transient notices (errors, command feedback) auto-clear after a few seconds so
226
+ // a stale "error: …" doesn't linger in the footer forever. `sticky` keeps a notice
227
+ // up until explicitly replaced (used by the reconnect indicator, which is cleared
228
+ // by hand once the socket redials).
229
+ const NOTICE_CLEAR_MS = 3_000;
230
+ let noticeTimer;
231
+ const setNotice = (msg, opts) => {
137
232
  notice = msg;
233
+ if (noticeTimer !== undefined) {
234
+ clearTimeout(noticeTimer);
235
+ noticeTimer = undefined;
236
+ }
237
+ if (msg && !opts?.sticky) {
238
+ noticeTimer = setTimeout(() => {
239
+ noticeTimer = undefined;
240
+ notice = '';
241
+ renderFooter();
242
+ }, NOTICE_CLEAR_MS);
243
+ }
138
244
  renderFooter();
139
245
  };
140
- // 5. Input layer (T6). onCommand/onDialogResponse socket; onNotice footer.
246
+ // Subscribed-node panel manager line above the editor, live reports below,
247
+ // read straight from canvas.db via nav-model (Feature 2). asksMap is refreshed
248
+ // by the low-rate poll below; beginFrame() inside buildCanvasPanelLines memoizes
249
+ // the db reads for each rebuild.
250
+ let asksMap = {};
251
+ const renderPanels = () => {
252
+ const { managers: mLines, reports: rLines } = buildCanvasPanelLines(nodeId, asksMap, pal);
253
+ managers.clear();
254
+ for (const l of mLines)
255
+ managers.addChild(new Text(l, 1, 0));
256
+ reports.clear();
257
+ for (const l of rLines)
258
+ reports.addChild(new Text(l, 1, 0));
259
+ tui.requestRender();
260
+ };
261
+ // The alt+g GRAPH overlay (Feature 3) — a bounded, centered modal navigator
262
+ // over the same nav-model graph, reading the live asksMap.
263
+ const graphOverlay = new GraphOverlay(tui, nodeId, () => asksMap, pal);
264
+ // Refresh all canvas chrome (panels + overlay if open) from canvas.db.
265
+ const refreshChrome = () => {
266
+ renderPanels();
267
+ graphOverlay.refresh();
268
+ };
269
+ // Git context painted into the RIGHT of the editor's top border: cwd leaf ·
270
+ // branch · status symbols (● dirty, ⇡ ahead, ⇣ behind). Refreshed by the poll.
271
+ const renderGitInfo = (info) => {
272
+ const parts = [pal.muted(info.dir)];
273
+ if (info.branch)
274
+ parts.push(pal.info(`⎇ ${info.branch}`));
275
+ const sym = [];
276
+ if (info.dirty)
277
+ sym.push(pal.warning('●'));
278
+ if (info.ahead > 0)
279
+ sym.push(pal.active(`⇡${info.ahead}`));
280
+ if (info.behind > 0)
281
+ sym.push(pal.active(`⇣${info.behind}`));
282
+ if (sym.length > 0)
283
+ parts.push(sym.join(''));
284
+ // A trailing space keeps the symbols off the very last border cell.
285
+ editor.info = ` ${parts.join(pal.muted(' · '))} `;
286
+ tui.requestRender();
287
+ };
288
+ const pollGitInfo = () => fetchGitInfo(meta.cwd, renderGitInfo);
289
+ // Queued steering/follow-up messages, shown above the editor so you can SEE
290
+ // what is waiting (not just the footer count). Texts arrive on `queue_update`;
291
+ // the welcome snapshot carries only a count, so the panel fills from the first
292
+ // queue change after attach.
293
+ let queuedMessages = [];
294
+ const renderQueued = () => {
295
+ queued.clear();
296
+ if (queuedMessages.length > 0) {
297
+ const span = Math.max(1, (process.stdout.columns ?? 80) - 4);
298
+ queued.addChild(new Text(pal.muted(`queued (${queuedMessages.length}):`), 1, 0));
299
+ for (const msg of queuedMessages) {
300
+ const oneLine = msg.replace(/\s+/g, ' ').trim();
301
+ queued.addChild(new Text(pal.faint(` ⋯ ${truncateToWidth(oneLine, span, '…')}`), 1, 0));
302
+ }
303
+ }
304
+ tui.requestRender();
305
+ };
306
+ // 5. Input layer (T6). onCommand/onDialogResponse → socket; onNotice → footer;
307
+ // nodeId/onGraph feed the slash context (/promote targets this node, /graph
308
+ // toggles the overlay); onRequest is the correlated read-op channel the
309
+ // native pickers ride on.
141
310
  const input = new InputController(tui, editor, km, {
142
311
  onCommand: (frame) => socket.send(frame),
143
312
  onDialogResponse: (resp) => socket.send(resp),
144
313
  onNotice: (msg) => setNotice(msg),
314
+ nodeId,
315
+ onGraph: () => graphOverlay.toggle(),
316
+ // `/quit` — detach this viewer (the engine keeps running). Must tear down
317
+ // locally; a bare `bye` would just trigger the reconnect loop.
318
+ onQuit: () => teardown('detach'),
319
+ // `/copy` — last assistant message → system clipboard.
320
+ onCopy: () => copyLastAssistant(),
321
+ // Global display toggles (Ctrl+O tools / Ctrl+T thinking) — pure render state
322
+ // owned by ChatView, surfaced as a footer notice mirroring pi's status line.
323
+ onToggleToolsExpand: () => setNotice(chatView.toggleToolsExpanded() ? 'Tool output: expanded' : 'Tool output: collapsed'),
324
+ onToggleThinking: () => setNotice(chatView.toggleThinking() ? 'Thinking blocks: hidden' : 'Thinking blocks: visible'),
325
+ // Wire the correlated read-op channel so the native pickers (/model,
326
+ // /resume, /fork, /tree, /settings, /scoped-models) can fetch their payloads.
327
+ // Without this every picker degrades to the "isn't available in this viewer"
328
+ // notice (see slash-commands.ts + InputController.openPicker).
329
+ onRequest: (frame) => socket.request(frame),
330
+ // Mount the native pickers inline in the chrome stack, directly under the
331
+ // editor (queued-panel pattern) — not as a floating centered modal — so a
332
+ // picker reads as part of the editor, like pi's own selectors.
333
+ onMountPicker: (component) => {
334
+ pickerPanel.addChild(component);
335
+ tui.requestRender();
336
+ },
337
+ onUnmountPicker: () => {
338
+ pickerPanel.clear();
339
+ tui.requestRender();
340
+ },
341
+ // Auth pickers — viewer-local OAuth/API-key flows. Auth operations target the
342
+ // SAME auth.json the broker uses (broker + viewer share a filesystem in the
343
+ // tmux-local attach model), so these run entirely in the viewer process and
344
+ // then notify the broker to reload via a reload_auth frame.
345
+ openLoginPicker: () => {
346
+ const dir = brokerAgentDir ?? defaultAgentDir();
347
+ input.openAuthPicker((controls) => buildLoginPicker(tui, dir, () => socket.send({ type: 'reload_auth' }), (m) => setNotice(m), controls));
348
+ },
349
+ openLogoutPicker: () => {
350
+ const dir = brokerAgentDir ?? defaultAgentDir();
351
+ input.openAuthPicker((controls) => buildLogoutPicker(dir, () => socket.send({ type: 'reload_auth' }), (m) => setNotice(m), controls.close));
352
+ },
145
353
  });
354
+ // `/copy` — copy the last assistant message (held by ChatView, kept current
355
+ // from both the welcome snapshot and the live stream) to the system clipboard.
356
+ const copyLastAssistant = () => {
357
+ const text = chatView.getLastAssistantText();
358
+ if (!text) {
359
+ setNotice('Nothing to copy — no assistant message yet');
360
+ return;
361
+ }
362
+ setNotice(writeClipboardText(text) ? 'Copied last message to clipboard' : 'Copy failed — no clipboard tool found');
363
+ };
146
364
  // Feed fresh state into the input controller AND the footer. Steer-vs-prompt
147
365
  // routing (T6) reads `state.isStreaming`, so this must stay current — the
148
366
  // broker only ships full state in `welcome`, so we patch isStreaming/name/etc
@@ -150,7 +368,18 @@ async function runAttach(nodeId, observer) {
150
368
  const setLiveState = (state) => {
151
369
  liveState = state;
152
370
  input.setState(state);
371
+ // The session name lives in the editor's top border (solid chip); the border
372
+ // color tracks the agent's thinking level. Both follow live state.
373
+ editor.title = state.sessionName ? `⬢ ${state.sessionName}` : '';
374
+ editor.borderColor = thinkingBorderColor(state.thinkingLevel, pal.border);
375
+ // Title chip background = the same thinking-level color (bold white text), so
376
+ // the name chip and the border rule read as one hue.
377
+ editor.titleStyle = thinkingTitleStyle(state.thinkingLevel, defaultTitleStyle);
153
378
  renderFooter();
379
+ // Panel liveness rides the relayed event stream (agent_start/agent_end/
380
+ // queue_update/session_info_changed all patch state through here) plus the
381
+ // low-rate poll below for changes that emit no event (child status flips).
382
+ refreshChrome();
154
383
  };
155
384
  const patchState = (patch) => {
156
385
  if (liveState === undefined)
@@ -162,8 +391,16 @@ async function runAttach(nodeId, observer) {
162
391
  switch (frame.type) {
163
392
  case 'welcome': {
164
393
  role = frame.role;
394
+ brokerAgentDir = frame.agentDir;
165
395
  chatView.applySnapshot(frame.snapshot);
396
+ // Seed the footer's context indicator from the catch-up snapshot.
397
+ const cu = frame.snapshot.stats.contextUsage;
398
+ if (cu && cu.tokens != null)
399
+ contextTokens = cu.tokens;
166
400
  setLiveState(frame.snapshot.state);
401
+ // Ask the broker for its merged command list (engine/extension/skill
402
+ // commands) to enrich slash autocomplete beyond the builtins+canvas set.
403
+ socket.send({ type: 'get_commands' });
167
404
  if (frame.pending_dialog != null)
168
405
  input.attachDialog(frame.pending_dialog);
169
406
  break;
@@ -175,6 +412,12 @@ async function runAttach(nodeId, observer) {
175
412
  renderFooter();
176
413
  break;
177
414
  }
415
+ case 'model_changed': {
416
+ // The broker's own post-set_model/cycle_model announcement (pi emits no
417
+ // engine event for a model switch) — keeps the footer's model current.
418
+ patchState({ model: frame.model });
419
+ break;
420
+ }
178
421
  case 'error': {
179
422
  if (frame.code === 'not_controller') {
180
423
  setNotice('read-only — another viewer is the controller');
@@ -185,13 +428,51 @@ async function runAttach(nodeId, observer) {
185
428
  break;
186
429
  }
187
430
  case 'ack': {
188
- // Dynamic-command autocomplete (feeding get_commands into T6's palette)
189
- // is DEFERRED for Phase 4 (would require editing T6's input-controller —
190
- // out of my 3-file scope; the fall-through-to-prompt path already makes
191
- // extension commands functional). Surface only command failures here.
431
+ // get_commands replies with the broker's merged command list as JSON in
432
+ // `detail` rebuild the autocomplete provider with it (slashCommandList
433
+ // re-appends the native canvas commands, deduped).
434
+ if (frame.for === 'get_commands' && frame.ok && frame.detail) {
435
+ try {
436
+ const list = JSON.parse(frame.detail);
437
+ setCommands(list);
438
+ }
439
+ catch {
440
+ /* malformed — keep the current provider */
441
+ }
442
+ break;
443
+ }
444
+ // navigate_tree's detail is the navigated-to user message's text (pi
445
+ // parity: the tree navigator restores it to the editor for re-editing);
446
+ // the rewound transcript itself arrives via the broker's re-welcome.
447
+ if (frame.for === 'navigate_tree' && frame.ok) {
448
+ if (frame.detail) {
449
+ editor.setText(frame.detail);
450
+ tui.requestRender();
451
+ }
452
+ break;
453
+ }
454
+ if (frame.for === 'share') {
455
+ if (frame.ok && frame.detail) {
456
+ setNotice(`Shared: ${frame.detail}`);
457
+ writeClipboardText(frame.detail);
458
+ }
459
+ else if (!frame.ok) {
460
+ setNotice(`share failed: ${frame.detail ?? 'unknown error'}`);
461
+ }
462
+ break;
463
+ }
464
+ if (frame.for === 'clone' && !frame.ok) {
465
+ setNotice(`clone failed: ${frame.detail ?? 'unknown error'}`);
466
+ break;
467
+ }
192
468
  if (!frame.ok) {
193
469
  setNotice(`command failed: ${frame.for}${frame.detail ? ` — ${frame.detail}` : ''}`);
470
+ break;
194
471
  }
472
+ // Confirm commands whose effect is otherwise invisible — without this a
473
+ // `/reload` looks like it did nothing (the engine reloads silently).
474
+ if (frame.for === 'reload')
475
+ setNotice('Reloaded keybindings, extensions, skills, prompts, and themes');
195
476
  break;
196
477
  }
197
478
  case 'extension_ui_request': {
@@ -202,16 +483,27 @@ async function runAttach(nodeId, observer) {
202
483
  // A relayed AgentSessionEvent — render it, and keep local state fresh.
203
484
  const event = frame;
204
485
  chatView.handleEvent(event);
486
+ // The most recent assistant turn's prompt tokens ARE the live context
487
+ // size — read straight off the relayed message, no broker round-trip.
488
+ if (event.type === 'message_end' && event.message.role === 'assistant') {
489
+ const u = event.message.usage;
490
+ contextTokens = u.input + u.cacheRead + u.cacheWrite;
491
+ renderFooter();
492
+ }
205
493
  if (event.type === 'agent_start')
206
494
  patchState({ isStreaming: true });
207
495
  else if (event.type === 'agent_end')
208
496
  patchState({ isStreaming: false });
209
497
  else if (event.type === 'session_info_changed')
210
498
  patchState({ sessionName: event.name });
211
- else if (event.type === 'thinking_level_changed')
499
+ else if (event.type === 'thinking_level_changed') {
212
500
  patchState({ thinkingLevel: event.level });
501
+ setNotice(`Thinking level: ${event.level}`);
502
+ }
213
503
  else if (event.type === 'queue_update') {
214
- patchState({ pendingMessageCount: event.steering.length + event.followUp.length });
504
+ queuedMessages = [...event.steering, ...event.followUp];
505
+ renderQueued();
506
+ patchState({ pendingMessageCount: queuedMessages.length });
215
507
  }
216
508
  break;
217
509
  }
@@ -239,10 +531,16 @@ async function runAttach(nodeId, observer) {
239
531
  const done = new Promise((r) => {
240
532
  resolveDone = r;
241
533
  });
534
+ let asksTimer;
242
535
  const teardown = (reason) => {
243
536
  if (tornDown)
244
537
  return;
245
538
  tornDown = true;
539
+ if (asksTimer !== undefined)
540
+ clearInterval(asksTimer);
541
+ if (noticeTimer !== undefined)
542
+ clearTimeout(noticeTimer);
543
+ graphOverlay.close();
246
544
  clearPaneTag();
247
545
  removeKeyListener();
248
546
  // A clean detach tells the broker to drop this viewer (the engine runs on);
@@ -264,6 +562,60 @@ async function runAttach(nodeId, observer) {
264
562
  }
265
563
  resolveDone();
266
564
  };
565
+ // The handshake the viewer sends on connect AND on every successful redial.
566
+ // After a broker refresh / crash-revive the fresh process has no controller and
567
+ // no memory of us, so re-`hello` reclaims our role and the `welcome` snapshot
568
+ // repaints full scrollback/state. Sends the CURRENT `role`, not the constructor
569
+ // `observer` flag: if we were preempted to observer at runtime (a
570
+ // `control_changed` demoted us), re-hello'ing as 'controller' would let us
571
+ // reclaim control from the new controller after a refresh — so the demotion
572
+ // must survive the reconnect.
573
+ const sendHello = () => {
574
+ socket.send({
575
+ type: 'hello',
576
+ role,
577
+ client_id: clientId,
578
+ term: { cols: process.stdout.columns ?? 80, rows: process.stdout.rows ?? 24 },
579
+ });
580
+ };
581
+ // Reconnect supervisor. A resident broker node that yields EXITS its broker
582
+ // (socket close) and is revived FRESH by the daemon on the SAME `view.sock`
583
+ // path within a few seconds. So on close we HOLD the pane and re-dial while
584
+ // the canvas-db row says the node is still alive (status active/idle), giving
585
+ // up only when it is genuinely terminal (done/dead/canceled or row gone) or a
586
+ // bounded ~30s of failed redials elapses (stale-row safety). A successful
587
+ // redial re-sends `hello`; the new broker's `welcome` runs applySnapshot,
588
+ // which resetChat()s the pane and rebuilds from the persisted .jsonl — a clean
589
+ // "clear + continuation" for free.
590
+ const RECONNECT_GIVEUP_MS = 30_000;
591
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
592
+ let reconnecting = false;
593
+ const attemptReconnect = async () => {
594
+ reconnecting = true;
595
+ setNotice('↻ node refreshing — reconnecting…', { sticky: true });
596
+ const deadline = Date.now() + RECONNECT_GIVEUP_MS;
597
+ let delay = 200;
598
+ while (!tornDown && Date.now() < deadline) {
599
+ if (reconnectShouldGiveUp(getRow(nodeId))) {
600
+ break; // genuinely gone → fall through to broker-gone
601
+ }
602
+ try {
603
+ await socket.redial();
604
+ reconnecting = false;
605
+ sendHello();
606
+ setNotice('');
607
+ return; // reconnected; the welcome snapshot clears + continues the pane
608
+ }
609
+ catch {
610
+ // ENOENT (socket not recreated yet) / ECONNREFUSED (listen not ready) → retry.
611
+ await sleep(Math.min(delay, 1000));
612
+ delay *= 2;
613
+ }
614
+ }
615
+ reconnecting = false;
616
+ if (!tornDown)
617
+ teardown('broker-gone');
618
+ };
267
619
  // Permanent socket wiring (the connect-race listeners are gone).
268
620
  socket.on('frame', handleBrokerFrame);
269
621
  socket.on('error', (err) => {
@@ -271,15 +623,44 @@ async function runAttach(nodeId, observer) {
271
623
  // broker-gone message prefers this over the generic text.
272
624
  lastSocketError = err.message;
273
625
  });
274
- socket.on('close', () => teardown('broker-gone'));
275
- // ctrl+c / ctrl+d → detach (T6 left these unwired; lifecycle is ours). A
276
- // global TUI input listener fires BEFORE the focused component (and before any
277
- // dialog overlay), so detach is unconditional and consistent.
626
+ socket.on('close', () => {
627
+ if (tornDown)
628
+ return; // a real detach/signal already won
629
+ if (reconnecting)
630
+ return; // the dead socket's own close during the redial loop
631
+ void attemptReconnect();
632
+ });
633
+ // ctrl+d → detach immediately (lifecycle is ours). ctrl+c is gentler: a single
634
+ // tap CLEARS the editor (and arms a window); a second tap within that window
635
+ // detaches. A global TUI input listener fires BEFORE the focused component (and
636
+ // before any dialog overlay), so this is consistent regardless of focus.
637
+ const CTRL_C_DETACH_MS = 1000;
638
+ let lastCtrlCAt = 0;
278
639
  const removeKeyListener = tui.addInputListener((data) => {
279
- if (matchesKey(data, 'ctrl+c') || matchesKey(data, 'ctrl+d')) {
640
+ if (matchesKey(data, 'ctrl+d')) {
280
641
  teardown('detach');
281
642
  return { consume: true };
282
643
  }
644
+ if (matchesKey(data, 'ctrl+c')) {
645
+ const now = Date.now();
646
+ if (now - lastCtrlCAt < CTRL_C_DETACH_MS) {
647
+ lastCtrlCAt = 0;
648
+ teardown('detach');
649
+ return { consume: true };
650
+ }
651
+ lastCtrlCAt = now;
652
+ editor.setText('');
653
+ setNotice('Editor cleared — press Ctrl+C again to detach');
654
+ tui.requestRender();
655
+ return { consume: true };
656
+ }
657
+ // alt+g toggles the GRAPH overlay. The global listener fires BEFORE the
658
+ // focused component, so this works whether the editor or the overlay holds
659
+ // focus (closing it from inside the overlay too).
660
+ if (matchesKey(data, 'alt+g')) {
661
+ graphOverlay.toggle();
662
+ return { consume: true };
663
+ }
283
664
  return undefined;
284
665
  });
285
666
  // Pane kill / daemon teardown arrives as a signal (raw mode swallows ctrl+c as
@@ -291,17 +672,40 @@ async function runAttach(nodeId, observer) {
291
672
  // 8. Lay out, focus the editor, start, then handshake. Starting before the
292
673
  // handshake means the welcome's applySnapshot renders into a live TUI.
293
674
  tui.addChild(chatContainer);
294
- tui.addChild(footer);
675
+ tui.addChild(chrome);
676
+ tui.addChild(managers);
677
+ tui.addChild(queued);
295
678
  tui.addChild(editor);
679
+ tui.addChild(pickerPanel);
680
+ tui.addChild(reports);
681
+ tui.addChild(footer);
296
682
  tui.setFocus(editor);
297
683
  renderFooter();
684
+ renderPanels();
685
+ renderQueued();
686
+ pollGitInfo();
298
687
  tui.start();
299
- socket.send({
300
- type: 'hello',
301
- role: observer ? 'observer' : 'controller',
302
- client_id: clientId,
303
- term: { cols: process.stdout.columns ?? 80, rows: process.stdout.rows ?? 24 },
304
- });
688
+ // ProcessTerminal negotiated the keyboard protocol on OUR pi-tui instance; the
689
+ // editor reads the flag from its own (possibly separate) instance, so mirror it.
690
+ void mirrorKittyProtocolToEditor(procTerminal.kittyProtocolActive);
691
+ sendHello();
692
+ // Low-rate canvas-chrome refresh: repaint panels/overlay from canvas.db (child
693
+ // status flips emit no broker event) and re-poll the ask map asynchronously.
694
+ const ASKS_POLL_MS = 5_000;
695
+ const pollChrome = () => {
696
+ refreshChrome();
697
+ pollGitInfo();
698
+ fetchAsksAsync(climbRoot(nodeId), (counts) => {
699
+ if (JSON.stringify(counts) !== JSON.stringify(asksMap)) {
700
+ asksMap = counts;
701
+ refreshChrome();
702
+ }
703
+ });
704
+ };
705
+ pollChrome();
706
+ asksTimer = setInterval(pollChrome, ASKS_POLL_MS);
707
+ if (typeof asksTimer.unref === 'function')
708
+ asksTimer.unref();
305
709
  await done;
306
710
  }
307
711
  // ---------------------------------------------------------------------------
@@ -0,0 +1,9 @@
1
+ import type { TUI } from '@earendil-works/pi-tui';
2
+ import type { Picker, PickerControls } from './pickers.js';
3
+ /** `/login` — show the provider selector, then run pi's native login flow in
4
+ * the viewer. On success, notifies the broker to reload auth via reload_auth.
5
+ * Returns a Picker (OAuthSelectorComponent as the overlay). */
6
+ export declare function buildLoginPicker(tui: TUI, agentDir: string, onAuthReloaded: () => void, notify: (msg: string) => void, controls: PickerControls): Picker;
7
+ /** `/logout` — show the provider selector in logout mode. On select, calls
8
+ * authStorage.logout(providerId) directly, then notifies the broker to reload. */
9
+ export declare function buildLogoutPicker(agentDir: string, onAuthReloaded: () => void, notify: (msg: string) => void, close: () => void): Picker;