@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
@@ -0,0 +1,171 @@
1
+ // pickers.ts — native interactive pickers for `crtr attach`, built against the
2
+ // operator-view wire contract (broker read-op `data` frames). Each builder turns
3
+ // a serialized picker payload into the REAL pi `*SelectorComponent`, wired so a
4
+ // selection dispatches the SAME command frame the text-arg slash path sends and
5
+ // `close()` tears the overlay down. InputController owns the overlay lifecycle
6
+ // (showOverlay/dismiss/focus) and the async data fetch; these are pure builders.
7
+ //
8
+ // FOCUS (load-bearing): some selectors put `handleInput` on an INNER list, not
9
+ // the outer Container (UserMessageSelectorComponent → getMessageList(),
10
+ // SettingsSelectorComponent → getSettingsList()). pi-tui drops every keystroke
11
+ // when the focused component has no `handleInput`, so each builder returns BOTH
12
+ // the overlay `component` AND the `focus` target the caller must focus — exactly
13
+ // as pi's own interactive-mode does (`{ component, focus }`).
14
+ //
15
+ // R1 (contract): the tree/fork/settings payloads are PURE DATA — their selectors
16
+ // build verbatim, no reconstruction. The MODEL picker's `ModelSelectorComponent`
17
+ // needs a live `ModelRegistry`/`SettingsManager`: we feed the REAL component a
18
+ // minimal in-memory `SettingsManager` plus a tiny `ModelRegistry` adapter (the
19
+ // component calls only refresh/getError/getAvailable/find — verified against
20
+ // model-selector.js), preserving its full search + scope-toggle UI without
21
+ // crossing engine state over the socket. The SESSION picker reconstructs
22
+ // faithfully from async loaders (no shim). The SCOPED-MODELS component is NOT
23
+ // re-exported by pi (through 0.79.1) AND has no mutation frame in the contract,
24
+ // so it renders read-only via a pi-tui SelectList.
25
+ //
26
+ // pi pin is `^0.79.1` (a RANGE): the duck-typed registry adapter relies on the
27
+ // component's current 4-method registry surface — a future 0.79.x adding a
28
+ // registry call would surface at runtime (caught by live verification).
29
+ import { SelectList, } from '@earendil-works/pi-tui';
30
+ import { getSelectListTheme, ModelSelectorComponent, SessionSelectorComponent, SettingsSelectorComponent, TreeSelectorComponent, UserMessageSelectorComponent, SettingsManager, } from '@earendil-works/pi-coding-agent';
31
+ /** A minimal `ModelRegistry` for `ModelSelectorComponent`: the component calls
32
+ * only `refresh()`, `getError()`, `getAvailable()`, and `find()` (verified
33
+ * against pi `model-selector.js`). We satisfy exactly those from the wire
34
+ * payload — no auth/registry state crosses the socket — and cast to the nominal
35
+ * class the ctor wants. */
36
+ function modelRegistryAdapter(all, available) {
37
+ const adapter = {
38
+ refresh() { },
39
+ getError() {
40
+ return undefined;
41
+ },
42
+ getAvailable() {
43
+ return available;
44
+ },
45
+ find(provider, id) {
46
+ return all.find((m) => m.provider === provider && m.id === id);
47
+ },
48
+ };
49
+ return adapter;
50
+ }
51
+ /** `/model` (and ctrl+l) — the real model selector with search + scope toggle.
52
+ * Select → `set_model` with the resolved `provider/id` (the broker's
53
+ * `findModelSpec` requires that form). */
54
+ export function buildModelPicker(tui, data, send, close) {
55
+ const all = data.models;
56
+ const availableSet = new Set(data.availableIds);
57
+ const available = all.filter((m) => availableSet.has(`${m.provider}/${m.id}`));
58
+ const current = data.current
59
+ ? all.find((m) => m.provider === data.current.provider && m.id === data.current.id)
60
+ : undefined;
61
+ const component = new ModelSelectorComponent(tui, current, SettingsManager.inMemory(), modelRegistryAdapter(all, available), data.scopedModels, (model) => {
62
+ send({ type: 'set_model', model: `${model.provider}/${model.id}` });
63
+ close();
64
+ }, () => close());
65
+ return { component, focus: component };
66
+ }
67
+ /** `/resume` — the real session selector. Reconstructs from async loaders that
68
+ * fetch each scope on demand (cwd is pre-fetched so the current-session marker
69
+ * shows immediately; the `all` scope loads only on toggle). Select →
70
+ * `switch_session`. Rename/delete are out of scope (no command frame). */
71
+ export function buildSessionPicker(tui, prefetchedCwd, request, keybindings, send, close) {
72
+ const revive = (d) => d.sessions.map((s) => ({ ...s, created: new Date(s.created), modified: new Date(s.modified) }));
73
+ let firstCwd = true;
74
+ const currentLoader = async () => {
75
+ if (firstCwd) {
76
+ firstCwd = false;
77
+ return revive(prefetchedCwd);
78
+ }
79
+ return revive((await request({ type: 'list_sessions', scope: 'cwd' })));
80
+ };
81
+ const allLoader = async () => revive((await request({ type: 'list_sessions', scope: 'all' })));
82
+ const component = new SessionSelectorComponent(currentLoader, allLoader, (path) => {
83
+ send({ type: 'switch_session', path });
84
+ close();
85
+ }, () => close(), () => close(), () => tui.requestRender(), { showRenameHint: false, keybindings: keybindings }, prefetchedCwd.currentSessionFile);
86
+ return { component, focus: component };
87
+ }
88
+ /** `/tree` — the real tree navigator (pure data). Select → `navigate_tree`.
89
+ * Label-edit is out of scope (no command frame) → no `onLabelChange`. */
90
+ export function buildTreePicker(tui, data, send, close) {
91
+ const terminalHeight = process.stdout.rows ?? 24;
92
+ const component = new TreeSelectorComponent(data.tree, data.currentLeafId, terminalHeight, (entryId) => {
93
+ send({ type: 'navigate_tree', targetId: entryId });
94
+ close();
95
+ }, () => close());
96
+ return { component, focus: component };
97
+ }
98
+ /** `/fork` — the real prior-user-message selector (pure data, from
99
+ * `get_tree.forkPoints`). Select → `fork`. The outer component has no
100
+ * `handleInput`; the inner `getMessageList()` does (focus target). */
101
+ export function buildForkPicker(data, send, close) {
102
+ const component = new UserMessageSelectorComponent(data.forkPoints, (entryId) => {
103
+ send({ type: 'fork', entryId });
104
+ close();
105
+ }, () => close());
106
+ return { component, focus: component.getMessageList() };
107
+ }
108
+ /** `/settings` — the real settings menu (the wire `settings` IS a SettingsConfig
109
+ * superset). Only the contract-enumerated mutations are wired: thinking level →
110
+ * `set_thinking_level`, auto-compact → `set_auto_compaction`. Every other toggle
111
+ * has no command frame (out of scope), so its callback notifies rather than
112
+ * silently lying that the change took. The theme submenu is viewer-local (CTO).
113
+ * The outer component has no `handleInput`; `getSettingsList()` is the focus
114
+ * target. */
115
+ export function buildSettingsPicker(data, send, close, notify) {
116
+ const out = () => notify('Only thinking level + auto-compaction are adjustable over view.sock');
117
+ const callbacks = {
118
+ onThinkingLevelChange: (level) => send({ type: 'set_thinking_level', level }),
119
+ onAutoCompactChange: (enabled) => send({ type: 'set_auto_compaction', enabled }),
120
+ onCancel: () => close(),
121
+ onShowImagesChange: out,
122
+ onImageWidthCellsChange: out,
123
+ onAutoResizeImagesChange: out,
124
+ onBlockImagesChange: out,
125
+ onEnableSkillCommandsChange: out,
126
+ onSteeringModeChange: out,
127
+ onFollowUpModeChange: out,
128
+ onTransportChange: out,
129
+ onHttpIdleTimeoutMsChange: out,
130
+ onThemeChange: out,
131
+ onHideThinkingBlockChange: out,
132
+ onCollapseChangelogChange: out,
133
+ onEnableInstallTelemetryChange: out,
134
+ onDoubleEscapeActionChange: out,
135
+ onTreeFilterModeChange: out,
136
+ onShowHardwareCursorChange: out,
137
+ onEditorPaddingXChange: out,
138
+ onAutocompleteMaxVisibleChange: out,
139
+ onQuietStartupChange: out,
140
+ onDefaultProjectTrustChange: out,
141
+ onClearOnShrinkChange: out,
142
+ onShowTerminalProgressChange: out,
143
+ onWarningsChange: out,
144
+ };
145
+ const component = new SettingsSelectorComponent(data.settings, callbacks);
146
+ return { component, focus: component.getSettingsList() };
147
+ }
148
+ /** `/scoped-models` — READ-ONLY via a pi-tui SelectList. CONTRACT GAP: pi (through
149
+ * 0.79.1) does not re-export `ScopedModelsSelectorComponent`, and the contract
150
+ * exposes no command frame for its enable/disable/persist mutations, so a
151
+ * faithful editable picker is not buildable in scope. This shows the registry
152
+ * with the enabled set marked; select notifies that toggling is unsupported. */
153
+ export function buildScopedModelsPicker(data, close, notify) {
154
+ const enabled = data.enabledModelIds;
155
+ const isEnabled = (m) => {
156
+ if (enabled === null)
157
+ return true; // null = no scoping, every model cycles
158
+ const ref = `${m.provider}/${m.id}`;
159
+ return enabled.includes(ref) || enabled.includes(m.id) || enabled.includes('*');
160
+ };
161
+ const items = data.allModels.map((m) => ({
162
+ value: `${m.provider}/${m.id}`,
163
+ label: `${isEnabled(m) ? '✓' : ' '} ${m.provider}/${m.id}`,
164
+ description: m.name,
165
+ }));
166
+ const theme = getSelectListTheme();
167
+ const list = new SelectList(items, 12, theme);
168
+ list.onSelect = () => notify("Enabling/disabling scoped models isn't supported over view.sock yet");
169
+ list.onCancel = () => close();
170
+ return { component: list, focus: list };
171
+ }
@@ -21,6 +21,28 @@ export interface SlashContext {
21
21
  * registers `/graph` as a stub that calls this hook. Unit C/Q owns the overlay
22
22
  * itself and populates this; when absent `/graph` notifies it isn't wired yet. */
23
23
  onGraph?: () => void;
24
+ /** Detach this viewer (`/quit`). A viewer quit DETACHES — it tears down the
25
+ * local TUI/socket and leaves the shared engine running. Wired by attach-cmd to
26
+ * `teardown('detach')`; without it `/quit` cannot actually exit (just sending
27
+ * `bye` makes the broker drop the socket, which the viewer treats as a dropped
28
+ * connection and reconnects). */
29
+ onQuit?: () => void;
30
+ /** Copy the last assistant message to the clipboard (`/copy`). Wired by
31
+ * attach-cmd (it owns the ChatView that holds the text + the clipboard writer). */
32
+ onCopy?: () => void;
33
+ /** OPTIONAL native-picker openers — the InputController fills these from its
34
+ * read-op request channel. When absent (channel unwired) the value-picker
35
+ * builtins fall back to the text-arg notice. */
36
+ openModelPicker?: () => void;
37
+ openSessionPicker?: () => void;
38
+ openForkPicker?: () => void;
39
+ openTreePicker?: () => void;
40
+ openSettingsPicker?: () => void;
41
+ openScopedModelsPicker?: () => void;
42
+ /** Open the native /login provider selector. */
43
+ openLoginPicker?: () => void;
44
+ /** Open the native /logout provider selector. */
45
+ openLogoutPicker?: () => void;
24
46
  }
25
47
  /** Canvas slash-commands — reimplemented NATIVE in the viewer (the canvas chrome
26
48
  * pi-extensions cannot load here; see findings-chrome-parity.md Feature 3). Each
@@ -7,23 +7,23 @@
7
7
  // doesn't recognize) fall through to be sent as a `prompt` for the engine's
8
8
  // extension runner to handle.
9
9
  //
10
- // SELECTORS — the Phase-4 reality. The plan called for `/model`, `/resume`,
11
- // `/fork`, `/tree`, `/settings` to open pi's exported `*SelectorComponent`s. The
12
- // authoritative 0.78.1 .d.ts shows those components require ENGINE-side state a
13
- // socket-only viewer does not have (`ModelSelectorComponent` needs the
14
- // `ModelRegistry` + `SettingsManager`; `SessionSelectorComponent` needs session
15
- // loaders; `TreeSelectorComponent` needs the `SessionTreeNode[]` tree), and the
16
- // §1.2 command-frame floor set exposes NO read op to fetch any of it. So in
17
- // Phase 4 these resolve via a TEXT ARGUMENT `/model <id>`, `/resume <path>`,
18
- // `/fork <entryId>`, `/tree <entryId>` which sends the SAME resolved value the
19
- // selector would have, i.e. zero engine capability is lost; only the interactive
20
- // picker UI waits for a future read op. A no-arg invocation shows a one-line
21
- // notice with the arg form. (This protocol gap is reported up to the parent.)
10
+ // SELECTORS — native interactive pickers. `/model`, `/resume`, `/fork`, `/tree`,
11
+ // `/settings`, `/scoped-models` open pi's real `*SelectorComponent`s as overlays,
12
+ // fed by the operator-view read-op `data` frames (broker-protocol) over the
13
+ // socket. With no arg they open the picker (via the `openXPicker` SlashContext
14
+ // callbacks the InputController exposes, fed by its read-op request channel —
15
+ // which attach-cmd MUST wire via `onRequest: (f) => socket.request(f)`). With an
16
+ // arg the same resolved value is sent directly (`/model <provider/id>`, `/resume
17
+ // <path>`, `/fork <entryId>`, `/tree <entryId>`) without the UI. If the request
18
+ // channel is unwired the picker degrades to the arg-form notice. (Verified
19
+ // against pi 0.79.1 `ScopedModelsSelectorComponent` is the one selector pi does
20
+ // not re-export, so /scoped-models renders read-only via a SelectList.)
22
21
  //
23
22
  // SCOPED OUT (review m2 — no engine method): `/login`, `/logout`, `/share`,
24
23
  // `/trust` — a brief "not supported in attach" notice, never a frame.
25
24
  import { execFile } from 'node:child_process';
26
25
  import { BUILTIN_SLASH_COMMANDS } from '../../core/runtime/pi-vendored.js';
26
+ import { surfaceTmuxStyleArgs } from '../../core/runtime/surface-bg.js';
27
27
  /** Canvas slash-commands — reimplemented NATIVE in the viewer (the canvas chrome
28
28
  * pi-extensions cannot load here; see findings-chrome-parity.md Feature 3). Each
29
29
  * ports the action of its extension: `/promote` ← canvas-commands.ts, `/resume-node`
@@ -39,7 +39,7 @@ export const CANVAS_SLASH_COMMANDS = [
39
39
  ];
40
40
  const CANVAS_NAMES = new Set(CANVAS_SLASH_COMMANDS.map((c) => c.name));
41
41
  /** Builtins with no Phase-4 engine method — scoped out (review m2). */
42
- const SCOPED_OUT = new Set(['login', 'logout', 'share', 'trust']);
42
+ const SCOPED_OUT = new Set(['trust']);
43
43
  /** Valid `/settings thinking` levels — the `SetThinkingLevelFrame['level']`
44
44
  * union (= AgentSession['thinkingLevel'], pi-agent-core ThinkingLevel),
45
45
  * mirrored as a runtime set so an invalid arg is rejected with the options
@@ -95,6 +95,13 @@ export function dispatchSlashCommand(text, ctx) {
95
95
  return true;
96
96
  case 'quit':
97
97
  // A viewer "quit" DETACHES — the shared engine keeps running (one-writer).
98
+ // onQuit tears the viewer down locally (which sends `bye` itself). Falling
99
+ // back to a bare `bye` would just make the broker close the socket, which
100
+ // the viewer reconnects to — so the detach MUST go through onQuit.
101
+ if (ctx.onQuit) {
102
+ ctx.onQuit();
103
+ return true;
104
+ }
98
105
  ctx.send({ type: 'bye' });
99
106
  ctx.notify('Detaching — the engine keeps running');
100
107
  return true;
@@ -116,27 +123,34 @@ export function dispatchSlashCommand(text, ctx) {
116
123
  return usage(ctx, '/import <path-to-session.jsonl>');
117
124
  ctx.send({ type: 'switch_session', path: arg });
118
125
  return true;
119
- // --- value-picker builtins: arg form (selector deferred, see header) ---
126
+ // --- value-picker builtins: native picker (no arg) OR direct value (arg) ---
120
127
  case 'model':
121
128
  if (!arg)
122
- return pickerHint(ctx, 'model', '/model <id>');
129
+ return openOrHint(ctx.openModelPicker, ctx, 'model', '/model <provider/id>');
123
130
  ctx.send({ type: 'set_model', model: arg });
124
131
  return true;
125
132
  case 'resume':
126
133
  if (!arg)
127
- return pickerHint(ctx, 'session', '/resume <session-path>');
134
+ return openOrHint(ctx.openSessionPicker, ctx, 'session', '/resume <session-path>');
128
135
  ctx.send({ type: 'switch_session', path: arg });
129
136
  return true;
130
137
  case 'fork':
131
138
  if (!arg)
132
- return pickerHint(ctx, 'fork point', '/fork <entryId>');
139
+ return openOrHint(ctx.openForkPicker, ctx, 'fork point', '/fork <entryId>');
133
140
  ctx.send({ type: 'fork', entryId: arg });
134
141
  return true;
135
142
  case 'tree':
136
143
  if (!arg)
137
- return pickerHint(ctx, 'tree', '/tree <entryId>');
144
+ return openOrHint(ctx.openTreePicker, ctx, 'tree', '/tree <entryId>');
138
145
  ctx.send({ type: 'navigate_tree', targetId: arg });
139
146
  return true;
147
+ case 'scoped-models':
148
+ if (ctx.openScopedModelsPicker) {
149
+ ctx.openScopedModelsPicker();
150
+ return true;
151
+ }
152
+ ctx.notify('The scoped-models view needs the read-op channel, which this viewer has not wired');
153
+ return true;
140
154
  // --- settings (sub-command form; interactive menu needs read ops) ------
141
155
  case 'settings':
142
156
  return handleSettings(arg, ctx);
@@ -144,12 +158,44 @@ export function dispatchSlashCommand(text, ctx) {
144
158
  case 'session':
145
159
  ctx.notify(describeSession(ctx.state));
146
160
  return true;
147
- // --- recognized builtins with no Phase-4 wire path --------------------
148
161
  case 'copy':
162
+ // Copy the last assistant message to the clipboard — viewer-owned (the text
163
+ // is in the ChatView; no engine round-trip). attach-cmd does the actual copy.
164
+ if (ctx.onCopy) {
165
+ ctx.onCopy();
166
+ return true;
167
+ }
168
+ ctx.notify('/copy is not available in this viewer');
169
+ return true;
170
+ // --- auth commands — viewer-local OAuth/API-key flows ------------------
171
+ case 'login':
172
+ if (ctx.openLoginPicker) {
173
+ ctx.openLoginPicker();
174
+ return true;
175
+ }
176
+ ctx.notify('/login is not available in this viewer');
177
+ return true;
178
+ case 'logout':
179
+ if (ctx.openLogoutPicker) {
180
+ ctx.openLogoutPicker();
181
+ return true;
182
+ }
183
+ ctx.notify('/logout is not available in this viewer');
184
+ return true;
185
+ // --- share / clone / changelog -----------------------------------------
186
+ case 'share':
187
+ ctx.send({ type: 'share' });
188
+ ctx.notify('Sharing session…');
189
+ return true;
190
+ case 'clone':
191
+ ctx.send({ type: 'clone' });
192
+ ctx.notify('Cloning session…');
193
+ return true;
149
194
  case 'changelog':
195
+ ctx.notify('/changelog: see https://github.com/earendil-works/pi for release notes (no bundled changelog in this build)');
196
+ return true;
197
+ // --- recognized builtins with no Phase-4 wire path --------------------
150
198
  case 'hotkeys':
151
- case 'scoped-models':
152
- case 'clone':
153
199
  ctx.notify(`/${name} is not available in attach (Phase 4)`);
154
200
  return true;
155
201
  default:
@@ -295,7 +341,7 @@ function openView(arg, ctx) {
295
341
  * screen and closes itself when the command exits, dropping back to this pane. */
296
342
  function popup(cmd) {
297
343
  try {
298
- execFile('tmux', ['display-popup', '-E', '-w', '90%', '-h', '85%', cmd], () => {
344
+ execFile('tmux', ['display-popup', '-E', '-w', '90%', '-h', '85%', ...surfaceTmuxStyleArgs(), cmd], () => {
299
345
  /* best-effort: popup is self-contained */
300
346
  });
301
347
  }
@@ -316,6 +362,16 @@ function pickerHint(ctx, what, form) {
316
362
  ctx.notify(`Interactive ${what} picker isn't available over view.sock yet — use ${form}`);
317
363
  return true;
318
364
  }
365
+ /** Open a native picker if its opener is wired; otherwise fall back to the
366
+ * text-arg hint. Keeps every value-picker builtin working even when the
367
+ * read-op request channel is absent. */
368
+ function openOrHint(open, ctx, what, form) {
369
+ if (open) {
370
+ open();
371
+ return true;
372
+ }
373
+ return pickerHint(ctx, what, form);
374
+ }
319
375
  function handleSettings(arg, ctx) {
320
376
  const [sub, ...rest] = arg.split(/\s+/);
321
377
  const value = rest.join(' ').trim();
@@ -335,12 +391,22 @@ function handleSettings(arg, ctx) {
335
391
  case 'auto-compaction':
336
392
  ctx.send({ type: 'set_auto_compaction', enabled: parseBool(value) });
337
393
  return true;
394
+ case '':
395
+ // Bare `/settings` opens the native settings menu (falls back to the
396
+ // sub-command summary when the picker channel is unwired).
397
+ if (ctx.openSettingsPicker) {
398
+ ctx.openSettingsPicker();
399
+ return true;
400
+ }
401
+ return settingsSummary(ctx);
338
402
  default:
339
- ctx.notify('Settings: /settings thinking <level> | auto-retry on|off | auto-compaction on|off ' +
340
- '(the interactive settings menu needs data not exposed over view.sock in Phase 4)');
341
- return true;
403
+ return settingsSummary(ctx);
342
404
  }
343
405
  }
406
+ function settingsSummary(ctx) {
407
+ ctx.notify('Settings: /settings thinking <level> | auto-retry on|off | auto-compaction on|off');
408
+ return true;
409
+ }
344
410
  function parseBool(value) {
345
411
  return /^(on|true|yes|1|enabled?)$/i.test(value.trim());
346
412
  }
@@ -0,0 +1,33 @@
1
+ import { CustomEditor } from '@earendil-works/pi-coding-agent';
2
+ /** Thinking levels pi cycles through (shift+tab), lowest → highest budget. */
3
+ export type ThinkingLevel = 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh';
4
+ /** The default (thinking `off`) title chip: reverse video + a space of padding
5
+ * each side, so the name reads as a label sitting on the border rule. Used as
6
+ * the fallback when no thinking color applies. */
7
+ export declare const defaultTitleStyle: (s: string) => string;
8
+ /** Resolve the editor border colorizer for a thinking level. Unknown / `off` →
9
+ * the supplied `fallback` (the theme's default border color). */
10
+ export declare function thinkingBorderColor(level: string | undefined, fallback: (s: string) => string): (s: string) => string;
11
+ /** Resolve the title-chip styler for a thinking level: the level's color as the
12
+ * chip BACKGROUND with bold white text (a space of padding each side), so the
13
+ * session name reads as a solid label in the same hue as the border. Unknown /
14
+ * `off` → the supplied `fallback` (the reverse-video default chip). */
15
+ export declare function thinkingTitleStyle(level: string | undefined, fallback: (s: string) => string): (s: string) => string;
16
+ /** Compose the replacement top-border line: solid title chip + border rule +
17
+ * info chip, never wider than `width` (pi-tui hard-crashes on an over-wide
18
+ * line). The info chip yields entirely when the chip leaves it almost no room,
19
+ * and truncates when it only partially fits. Exported pure for the overflow
20
+ * regression test. */
21
+ export declare function composeTopBorder(width: number, title: string, info: string, titleStyle: (s: string) => string, borderColor: (s: string) => string): string;
22
+ export declare class TitledEditor extends CustomEditor {
23
+ /** Session-name chip painted into the LEFT of the top border. Empty → plain. */
24
+ title: string;
25
+ /** Pre-styled context string painted into the RIGHT of the top border (cwd /
26
+ * branch / git status). Already colorized by the caller; empty → omitted. */
27
+ info: string;
28
+ /** Paint the chip solid so the name reads as a label sitting on the border
29
+ * rule. Defaults to the reverse-video chip; attach-cmd swaps in a
30
+ * thinking-colored background (bold white text) on each state update. */
31
+ titleStyle: (s: string) => string;
32
+ render(width: number): string[];
33
+ }
@@ -0,0 +1,84 @@
1
+ // titled-editor.ts — the `crtr attach` input editor with two bits of chrome pi's
2
+ // stock editor doesn't give us:
3
+ // 1. the session name painted INTO the top border as a solid-background chip,
4
+ // so it stays pinned to the editor instead of scrolling off as a free badge;
5
+ // 2. a border color that tracks the agent's thinking level (set by attach-cmd
6
+ // on each state update), mirroring pi's `getThinkingBorderColor`.
7
+ // Both are pure render-layer chrome; nothing here touches the socket or session.
8
+ import { CustomEditor } from '@earendil-works/pi-coding-agent';
9
+ import { truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
10
+ /** Per-level border/chip color as a 24-bit RGB triple — ONE blue-violet hue at
11
+ * CONSTANT perceived lightness (luma ≈ 120) whose SATURATION alone climbs with
12
+ * the budget: a near-gray blue at `minimal` deepening to a vivid violet at
13
+ * `xhigh`, never getting lighter or darker — only more colorful. (pi paints its
14
+ * own `thinking*` theme colors off its public palette surface, so we own this
15
+ * ramp directly — truecolor, not the 16-color ANSI hues, so the saturation
16
+ * climb actually reads.) `off` falls back to the caller's default border color. */
17
+ const THINKING_RGB = {
18
+ off: undefined, // default border color
19
+ minimal: [125, 115, 135], // near-gray blue-violet
20
+ low: [129, 109, 151], // muted blue-violet
21
+ medium: [134, 103, 168], // violet
22
+ high: [139, 98, 184], // deeper violet
23
+ xhigh: [144, 92, 200], // vivid violet
24
+ };
25
+ /** The default (thinking `off`) title chip: reverse video + a space of padding
26
+ * each side, so the name reads as a label sitting on the border rule. Used as
27
+ * the fallback when no thinking color applies. */
28
+ export const defaultTitleStyle = (s) => `\x1b[7m ${s} \x1b[27m`;
29
+ /** Resolve the editor border colorizer for a thinking level. Unknown / `off` →
30
+ * the supplied `fallback` (the theme's default border color). */
31
+ export function thinkingBorderColor(level, fallback) {
32
+ const rgb = level === undefined ? undefined : THINKING_RGB[level];
33
+ if (rgb === undefined)
34
+ return fallback;
35
+ const [r, g, b] = rgb;
36
+ return (s) => `\x1b[38;2;${r};${g};${b}m${s}\x1b[39m`;
37
+ }
38
+ /** Resolve the title-chip styler for a thinking level: the level's color as the
39
+ * chip BACKGROUND with bold white text (a space of padding each side), so the
40
+ * session name reads as a solid label in the same hue as the border. Unknown /
41
+ * `off` → the supplied `fallback` (the reverse-video default chip). */
42
+ export function thinkingTitleStyle(level, fallback) {
43
+ const rgb = level === undefined ? undefined : THINKING_RGB[level];
44
+ if (rgb === undefined)
45
+ return fallback;
46
+ const [r, g, b] = rgb;
47
+ return (s) => `\x1b[48;2;${r};${g};${b}m\x1b[97m\x1b[1m ${s} \x1b[0m`;
48
+ }
49
+ /** Compose the replacement top-border line: solid title chip + border rule +
50
+ * info chip, never wider than `width` (pi-tui hard-crashes on an over-wide
51
+ * line). The info chip yields entirely when the chip leaves it almost no room,
52
+ * and truncates when it only partially fits. Exported pure for the overflow
53
+ * regression test. */
54
+ export function composeTopBorder(width, title, info, titleStyle, borderColor) {
55
+ const chip = title ? titleStyle(truncateToWidth(title, Math.max(1, width - 4), '…')) : '';
56
+ const chipW = visibleWidth(chip);
57
+ const avail = width - chipW;
58
+ let infoChip = '';
59
+ if (info && avail > 4) {
60
+ infoChip = visibleWidth(info) <= avail ? info : truncateToWidth(info, avail, '…');
61
+ }
62
+ const fill = Math.max(0, width - chipW - visibleWidth(infoChip));
63
+ return chip + borderColor('─'.repeat(fill)) + infoChip;
64
+ }
65
+ export class TitledEditor extends CustomEditor {
66
+ /** Session-name chip painted into the LEFT of the top border. Empty → plain. */
67
+ title = '';
68
+ /** Pre-styled context string painted into the RIGHT of the top border (cwd /
69
+ * branch / git status). Already colorized by the caller; empty → omitted. */
70
+ info = '';
71
+ /** Paint the chip solid so the name reads as a label sitting on the border
72
+ * rule. Defaults to the reverse-video chip; attach-cmd swaps in a
73
+ * thinking-colored background (bold white text) on each state update. */
74
+ titleStyle = defaultTitleStyle;
75
+ render(width) {
76
+ const lines = super.render(width);
77
+ if ((this.title || this.info) && lines.length > 0) {
78
+ // Replace the stock top border; dashes in the current (thinking-aware)
79
+ // border color.
80
+ lines[0] = composeTopBorder(width, this.title, this.info, this.titleStyle, (s) => this.borderColor(s));
81
+ }
82
+ return lines;
83
+ }
84
+ }
@@ -1,6 +1,11 @@
1
1
  import { EventEmitter } from 'node:events';
2
2
  import type { NodeRow } from '../../core/canvas/types.js';
3
- import { type BrokerToClient, type ClientToBroker } from '../../core/runtime/broker-protocol.js';
3
+ import { type BrokerDataFrame, type BrokerToClient, type ClientToBroker, type DequeueFrame, type GetSettingsFrame, type GetTreeFrame, type ListModelsFrame, type ListScopedModelsFrame, type ListSessionsFrame } from '../../core/runtime/broker-protocol.js';
4
+ /** A correlated read-op (or `dequeue`) request MINUS the client-chosen `id` —
5
+ * {@link ViewSocketClient.request} mints the `id`, sends the frame, and resolves
6
+ * with the matching `data` reply. The picker/operator code builds these; the
7
+ * socket owns the correlation token so callers never hand-roll one. */
8
+ export type ReadOpRequest = Omit<ListModelsFrame, 'id'> | Omit<ListSessionsFrame, 'id'> | Omit<GetTreeFrame, 'id'> | Omit<GetSettingsFrame, 'id'> | Omit<ListScopedModelsFrame, 'id'> | Omit<DequeueFrame, 'id'>;
4
9
  /** Surfaced when the node has no reachable broker at connect time (no socket
5
10
  * file, or a stale socket with nothing listening). The command catches this to
6
11
  * exit non-zero with a focus/revive hint. */
@@ -36,7 +41,19 @@ export declare class ViewSocketClient extends EventEmitter {
36
41
  private socket;
37
42
  private decoder;
38
43
  private closeEmitted;
44
+ /** In-flight correlated read-ops, keyed by the `id` minted in {@link request}.
45
+ * Resolved by the matching `data` frame / rejected by the matching `error`
46
+ * frame in {@link onData}, the request's timeout, or socket teardown. */
47
+ private pending;
39
48
  constructor(nodeId: string);
49
+ /** Issue a correlated read-op and resolve with the broker's `data` reply (or
50
+ * reject on the correlated `error`, a timeout, or socket teardown). Mints the
51
+ * `id`, sends `{...frame, id}`, and parks a resolver consumed by {@link onData}.
52
+ * The reply is narrowed by the caller on its `kind`. */
53
+ request(frame: ReadOpRequest): Promise<BrokerDataFrame>;
54
+ /** Reject + clear every in-flight request (socket gone / decode error) so a
55
+ * picker fetch never hangs past the connection it rode on. */
56
+ private rejectAllPending;
40
57
  /** The broker binds `join(nodeDir(id), 'view.sock')` — resolve it the same way. */
41
58
  get socketPath(): string;
42
59
  /** Open the connection. Wire up listeners; emits `connect` on success or