@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
@@ -2,31 +2,40 @@
2
2
  // a running pi process on the canvas. Composes canvas (birth + spine), persona
3
3
  // (resolve), launch (pi argv), and tmux (placement).
4
4
  //
5
- // bootRoot — the user-opened front door (bare `crtr`). Resident; runs pi
6
- // inline, taking over the current terminal.
5
+ // bootRoot — the user-opened front door (bare `crtr`). Resident; launches the
6
+ // root's broker engine, then execs `crtr attach` inline so THIS
7
+ // terminal becomes the broker's controller-viewer.
7
8
  // spawnChild — a node spawned by a live node (`crtr node new`). A managed,
8
9
  // terminal background worker by default; with `root`, an
9
10
  // INDEPENDENT resident root (no subscription back to the spawner,
10
11
  // provenance via spawned_by) brought forefront for direct driving.
11
12
  import { spawnSync } from 'node:child_process';
12
- import { join } from 'node:path';
13
- import { FRONT_DOOR_ENV } from './front-door.js';
14
- import { readConfig } from '../config.js';
15
- import { jobDir } from '../canvas/paths.js';
16
- import { spawnNode, currentNodeContext, resolveBirthSession, nodeSession, rootOfSpine } from './nodes.js';
13
+ import { readdirSync, existsSync } from 'node:fs';
14
+ import { isAbsolute, resolve, join } from 'node:path';
15
+ import { homedir } from 'node:os';
16
+ import { spawnNode, currentNodeContext, rootOfSpine } from './nodes.js';
17
17
  import { buildLaunchSpec, buildPiArgv } from './launch.js';
18
18
  import { writeGoal } from './kickoff.js';
19
19
  import { hasRoadmap, seedRoadmap } from './roadmap.js';
20
20
  import { buildIdentityAssertion, buildWakeBearings } from './bearings.js';
21
21
  import { installMenuBinding, installNavBindings, installViewNavBindings } from './tmux-chrome.js';
22
- import { setPresence, updateNode, getNode, fullName } from '../canvas/index.js';
23
- import { registerRootFocus, ensureSession, openNodeWindow, piCommand, currentTmux, inTmux, focusWindow, waitForBrokerViewSocket, } from './placement.js';
22
+ import { getNode, fullName } from '../canvas/index.js';
23
+ import { registerViewerFocus, openViewerWindow, waitForBrokerViewSocket, viewerSplitEnv, windowOfPane, currentTmux, inTmux, focusWindow, } from './placement.js';
24
24
  import { transition } from './lifecycle.js';
25
25
  import { headlessBrokerHost } from './host.js';
26
26
  import { ensureDaemon } from '../../daemon/manage.js';
27
- /** Create a root node and bring up its pi. Returns the node; for 'inline' this
28
- * only returns after pi exits (it took over the terminal). */
27
+ /** Create the front-door root: launch its broker engine, then exec `crtr attach`
28
+ * inline so THIS terminal becomes the broker's controller-viewer. Does not
29
+ * return — it process.exit()s when the inline attach exits (detach leaves the
30
+ * resident broker running, to be reconnected later). */
29
31
  export function bootRoot(opts) {
32
+ // crtr is tmux-only: the front door execs `crtr attach` inline in THIS pane to
33
+ // become the root's controller-viewer, so there must be a tmux pane to anchor
34
+ // on. Outside tmux there is no viewer surface — throw with a friendly message
35
+ // rather than boot a root nobody can see.
36
+ if (!inTmux()) {
37
+ throw new Error('crtr must be started from inside a tmux session — start tmux first (e.g. `tmux new -s work`), then run `crtr` there.');
38
+ }
30
39
  // The thin supervisor must be up before any node exists, so a refresh-yield
31
40
  // or crash can be reaped/revived. Idempotent.
32
41
  try {
@@ -34,10 +43,6 @@ export function bootRoot(opts) {
34
43
  }
35
44
  catch { /* daemon is best-effort */ }
36
45
  const kind = opts.kind ?? 'general';
37
- // Host precedence: explicit --headless/--no-headless flag > config `headless`
38
- // default > tmux. Mirrors `crtr node new`'s resolution (commands/node.ts).
39
- const headless = opts.headless ?? readConfig('user').headless === true;
40
- const hostKind = headless ? 'broker' : 'tmux';
41
46
  // A born-resident root starts in base mode; it earns the orchestrator persona
42
47
  // the first time it delegates (or on promotion). Resident lifecycle either way.
43
48
  const { launch } = buildLaunchSpec(kind, 'base', { lifecycle: 'resident', hasManager: false });
@@ -52,135 +57,140 @@ export function bootRoot(opts) {
52
57
  cwd: opts.cwd,
53
58
  name: opts.name ?? kind,
54
59
  parent: null,
55
- hostKind,
56
60
  launch,
57
61
  });
58
62
  // Persist the spawning prompt as the goal so a fresh revive can re-read its
59
63
  // mandate (bare `crtr` has none — writeGoal no-ops on empty).
60
64
  if (opts.prompt !== undefined)
61
65
  writeGoal(meta.node_id, opts.prompt);
62
- // Every node window — root or child — lives in the one shared session.
63
- const session = nodeSession();
64
- ensureSession(session, opts.cwd);
65
66
  // Make the Alt+C action menu + Alt+] / Alt+[ node-nav keys + Alt+V then ]/[
66
- // view-nav chord live on this server (idempotent, in-tmux only).
67
- if (inTmux()) {
68
- try {
69
- installMenuBinding();
70
- }
71
- catch { /* best-effort */ }
72
- try {
73
- installNavBindings();
74
- }
75
- catch { /* best-effort */ }
76
- try {
77
- installViewNavBindings();
78
- }
79
- catch { /* best-effort */ }
67
+ // view-nav chord live on this server (idempotent; guaranteed in tmux above).
68
+ try {
69
+ installMenuBinding();
80
70
  }
81
- // HEADLESS front door: a broker root has NO engine pane — headlessBrokerHost
82
- // launches a DETACHED broker that hosts the single engine + binds view.sock,
83
- // opening no tmux window. So instead of exec'ing pi inline we boot the broker,
84
- // wait for its socket, and auto-attach a VIEWER into THIS pane (the same
85
- // `crtr attach` viewer focusBroker splits beside a caller). Presence stays
86
- // null and NO focus row is registered — the viewer pane self-tags @crtr_node
87
- // on connect, the handle node cycle/close/lifecycle resolve a broker by.
88
- if (meta.host_kind === 'broker')
89
- return bootRootBroker(meta, session, opts);
90
- // inline: the root's pi takes over THIS terminal, so its own window stays
91
- // where the user is (its tmux_session tracks that real pane so supervision
92
- // sees it alive). But its children spawn into the shared global session via
93
- // CRTR_ROOT_SESSION they never clutter the user's working session.
71
+ catch { /* best-effort */ }
72
+ try {
73
+ installNavBindings();
74
+ }
75
+ catch { /* best-effort */ }
76
+ try {
77
+ installViewNavBindings();
78
+ }
79
+ catch { /* best-effort */ }
80
+ // This terminal becomes the root's attach VIEWER in the user's CURRENT session,
81
+ // so its window stays where the user already is. The node row carries NO
82
+ // presence a broker node's pane/window/tmux_session stay NULL; the viewer
83
+ // pane lives in the focuses table (below).
94
84
  const here = currentTmux();
95
- const adopted = resolveBirthSession({ adoptCaller: true, here, rootSession: undefined });
96
- setPresence(meta.node_id, { tmux_session: adopted, window: here?.window ?? null, pane: here?.pane ?? null });
97
- // REVIVE-HOME: the inline root's durable revive target is the session it
98
- // adopts (the caller's when inside tmux, else the shared backstage). Set once
99
- // at birth, alongside the live LOCATION above.
100
- updateNode(meta.node_id, { home_session: adopted });
101
- // Root boot registers focus #1 (§2.6): the FOREGROUND inline root owns the
102
- // user's viewport, so its OWN pane becomes a durable focus (remain-on-exit so
103
- // a clean exit freezes rather than detaching the terminal). A background
104
- // `--root` (spawnChild) does NOT — it stays a plain window until the user
105
- // focuses it (§6). Only possible inside tmux (a pane to anchor on).
106
- if (here) {
85
+ // The FOREGROUND front-door terminal is the root's one viewer: record its pane
86
+ // as the node's viewer focus row so focus/nav can find it. The `crtr attach`
87
+ // exec below makes the terminal a controller-viewer of the broker; detaching
88
+ // leaves the resident broker running.
89
+ if (here !== null) {
107
90
  try {
108
- registerRootFocus(meta.node_id, here.pane, adopted, here.window);
91
+ registerViewerFocus(meta.node_id, here.pane, here.session, here.window);
109
92
  }
110
93
  catch { /* best-effort */ }
111
94
  }
112
- const withSession = getNode(meta.node_id);
113
- const inv = buildPiArgv(withSession, { prompt: opts.prompt });
114
- const env = { ...process.env, ...inv.env, CRTR_ROOT_SESSION: session, CRTR_SUBTREE: rootOfSpine(meta.node_id), [FRONT_DOOR_ENV]: '1' };
115
- const r = spawnSync('pi', inv.argv, { stdio: 'inherit', env });
95
+ const inv = buildPiArgv(meta, { prompt: opts.prompt });
96
+ // Broker is the only host: the root runs as a detached broker engine (so its
97
+ // first refresh/revive doesn't strand this terminal the daemon revives the
98
+ // broker and the viewer reconnects), and THIS terminal execs `crtr attach`
99
+ // inline to become its controller-viewer. The host sets CRTR_FRONT_DOOR.
100
+ inv.env = { ...inv.env, CRTR_SUBTREE: rootOfSpine(meta.node_id) };
101
+ const placed = headlessBrokerHost.launch(meta.node_id, inv, {
102
+ cwd: opts.cwd,
103
+ name: fullName(meta),
104
+ resuming: false,
105
+ });
106
+ // No broker pid ⇒ the root has no engine. Crash it (so the daemon won't watch a
107
+ // zombie 'active') and throw — there is nothing to attach to.
108
+ if (placed.pid === null) {
109
+ transition(meta.node_id, 'crash');
110
+ throw new Error(`failed to launch the broker engine for the front-door root ${meta.node_id} — nothing to attach to.`);
111
+ }
112
+ // The terminal has nothing to attach to until view.sock accepts. The root is
113
+ // useless without its engine, so a socket timeout IS fatal here (unlike a
114
+ // background child's optional viewer, which is skipped on timeout).
115
+ if (!waitForBrokerViewSocket(meta.node_id)) {
116
+ throw new Error(`the front-door root broker ${meta.node_id} never bound its view socket — cannot attach.`);
117
+ }
118
+ const attachEnv = { ...process.env, ...viewerSplitEnv() };
119
+ const r = spawnSync('crtr', ['attach', 'to', meta.node_id], { stdio: 'inherit', env: attachEnv });
116
120
  process.exit(r.status ?? 0);
117
121
  }
118
- /** The headless front door (host_kind==='broker'): launch a detached broker for
119
- * this root, wait for its view.sock, then auto-attach a viewer INLINE in this
120
- * pane. Mirrors placement.focusBroker (ensure-broker wait-socket viewer)
121
- * but the viewer runs in the CURRENT pane (the front door owns it) rather than
122
- * a split beside a caller. Never returns it exits with the viewer's status. */
123
- function bootRootBroker(meta, session, opts) {
124
- // The shared backstage this subtree's descendants flow into (CRTR_ROOT_SESSION
125
- // below). Mirrors the inline path; for a broker it is also the (moot) revive
126
- // home broker revive launches a detached process, ignoring the session.
127
- updateNode(meta.node_id, { home_session: session });
128
- const withSession = getNode(meta.node_id);
129
- const inv = buildPiArgv(withSession, { prompt: opts.prompt });
130
- // Authoritative backstage routing on the ENGINE's env (the broker host merges
131
- // inv.env into the detached broker process), mirroring the inline pi env.
132
- inv.env = { ...inv.env, CRTR_ROOT_SESSION: session, CRTR_SUBTREE: rootOfSpine(meta.node_id) };
133
- // Launch the detached broker (it records its own pid as pi_pid via the
134
- // stophook and binds view.sock). Returns placement all-null no tmux window.
135
- headlessBrokerHost.launch(meta.node_id, inv, { cwd: meta.cwd, name: fullName(meta), resuming: false });
136
- // Close the cold-start race: the broker records its pid during extension bind,
137
- // then opens view.sock LATER; attach connects once and exits "no broker" if we
138
- // run it before listen(). Probe for an ACCEPTING socket before attaching. A
139
- // timeout here does NOT mean the broker died — the engine is a resident root
140
- // that keeps booting; we just can't attach yet, so point the user at re-focus.
141
- if (!waitForBrokerViewSocket(meta.node_id)) {
142
- process.stderr.write(`crtr: ${meta.node_id} is still starting its broker — it is running, but its viewer socket did not come up in time. ` +
143
- `Re-focus the node to attach (\`crtr node focus ${meta.node_id}\`); see ${join(jobDir(meta.node_id), 'broker.log')} if it never appears.\n`);
144
- process.exit(1);
122
+ /** pi's sessions root, VENDORED from pi `config.getSessionsDir()` (= `<agentDir>/
123
+ * sessions`). pi's package `exports` map is `.`-only, so config.js can't be
124
+ * deep-imported, and a ROOT import of `getAgentDir` would eager-load the entire
125
+ * heavy pi SDK index on crtr's front-door hot path (the reason broker-sdk.ts
126
+ * dynamic-imports the engine). Mirrors pi: `PI_CODING_AGENT_DIR` env, else
127
+ * `~/.pi/agent` (APP_NAME defaults to 'pi'). Re-sync on a pi SDK bump that moves
128
+ * the sessions dir same vendoring rationale as pi-vendored.ts. */
129
+ function piSessionsRoot() {
130
+ const env = process.env['PI_CODING_AGENT_DIR'];
131
+ const agentDir = env !== undefined && env !== '' ? env : join(homedir(), '.pi', 'agent');
132
+ return join(agentDir, 'sessions');
133
+ }
134
+ /** Resolve a bare/partial pi session uuid to its ABSOLUTE `.jsonl` path by
135
+ * scanning pi's sessions store the way pi's own CLI does (exact id, else a
136
+ * unique prefix). The broker fork seam (`SessionManager.forkFrom`) loads a FILE
137
+ * and does NOT resolve a uuid, so a bare id must be resolved here or the fork
138
+ * throws at boot. Session files are `<sessions>/<project>/<ts>_<id>.jsonl`. */
139
+ function resolveSessionUuid(uuid) {
140
+ const sessionsRoot = piSessionsRoot();
141
+ if (!existsSync(sessionsRoot)) {
142
+ throw new Error(`--fork-from '${uuid}': no pi sessions store at ${sessionsRoot} to resolve it from pass a node id or an absolute .jsonl path.`);
145
143
  }
146
- // Auto-attach the VIEWER inline: re-invoke OUR OWN cli (process.execPath +
147
- // its own execArgv + argv[1]) as `attach to <id>` so it needs no PATH `crtr`
148
- // AND survives a loader-based launch (dev `tsx`/`--import` flags ride
149
- // execArgv); stdio:'inherit' hands this pane to the viewer's raw-mode TUI.
150
- // runAttach self-tags the pane @crtr_node on connect; ctrl+c/ctrl+d detaches
151
- // and the broker engine runs on.
152
- const entry = process.argv[1];
153
- if (entry === undefined) {
154
- process.stderr.write(`crtr: cannot resolve the crtr entrypoint to auto-attach; run \`crtr node focus ${meta.node_id}\`.\n`);
155
- process.exit(1);
144
+ const matches = [];
145
+ for (const dir of readdirSync(sessionsRoot, { withFileTypes: true })) {
146
+ if (!dir.isDirectory())
147
+ continue;
148
+ const projDir = join(sessionsRoot, dir.name);
149
+ for (const f of readdirSync(projDir)) {
150
+ if (!f.endsWith('.jsonl'))
151
+ continue;
152
+ const stem = f.slice(0, -'.jsonl'.length);
153
+ const id = stem.slice(stem.lastIndexOf('_') + 1);
154
+ matches.push({ id, path: join(projDir, f) });
155
+ }
156
156
  }
157
- const r = spawnSync(process.execPath, [...process.execArgv, entry, 'attach', 'to', meta.node_id], {
158
- stdio: 'inherit',
159
- });
160
- process.exit(r.status ?? 0);
157
+ const exact = matches.filter((m) => m.id === uuid);
158
+ const hit = exact.length > 0 ? exact : matches.filter((m) => m.id.startsWith(uuid));
159
+ if (hit.length === 0)
160
+ throw new Error(`--fork-from '${uuid}': no pi session with that id found.`);
161
+ if (hit.length > 1) {
162
+ throw new Error(`--fork-from '${uuid}': ambiguous — ${hit.length} sessions match that id prefix; pass the full id or the .jsonl path.`);
163
+ }
164
+ return hit[0].path;
161
165
  }
162
- /** Resolve a `--fork-from` value to the source pi gets as `--fork <path|id>`.
163
- * A live node id resolves to its captured session FILE (absolute, cwd-immune),
164
- * falling back to its bare session id; a path or partial uuid passes straight
165
- * through to pi. Throws when a known node has no session to fork yet. */
166
+ /** Resolve a `--fork-from` value to an ABSOLUTE `.jsonl` source path for the
167
+ * broker fork (`SessionManager.forkFrom`, which loads a file never a bare id).
168
+ * A live node id resolves to its captured session FILE (absolute, cwd-immune); a
169
+ * relative path is made absolute; a bare/partial uuid is resolved against pi's
170
+ * sessions store. Throws when a known node has no captured session file, or when
171
+ * a uuid resolves to zero or multiple sessions. */
166
172
  export function resolveForkSource(value) {
167
173
  const v = value.trim();
168
174
  if (v === '')
169
175
  throw new Error('--fork-from requires a node id, session file, or session uuid.');
170
- // A path (contains `/` or ends `.jsonl`) is a session file — hand it to pi as-is.
171
- if (v.includes('/') || v.endsWith('.jsonl'))
172
- return v;
173
- // A live node id fork from the conversation it has accumulated.
176
+ // A path (contains `/` or ends `.jsonl`) is a session file — ensure it is
177
+ // ABSOLUTE so the broker, launched in a different cwd, still resolves it.
178
+ if (v.includes('/') || v.endsWith('.jsonl')) {
179
+ return isAbsolute(v) ? v : resolve(process.cwd(), v);
180
+ }
181
+ // A live node id — fork from the conversation it has accumulated. Its captured
182
+ // session FILE is absolute; the broker cannot fork a bare session id.
174
183
  const n = getNode(v);
175
184
  if (n !== null) {
176
- const src = n.pi_session_file ?? n.pi_session_id;
185
+ const src = n.pi_session_file;
177
186
  if (src === undefined || src === null || src === '') {
178
- throw new Error(`node ${v} has no pi session yet — it has not started a conversation to fork from.`);
187
+ throw new Error(`node ${v} has no pi session yet — it has not captured a session FILE to fork from (the broker forks a .jsonl path, not a bare id).`);
179
188
  }
180
189
  return src;
181
190
  }
182
- // Not a known node — treat as a bare/partial pi session id for pi to resolve.
183
- return v;
191
+ // Not a known node — a bare/partial pi session uuid; resolve it to an absolute
192
+ // .jsonl (the broker fork won't resolve a uuid).
193
+ return resolveSessionUuid(v);
184
194
  }
185
195
  /** Resolve who a spawn is attributed to. A managed child needs a spine parent
186
196
  * (explicit `--parent` or the calling node's CRTR_NODE_ID). A --root spawn
@@ -237,7 +247,6 @@ export function spawnChild(opts) {
237
247
  // the boot intro can detect this is a fork and re-assert the node's own
238
248
  // identity over the source's copied-in conversation.
239
249
  forkFrom: opts.forkFrom,
240
- hostKind: opts.hostKind,
241
250
  modelOverride: opts.model,
242
251
  launch,
243
252
  });
@@ -271,90 +280,52 @@ export function spawnChild(opts) {
271
280
  }
272
281
  // (The three scoped long-term memory stores are seeded for EVERY node at birth
273
282
  // in spawnNode — no orchestrator-gated seeding needed here.)
274
- // A managed CHILD lands in the shared global session: inherited from the
275
- // parent's CRTR_ROOT_SESSION, else the default node session. A --root spawned
276
- // from inside tmux instead opens its window in the CALLER'S CURRENT session,
277
- // so it appears where the spawner is working rather than exiled to a separate
278
- // crtr session. Either way the root's OWN descendants still flow to the shared
279
- // session (childSession) via CRTR_ROOT_SESSION, to keep the subtree from
280
- // cluttering the user's session.
281
- const rootSessionEnv = process.env['CRTR_ROOT_SESSION'];
283
+ // A --root spawned from inside tmux opens its one viewer in the CALLER'S
284
+ // CURRENT session (below), so it appears where the spawner is working; a
285
+ // managed child is a headless broker with NO viewer at all.
282
286
  const here = root ? currentTmux() : null;
283
- // The shared backstage the whole subtree flows into (this child's own
284
- // CRTR_ROOT_SESSION): the inherited root session, else the default `crtr`.
285
- const childSession = resolveBirthSession({ adoptCaller: false, here, rootSession: rootSessionEnv });
286
- // Where THIS node's window opens — and its durable REVIVE-HOME. A managed
287
- // child lands in the backstage; a --root adopts the caller's current session
288
- // when inside tmux, so it appears where the spawner is working.
289
- const session = resolveBirthSession({ adoptCaller: root, here, rootSession: rootSessionEnv });
290
- ensureSession(session, opts.cwd);
291
- // REVIVE-HOME set once at birth: a managed child's revive target is the
292
- // backstage, never a user session — this is what keeps a background revive
293
- // off the user's screen (the focus taint cannot reach it).
294
- updateNode(meta.node_id, { home_session: session });
295
287
  const inv = buildPiArgv(meta, { prompt: kickoff, forkFrom });
296
- // Belt-and-suspenders backstage routing emitted at EVERY launch site: the
297
- // authoritative CRTR_ROOT_SESSION (this subtree's shared backstage) +
298
- // CRTR_SUBTREE (this node's spine root). Hoisted ABOVE the host branch so the
299
- // broker launch and the tmux command consume ONE authoritative env the
300
- // broker host merges inv.env, so a --headless --root broker would otherwise
301
- // inherit the SPAWNER's subtree id via the lossy nodeEnv passthrough (review
302
- // reuse MINOR-2). FRONT_DOOR is added per-consumer below (the broker host sets
303
- // it itself; the tmux env const adds it).
304
- inv.env = { ...inv.env, CRTR_ROOT_SESSION: childSession, CRTR_SUBTREE: rootOfSpine(meta.node_id) };
305
- // Birth LAUNCH branch on the PERSISTED host_kind (set at birth by spawnNode).
306
- // A broker birth diverts AWAY from the tmux pane: the headless broker host
307
- // launches a detached broker process (which records its own pid as pi_pid via
308
- // the stophook) and returns placement fields all null — so we open NO tmux
309
- // window and write NO tmux placement (presence stays null for a broker). The
310
- // tmux path below is left completely UNCHANGED (byte-identical) for every
311
- // non-broker spawn. Mirrors reviveNode's `hostFor(meta).launch(...)` shape.
312
- if (meta.host_kind === 'broker') {
313
- const placed = headlessBrokerHost.launch(meta.node_id, inv, {
314
- cwd: meta.cwd,
315
- name: fullName(meta),
316
- resuming: false,
317
- });
318
- const saved = getNode(meta.node_id);
319
- return { node: saved, window: placed.window, session: placed.session };
320
- }
321
- const env = { ...inv.env, [FRONT_DOOR_ENV]: '1' };
322
- const command = piCommand(inv.argv);
323
- // openNodeWindow now returns {window, pane}; pane is unused until the
324
- // placement layer lands, so destructure the window and proceed unchanged.
325
- const opened = openNodeWindow({
326
- session,
288
+ // CRTR_SUBTREE (the spine root) rides the detached broker's env so it can group
289
+ // the subtree; the host sets CRTR_FRONT_DOOR itself.
290
+ inv.env = { ...inv.env, CRTR_SUBTREE: rootOfSpine(meta.node_id) };
291
+ // Broker is the only host: launch the detached broker ENGINE. A managed child
292
+ // gets NO viewer spawning it returns once the engine is launched and a human
293
+ // opens a viewer on demand via `crtr node focus` / `crtr attach`. A --root gets
294
+ // one foreground viewer below.
295
+ const placed = headlessBrokerHost.launch(meta.node_id, inv, {
296
+ cwd: meta.cwd,
327
297
  name: fullName(meta),
328
- cwd: opts.cwd,
329
- env,
330
- command,
298
+ resuming: false,
331
299
  });
332
- const window = opened?.window ?? null;
333
- // Two-stage failure model. Opening the window is instant and definitive, so a
334
- // failure here is reported SYNCHRONOUSLY: crash the node (so it isn't a zombie
335
- // 'active' the daemon can't reap — it has no window to watch) and throw so
336
- // `crtr node new` exits non-zero with a clear message for the caller. The node
337
- // is still 'active' from spawnNode, so transition('crash') is a legal from-LIVE
338
- // move — the last scattered node-status write, now through the lifecycle machine.
339
- //
340
- // pi BOOTING inside the window, by contrast, is inherently slow (and slower
341
- // under load), so we stay optimistic and return status='active' the instant
342
- // the window exists. A vehicle that then dies before its first session_start
343
- // is caught by the daemon — it surfaces the boot failure up the spine rather
344
- // than letting the node die silently (see crtrd.ts surfaceBootFailure).
345
- if (window === null) {
300
+ // Definitive failure: no broker pid ⇒ the node has no engine. Crash it (so the
301
+ // daemon doesn't watch a zombie 'active') and throw so `crtr node new` exits
302
+ // non-zero. transition('crash') is a legal from-LIVE move (still 'active' from
303
+ // spawnNode). Mirrors the old window===null crash.
304
+ if (placed.pid === null) {
346
305
  transition(meta.node_id, 'crash');
347
- throw new Error(`failed to open a tmux window for ${meta.node_id} (${meta.name}) in session '${session}' — the node was not started.`);
306
+ throw new Error(`failed to launch the broker engine for ${meta.node_id} (${meta.name}) — the node was not started.`);
348
307
  }
349
- setPresence(meta.node_id, { tmux_session: session, window });
350
- const saved = getNode(meta.node_id);
351
- // A root is spawned to be driven directly bring it forefront so whoever
352
- // asked for it picks up the conversation. A child stays a background window.
353
- if (root) {
354
- try {
355
- focusWindow(session, window);
308
+ // A --root is spawned to be DRIVEN directly: when the caller is inside tmux,
309
+ // open ONE foreground viewer in the caller's CURRENT session and bring it
310
+ // forefront so whoever asked for it picks up the conversation. Outside tmux a
311
+ // --root opens no viewer (an agent handing a root off) — never a throw. A
312
+ // managed child opens nothing. Non-fatal if tmux refuses (openViewerWindow
313
+ // returns null) or the socket never binds — `focus` opens a viewer later.
314
+ let window = null;
315
+ let session = null;
316
+ if (root && here !== null && waitForBrokerViewSocket(meta.node_id)) {
317
+ const viewer = openViewerWindow(meta.node_id, here.session, { name: fullName(meta), cwd: opts.cwd });
318
+ if (viewer !== null && viewer.pane !== null && viewer.pane !== '') {
319
+ session = here.session;
320
+ window = windowOfPane(viewer.pane);
321
+ if (window !== null) {
322
+ try {
323
+ focusWindow(here.session, window);
324
+ }
325
+ catch { /* best-effort */ }
326
+ }
356
327
  }
357
- catch { /* best-effort */ }
358
328
  }
329
+ const saved = getNode(meta.node_id);
359
330
  return { node: saved, window, session };
360
331
  }
@@ -0,0 +1,11 @@
1
+ /** The SGR escape that turns ON the distinct-surface background (theme
2
+ * `selectedBg`), e.g. `\x1b[48;2;58;58;74m`. Pair with `\x1b[49m` to reset just
3
+ * the background. Throws if no process has initialized the theme — callers on a
4
+ * render path (the attach overlay) always have, by construction. */
5
+ export declare function surfaceBgAnsi(): string;
6
+ /** tmux flags that frame a `display-menu` / `display-popup` on the distinct
7
+ * surface background — a rounded border + the surface bg on the body and border.
8
+ * Empty when the theme is unavailable in this process (chrome stays default; the
9
+ * attach viewer re-installs the menu with these once it has themed). One knob:
10
+ * every crtr tmux float shares this single definition. */
11
+ export declare function surfaceTmuxStyleArgs(): string[];
@@ -0,0 +1,68 @@
1
+ // surface-bg.ts — the "distinct popup surface" background, theme-derived.
2
+ //
3
+ // crtr's overlays and popups must read as a SEPARATE surface from the normal one
4
+ // (CTO ruling, taste/inline-ui-placement #11): a popup whose background matches
5
+ // the surface has invisible edges, so you can't tell where the float begins. We
6
+ // paint them with the active pi theme's `selectedBg` role — theme-derived, so it
7
+ // adapts to light/dark and is ONE centralized knob, not a scattered hardcode.
8
+ //
9
+ // pi 0.79 does not re-export `selectedBg` through its public API (the `.` entry
10
+ // omits `getResolvedThemeColors` and the live `theme` singleton). It DOES publish
11
+ // the live `Theme` instance on a process-global symbol once `initTheme()` has run
12
+ // — the same hook pi's own SDK reads (see broker.ts `ctx.ui.theme`). We read it
13
+ // there and use the public `Theme.bg()` / `Theme.getBgAnsi()` methods.
14
+ //
15
+ // CRITICAL: this module must NOT statically import `@earendil-works/pi-coding-agent`
16
+ // — that package costs ~400ms to load and would land on the cold `crtr` front
17
+ // door (which never otherwise loads pi). We only READ the process-global symbol;
18
+ // any process that needs styling (the attach viewer, a node's broker) has already
19
+ // called `initTheme()` and populated it. Where the symbol is absent (the front
20
+ // door, before it execs into the themed `crtr attach`), the tmux helpers return
21
+ // no style args and the chrome stays at its default — the viewer re-themes it.
22
+ const THEME_SYMBOL = Symbol.for('@earendil-works/pi-coding-agent:theme');
23
+ /** The live pi Theme if a process has already run `initTheme()`, else undefined.
24
+ * Read-only: never initializes (that would pull pi into the front door). */
25
+ function liveTheme() {
26
+ const t = globalThis[THEME_SYMBOL];
27
+ return (t ?? undefined);
28
+ }
29
+ /** The SGR escape that turns ON the distinct-surface background (theme
30
+ * `selectedBg`), e.g. `\x1b[48;2;58;58;74m`. Pair with `\x1b[49m` to reset just
31
+ * the background. Throws if no process has initialized the theme — callers on a
32
+ * render path (the attach overlay) always have, by construction. */
33
+ export function surfaceBgAnsi() {
34
+ const t = liveTheme();
35
+ if (t === undefined)
36
+ throw new Error('surfaceBgAnsi: pi theme not initialized in this process');
37
+ return t.getBgAnsi('selectedBg');
38
+ }
39
+ /** The distinct-surface background as a tmux colour token (`#rrggbb` on a
40
+ * truecolor theme, `colourN` on a 256-colour theme), or undefined when the theme
41
+ * is not yet loaded in this process OR defines no selection background. tmux
42
+ * cannot read pi's theme, so we translate the SGR pi resolved for us. */
43
+ function surfaceBgTmux() {
44
+ const t = liveTheme();
45
+ if (t === undefined)
46
+ return undefined;
47
+ const ansi = t.getBgAnsi('selectedBg');
48
+ const tc = /48;2;(\d+);(\d+);(\d+)/.exec(ansi);
49
+ if (tc !== null) {
50
+ const hex = (n) => Number(n).toString(16).padStart(2, '0');
51
+ return `#${hex(tc[1])}${hex(tc[2])}${hex(tc[3])}`;
52
+ }
53
+ const c256 = /48;5;(\d+)/.exec(ansi);
54
+ if (c256 !== null)
55
+ return `colour${c256[1]}`;
56
+ return undefined; // empty/default selectedBg → nothing distinct to paint
57
+ }
58
+ /** tmux flags that frame a `display-menu` / `display-popup` on the distinct
59
+ * surface background — a rounded border + the surface bg on the body and border.
60
+ * Empty when the theme is unavailable in this process (chrome stays default; the
61
+ * attach viewer re-installs the menu with these once it has themed). One knob:
62
+ * every crtr tmux float shares this single definition. */
63
+ export function surfaceTmuxStyleArgs() {
64
+ const bg = surfaceBgTmux();
65
+ if (bg === undefined)
66
+ return [];
67
+ return ['-b', 'rounded', '-s', `bg=${bg}`, '-S', `bg=${bg}`];
68
+ }