@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
@@ -12,41 +12,43 @@
12
12
  // is what calls `matchesKey` internally, so the controller wires intent via the
13
13
  // editor's `onSubmit`/`onEscape`/`onPasteImage` hooks + `onAction(app.*)`
14
14
  // registrations rather than matching raw keys itself.
15
+ import { spawn } from 'node:child_process';
16
+ import { readFileSync, unlinkSync, writeFileSync } from 'node:fs';
17
+ import { tmpdir } from 'node:os';
18
+ import { join } from 'node:path';
15
19
  import { BROKER_READ_CAPS, encodeFrame, } from '../../core/runtime/broker-protocol.js';
16
20
  import { renderDialog } from './extension-dialogs.js';
17
- import { readClipboardImage } from './clipboard-image.js';
21
+ import { readClipboardImage, writeClipboardImageToFile } from './clipboard-image.js';
18
22
  import { dispatchSlashCommand, isSlashCommand } from './slash-commands.js';
19
- /** Aggregate budget for images held for the next message (review M2). A drained
20
- * prompt/steer/follow_up frame inlines ALL pending images as base64 in a single
21
- * frame; these ceilings keep the images[] portion well within the broker's
22
- * 24 MiB line cap. Each clipboard image is 3 MiB base64 (clipboard-image
23
- * MAX_BYTES), so the worst case is MAX_PENDING_IMAGES × 3 MiB = 12 MiB. A paste
24
- * that would exceed EITHER bound is refused (not accumulated). The WHOLE frame
25
- * (images + unbounded text + JSON envelope) is then bounded airtight by the
26
- * MAX_FRAME_BYTES guard in `emitDrive` — this budget alone is not enough, since
27
- * `text` is otherwise unbounded. */
28
- const MAX_PENDING_IMAGE_BYTES = 16 * 1024 * 1024;
29
- const MAX_PENDING_IMAGES = 4;
23
+ import { buildForkPicker, buildModelPicker, buildScopedModelsPicker, buildSessionPicker, buildSettingsPicker, buildTreePicker, } from './pickers.js';
24
+ /** Overlay geometry for the native pickers a touch wider/taller than the
25
+ * extension-dialog default since the model/session/tree lists are denser. */
26
+ const PICKER_OVERLAY_OPTIONS = { anchor: 'center', width: '80%', maxHeight: '85%' };
27
+ /** Max gap between two Esc presses to count as a double-tap (pi uses 500ms). */
28
+ const DOUBLE_ESCAPE_MS = 500;
30
29
  /** Largest drive frame the controller will emit. The broker DESTROYS the viewer
31
30
  * socket on any client line over BROKER_READ_CAPS.maxLineBytes (24 MiB), so we
32
31
  * refuse to emit within a 4 MiB margin of that. `emitDrive` measures the ACTUAL
33
- * encoded frame (text + base64 images[] + JSON envelope), so the cap holds no
34
- * matter how the bytes split closing the gap where a large text paste plus
35
- * max images could otherwise overflow and tear down the socket. */
32
+ * encoded frame (text + JSON envelope), so the cap holds however the bytes
33
+ * split. Pasted images go to a temp file and enter the prompt as a path, never
34
+ * as inline base64, so a paste can no longer push a frame toward this ceiling. */
36
35
  const MAX_FRAME_BYTES = BROKER_READ_CAPS.maxLineBytes - 4 * 1024 * 1024;
37
36
  export class InputController {
38
37
  tui;
39
38
  editor;
40
39
  keybindings;
41
40
  hooks;
42
- /** Images pasted since the last send, attached to the next prompt/follow-up. */
43
- pendingImages = [];
44
- /** Running base64-byte total of `pendingImages` (review M2 aggregate budget). */
45
- pendingImageBytes = 0;
46
41
  /** The currently-rendered blocking dialog, if any (for supersede/dismiss). */
47
42
  dialog;
43
+ /** The currently-open native picker overlay, if any (for supersede/dismiss). */
44
+ picker;
45
+ /** Monotonic open counter — a slow read-op reply whose `seq` is no longer the
46
+ * latest is dropped, so an earlier picker never pops over a later request. */
47
+ openSeq = 0;
48
48
  /** Latest engine state from `welcome`/`session_info_changed` (for `/session`). */
49
49
  state;
50
+ /** Timestamp of the last lone Esc, for double-tap → tree detection (0 = none). */
51
+ lastEscapeAt = 0;
50
52
  constructor(tui, editor, keybindings, hooks) {
51
53
  this.tui = tui;
52
54
  this.editor = editor;
@@ -73,16 +75,52 @@ export class InputController {
73
75
  // -------------------------------------------------------------------------
74
76
  wire() {
75
77
  this.editor.onSubmit = (text) => this.handleSubmit(text);
76
- // Esc / app.interrupt abort the running turn.
77
- this.editor.onEscape = () => this.hooks.onCommand({ type: 'abort' });
78
- // Ctrl+V / Alt+V read clipboard image, hold it for the next message.
78
+ // Esc mirror pi's native interactive escape (interactive-mode
79
+ // setupKeyHandlers): while streaming, abort the turn; with an EMPTY editor
80
+ // and idle, a double-tap (≤500ms) opens the session-tree navigator to rewind
81
+ // to an earlier point (a lone tap just arms it); a non-empty editor swallows
82
+ // Esc (it never clears text). openTreePicker is the same get_tree →
83
+ // navigate_tree path that `/tree` and app.session.tree use.
84
+ this.editor.onEscape = () => {
85
+ if (this.state?.isStreaming) {
86
+ this.hooks.onCommand({ type: 'abort' });
87
+ return;
88
+ }
89
+ if (this.editor.getText().trim())
90
+ return;
91
+ const now = Date.now();
92
+ if (now - this.lastEscapeAt < DOUBLE_ESCAPE_MS) {
93
+ this.lastEscapeAt = 0;
94
+ void this.openTreePicker();
95
+ }
96
+ else {
97
+ this.lastEscapeAt = now;
98
+ }
99
+ };
100
+ // Ctrl+V / Alt+V → read clipboard image, save it to a temp file, and splice
101
+ // the file PATH into the editor at the caret (so the agent can read it).
79
102
  this.editor.onPasteImage = () => {
80
103
  void this.handlePaste();
81
104
  };
82
105
  // Keyboard shortcuts that map 1:1 to a frame needing no engine-side data.
83
106
  this.editor.onAction('app.session.new', () => this.hooks.onCommand({ type: 'new_session' }));
84
- this.editor.onAction('app.model.cycleForward', () => this.hooks.onCommand({ type: 'cycle_model' }));
107
+ this.editor.onAction('app.model.cycleForward', () => this.hooks.onCommand({ type: 'cycle_model', direction: 'forward' }));
108
+ this.editor.onAction('app.model.cycleBackward', () => this.hooks.onCommand({ type: 'cycle_model', direction: 'backward' }));
109
+ this.editor.onAction('app.thinking.cycle', () => this.hooks.onCommand({ type: 'cycle_thinking' }));
85
110
  this.editor.onAction('app.message.followUp', () => this.handleFollowUp());
111
+ this.editor.onAction('app.message.dequeue', () => void this.handleDequeue());
112
+ // Pure client-side render toggles (ChatView owns the components).
113
+ this.editor.onAction('app.tools.expand', () => this.hooks.onToggleToolsExpand?.());
114
+ this.editor.onAction('app.thinking.toggle', () => this.hooks.onToggleThinking?.());
115
+ // Ctrl+G — compose the prompt in $VISUAL/$EDITOR, then load it back.
116
+ this.editor.onAction('app.editor.external', () => void this.openExternalEditor());
117
+ // Native pickers via keybinding. `app.model.select` is bound to ctrl+l by
118
+ // default (vendored in config-load); the session.* actions ship with no
119
+ // default key but fire if the user binds one — wiring them keeps parity.
120
+ this.editor.onAction('app.model.select', () => void this.openModelPicker());
121
+ this.editor.onAction('app.session.tree', () => void this.openTreePicker());
122
+ this.editor.onAction('app.session.fork', () => void this.openForkPicker());
123
+ this.editor.onAction('app.session.resume', () => void this.openSessionPicker());
86
124
  }
87
125
  slashContext() {
88
126
  return {
@@ -92,13 +130,161 @@ export class InputController {
92
130
  cwd: process.cwd(),
93
131
  nodeId: this.hooks.nodeId,
94
132
  onGraph: this.hooks.onGraph,
133
+ onQuit: this.hooks.onQuit,
134
+ onCopy: this.hooks.onCopy,
135
+ openModelPicker: () => void this.openModelPicker(),
136
+ openSessionPicker: () => void this.openSessionPicker(),
137
+ openForkPicker: () => void this.openForkPicker(),
138
+ openTreePicker: () => void this.openTreePicker(),
139
+ openSettingsPicker: () => void this.openSettingsPicker(),
140
+ openScopedModelsPicker: () => void this.openScopedModelsPicker(),
141
+ openLoginPicker: this.hooks.openLoginPicker ? () => this.hooks.openLoginPicker() : undefined,
142
+ openLogoutPicker: this.hooks.openLogoutPicker ? () => this.hooks.openLogoutPicker() : undefined,
143
+ };
144
+ }
145
+ // -------------------------------------------------------------------------
146
+ // Native interactive pickers (overlays). Each fetches its payload via the
147
+ // correlated read-op, builds the REAL pi selector, and routes selection to the
148
+ // same command frame the text-arg slash path sends. Opened by slash command
149
+ // (via slashContext) AND by keybinding (onAction, below).
150
+ // -------------------------------------------------------------------------
151
+ /** Send a command frame to the broker (picker selection sink). */
152
+ send = (frame) => this.hooks.onCommand(frame);
153
+ /** Fetch a picker payload (narrowed on `kind`), then build + show its overlay
154
+ * iff this open is still the latest (m4: a stale slow reply is dropped). Any
155
+ * failure — unwired channel, wrong kind, request error, or a throwing builder
156
+ * — surfaces a notice instead of an unhandled rejection (m5). */
157
+ async openPicker(frame, kind, label, build) {
158
+ if (this.hooks.onRequest === undefined) {
159
+ this.notify(`The ${label} picker isn't available in this viewer`);
160
+ return;
161
+ }
162
+ const seq = ++this.openSeq;
163
+ let reply;
164
+ try {
165
+ reply = await this.hooks.onRequest(frame);
166
+ }
167
+ catch (err) {
168
+ this.notify(`Could not open the ${label} picker: ${err instanceof Error ? err.message : String(err)}`);
169
+ return;
170
+ }
171
+ if (seq !== this.openSeq)
172
+ return; // a newer picker open superseded this one
173
+ if (reply.kind !== kind) {
174
+ this.notify(`Unexpected reply building the ${label} picker`);
175
+ return;
176
+ }
177
+ this.showPicker((controls) => build(reply, controls.close));
178
+ }
179
+ /** Show a picker built by `build` (given a `close` that tears it down).
180
+ * Mounted INLINE under the editor when attach-cmd wires onMountPicker/
181
+ * onUnmountPicker (the integrated chrome path); otherwise falls back to a
182
+ * centered overlay. Supersedes any picker already on screen. Routes focus to
183
+ * the builder's declared focus target (an inner list for fork/settings) and
184
+ * ALWAYS restores editor focus on close — `OverlayHandle.hide()` only
185
+ * auto-restores when the OUTER component held focus, which is not the case
186
+ * once we focus an inner child. A throwing builder is caught into a notice (m5). */
187
+ showPicker(build) {
188
+ this.picker?.dismiss();
189
+ const inline = this.hooks.onMountPicker !== undefined && this.hooks.onUnmountPicker !== undefined;
190
+ let handle;
191
+ let current;
192
+ let done = false;
193
+ const disposeCurrent = () => {
194
+ const disposable = current?.component;
195
+ try {
196
+ disposable?.dispose?.();
197
+ }
198
+ catch {
199
+ /* ignore dispose errors during teardown */
200
+ }
201
+ };
202
+ const mount = (p) => {
203
+ if (inline)
204
+ this.hooks.onMountPicker(p.component);
205
+ else
206
+ handle = this.tui.showOverlay(p.component, PICKER_OVERLAY_OPTIONS);
207
+ this.tui.setFocus(p.focus);
208
+ };
209
+ const unmount = () => {
210
+ if (inline)
211
+ this.hooks.onUnmountPicker?.();
212
+ else
213
+ handle?.hide();
214
+ };
215
+ // Swap the mounted component IN PLACE (same inline slot) — multi-step flows
216
+ // like /login replace the provider selector with the login dialog without
217
+ // ever popping a centered overlay. Does NOT restore editor focus (only a
218
+ // genuine close does); focus moves to the new component.
219
+ const replace = (component, focus) => {
220
+ if (done)
221
+ return;
222
+ disposeCurrent();
223
+ unmount();
224
+ current = { component, focus };
225
+ mount(current);
226
+ this.tui.requestRender();
227
+ };
228
+ const close = () => {
229
+ if (done)
230
+ return;
231
+ done = true;
232
+ disposeCurrent();
233
+ unmount();
234
+ this.picker = undefined;
235
+ this.tui.setFocus(this.editor);
236
+ this.tui.requestRender();
237
+ };
238
+ try {
239
+ current = build({ close, replace });
240
+ }
241
+ catch (err) {
242
+ this.notify(`Could not open picker: ${err instanceof Error ? err.message : String(err)}`);
243
+ return;
244
+ }
245
+ mount(current);
246
+ this.picker = {
247
+ // Supersede: tear down without restoring editor focus — the new picker
248
+ // grabs focus next. Genuine close restores the editor (above).
249
+ dismiss: () => {
250
+ if (done)
251
+ return;
252
+ done = true;
253
+ disposeCurrent();
254
+ unmount();
255
+ },
95
256
  };
257
+ this.tui.requestRender();
258
+ }
259
+ openModelPicker() {
260
+ return this.openPicker({ type: 'list_models' }, 'list_models', 'model', (data, close) => buildModelPicker(this.tui, data, this.send, close));
261
+ }
262
+ openSessionPicker() {
263
+ return this.openPicker({ type: 'list_sessions', scope: 'cwd' }, 'list_sessions', 'session', (data, close) => buildSessionPicker(this.tui, data, this.hooks.onRequest, this.keybindings, this.send, close));
264
+ }
265
+ openTreePicker() {
266
+ return this.openPicker({ type: 'get_tree' }, 'get_tree', 'tree', (data, close) => buildTreePicker(this.tui, data, this.send, close));
267
+ }
268
+ openForkPicker() {
269
+ return this.openPicker({ type: 'get_tree' }, 'get_tree', 'fork', (data, close) => buildForkPicker(data, this.send, close));
270
+ }
271
+ openSettingsPicker() {
272
+ return this.openPicker({ type: 'get_settings' }, 'get_settings', 'settings', (data, close) => buildSettingsPicker(data, this.send, close, (m) => this.notify(m)));
273
+ }
274
+ openScopedModelsPicker() {
275
+ return this.openPicker({ type: 'list_scoped_models' }, 'list_scoped_models', 'scoped-models', (data, close) => buildScopedModelsPicker(data, close, (m) => this.notify(m)));
276
+ }
277
+ /** Open a viewer-local auth picker (login/logout) — uses the same showPicker
278
+ * machinery as read-op pickers, but with a locally-built component (no read-op
279
+ * request; auth operations are viewer-local in tmux-local attach). */
280
+ openAuthPicker(build) {
281
+ this.showPicker(build);
96
282
  }
97
283
  handleSubmit(text) {
98
284
  const trimmed = text.trim();
99
- if (!trimmed && this.pendingImages.length === 0)
285
+ if (!trimmed)
100
286
  return;
101
- if (trimmed && isSlashCommand(trimmed)) {
287
+ if (isSlashCommand(trimmed)) {
102
288
  // Recognized builtin/scoped-out → handled here; unrecognized falls through
103
289
  // and is sent to the engine as a prompt (extension command).
104
290
  if (dispatchSlashCommand(trimmed, this.slashContext())) {
@@ -106,34 +292,105 @@ export class InputController {
106
292
  return;
107
293
  }
108
294
  }
109
- const images = this.pendingImagesPayload();
110
295
  // Submit-while-streaming = steer (pi-native parity): interject into the
111
296
  // running turn instead of queueing a fresh prompt. `isStreaming` is the
112
297
  // broker snapshot's busy signal; the broker routes `steer` → session.steer()
113
298
  // and `prompt` → session.prompt(). Idle / no state yet → prompt (safe default).
114
299
  const busy = this.state?.isStreaming === true;
115
300
  const frame = busy
116
- ? { type: 'steer', text: trimmed, images }
117
- : { type: 'prompt', text: trimmed, images };
301
+ ? { type: 'steer', text: trimmed }
302
+ : { type: 'prompt', text: trimmed };
118
303
  if (!this.emitDrive(frame))
119
- return; // too large — keep editor + pending to trim
120
- this.clearPendingImages();
121
- if (trimmed)
122
- this.editor.addToHistory(trimmed);
304
+ return; // too large — keep editor to trim
305
+ this.editor.addToHistory(trimmed);
123
306
  this.editor.setText('');
124
307
  }
125
308
  handleFollowUp() {
126
309
  const text = this.editor.getText().trim();
127
- if (!text && this.pendingImages.length === 0)
310
+ if (!text)
128
311
  return;
129
- const images = this.pendingImagesPayload();
130
- if (!this.emitDrive({ type: 'follow_up', text, images }))
312
+ if (!this.emitDrive({ type: 'follow_up', text }))
131
313
  return;
132
- this.clearPendingImages();
133
- if (text)
134
- this.editor.addToHistory(text);
314
+ this.editor.addToHistory(text);
135
315
  this.editor.setText('');
136
316
  }
317
+ /** Ctrl+G / app.editor.external — open the current editor text in $VISUAL/$EDITOR
318
+ * and load the saved result back. Ported from pi's `openExternalEditor`: stop
319
+ * the TUI to release the terminal, run the editor with inherited stdio, and on
320
+ * a clean exit (status 0) replace the editor text; the socket keeps running in
321
+ * the background throughout. */
322
+ async openExternalEditor() {
323
+ const editorCmd = process.env.VISUAL || process.env.EDITOR;
324
+ if (!editorCmd) {
325
+ this.notify('No editor configured — set $VISUAL or $EDITOR');
326
+ return;
327
+ }
328
+ const editorWithExpanded = this.editor;
329
+ const currentText = editorWithExpanded.getExpandedText?.() ?? this.editor.getText();
330
+ const tmpFile = join(tmpdir(), `crtr-attach-editor-${Date.now()}.pi.md`);
331
+ try {
332
+ writeFileSync(tmpFile, currentText, 'utf-8');
333
+ this.tui.stop();
334
+ // Split on spaces so `code --wait`-style commands keep their args.
335
+ const [cmd, ...args] = editorCmd.split(' ');
336
+ process.stdout.write(`Launching external editor: ${editorCmd}\nThe viewer resumes when it exits.\n`);
337
+ const status = await new Promise((resolve) => {
338
+ const child = spawn(cmd, [...args, tmpFile], {
339
+ stdio: 'inherit',
340
+ shell: process.platform === 'win32',
341
+ });
342
+ child.on('error', () => resolve(null));
343
+ child.on('close', (code) => resolve(code));
344
+ });
345
+ if (status === 0) {
346
+ this.editor.setText(readFileSync(tmpFile, 'utf-8').replace(/\n$/, ''));
347
+ }
348
+ }
349
+ catch (err) {
350
+ this.notify(`External editor failed: ${err instanceof Error ? err.message : String(err)}`);
351
+ }
352
+ finally {
353
+ try {
354
+ unlinkSync(tmpFile);
355
+ }
356
+ catch {
357
+ /* ignore temp-file cleanup errors */
358
+ }
359
+ this.tui.start();
360
+ this.tui.requestRender(true);
361
+ }
362
+ }
363
+ /** Alt+Up / app.message.dequeue — clear the broker's queued steering + follow-up
364
+ * messages and restore them to the editor, prepended to any in-progress text
365
+ * (pi's `restoreQueuedMessagesToEditor`: queued text first, joined by blank
366
+ * lines). A read-AND-mutate op over the correlated request channel. */
367
+ async handleDequeue() {
368
+ if (this.hooks.onRequest === undefined) {
369
+ this.notify("Restoring queued messages isn't available in this viewer");
370
+ return;
371
+ }
372
+ let reply;
373
+ try {
374
+ reply = await this.hooks.onRequest({ type: 'dequeue' });
375
+ }
376
+ catch (err) {
377
+ this.notify(`Could not restore queued messages: ${err instanceof Error ? err.message : String(err)}`);
378
+ return;
379
+ }
380
+ if (reply.kind !== 'dequeue') {
381
+ this.notify('Unexpected reply restoring queued messages');
382
+ return;
383
+ }
384
+ const queued = [...reply.steering, ...reply.followUp];
385
+ if (queued.length === 0) {
386
+ this.notify('No queued messages to restore');
387
+ return;
388
+ }
389
+ const combined = [queued.join('\n\n'), this.editor.getText()].filter((t) => t.trim()).join('\n\n');
390
+ this.editor.setText(combined);
391
+ this.tui.requestRender();
392
+ this.notify(`Restored ${queued.length} queued message${queued.length > 1 ? 's' : ''} to editor`);
393
+ }
137
394
  /** Send a drive frame iff the WHOLE encoded frame fits under MAX_FRAME_BYTES
138
395
  * (the broker destroys the socket on any line over its 24 MiB read cap). Over
139
396
  * the ceiling → notify + refuse so the caller leaves the editor + pending
@@ -162,44 +419,25 @@ export class InputController {
162
419
  this.notify('No image in the clipboard');
163
420
  return;
164
421
  }
165
- // The clipboard layer read an image but DROPPED it (over its per-image
166
- // ceiling) — surface the reason and attach nothing.
422
+ // The clipboard layer read an image but DROPPED it surface the reason.
167
423
  if (!result.image) {
168
- this.notify(result.note ?? 'Image not attached');
424
+ this.notify(result.note ?? 'Image not pasted');
169
425
  return;
170
426
  }
171
- // Enforce the aggregate pending-image budget so the eventual drained frame
172
- // stays under the broker cap: refuse a paste that would breach the count or
173
- // byte ceiling rather than accumulate an over-cap images[] (review M2).
174
- const bytes = Buffer.byteLength(result.image.data);
175
- if (this.pendingImages.length + 1 > MAX_PENDING_IMAGES ||
176
- this.pendingImageBytes + bytes > MAX_PENDING_IMAGE_BYTES) {
177
- this.notify('Image not attached: pending image budget exceeded — send your message first');
178
- return;
179
- }
180
- this.pendingImages.push(result.image);
181
- this.pendingImageBytes += bytes;
182
- this.notify(result.note
183
- ? `Image attached (${result.note}) — sends with your next message`
184
- : 'Image attached — sends with your next message');
427
+ // Persist to a temp file and splice the PATH into the editor at the caret,
428
+ // so the agent reads the image off disk instead of relying on inline base64
429
+ // the viewer can't render.
430
+ const path = writeClipboardImageToFile(result.image);
431
+ const cur = this.editor.getText();
432
+ const sep = cur && !/\s$/.test(cur) ? ' ' : '';
433
+ this.editor.insertTextAtCursor(`${sep}${path} `);
434
+ this.notify(result.note ? `Image saved (${result.note}) — path inserted` : 'Image saved — path inserted');
185
435
  this.tui.requestRender();
186
436
  }
187
437
  catch {
188
438
  this.notify('Could not read the clipboard image');
189
439
  }
190
440
  }
191
- /** Snapshot the pending images as an `images?` payload (undefined when none)
192
- * WITHOUT clearing — so a frame refused by `emitDrive` (too large) keeps them
193
- * for the user to trim. The caller clears via `clearPendingImages` only after a
194
- * successful send. */
195
- pendingImagesPayload() {
196
- return this.pendingImages.length === 0 ? undefined : this.pendingImages.slice();
197
- }
198
- /** Drop all pending images + reset the aggregate byte counter (after a send). */
199
- clearPendingImages() {
200
- this.pendingImages = [];
201
- this.pendingImageBytes = 0;
202
- }
203
441
  notify(message) {
204
442
  this.hooks.onNotice?.(message);
205
443
  }
@@ -0,0 +1,58 @@
1
+ import { type Component, type KeybindingsManager, type TUI } from '@earendil-works/pi-tui';
2
+ import type { ClientToBroker, GetSettingsData, GetTreeData, ListModelsData, ListScopedModelsData, ListSessionsData } from '../../core/runtime/broker-protocol.js';
3
+ import type { ReadOpRequest } from './view-socket.js';
4
+ /** A built picker overlay: the `component` to show, and the `focus` target the
5
+ * caller must `setFocus` (the component itself, or the inner list that owns
6
+ * `handleInput`). */
7
+ export interface Picker {
8
+ component: Component;
9
+ focus: Component;
10
+ }
11
+ /** Controls handed to a picker builder by InputController.showPicker. `close`
12
+ * tears the whole picker down + restores editor focus; `replace` swaps the
13
+ * CURRENTLY-mounted component for a new one IN THE SAME inline slot (no centered
14
+ * overlay), focusing it — used by multi-step flows like /login, where selecting
15
+ * a provider replaces the selector with the login dialog right under the editor. */
16
+ export interface PickerControls {
17
+ close: () => void;
18
+ replace: (component: Component, focus: Component) => void;
19
+ }
20
+ /** Send a command frame to the broker (= InputController hooks.onCommand). */
21
+ type Send = (frame: ClientToBroker) => void;
22
+ /** Issue a correlated read-op (lazy loaders need it after construction). */
23
+ type Request = (frame: ReadOpRequest) => Promise<unknown>;
24
+ /** Tear the picker overlay down (owned by InputController). */
25
+ type Close = () => void;
26
+ /** Surface a one-line viewer notice. */
27
+ type Notify = (message: string) => void;
28
+ /** `/model` (and ctrl+l) — the real model selector with search + scope toggle.
29
+ * Select → `set_model` with the resolved `provider/id` (the broker's
30
+ * `findModelSpec` requires that form). */
31
+ export declare function buildModelPicker(tui: TUI, data: ListModelsData, send: Send, close: Close): Picker;
32
+ /** `/resume` — the real session selector. Reconstructs from async loaders that
33
+ * fetch each scope on demand (cwd is pre-fetched so the current-session marker
34
+ * shows immediately; the `all` scope loads only on toggle). Select →
35
+ * `switch_session`. Rename/delete are out of scope (no command frame). */
36
+ export declare function buildSessionPicker(tui: TUI, prefetchedCwd: ListSessionsData, request: Request, keybindings: KeybindingsManager, send: Send, close: Close): Picker;
37
+ /** `/tree` — the real tree navigator (pure data). Select → `navigate_tree`.
38
+ * Label-edit is out of scope (no command frame) → no `onLabelChange`. */
39
+ export declare function buildTreePicker(tui: TUI, data: GetTreeData, send: Send, close: Close): Picker;
40
+ /** `/fork` — the real prior-user-message selector (pure data, from
41
+ * `get_tree.forkPoints`). Select → `fork`. The outer component has no
42
+ * `handleInput`; the inner `getMessageList()` does (focus target). */
43
+ export declare function buildForkPicker(data: GetTreeData, send: Send, close: Close): Picker;
44
+ /** `/settings` — the real settings menu (the wire `settings` IS a SettingsConfig
45
+ * superset). Only the contract-enumerated mutations are wired: thinking level →
46
+ * `set_thinking_level`, auto-compact → `set_auto_compaction`. Every other toggle
47
+ * has no command frame (out of scope), so its callback notifies rather than
48
+ * silently lying that the change took. The theme submenu is viewer-local (CTO).
49
+ * The outer component has no `handleInput`; `getSettingsList()` is the focus
50
+ * target. */
51
+ export declare function buildSettingsPicker(data: GetSettingsData, send: Send, close: Close, notify: Notify): Picker;
52
+ /** `/scoped-models` — READ-ONLY via a pi-tui SelectList. CONTRACT GAP: pi (through
53
+ * 0.79.1) does not re-export `ScopedModelsSelectorComponent`, and the contract
54
+ * exposes no command frame for its enable/disable/persist mutations, so a
55
+ * faithful editable picker is not buildable in scope. This shows the registry
56
+ * with the enabled set marked; select notifies that toggling is unsupported. */
57
+ export declare function buildScopedModelsPicker(data: ListScopedModelsData, close: Close, notify: Notify): Picker;
58
+ export {};