@north-light/crouter 0.3.172 → 0.3.174

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 (673) hide show
  1. package/bin/runtime-selector.mjs +8 -1
  2. package/dist/api/__tests__/{client.test.js → serial/client.test.js} +2 -135
  3. package/dist/api/client.d.ts +12 -21
  4. package/dist/api/client.js +22 -31
  5. package/dist/api/dto/broker-ops.d.ts +158 -0
  6. package/dist/api/dto/broker-ops.js +6 -0
  7. package/dist/api/dto/crons.d.ts +6 -0
  8. package/dist/api/dto/human.d.ts +7 -1
  9. package/dist/api/dto/inbox.d.ts +1 -71
  10. package/dist/api/dto/inbox.js +1 -9
  11. package/dist/api/dto/nodes.d.ts +6 -7
  12. package/dist/api/dto/reviews.d.ts +1 -11
  13. package/dist/api/routes.d.ts +7 -4
  14. package/dist/api/routes.js +8 -5
  15. package/dist/build-root.js +3 -3
  16. package/dist/builtin-memory/04-base-worker.md +1 -4
  17. package/dist/builtin-memory/05-kinds/advisor/00-base.md +1 -5
  18. package/dist/builtin-memory/05-kinds/advisor/01-orchestrator.md +2 -7
  19. package/dist/builtin-memory/05-kinds/design/00-base.md +1 -6
  20. package/dist/builtin-memory/05-kinds/developer/00-base.md +1 -6
  21. package/dist/builtin-memory/05-kinds/explore/00-base.md +1 -4
  22. package/dist/builtin-memory/05-kinds/explore/01-orchestrator.md +1 -2
  23. package/dist/builtin-memory/05-kinds/general/00-base.md +1 -3
  24. package/dist/builtin-memory/05-kinds/plan/00-base.md +1 -6
  25. package/dist/builtin-memory/05-kinds/plan/reviewers/00-base.md +1 -2
  26. package/dist/builtin-memory/05-kinds/plan/reviewers/architecture-fit.md +1 -3
  27. package/dist/builtin-memory/05-kinds/plan/reviewers/code-smells.md +1 -3
  28. package/dist/builtin-memory/05-kinds/plan/reviewers/pattern-consistency.md +1 -3
  29. package/dist/builtin-memory/05-kinds/plan/reviewers/requirements-coverage.md +1 -3
  30. package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +1 -4
  31. package/dist/builtin-memory/05-kinds/review/companion/00-base.md +2 -3
  32. package/dist/builtin-memory/05-kinds/spec/00-base.md +1 -2
  33. package/dist/builtin-memory/advisor/council.md +1 -6
  34. package/dist/builtin-memory/design.md +2 -7
  35. package/dist/builtin-memory/development.md +9 -14
  36. package/dist/builtin-memory/internal/marketplaces.md +2 -6
  37. package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
  38. package/dist/builtin-memory/internal/plugins.md +2 -8
  39. package/dist/builtin-memory/internal/storage-tiers.md +1 -2
  40. package/dist/builtin-memory/testing.md +41 -0
  41. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/{provider-rotation.test.ts → serial/provider-rotation.test.ts} +106 -177
  42. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/claude-plugin-commands.ts +5 -15
  43. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/frontmatter-rules/index.ts +1 -28
  44. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +43 -88
  45. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +55 -0
  46. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +60 -0
  47. package/dist/cli.js +2 -2
  48. package/dist/clients/attach/__tests__/attach-remote-readonly.test.js +37 -131
  49. package/dist/clients/attach/__tests__/chat-view-snapshot-ordering.test.js +71 -152
  50. package/dist/clients/attach/__tests__/context-message.test.js +16 -174
  51. package/dist/clients/attach/__tests__/crtr-output.test.js +36 -2069
  52. package/dist/clients/attach/__tests__/diagram.test.js +2 -2
  53. package/dist/clients/attach/__tests__/edit-diff.test.js +0 -4
  54. package/dist/clients/attach/__tests__/file-review-focus.test.js +2 -1
  55. package/dist/clients/attach/__tests__/group-activity.test.js +40 -229
  56. package/dist/clients/attach/__tests__/oauth-dialog-lifecycle.test.js +26 -76
  57. package/dist/clients/attach/__tests__/prompt-preview.test.js +4 -70
  58. package/dist/clients/attach/__tests__/ref-autocomplete.test.js +11 -125
  59. package/dist/clients/attach/__tests__/titled-editor-preview.test.js +14 -205
  60. package/dist/clients/attach/command.d.ts +2 -1
  61. package/dist/clients/attach/command.js +81 -76
  62. package/dist/clients/attach/input/capabilities.d.ts +27 -0
  63. package/dist/clients/attach/input/capabilities.js +96 -0
  64. package/dist/clients/attach/input/controller.d.ts +20 -182
  65. package/dist/clients/attach/input/controller.js +128 -537
  66. package/dist/clients/attach/input/overlay-owner.d.ts +41 -0
  67. package/dist/clients/attach/input/overlay-owner.js +199 -0
  68. package/dist/clients/attach/overlays/file-review.d.ts +0 -4
  69. package/dist/clients/attach/overlays/file-review.js +13 -5
  70. package/dist/clients/attach/overlays/graph.js +2 -10
  71. package/dist/clients/attach/render/chat-view.d.ts +4 -3
  72. package/dist/clients/attach/render/chat-view.js +39 -33
  73. package/dist/clients/attach/render/group-activity.d.ts +1 -0
  74. package/dist/clients/attach/render/group-activity.js +20 -13
  75. package/dist/clients/attach/render/group-recap.js +12 -3
  76. package/dist/clients/attach/session/connection.js +9 -15
  77. package/dist/clients/attach/session/editor-inventory.d.ts +6 -5
  78. package/dist/clients/attach/session/editor-inventory.js +7 -8
  79. package/dist/clients/attach/session/file-links.d.ts +13 -0
  80. package/dist/clients/attach/session/file-links.js +107 -0
  81. package/dist/clients/attach/session/frame.d.ts +3 -0
  82. package/dist/clients/attach/session/frame.js +10 -1
  83. package/dist/clients/attach/session/frames.js +2 -3
  84. package/dist/clients/attach/session/input-wiring.d.ts +4 -1
  85. package/dist/clients/attach/session/input-wiring.js +15 -14
  86. package/dist/clients/attach/session/mouse.d.ts +6 -1
  87. package/dist/clients/attach/session/mouse.js +8 -5
  88. package/dist/clients/attach/session/profile-files.js +18 -12
  89. package/dist/clients/attach/session/reconnect.js +1 -1
  90. package/dist/clients/attach/session/state-sync.js +1 -2
  91. package/dist/clients/attach/session/surface-host.d.ts +3 -2
  92. package/dist/clients/attach/session/surface-host.js +4 -4
  93. package/dist/clients/attach/slash/dispatch.d.ts +8 -31
  94. package/dist/clients/attach/slash/dispatch.js +34 -81
  95. package/dist/clients/attach/viewer.js +820 -860
  96. package/dist/clients/conversation/projection.d.ts +79 -0
  97. package/dist/clients/conversation/projection.js +341 -0
  98. package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +159 -0
  99. package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +151 -0
  100. package/dist/clients/inbox/controller.d.ts +0 -4
  101. package/dist/clients/inbox/controller.js +0 -2
  102. package/dist/clients/inbox/review/__tests__/review-core.test.js +4 -293
  103. package/dist/clients/inbox/review/__tests__/teardown-notice.test.js +100 -0
  104. package/dist/clients/inbox/review/comments-client.d.ts +3 -2
  105. package/dist/clients/inbox/review/comments-client.js +12 -12
  106. package/dist/clients/inbox/review/companion-pane.d.ts +30 -0
  107. package/dist/clients/inbox/review/companion-pane.js +86 -0
  108. package/dist/clients/inbox/review/document-surface.d.ts +182 -0
  109. package/dist/clients/inbox/review/{document-region.js → document-surface.js} +252 -48
  110. package/dist/clients/inbox/review/frame.js +5 -4
  111. package/dist/clients/inbox/review/launch.d.ts +8 -8
  112. package/dist/clients/inbox/review/launch.js +52 -92
  113. package/dist/clients/inbox/review/nvim.d.ts +2 -0
  114. package/dist/clients/inbox/review/nvim.js +14 -12
  115. package/dist/clients/inbox/review/review-client.d.ts +12 -8
  116. package/dist/clients/inbox/review/review-client.js +46 -35
  117. package/dist/clients/inbox/review-adapter.d.ts +3 -10
  118. package/dist/clients/inbox/review-adapter.js +27 -19
  119. package/dist/clients/inbox/surface.d.ts +0 -4
  120. package/dist/clients/inbox/surface.js +0 -2
  121. package/dist/clients/surfaces/host.d.ts +11 -3
  122. package/dist/commands/__tests__/api-client-cold-start-diagnostic.test.js +7 -34
  123. package/dist/commands/__tests__/human.test.js +11 -322
  124. package/dist/commands/api-client.d.ts +5 -5
  125. package/dist/commands/api-client.js +6 -85
  126. package/dist/commands/canvas-browse.js +3 -29
  127. package/dist/commands/canvas-config.js +1 -1
  128. package/dist/commands/canvas-use.js +3 -3
  129. package/dist/commands/canvas.js +1 -1
  130. package/dist/commands/cron.js +5 -1
  131. package/dist/commands/human/queue.d.ts +0 -4
  132. package/dist/commands/human/queue.js +7 -40
  133. package/dist/commands/memory/list.d.ts +14 -0
  134. package/dist/commands/memory/list.js +35 -31
  135. package/dist/commands/memory/read.d.ts +7 -0
  136. package/dist/commands/memory/read.js +14 -8
  137. package/dist/commands/node/bash.d.ts +1 -0
  138. package/dist/commands/node/bash.js +198 -0
  139. package/dist/commands/node/create.d.ts +23 -0
  140. package/dist/commands/node/create.js +207 -0
  141. package/dist/commands/node/inspect.d.ts +2 -0
  142. package/dist/commands/node/inspect.js +302 -0
  143. package/dist/commands/node/lifecycle.d.ts +4 -0
  144. package/dist/commands/node/lifecycle.js +415 -0
  145. package/dist/commands/node/message.d.ts +1 -0
  146. package/dist/commands/node/message.js +164 -0
  147. package/dist/commands/node/pyramid.d.ts +2 -0
  148. package/dist/commands/node/pyramid.js +79 -0
  149. package/dist/commands/node/subscription.d.ts +1 -0
  150. package/dist/commands/node/subscription.js +82 -0
  151. package/dist/commands/node/wait.d.ts +1 -0
  152. package/dist/commands/node/wait.js +159 -0
  153. package/dist/commands/node-lifecycle-revive.js +10 -57
  154. package/dist/commands/node.d.ts +0 -42
  155. package/dist/commands/node.js +11 -2065
  156. package/dist/commands/search.js +1 -1
  157. package/dist/commands/surface/node/cycle.d.ts +1 -0
  158. package/dist/commands/surface/node/cycle.js +83 -0
  159. package/dist/commands/surface/node/focus.d.ts +1 -0
  160. package/dist/commands/surface/node/focus.js +67 -0
  161. package/dist/commands/surface/node/id.d.ts +1 -0
  162. package/dist/commands/surface/node/id.js +38 -0
  163. package/dist/commands/surface/node/pin.d.ts +1 -0
  164. package/dist/commands/surface/node/pin.js +42 -0
  165. package/dist/commands/surface/node/placement.d.ts +46 -0
  166. package/dist/commands/surface/node/placement.js +124 -0
  167. package/dist/commands/surface-node.js +4 -1
  168. package/dist/commands/surface-reload.js +1 -2
  169. package/dist/commands/surface-tmux-spread.js +1 -1
  170. package/dist/commands/surface.js +5 -20
  171. package/dist/commands/sys/__tests__/setup-core.test.js +18 -271
  172. package/dist/commands/sys/config.js +6 -6
  173. package/dist/commands/sys/logs.js +1 -1
  174. package/dist/commands/sys/panels/broker-limits-panel.d.ts +17 -0
  175. package/dist/commands/sys/panels/broker-limits-panel.js +104 -0
  176. package/dist/commands/sys/panels/dependencies-panel.d.ts +14 -0
  177. package/dist/commands/sys/panels/dependencies-panel.js +80 -0
  178. package/dist/commands/sys/panels/keybindings-panel.d.ts +56 -0
  179. package/dist/commands/sys/panels/keybindings-panel.js +703 -0
  180. package/dist/commands/sys/panels/keys-panel.d.ts +16 -0
  181. package/dist/commands/sys/panels/keys-panel.js +60 -0
  182. package/dist/commands/sys/panels/models-panel.d.ts +77 -0
  183. package/dist/commands/sys/panels/models-panel.js +647 -0
  184. package/dist/commands/sys/panels/panel.d.ts +45 -0
  185. package/dist/commands/sys/panels/panel.js +137 -0
  186. package/dist/commands/sys/panels/plugins-panel.d.ts +16 -0
  187. package/dist/commands/sys/panels/plugins-panel.js +80 -0
  188. package/dist/commands/sys/panels/prefs-panel.d.ts +21 -0
  189. package/dist/commands/sys/panels/prefs-panel.js +85 -0
  190. package/dist/commands/sys/panels/provider-panel.d.ts +62 -0
  191. package/dist/commands/sys/panels/provider-panel.js +538 -0
  192. package/dist/commands/sys/settings-shell.d.ts +55 -0
  193. package/dist/commands/sys/settings-shell.js +400 -0
  194. package/dist/commands/sys/settings.d.ts +2 -0
  195. package/dist/commands/sys/settings.js +31 -0
  196. package/dist/commands/sys/setup-core.d.ts +3 -33
  197. package/dist/commands/sys/setup-core.js +27 -78
  198. package/dist/commands/sys/setup-settings-runner.d.ts +24 -0
  199. package/dist/commands/sys/setup-settings-runner.js +63 -0
  200. package/dist/commands/sys/setup-wizard.d.ts +26 -125
  201. package/dist/commands/sys/setup-wizard.js +209 -2473
  202. package/dist/commands/sys/setup.js +5 -7
  203. package/dist/commands/sys.js +3 -2
  204. package/dist/core/__tests__/broker-extension-canvas-db-boundary.test.js +50 -0
  205. package/dist/core/__tests__/broker-launch-candidates.test.js +20 -9
  206. package/dist/core/__tests__/broker-preflight.test.js +1 -1
  207. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +108 -27
  208. package/dist/core/__tests__/canvas-inbox-watcher.test.js +14 -0
  209. package/dist/core/__tests__/canvas.test.js +2 -36
  210. package/dist/core/__tests__/close.test.js +14 -3
  211. package/dist/core/__tests__/context-intro.test.js +32 -143
  212. package/dist/core/__tests__/cron-broker-capacity.test.js +22 -3
  213. package/dist/core/__tests__/daemon-boot.test.js +2 -20
  214. package/dist/core/__tests__/daemon-wedge.test.js +1 -1
  215. package/dist/core/__tests__/dead-node-policy-table.test.js +1 -18
  216. package/dist/core/__tests__/fixtures/fake-engine.d.ts +1 -1
  217. package/dist/core/__tests__/fixtures/fake-engine.js +2 -2
  218. package/dist/core/__tests__/fixtures/fake-pi-host.js +2 -2
  219. package/dist/core/__tests__/focuses.test.js +4 -98
  220. package/dist/core/__tests__/helpers/broker-clients.d.ts +0 -3
  221. package/dist/core/__tests__/helpers/broker-clients.js +0 -14
  222. package/dist/core/__tests__/helpers/harness.d.ts +0 -8
  223. package/dist/core/__tests__/helpers/harness.js +7 -87
  224. package/dist/core/__tests__/helpers/viewer-panes.js +1 -1
  225. package/dist/core/__tests__/inline-memory-refs.test.js +5 -134
  226. package/dist/core/__tests__/kickoff.test.js +1 -1
  227. package/dist/core/__tests__/memory-resolver-precedence.test.js +29 -1
  228. package/dist/core/__tests__/migration.test.js +35 -48
  229. package/dist/core/__tests__/on-read-dedup-resume.test.js +22 -12
  230. package/dist/core/__tests__/passive-subscription.test.js +26 -49
  231. package/dist/core/__tests__/preview-registry-sync.test.js +1 -2
  232. package/dist/core/__tests__/prune.test.js +94 -1
  233. package/dist/core/__tests__/review-model-floor.test.js +0 -32
  234. package/dist/core/__tests__/seam/broker-attach-multiclient.test.js +255 -0
  235. package/dist/core/__tests__/{full → seam}/broker-attach-stream.test.js +39 -7
  236. package/dist/core/__tests__/{full → seam}/broker-crash-teardown.test.js +30 -29
  237. package/dist/core/__tests__/{full → seam}/broker-pane-resolution.test.js +54 -5
  238. package/dist/core/__tests__/{full → seam}/broker-provider-retry.test.js +134 -82
  239. package/dist/core/__tests__/{full → seam}/broker-whip-interrupt.test.js +15 -4
  240. package/dist/core/__tests__/{full/idle-release-settlement.test.js → seam/dormancy-release.test.js} +69 -17
  241. package/dist/core/__tests__/{full/yield-message-transaction.test.js → seam/yield-refresh-transaction.test.js} +31 -9
  242. package/dist/core/__tests__/secrets.test.js +1 -7
  243. package/dist/core/__tests__/{broker-fork-seam.test.js → serial/broker-fork-seam.test.js} +58 -7
  244. package/dist/core/__tests__/{broker-sdk-wiring.test.js → serial/broker-sdk-wiring.test.js} +18 -162
  245. package/dist/core/__tests__/{broker-snapshot-history.test.js → serial/broker-snapshot-history.test.js} +9 -9
  246. package/dist/core/__tests__/{command-plugins.test.js → serial/command-plugins.test.js} +105 -15
  247. package/dist/core/__tests__/{deferred-no-wake.test.js → serial/deferred-no-wake.test.js} +23 -20
  248. package/dist/core/__tests__/{flagship-lifecycle.test.js → serial/flagship-lifecycle.test.js} +7 -7
  249. package/dist/core/__tests__/{host-teardown-process-group.test.js → serial/host-teardown-process-group.test.js} +6 -6
  250. package/dist/core/__tests__/{human-deliver-e2e.test.js → serial/human-deliver-e2e.test.js} +12 -12
  251. package/dist/core/__tests__/{live-mutation.test.js → serial/live-mutation.test.js} +5 -5
  252. package/dist/core/__tests__/{refresh-stall-recycle.test.js → serial/refresh-stall-recycle.test.js} +5 -14
  253. package/dist/core/__tests__/{revive.test.js → serial/revive.test.js} +15 -100
  254. package/dist/core/__tests__/{spawn-root.test.js → serial/spawn-root.test.js} +32 -15
  255. package/dist/core/__tests__/serial/subscription-delivery.test.js +126 -0
  256. package/dist/core/__tests__/serial/tmux-surface.test.js +256 -0
  257. package/dist/core/__tests__/{worktree.test.js → serial/worktree.test.js} +15 -15
  258. package/dist/core/__tests__/session-model.test.js +0 -18
  259. package/dist/core/__tests__/spawn-worktree-id-conflict.test.js +9 -0
  260. package/dist/core/__tests__/watchdog-abort-arms-retry.test.js +50 -0
  261. package/dist/core/broker-client/__tests__/transport-relay.test.js +15 -16
  262. package/dist/core/broker-client/__tests__/transport-socket.test.js +28 -0
  263. package/dist/core/broker-client/client.d.ts +2 -2
  264. package/dist/core/broker-client/client.js +4 -7
  265. package/dist/core/broker-client/transport-relay.d.ts +13 -19
  266. package/dist/core/broker-client/transport-relay.js +19 -34
  267. package/dist/core/broker-client/transport-socket.d.ts +2 -2
  268. package/dist/core/broker-client/transport-socket.js +9 -2
  269. package/dist/core/broker-client/transport.d.ts +7 -3
  270. package/dist/core/broker-client/transport.js +4 -4
  271. package/dist/core/canvas/__tests__/attention.test.js +1 -18
  272. package/dist/core/canvas/__tests__/remote-canvas-source.test.js +7 -64
  273. package/dist/core/canvas/__tests__/remote-transport.test.js +2 -3
  274. package/dist/core/canvas/__tests__/render-remote.test.js +7 -52
  275. package/dist/core/canvas/browse/app.d.ts +2 -17
  276. package/dist/core/canvas/browse/app.js +16 -88
  277. package/dist/core/canvas/browse/model.d.ts +0 -4
  278. package/dist/core/canvas/browse/model.js +1 -12
  279. package/dist/core/canvas/browse/render.js +6 -3
  280. package/dist/core/canvas/crons.d.ts +8 -5
  281. package/dist/core/canvas/crons.js +7 -6
  282. package/dist/core/canvas/extensions.d.ts +7 -3
  283. package/dist/core/canvas/extensions.js +16 -4
  284. package/dist/core/canvas/nav-model.d.ts +0 -4
  285. package/dist/core/canvas/nav-model.js +2 -14
  286. package/dist/core/canvas/node-order.d.ts +3 -0
  287. package/dist/core/canvas/node-order.js +12 -0
  288. package/dist/core/canvas/remote-canvas-source.d.ts +3 -6
  289. package/dist/core/canvas/remote-canvas-source.js +8 -15
  290. package/dist/core/command.js +11 -4
  291. package/dist/core/config.js +3 -1
  292. package/dist/core/events/envelope.js +1 -1
  293. package/dist/core/events/read.js +7 -21
  294. package/dist/core/events/source.d.ts +0 -2
  295. package/dist/core/events/source.js +0 -16
  296. package/dist/core/events/types.d.ts +1 -1
  297. package/dist/core/fault-classifier.d.ts +1 -1
  298. package/dist/core/feed/passive.d.ts +0 -11
  299. package/dist/core/feed/passive.js +1 -62
  300. package/dist/core/human/__tests__/{inbox-core.test.js → serial/inbox-core.test.js} +6 -6
  301. package/dist/core/keybindings/__tests__/resolve.test.js +1 -96
  302. package/dist/core/keybindings/background-bash.js +3 -4
  303. package/dist/core/keybindings/catalog.js +1 -1
  304. package/dist/core/memory/inline-ref-grammar.js +3 -3
  305. package/dist/core/memory-resolver.d.ts +11 -25
  306. package/dist/core/memory-resolver.js +51 -33
  307. package/dist/core/model-routes.d.ts +15 -0
  308. package/dist/core/model-routes.js +12 -0
  309. package/dist/core/pool-credential-store.js +1 -1
  310. package/dist/core/preview-registry.js +8 -9
  311. package/dist/core/profiles/__tests__/root-profile.test.js +4 -14
  312. package/dist/core/review/realize.d.ts +0 -13
  313. package/dist/core/review/realize.js +0 -40
  314. package/dist/core/review/types.d.ts +1 -1
  315. package/dist/core/runtime/__tests__/engine-route.test.js +2 -2
  316. package/dist/core/runtime/bearings.js +1 -11
  317. package/dist/core/runtime/broker/client-registry.d.ts +33 -0
  318. package/dist/core/runtime/broker/client-registry.js +164 -0
  319. package/dist/core/runtime/broker/daemon-ops.d.ts +19 -0
  320. package/dist/core/runtime/broker/daemon-ops.js +49 -0
  321. package/dist/core/runtime/broker/event-projection.d.ts +42 -0
  322. package/dist/core/runtime/broker/event-projection.js +178 -0
  323. package/dist/core/runtime/broker/fault-retry.d.ts +39 -0
  324. package/dist/core/runtime/broker/fault-retry.js +277 -0
  325. package/dist/core/runtime/broker/frame-dispatch.d.ts +238 -0
  326. package/dist/core/runtime/broker/frame-dispatch.js +1537 -0
  327. package/dist/core/runtime/broker/inbox.d.ts +22 -0
  328. package/dist/core/runtime/broker/inbox.js +167 -0
  329. package/dist/core/runtime/broker/passive.d.ts +16 -0
  330. package/dist/core/runtime/broker/passive.js +44 -0
  331. package/dist/core/runtime/broker/read-ops.d.ts +23 -0
  332. package/dist/core/runtime/broker/read-ops.js +150 -0
  333. package/dist/core/runtime/broker/rebind.d.ts +74 -0
  334. package/dist/core/runtime/broker/rebind.js +211 -0
  335. package/dist/core/runtime/broker/tool-groups.d.ts +39 -0
  336. package/dist/core/runtime/broker/tool-groups.js +244 -0
  337. package/dist/core/runtime/broker-extension-render.d.ts +11 -0
  338. package/dist/core/runtime/broker-extension-render.js +181 -0
  339. package/dist/core/runtime/broker-persona-guidance.d.ts +13 -0
  340. package/dist/core/runtime/broker-persona-guidance.js +58 -0
  341. package/dist/core/runtime/broker-protocol.d.ts +1 -1
  342. package/dist/core/runtime/broker-protocol.js +1 -1
  343. package/dist/core/runtime/broker.d.ts +5 -196
  344. package/dist/core/runtime/broker.js +276 -3059
  345. package/dist/core/runtime/canvas-extensions.js +11 -2
  346. package/dist/core/runtime/fault.js +1 -1
  347. package/dist/core/runtime/model-selection.d.ts +9 -0
  348. package/dist/core/runtime/model-selection.js +12 -0
  349. package/dist/core/runtime/model-swap.d.ts +3 -3
  350. package/dist/core/runtime/model-swap.js +33 -25
  351. package/dist/core/runtime/placement-tmux.d.ts +3 -14
  352. package/dist/core/runtime/placement-tmux.js +11 -80
  353. package/dist/core/runtime/placement.d.ts +4 -2
  354. package/dist/core/runtime/placement.js +7 -12
  355. package/dist/core/runtime/promote.d.ts +14 -1
  356. package/dist/core/runtime/promote.js +1 -1
  357. package/dist/core/runtime/reset.d.ts +1 -1
  358. package/dist/core/runtime/roadmap.js +1 -1
  359. package/dist/core/runtime/spawn.d.ts +8 -0
  360. package/dist/core/runtime/spawn.js +36 -6
  361. package/dist/core/runtime/stop-guard.d.ts +4 -3
  362. package/dist/core/runtime/stop-guard.js +36 -18
  363. package/dist/core/runtime/stop-signals.d.ts +4 -1
  364. package/dist/core/runtime/stream-watchdog.js +4 -5
  365. package/dist/core/runtime/structured-output.d.ts +15 -2
  366. package/dist/core/runtime/structured-output.js +35 -12
  367. package/dist/core/runtime/tmux-bindings.d.ts +25 -0
  368. package/dist/core/runtime/tmux-bindings.js +682 -0
  369. package/dist/core/runtime/tmux-chrome.d.ts +2 -1
  370. package/dist/core/runtime/tmux-chrome.js +3 -2
  371. package/dist/core/runtime/{tmux.d.ts → tmux-driver.d.ts} +18 -26
  372. package/dist/core/runtime/tmux-driver.js +598 -0
  373. package/dist/core/runtime/view-socket.d.ts +14 -0
  374. package/dist/core/runtime/view-socket.js +83 -0
  375. package/dist/core/session-model/session-state.d.ts +1 -2
  376. package/dist/core/session-model/session-state.js +5 -8
  377. package/dist/core/spawn.d.ts +1 -1
  378. package/dist/core/spawn.js +2 -2
  379. package/dist/core/substrate/gate.d.ts +1 -1
  380. package/dist/core/substrate/on-read-pure.d.ts +7 -0
  381. package/dist/core/substrate/on-read-pure.js +272 -0
  382. package/dist/core/termrender/termrender.js +32 -2
  383. package/dist/core/user-settings.d.ts +23 -1
  384. package/dist/core/user-settings.js +18 -1
  385. package/dist/daemon/api/__tests__/node-model-validation.test.js +134 -0
  386. package/dist/daemon/api/__tests__/review-terminal-conflict.test.js +98 -0
  387. package/dist/daemon/api/__tests__/{full → seam}/api-server.test.js +6 -48
  388. package/dist/daemon/api/__tests__/{full → seam}/b10-attach-modelauth.test.js +17 -44
  389. package/dist/daemon/api/__tests__/{full/push-parity.test.js → seam/leaf-api-parity.test.js} +61 -42
  390. package/dist/daemon/api/bridge.js +40 -81
  391. package/dist/daemon/api/handlers/broker-ops.d.ts +2 -0
  392. package/dist/daemon/api/handlers/broker-ops.js +383 -0
  393. package/dist/daemon/api/handlers/crons.js +9 -4
  394. package/dist/daemon/api/handlers/files.js +6 -7
  395. package/dist/daemon/api/handlers/inbox.js +2 -340
  396. package/dist/daemon/api/handlers/nodes.d.ts +10 -1
  397. package/dist/daemon/api/handlers/nodes.js +74 -39
  398. package/dist/daemon/api/handlers/review-comments.js +5 -1
  399. package/dist/daemon/api/handlers/reviews.js +0 -19
  400. package/dist/daemon/api/map.js +25 -4
  401. package/dist/daemon/api/router.js +2 -3
  402. package/dist/daemon/api/server.js +5 -7
  403. package/dist/daemon/cron-run.d.ts +4 -0
  404. package/dist/daemon/cron-run.js +30 -13
  405. package/dist/daemon/crtrd.d.ts +3 -58
  406. package/dist/daemon/crtrd.js +28 -1244
  407. package/dist/daemon/manage.js +0 -1
  408. package/dist/daemon/reconcilers/broker-supervision.d.ts +50 -0
  409. package/dist/daemon/reconcilers/broker-supervision.js +549 -0
  410. package/dist/daemon/reconcilers/cron-lane.d.ts +10 -0
  411. package/dist/daemon/reconcilers/cron-lane.js +32 -0
  412. package/dist/daemon/reconcilers/dormant-inbox.d.ts +18 -0
  413. package/dist/daemon/reconcilers/dormant-inbox.js +143 -0
  414. package/dist/daemon/reconcilers/storage-maintenance.d.ts +22 -0
  415. package/dist/daemon/reconcilers/storage-maintenance.js +213 -0
  416. package/dist/daemon/review/finish.js +2 -5
  417. package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +34 -53
  418. package/dist/pi-extensions/__tests__/canvas-recap.test.js +16 -113
  419. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +72 -99
  420. package/dist/pi-extensions/__tests__/canvas-stophook-context-nudge.test.js +29 -12
  421. package/dist/pi-extensions/broker-local.d.ts +20 -0
  422. package/dist/pi-extensions/broker-local.js +89 -0
  423. package/dist/pi-extensions/canvas-context-intro.d.ts +3 -3
  424. package/dist/pi-extensions/canvas-context-intro.js +22 -11
  425. package/dist/pi-extensions/canvas-doc-substrate.js +17 -34
  426. package/dist/pi-extensions/canvas-goal-capture.js +7 -12
  427. package/dist/pi-extensions/canvas-inbox-watcher.js +124 -139
  428. package/dist/pi-extensions/canvas-passive-context.d.ts +3 -3
  429. package/dist/pi-extensions/canvas-passive-context.js +22 -18
  430. package/dist/pi-extensions/canvas-recap.d.ts +2 -2
  431. package/dist/pi-extensions/canvas-recap.js +48 -21
  432. package/dist/pi-extensions/canvas-review-boundary.js +10 -4
  433. package/dist/pi-extensions/canvas-stophook.d.ts +0 -5
  434. package/dist/pi-extensions/canvas-stophook.js +149 -431
  435. package/dist/pi-extensions/canvas-structured-output.js +52 -30
  436. package/dist/pi-extensions/envelope-merge.d.ts +13 -0
  437. package/dist/pi-extensions/envelope-merge.js +29 -0
  438. package/dist/shared/working-activity.d.ts +3 -2
  439. package/dist/shared/working-activity.js +3 -2
  440. package/dist/types.d.ts +6 -5
  441. package/dist/types.js +2 -2
  442. package/docs/compat/hearth-crtr-v1.md +4 -12
  443. package/docs/compat/hearth-crtr-v2.md +3 -11
  444. package/docs/compat/hearth-crtr-v3.md +4 -12
  445. package/docs/compat/hearth-crtr-v4.md +3 -11
  446. package/docs/compat/hearth-crtr-v5.md +3 -11
  447. package/docs/public-api.md +3 -7
  448. package/package.json +7 -49
  449. package/runtime.lock.json +2766 -14533
  450. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/claude-plugin-commands.test.ts +0 -49
  451. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/memory-slash-commands.test.ts +0 -194
  452. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/statusline.test.ts +0 -68
  453. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/frontmatter-rules/test.ts +0 -119
  454. package/dist/clients/attach/__tests__/attach-chrome-remote.test.js +0 -439
  455. package/dist/clients/attach/__tests__/attach-keybindings.test.js +0 -170
  456. package/dist/clients/attach/__tests__/crtr-output-coverage.test.js +0 -57
  457. package/dist/clients/attach/__tests__/editor-frame-title.test.js +0 -19
  458. package/dist/clients/attach/__tests__/input-controller-extension-command.test.js +0 -32
  459. package/dist/clients/attach/__tests__/markdown-headings.test.js +0 -35
  460. package/dist/clients/attach/__tests__/session-identity-refresh.test.js +0 -32
  461. package/dist/clients/conversation/conversation.d.ts +0 -65
  462. package/dist/clients/conversation/conversation.js +0 -318
  463. package/dist/clients/conversation/frames.d.ts +0 -30
  464. package/dist/clients/conversation/frames.js +0 -52
  465. package/dist/clients/conversation/region.d.ts +0 -51
  466. package/dist/clients/conversation/region.js +0 -265
  467. package/dist/clients/inbox/__tests__/inbox-controller.test.js +0 -247
  468. package/dist/clients/inbox/__tests__/mount-panel.test.js +0 -776
  469. package/dist/clients/inbox/review/__tests__/remap.test.js +0 -196
  470. package/dist/clients/inbox/review/columns.d.ts +0 -22
  471. package/dist/clients/inbox/review/columns.js +0 -40
  472. package/dist/clients/inbox/review/composition.d.ts +0 -60
  473. package/dist/clients/inbox/review/composition.js +0 -278
  474. package/dist/clients/inbox/review/document-region.d.ts +0 -120
  475. package/dist/clients/surfaces/region.d.ts +0 -16
  476. package/dist/clients/web/__tests__/push-engine.test.js +0 -158
  477. package/dist/clients/web/__tests__/push-registry.test.d.ts +0 -1
  478. package/dist/clients/web/__tests__/push-registry.test.js +0 -177
  479. package/dist/clients/web/dev-server.d.ts +0 -7
  480. package/dist/clients/web/dev-server.js +0 -46
  481. package/dist/clients/web/events.d.ts +0 -19
  482. package/dist/clients/web/events.js +0 -163
  483. package/dist/clients/web/push-engine.d.ts +0 -44
  484. package/dist/clients/web/push-engine.js +0 -125
  485. package/dist/clients/web/push-registry.d.ts +0 -85
  486. package/dist/clients/web/push-registry.js +0 -373
  487. package/dist/clients/web/server.d.ts +0 -19
  488. package/dist/clients/web/server.js +0 -661
  489. package/dist/clients/web/web-client/shared/protocol.d.ts +0 -214
  490. package/dist/clients/web/web-client/shared/protocol.js +0 -8
  491. package/dist/clients/web/web-cmd.d.ts +0 -2
  492. package/dist/clients/web/web-cmd.js +0 -141
  493. package/dist/commands/__tests__/api-canvas-source.test.d.ts +0 -1
  494. package/dist/commands/__tests__/api-canvas-source.test.js +0 -37
  495. package/dist/commands/__tests__/canvas-config.test.d.ts +0 -1
  496. package/dist/commands/__tests__/canvas-config.test.js +0 -107
  497. package/dist/commands/__tests__/node-no-kickoff-flag.test.d.ts +0 -1
  498. package/dist/commands/__tests__/node-no-kickoff-flag.test.js +0 -73
  499. package/dist/commands/__tests__/node-worktree-flag.test.d.ts +0 -1
  500. package/dist/commands/__tests__/node-worktree-flag.test.js +0 -49
  501. package/dist/commands/__tests__/search-contents.test.d.ts +0 -1
  502. package/dist/commands/__tests__/search-contents.test.js +0 -52
  503. package/dist/commands/memory/__tests__/lint.test.d.ts +0 -1
  504. package/dist/commands/memory/__tests__/lint.test.js +0 -15
  505. package/dist/commands/sys/__tests__/config-broker-thresholds.test.d.ts +0 -1
  506. package/dist/commands/sys/__tests__/config-broker-thresholds.test.js +0 -95
  507. package/dist/commands/sys/__tests__/config-keybindings.test.d.ts +0 -1
  508. package/dist/commands/sys/__tests__/config-keybindings.test.js +0 -55
  509. package/dist/commands/sys/__tests__/helpers/setup-runtime.d.ts +0 -5
  510. package/dist/commands/sys/__tests__/helpers/setup-runtime.js +0 -19
  511. package/dist/commands/sys/__tests__/setup-front-door.test.d.ts +0 -1
  512. package/dist/commands/sys/__tests__/setup-front-door.test.js +0 -102
  513. package/dist/commands/sys/__tests__/setup-models.test.d.ts +0 -1
  514. package/dist/commands/sys/__tests__/setup-models.test.js +0 -205
  515. package/dist/commands/sys/__tests__/setup-providers-viewport.test.d.ts +0 -1
  516. package/dist/commands/sys/__tests__/setup-providers-viewport.test.js +0 -155
  517. package/dist/commands/sys/__tests__/sync-deps.test.d.ts +0 -1
  518. package/dist/commands/sys/__tests__/sync-deps.test.js +0 -179
  519. package/dist/commands/sys/__tests__/sync-import.test.d.ts +0 -1
  520. package/dist/commands/sys/__tests__/sync-import.test.js +0 -315
  521. package/dist/core/__tests__/base-worker-prompt.test.d.ts +0 -1
  522. package/dist/core/__tests__/base-worker-prompt.test.js +0 -57
  523. package/dist/core/__tests__/command-plugins-surfaces.test.d.ts +0 -1
  524. package/dist/core/__tests__/command-plugins-surfaces.test.js +0 -359
  525. package/dist/core/__tests__/connection-reconnect-resume.test.d.ts +0 -1
  526. package/dist/core/__tests__/connection-reconnect-resume.test.js +0 -36
  527. package/dist/core/__tests__/finalization-migration.test.d.ts +0 -1
  528. package/dist/core/__tests__/finalization-migration.test.js +0 -38
  529. package/dist/core/__tests__/fixtures/extra-extension.d.ts +0 -1
  530. package/dist/core/__tests__/fixtures/extra-extension.js +0 -3
  531. package/dist/core/__tests__/fixtures/spawn-proof-target.test.d.ts +0 -1
  532. package/dist/core/__tests__/fixtures/spawn-proof-target.test.js +0 -15
  533. package/dist/core/__tests__/fork.test.d.ts +0 -1
  534. package/dist/core/__tests__/fork.test.js +0 -108
  535. package/dist/core/__tests__/full/broker-attach-limits.test.d.ts +0 -1
  536. package/dist/core/__tests__/full/broker-attach-limits.test.js +0 -147
  537. package/dist/core/__tests__/full/broker-deliver-ack.test.d.ts +0 -1
  538. package/dist/core/__tests__/full/broker-deliver-ack.test.js +0 -73
  539. package/dist/core/__tests__/full/broker-dialogs.test.d.ts +0 -1
  540. package/dist/core/__tests__/full/broker-dialogs.test.js +0 -180
  541. package/dist/core/__tests__/full/broker-display-snapshot.test.d.ts +0 -1
  542. package/dist/core/__tests__/full/broker-display-snapshot.test.js +0 -75
  543. package/dist/core/__tests__/full/broker-dormant-wake.test.d.ts +0 -1
  544. package/dist/core/__tests__/full/broker-dormant-wake.test.js +0 -130
  545. package/dist/core/__tests__/full/broker-model-changed-broadcast.test.d.ts +0 -1
  546. package/dist/core/__tests__/full/broker-model-changed-broadcast.test.js +0 -57
  547. package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.d.ts +0 -1
  548. package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.js +0 -80
  549. package/dist/core/__tests__/full/cascade-close.test.d.ts +0 -1
  550. package/dist/core/__tests__/full/cascade-close.test.js +0 -240
  551. package/dist/core/__tests__/full/detach-focus.test.d.ts +0 -1
  552. package/dist/core/__tests__/full/detach-focus.test.js +0 -126
  553. package/dist/core/__tests__/full/idle-release-settlement.test.d.ts +0 -1
  554. package/dist/core/__tests__/full/issue-146-yield-spawn-focus.test.d.ts +0 -1
  555. package/dist/core/__tests__/full/issue-146-yield-spawn-focus.test.js +0 -107
  556. package/dist/core/__tests__/full/park-clock-release.test.d.ts +0 -1
  557. package/dist/core/__tests__/full/park-clock-release.test.js +0 -56
  558. package/dist/core/__tests__/full/yield-message-transaction.test.d.ts +0 -1
  559. package/dist/core/__tests__/memory-index-render.test.d.ts +0 -1
  560. package/dist/core/__tests__/memory-index-render.test.js +0 -60
  561. package/dist/core/__tests__/model-routes.test.d.ts +0 -1
  562. package/dist/core/__tests__/model-routes.test.js +0 -41
  563. package/dist/core/__tests__/on-read-identity.test.d.ts +0 -1
  564. package/dist/core/__tests__/on-read-identity.test.js +0 -61
  565. package/dist/core/__tests__/phase4-review-migration.test.d.ts +0 -1
  566. package/dist/core/__tests__/phase4-review-migration.test.js +0 -100
  567. package/dist/core/__tests__/prune-to-limit.test.d.ts +0 -1
  568. package/dist/core/__tests__/prune-to-limit.test.js +0 -126
  569. package/dist/core/__tests__/reset.test.d.ts +0 -1
  570. package/dist/core/__tests__/reset.test.js +0 -69
  571. package/dist/core/__tests__/spawn-no-kickoff.test.d.ts +0 -1
  572. package/dist/core/__tests__/spawn-no-kickoff.test.js +0 -115
  573. package/dist/core/__tests__/steer-note.test.d.ts +0 -1
  574. package/dist/core/__tests__/steer-note.test.js +0 -41
  575. package/dist/core/__tests__/subcommand-tier.test.d.ts +0 -1
  576. package/dist/core/__tests__/subcommand-tier.test.js +0 -99
  577. package/dist/core/__tests__/subscription-delivery.test.js +0 -211
  578. package/dist/core/__tests__/test-runner-cheap-sharding.test.d.ts +0 -1
  579. package/dist/core/__tests__/test-runner-cheap-sharding.test.js +0 -97
  580. package/dist/core/__tests__/tmux-surface.test.js +0 -484
  581. package/dist/core/__tests__/wake-bearings.test.d.ts +0 -1
  582. package/dist/core/__tests__/wake-bearings.test.js +0 -102
  583. package/dist/core/canvas/__tests__/remote-event-stream.test.d.ts +0 -1
  584. package/dist/core/canvas/__tests__/remote-event-stream.test.js +0 -146
  585. package/dist/core/canvas/__tests__/source-resolve.test.d.ts +0 -1
  586. package/dist/core/canvas/__tests__/source-resolve.test.js +0 -77
  587. package/dist/core/canvas/browse/__tests__/model.test.d.ts +0 -1
  588. package/dist/core/canvas/browse/__tests__/model.test.js +0 -290
  589. package/dist/core/canvas/browse/__tests__/rebuild-coalescer.test.d.ts +0 -1
  590. package/dist/core/canvas/browse/__tests__/rebuild-coalescer.test.js +0 -134
  591. package/dist/core/canvas/browse/__tests__/render.test.d.ts +0 -1
  592. package/dist/core/canvas/browse/__tests__/render.test.js +0 -111
  593. package/dist/core/canvas/browse/rebuild-coalescer.d.ts +0 -7
  594. package/dist/core/canvas/browse/rebuild-coalescer.js +0 -61
  595. package/dist/core/canvas/remote-event-stream.d.ts +0 -24
  596. package/dist/core/canvas/remote-event-stream.js +0 -94
  597. package/dist/core/feed/__tests__/coalesce-human.test.d.ts +0 -1
  598. package/dist/core/feed/__tests__/coalesce-human.test.js +0 -36
  599. package/dist/core/feed/__tests__/empty-node-guard.test.d.ts +0 -1
  600. package/dist/core/feed/__tests__/empty-node-guard.test.js +0 -45
  601. package/dist/core/host-exports/__tests__/export-prunes-boot-skill.test.d.ts +0 -1
  602. package/dist/core/host-exports/__tests__/export-prunes-boot-skill.test.js +0 -84
  603. package/dist/core/human/__tests__/visible-paths.test.d.ts +0 -1
  604. package/dist/core/human/__tests__/visible-paths.test.js +0 -28
  605. package/dist/core/keybindings/__tests__/inbox-affordance.test.d.ts +0 -1
  606. package/dist/core/keybindings/__tests__/inbox-affordance.test.js +0 -38
  607. package/dist/core/review/__tests__/project.test.d.ts +0 -1
  608. package/dist/core/review/__tests__/project.test.js +0 -71
  609. package/dist/core/runtime/__tests__/command-expansion.test.d.ts +0 -1
  610. package/dist/core/runtime/__tests__/command-expansion.test.js +0 -25
  611. package/dist/core/runtime/__tests__/session-visibility.test.d.ts +0 -1
  612. package/dist/core/runtime/__tests__/session-visibility.test.js +0 -119
  613. package/dist/core/runtime/__tests__/spawn-at-given-id.test.d.ts +0 -1
  614. package/dist/core/runtime/__tests__/spawn-at-given-id.test.js +0 -34
  615. package/dist/core/runtime/tmux.js +0 -1314
  616. package/dist/daemon/api/__tests__/full/push-parity.test.d.ts +0 -1
  617. package/dist/daemon/api/__tests__/full/worktree-parity.test.d.ts +0 -1
  618. package/dist/daemon/api/__tests__/full/worktree-parity.test.js +0 -160
  619. package/dist/daemon/api/__tests__/inbox.test.d.ts +0 -1
  620. package/dist/daemon/api/__tests__/inbox.test.js +0 -489
  621. package/dist/web-client/apple-touch-icon.png +0 -0
  622. package/dist/web-client/assets/fragment-mono-latin-400-normal-BYwT3kSJ.woff +0 -0
  623. package/dist/web-client/assets/fragment-mono-latin-400-normal-yxdJ5AmL.woff2 +0 -0
  624. package/dist/web-client/assets/fragment-mono-latin-ext-400-normal-BbKYyvR9.woff2 +0 -0
  625. package/dist/web-client/assets/fragment-mono-latin-ext-400-normal-CT4YFKeK.woff +0 -0
  626. package/dist/web-client/assets/index-CCPaLeW7.css +0 -2
  627. package/dist/web-client/assets/index-kyOWsnZK.js +0 -86
  628. package/dist/web-client/assets/martian-mono-cyrillic-wght-normal-B84CD5C_.woff2 +0 -0
  629. package/dist/web-client/assets/martian-mono-latin-ext-wght-normal-DlL6xMw5.woff2 +0 -0
  630. package/dist/web-client/assets/martian-mono-latin-wght-normal-5W32yIyr.woff2 +0 -0
  631. package/dist/web-client/assets/workbox-window.prod.es5-Bd17z0YL.js +0 -2
  632. package/dist/web-client/icon-192.png +0 -0
  633. package/dist/web-client/icon-512.png +0 -0
  634. package/dist/web-client/icon-maskable-512.png +0 -0
  635. package/dist/web-client/index.html +0 -17
  636. package/dist/web-client/manifest.webmanifest +0 -1
  637. package/dist/web-client/sw.js +0 -1
  638. /package/dist/api/__tests__/{client.test.d.ts → serial/client.test.d.ts} +0 -0
  639. /package/dist/clients/inbox/__tests__/{inbox-controller.test.d.ts → serial/inbox-controller.test.d.ts} +0 -0
  640. /package/dist/clients/inbox/__tests__/{mount-panel.test.d.ts → serial/mount-panel.test.d.ts} +0 -0
  641. /package/dist/clients/{attach/__tests__/attach-chrome-remote.test.d.ts → inbox/review/__tests__/teardown-notice.test.d.ts} +0 -0
  642. /package/dist/{clients/attach/__tests__/attach-keybindings.test.d.ts → core/__tests__/broker-extension-canvas-db-boundary.test.d.ts} +0 -0
  643. /package/dist/{clients/attach/__tests__/crtr-output-coverage.test.d.ts → core/__tests__/seam/broker-attach-multiclient.test.d.ts} +0 -0
  644. /package/dist/core/__tests__/{full → seam}/broker-attach-stream.test.d.ts +0 -0
  645. /package/dist/core/__tests__/{full → seam}/broker-crash-teardown.test.d.ts +0 -0
  646. /package/dist/core/__tests__/{full → seam}/broker-pane-resolution.test.d.ts +0 -0
  647. /package/dist/core/__tests__/{full → seam}/broker-provider-retry.test.d.ts +0 -0
  648. /package/dist/core/__tests__/{full → seam}/broker-whip-interrupt.test.d.ts +0 -0
  649. /package/dist/{clients/attach/__tests__/editor-frame-title.test.d.ts → core/__tests__/seam/dormancy-release.test.d.ts} +0 -0
  650. /package/dist/{clients/attach/__tests__/input-controller-extension-command.test.d.ts → core/__tests__/seam/yield-refresh-transaction.test.d.ts} +0 -0
  651. /package/dist/core/__tests__/{broker-fork-seam.test.d.ts → serial/broker-fork-seam.test.d.ts} +0 -0
  652. /package/dist/core/__tests__/{broker-sdk-wiring.test.d.ts → serial/broker-sdk-wiring.test.d.ts} +0 -0
  653. /package/dist/core/__tests__/{broker-snapshot-history.test.d.ts → serial/broker-snapshot-history.test.d.ts} +0 -0
  654. /package/dist/core/__tests__/{command-plugins.test.d.ts → serial/command-plugins.test.d.ts} +0 -0
  655. /package/dist/core/__tests__/{deferred-no-wake.test.d.ts → serial/deferred-no-wake.test.d.ts} +0 -0
  656. /package/dist/core/__tests__/{flagship-lifecycle.test.d.ts → serial/flagship-lifecycle.test.d.ts} +0 -0
  657. /package/dist/core/__tests__/{host-teardown-process-group.test.d.ts → serial/host-teardown-process-group.test.d.ts} +0 -0
  658. /package/dist/core/__tests__/{human-deliver-e2e.test.d.ts → serial/human-deliver-e2e.test.d.ts} +0 -0
  659. /package/dist/core/__tests__/{live-mutation.test.d.ts → serial/live-mutation.test.d.ts} +0 -0
  660. /package/dist/core/__tests__/{refresh-stall-recycle.test.d.ts → serial/refresh-stall-recycle.test.d.ts} +0 -0
  661. /package/dist/core/__tests__/{revive.test.d.ts → serial/revive.test.d.ts} +0 -0
  662. /package/dist/core/__tests__/{spawn-root.test.d.ts → serial/spawn-root.test.d.ts} +0 -0
  663. /package/dist/core/__tests__/{subscription-delivery.test.d.ts → serial/subscription-delivery.test.d.ts} +0 -0
  664. /package/dist/core/__tests__/{tmux-surface.test.d.ts → serial/tmux-surface.test.d.ts} +0 -0
  665. /package/dist/core/__tests__/{worktree.test.d.ts → serial/worktree.test.d.ts} +0 -0
  666. /package/dist/{clients/attach/__tests__/markdown-headings.test.d.ts → core/__tests__/watchdog-abort-arms-retry.test.d.ts} +0 -0
  667. /package/dist/{clients/attach/__tests__/session-identity-refresh.test.d.ts → core/broker-client/__tests__/transport-socket.test.d.ts} +0 -0
  668. /package/dist/core/human/__tests__/{inbox-core.test.d.ts → serial/inbox-core.test.d.ts} +0 -0
  669. /package/dist/{clients/inbox/review/__tests__/remap.test.d.ts → daemon/api/__tests__/node-model-validation.test.d.ts} +0 -0
  670. /package/dist/{clients/surfaces/region.js → daemon/api/__tests__/review-terminal-conflict.test.d.ts} +0 -0
  671. /package/dist/daemon/api/__tests__/{full → seam}/api-server.test.d.ts +0 -0
  672. /package/dist/daemon/api/__tests__/{full → seam}/b10-attach-modelauth.test.d.ts +0 -0
  673. /package/dist/{clients/web/__tests__/push-engine.test.d.ts → daemon/api/__tests__/seam/leaf-api-parity.test.d.ts} +0 -0
@@ -1,1375 +1,54 @@
1
- import { BROKER_LIMIT_OVERRIDE_PRECEDENCE } from './setup.js';
2
- import { getMarkdownTheme, getSelectListTheme, getSettingsListTheme } from '@earendil-works/pi-coding-agent';
3
- import { TUI, ProcessTerminal, Input, matchesKey, truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
4
- import { LoginDialogComponent, ModelRegistry, ModelRuntime, ModelSelectorComponent, ThinkingSelectorComponent, SettingsManager } from '@earendil-works/pi-coding-agent';
5
- import { readAuthCredentials } from '../../core/auth-file.js';
6
- import { PoolCredentialStore } from '../../core/pool-credential-store.js';
7
- import { dialogAuthInteraction } from '../../core/auth-interaction.js';
8
- import { execFile } from 'node:child_process';
9
- import { basename, dirname, join } from 'node:path';
10
- import { applyTheme, defaultAgentDir } from '../../clients/attach/config.js';
11
- import { ANTHROPIC_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID, isManagedProvider, promoteSubscription, readSubscriptionPool, renameSubscription, } from '../../core/subscription-state.js';
12
- import { removeProviderRow } from '../../core/provider-management.js';
13
- import { readConfig, readRawConfigAtRoot, readRawScopeConfig, updateConfig, updateRawConfigAtomically, persistDefaultKindModel, persistModelLadders, readMergedLaunchConfig } from '../../core/config.js';
14
- import { findProjectScopeRoots, userScopeRoot } from '../../core/scope.js';
15
- import { loadProfileManifest, profileRoot } from '../../core/profiles/manifest.js';
16
- import { USER_SETTINGS } from '../../core/user-settings.js';
17
- import { BINDING_CATALOG, formatBinding, formatGesture, isAttachPaneBinding, matchesPiTuiInput, persistUserKeybindings, resolveKeybindings, KeybindingConcurrentEditError, KeybindingValidationError, } from '../../core/keybindings/index.js';
18
- import { configuredLadderProviders, inferPortableModelSelection } from '../../core/runtime/model-selection.js';
19
- import { cliClient } from '../api-client.js';
20
- import { installTmuxBindings, occupiedTmuxRootGestures } from '../../core/runtime/tmux-chrome.js';
21
- const ANSI_GREEN = (s) => `\x1b[32m${s}\x1b[39m`;
22
- const ANSI_YELLOW = (s) => `\x1b[33m${s}\x1b[39m`;
23
- const theme = {
24
- fg(role, text) {
25
- switch (role) {
26
- case 'border':
27
- return getMarkdownTheme().hr(text);
28
- case 'accent':
29
- return getMarkdownTheme().heading(text);
30
- case 'dim':
31
- return getSettingsListTheme().description(text);
32
- case 'muted':
33
- return getSelectListTheme().description(text);
34
- case 'mdLink':
35
- return getMarkdownTheme().link(text);
36
- case 'mdLinkUrl':
37
- return getMarkdownTheme().linkUrl(text);
38
- case 'success':
39
- return ANSI_GREEN(text);
40
- case 'warning':
41
- return ANSI_YELLOW(text);
42
- }
43
- },
44
- bg(_role, text) {
45
- return text;
46
- },
47
- bold(text) {
48
- return getMarkdownTheme().bold(text);
49
- },
50
- };
51
- import { buildSetupInitialState, executeSetupSubmission, formatSetupExecutionSummary, reconcileInboxFrontDoor, renderSetupStaticInstructions, resolveExaKeyPath, detectExaKey, SetupKeybindingsDraft, } from './setup-core.js';
52
- function wrapText(text, width) {
53
- const clean = text.trim();
54
- if (clean === '')
55
- return [''];
56
- if (width <= 1)
57
- return [truncateToWidth(clean, width)];
58
- const words = clean.split(/\s+/);
59
- const lines = [];
60
- let current = '';
61
- for (const word of words) {
62
- if (current === '') {
63
- current = word;
64
- continue;
65
- }
66
- const next = `${current} ${word}`;
67
- if (truncateToWidth(next, width) === next) {
68
- current = next;
69
- }
70
- else {
71
- lines.push(truncateToWidth(current, width));
72
- current = word;
73
- }
74
- }
75
- if (current !== '')
76
- lines.push(truncateToWidth(current, width));
77
- return lines.length > 0 ? lines : [''];
78
- }
79
- function currentWidth() {
80
- return Math.max(1, process.stdout.columns ?? 80);
81
- }
82
- function toolLabel(tool) {
83
- if (tool === 'nvim')
84
- return 'nvim';
85
- if (tool === 'nerd-font')
86
- return 'Nerd Font';
87
- return 'tmux';
88
- }
89
- function positiveInteger(value) {
90
- if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 1)
91
- return undefined;
92
- return value;
93
- }
94
- function parsePositiveInteger(value) {
95
- if (!/^\d+$/.test(value))
96
- return undefined;
97
- return positiveInteger(Number(value));
98
- }
99
- /** Inline badge for an optional API-key tab header: green ✓ when a key is
100
- * already resolvable (naming its source so an env-provided key isn't mistaken
101
- * for the writable file), dim — when absent. */
102
- function keyStatusBadge(presence, envVar) {
103
- if (!presence.present)
104
- return theme.fg('dim', ' · not set');
105
- const where = presence.source === 'env' ? `${envVar} env` : 'saved';
106
- return theme.fg('success', ` ✓ configured (${where})`);
107
- }
108
- function renderRows(lines, width) {
109
- return lines.map((line) => truncateToWidth(line, width));
110
- }
111
- function padAnsi(text, width) {
112
- const used = visibleWidth(text);
113
- if (used >= width)
114
- return truncateToWidth(text, width);
115
- return `${text}${' '.repeat(width - used)}`;
116
- }
117
- function rule(width) {
118
- return theme.fg('border', '─'.repeat(Math.max(0, width)));
119
- }
120
- function tabPill(label, active) {
121
- return active
122
- ? theme.bg('selectedBg', theme.fg('accent', theme.bold(` ${label} `)))
123
- : theme.fg('dim', ` ${label} `);
124
- }
125
- /** Centered, selection-following [start, end) slice of `count` items that
126
- * always includes `selected`, clamped to at most `capacity` entries. Mirrors
127
- * the oauth/config/session selector formula used elsewhere in pi. Exported for
128
- * direct unit coverage of the windowing math (see setup-providers-viewport.test.ts). */
129
- export function visibleRange(count, selected, capacity) {
130
- if (count <= 0)
131
- return { start: 0, end: 0 };
132
- const cap = Math.max(1, Math.min(capacity, count));
133
- const start = Math.max(0, Math.min(selected - Math.floor(cap / 2), count - cap));
134
- return { start, end: Math.min(count, start + cap) };
135
- }
136
- function framed(lines, width) {
137
- if (width < 4)
138
- return lines.map((line) => truncateToWidth(line, width));
139
- const innerWidth = width - 4;
140
- const border = theme.fg('border', '│');
141
- const top = theme.fg('border', `┌${'─'.repeat(width - 2)}┐`);
142
- const bottom = theme.fg('border', `└${'─'.repeat(width - 2)}┘`);
143
- const body = lines.map((line) => {
144
- const content = padAnsi(truncateToWidth(line, innerWidth), innerWidth);
145
- return `${border} ${content} ${border}`;
146
- });
147
- return [top, ...body, bottom];
148
- }
149
- // The frame's top/bottom border rows, spent by `framed()`.
150
- const FRAME_OVERHEAD = 2;
151
- // Models panel chrome: title, subtitle, blank — the first lines dropped when the
152
- // panel body has to fit a short terminal.
153
- const MODELS_CHROME_LINES = 3;
154
- /** Fits a modal's static content lines into `budget`, keeping the tail — the
155
- * actionable line every modal ends on (a confirm prompt, a live text input,
156
- * a login dialog's current prompt/hint) — and dropping from the head, where
157
- * informational chrome (title, count, a dialog's older status lines) lives.
158
- * Mirrors the roster/catalog priority (mandatory content survives, chrome
159
- * collapses first) for the panel's static modal bodies. */
160
- function fitTail(lines, budget) {
161
- return budget >= lines.length ? lines : lines.slice(lines.length - budget);
162
- }
163
- // Every panel interested in the same auth file shares its in-flight snapshot. The setup wizard
164
- // can rebuild panels faster than a cross-process lock round-trip; sharing that read avoids a
165
- // backlog of obsolete lock contenders while each panel still updates from the completed snapshot.
166
- const pendingAuthSnapshots = new Map();
167
- function readAuthSnapshot(authPath) {
168
- const existing = pendingAuthSnapshots.get(authPath);
169
- if (existing !== undefined)
170
- return existing;
171
- const pending = readAuthCredentials(authPath);
172
- pendingAuthSnapshots.set(authPath, pending);
173
- void pending.then(() => { if (pendingAuthSnapshots.get(authPath) === pending)
174
- pendingAuthSnapshots.delete(authPath); }, () => { if (pendingAuthSnapshots.get(authPath) === pending)
175
- pendingAuthSnapshots.delete(authPath); });
176
- return pending;
177
- }
178
- /** Curated setup-catalog view over pi's OAuth providers + the model registry. */
179
- export function buildProviderCatalog(runtime, registry) {
180
- const oauth = runtime.getProviders().filter((provider) => provider.auth.oauth !== undefined);
181
- const oauthIds = new Set(oauth.map((provider) => provider.id));
182
- const seen = new Set();
183
- return [
184
- ...oauth.map((provider) => ({
185
- providerId: provider.id,
186
- name: provider.name,
187
- managed: provider.id === ANTHROPIC_PROVIDER_ID || provider.id === OPENAI_CODEX_PROVIDER_ID,
188
- authType: 'oauth',
189
- })),
190
- ...registry.getAll().map((model) => ({
191
- providerId: model.provider,
192
- name: registry.getProviderDisplayName(model.provider),
193
- managed: model.provider === ANTHROPIC_PROVIDER_ID || model.provider === OPENAI_CODEX_PROVIDER_ID,
194
- authType: oauthIds.has(model.provider) ? 'oauth' : 'api_key',
195
- })),
196
- ].filter((provider) => !seen.has(provider.providerId) && Boolean(seen.add(provider.providerId)));
197
- }
198
- /** Classifies a `startLogin` failure into a user-facing status line. */
199
- export function classifyLoginFailure(error, aborted) {
200
- if (aborted)
201
- return 'Login cancelled';
202
- if (error instanceof Error && error.name === 'AbortError')
203
- return 'Login cancelled';
204
- return `Login error: ${error instanceof Error ? error.message : String(error)}`;
205
- }
206
- class ProviderPanel {
207
- tui;
208
- agentDir;
209
- runtime;
210
- registry;
211
- renameInput = new Input();
212
- rows = [];
213
- selected = 0;
214
- mode = 'list';
215
- notice = '';
216
- catalog = [];
217
- catalogIndex = 0;
218
- loginProvider;
219
- dialog;
220
- loginFlow;
221
- mutationInFlight = false;
222
- reloadGeneration = 0;
223
- disposed = false;
224
- // Wired by the host wizard to bust its render-line cache. The panel proxies pi's
225
- // LoginDialogComponent (it is not a real pi TUI child), so the host only repaints on
226
- // synchronous input. startLogin drives OAuth through async promise chains whose state
227
- // transitions land between keystrokes; notifyChange() lets each one force a fresh frame.
228
- onChange;
229
- constructor(tui, agentDir, runtime, registry) {
230
- this.tui = tui;
231
- this.agentDir = agentDir;
232
- this.runtime = runtime;
233
- this.registry = registry;
234
- this.renameInput.onSubmit = () => this.rename();
235
- this.renameInput.onEscape = () => { this.mode = 'list'; };
236
- this.reload();
237
- }
238
- // Repaint the host after an async login transition: bust the wizard's cached lines
239
- // (onChange) so render() recomputes the NEW state, then ask pi's loop to draw a frame.
240
- notifyChange() {
241
- this.onChange?.();
242
- this.tui.requestRender();
243
- }
244
- /** auth.json changed under us (a login landed, a provider was removed). pi 0.82
245
- * serves provider auth status from the runtime's SNAPSHOT rather than a live file
246
- * read, so the snapshot has to be re-taken before the roster can see the change.
247
- * `refresh()` is local-only here (the runtime is composed with model network access
248
- * off), so this is a credential re-list, not a catalog fetch. */
249
- reloadAfterAuthChange() {
250
- this.reload();
251
- void this.registry.refresh().then(() => {
252
- this.reload();
253
- this.notifyChange();
254
- });
255
- }
256
- reload() {
257
- const registry = this.registry;
258
- const authPath = join(this.agentDir, 'auth.json');
259
- const rows = [];
260
- for (const provider of buildProviderCatalog(this.runtime, registry)) {
261
- const status = registry.getProviderAuthStatus(provider.providerId);
262
- if (provider.managed) {
263
- const pool = readSubscriptionPool(provider.providerId);
264
- if (pool.length === 0 && !status.source)
265
- continue;
266
- // Every stored account carries a credential, so pool occupancy is what makes the
267
- // provider configured.
268
- rows.push({ ...provider, configured: pool.length > 0 || status.configured, source: status.source, sourceLabel: status.label });
269
- pool.forEach((account, index) => rows.push({ ...provider, label: account.label, preferred: index === 0, cooldownUntil: account.rateLimitedUntil, accountId: account.accountId }));
270
- continue;
271
- }
272
- if (!status.source)
273
- continue;
274
- rows.push({ ...provider, configured: status.configured, source: status.source, sourceLabel: status.label });
275
- }
276
- this.rows = rows;
277
- this.selected = Math.min(this.selected, Math.max(0, rows.length));
278
- const generation = ++this.reloadGeneration;
279
- void readAuthSnapshot(authPath).then((credentials) => {
280
- if (this.disposed || generation !== this.reloadGeneration)
281
- return;
282
- for (const row of this.rows) {
283
- if (row.managed)
284
- continue;
285
- const accountId = credentials[row.providerId]?.accountId;
286
- row.accountId = typeof accountId === 'string' ? accountId : undefined;
287
- }
288
- this.notifyChange();
289
- }).catch((error) => {
290
- if (this.disposed || generation !== this.reloadGeneration)
291
- return;
292
- this.notice = error instanceof Error ? error.message : String(error);
293
- this.notifyChange();
294
- });
295
- }
296
- selectedRow() { return this.rows[this.selected]; }
297
- openBrowser(url) {
298
- const command = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';
299
- const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
300
- execFile(command, args, { timeout: 5_000 }, () => { });
301
- }
302
- openCatalog() {
303
- this.catalog = buildProviderCatalog(this.runtime, this.registry);
304
- this.catalogIndex = 0;
305
- this.mode = 'catalog';
306
- }
307
- startLogin(provider, label) {
308
- this.loginProvider = provider;
309
- let flow;
310
- const complete = (message) => {
311
- if (!flow.active || this.loginFlow !== flow)
312
- return;
313
- flow.active = false;
314
- this.loginFlow = undefined;
315
- this.dialog = undefined;
316
- this.mode = 'list';
317
- this.notice = message;
318
- this.reloadAfterAuthChange();
319
- this.notifyChange();
320
- };
321
- const dialog = new LoginDialogComponent(this.tui, provider.providerId, (success, message) => {
322
- if (!success)
323
- complete(message ? `Login failed: ${message}` : 'Login cancelled');
324
- }, provider.name);
325
- flow = { dialog, active: true, committing: false };
326
- this.loginFlow = flow;
327
- this.dialog = dialog;
328
- this.mode = 'login';
329
- const handleDialogInput = dialog.handleInput.bind(dialog);
330
- dialog.handleInput = (data) => { if (!flow.committing)
331
- handleDialogInput(data); };
332
- const isActive = () => flow.active && this.loginFlow === flow && !dialog.signal.aborted;
333
- const beginCommit = () => {
334
- if (!isActive())
335
- return false;
336
- flow.committing = true;
337
- dialog.showDetails(['Saving credentials…']);
338
- return true;
339
- };
340
- const fail = (error) => {
341
- complete(classifyLoginFailure(error, dialog.signal.aborted));
342
- };
343
- if (provider.authType === 'api_key') {
344
- void dialog.showPrompt(`Enter API key for ${provider.name}:`, 'sk-...').then(async (key) => {
345
- if (!isActive())
346
- return;
347
- if (!key.trim()) {
348
- complete('No key entered — login cancelled');
349
- return;
350
- }
351
- // Interactive collection stays client-side (TTY); the WRITE moves to the
352
- // API (PUT /model-auth/{provider}) so this process never touches auth.json.
353
- if (!beginCommit())
354
- return;
355
- await cliClient().installCredential(provider.providerId, { kind: 'api_key', api_key: key.trim() });
356
- complete(`API key saved for ${provider.name}`);
357
- }).catch(fail);
358
- return;
359
- }
360
- // pi 0.82 collapsed the six login callbacks into one AuthInteraction; the adapter
361
- // onto LoginDialogComponent is shared with the attach viewer's /login overlay.
362
- const interaction = dialogAuthInteraction(dialog, {
363
- openBrowser: (url) => this.openBrowser(url),
364
- onChange: () => this.notifyChange(),
365
- isActive,
366
- });
367
- const oauth = this.runtime.getProvider(provider.providerId)?.auth.oauth;
368
- if (!oauth) {
369
- complete(`Unknown OAuth provider: ${provider.providerId}`);
370
- return;
371
- }
372
- if (!provider.managed) {
373
- // Collect tokens client-side (browser/TTY), then WRITE via the API so this
374
- // process never touches auth.json. In 0.82 `provider.auth.oauth.login()`
375
- // RETURNS the credential without persisting it (persistence is app-owned),
376
- // which is exactly the collect-without-persist half this needs.
377
- void oauth.login(interaction).then(async (credentials) => {
378
- if (!isActive())
379
- return;
380
- if (!beginCommit())
381
- return;
382
- // Forward non-standard credential keys (e.g. github-copilot's `enterpriseUrl`)
383
- // so enterprise routing and token refresh retain the provider data they require.
384
- // The named tokens and store `type` are handled separately; all other keys travel
385
- // through `extra` and the handler writes them into auth.json.
386
- const { type: _type, access, refresh, expires, ...extra } = credentials;
387
- await cliClient().installCredential(provider.providerId, {
388
- kind: 'oauth',
389
- access_token: access,
390
- refresh_token: refresh,
391
- expires_at: new Date(expires).toISOString(),
392
- ...(Object.keys(extra).length > 0 ? { extra } : {}),
393
- });
394
- if (isActive())
395
- complete(`Logged in to ${provider.name}`);
396
- }).catch(fail);
397
- return;
398
- }
399
- // Snapshot the newest pool cooldown before OAuth so a later 429 remains authoritative.
400
- const lastRateLimitedAt = Math.max(0, ...readSubscriptionPool(provider.providerId).map((entry) => entry.lastRateLimitedAt));
401
- void oauth.login(interaction).then(async (credential) => {
402
- if (!isActive())
403
- return;
404
- if (!beginCommit())
405
- return;
406
- const accountId = typeof credential.accountId === 'string' && credential.accountId.trim() ? credential.accountId.trim() : undefined;
407
- // Collected via pi's managed login (client); the API persists it into the pool, where
408
- // account identity selects an existing row or labels a newly seen account.
409
- await cliClient().installCredential(provider.providerId, {
410
- kind: 'managed_account',
411
- ...(label ? { label } : {}),
412
- ...(label ? {} : { auto_label: true }),
413
- ...(accountId ? { account_id: accountId } : {}),
414
- access_token: credential.access,
415
- refresh_token: credential.refresh,
416
- expires_at: new Date(credential.expires).toISOString(),
417
- last_rate_limited_at: lastRateLimitedAt,
418
- });
419
- if (isActive())
420
- complete(`Logged in to ${provider.name}`);
421
- }).catch(fail);
422
- }
423
- isNestedFlowActive() { return this.mode === 'login'; }
424
- dispose() {
425
- this.disposed = true;
426
- const flow = this.loginFlow;
427
- if (!flow)
428
- return;
429
- flow.active = false;
430
- this.loginFlow = undefined;
431
- this.dialog = undefined;
432
- flow.dialog.handleInput('\u001b');
433
- }
434
- runMutation(operation) {
435
- if (this.mutationInFlight)
436
- return;
437
- this.mutationInFlight = true;
438
- void operation().catch((error) => {
439
- this.notice = error instanceof Error ? error.message : String(error);
440
- }).finally(() => {
441
- this.mutationInFlight = false;
442
- this.notifyChange();
443
- });
444
- }
445
- rename() {
446
- const row = this.selectedRow();
447
- if (!row?.managed || !row.label)
448
- return;
449
- this.mode = 'list';
450
- this.runMutation(async () => {
451
- await renameSubscription(row.providerId, row.label, this.renameInput.getValue());
452
- this.notice = `Renamed ${row.name} account`;
453
- this.reload();
454
- });
455
- }
456
- remove(row) {
457
- this.runMutation(async () => {
458
- this.notice = await removeProviderRow(this.agentDir, row);
459
- this.reloadAfterAuthChange();
460
- });
461
- }
462
- promote(row) {
463
- this.runMutation(async () => {
464
- await promoteSubscription(row.providerId, row.label);
465
- this.notice = `Preferred ${row.name} account`;
466
- this.reload();
467
- });
468
- }
469
- handleInput(data) {
470
- if (this.mode === 'login') {
471
- this.dialog?.handleInput(data);
472
- return true;
473
- }
474
- if (this.mutationInFlight)
475
- return true;
476
- if (this.mode === 'rename' || this.mode === 'label') {
477
- this.renameInput.handleInput(data);
478
- return true;
479
- }
480
- if (this.mode === 'catalog') {
481
- if (matchesKey(data, 'escape')) {
482
- this.mode = 'list';
483
- return true;
484
- }
485
- if (matchesKey(data, 'j') || matchesKey(data, 'down')) {
486
- this.catalogIndex = Math.min(this.catalog.length - 1, this.catalogIndex + 1);
487
- return true;
488
- }
489
- if (matchesKey(data, 'k') || matchesKey(data, 'up')) {
490
- this.catalogIndex = Math.max(0, this.catalogIndex - 1);
491
- return true;
492
- }
493
- if (matchesKey(data, 'enter')) {
494
- const provider = this.catalog[this.catalogIndex];
495
- if (provider.managed) {
496
- this.loginProvider = provider;
497
- this.renameInput.setValue('');
498
- this.renameInput.onSubmit = () => { const label = this.renameInput.getValue().trim(); if (label)
499
- this.startLogin(provider, label); };
500
- this.mode = 'label';
501
- }
502
- else
503
- this.startLogin(provider);
504
- return true;
505
- }
506
- return true;
507
- }
508
- if (this.mode === 'remove') {
509
- if (matchesKey(data, 'y') || matchesKey(data, 'enter')) {
510
- const row = this.selectedRow();
511
- if (row && (row.managed || row.configured)) {
512
- this.remove(row);
513
- }
514
- }
515
- this.mode = 'list';
516
- return true;
517
- }
518
- if (matchesKey(data, 'j') || matchesKey(data, 'down')) {
519
- this.selected = Math.min(this.rows.length, this.selected + 1);
520
- return true;
521
- }
522
- if (matchesKey(data, 'k') || matchesKey(data, 'up')) {
523
- this.selected = Math.max(0, this.selected - 1);
524
- return true;
525
- }
526
- const row = this.selectedRow();
527
- if (matchesKey(data, 'p') && row?.managed && row.label) {
528
- this.promote(row);
529
- return true;
530
- }
531
- if (matchesKey(data, 'r') && row?.managed && row.label) {
532
- this.renameInput.setValue(row.label);
533
- this.renameInput.onSubmit = () => this.rename();
534
- this.mode = 'rename';
535
- return true;
536
- }
537
- if (matchesKey(data, 'x') && (row?.managed || row?.configured)) {
538
- this.mode = 'remove';
539
- return true;
540
- }
541
- if (matchesKey(data, 'a')) {
542
- this.openCatalog();
543
- return true;
544
- }
545
- const viewOnly = row?.source === 'environment' || row?.source === 'runtime';
546
- if (matchesKey(data, 'l') && row && !viewOnly) {
547
- if (row.managed && row.label)
548
- this.startLogin(row, row.label);
549
- else
550
- this.startLogin(row);
551
- return true;
552
- }
553
- if (matchesKey(data, 'enter')) {
554
- if (row && !viewOnly)
555
- this.startLogin(row, row.managed && row.label ? row.label : undefined);
556
- else if (!row)
557
- this.openCatalog();
558
- return true;
559
- }
560
- return false;
561
- }
562
- // `height` is the panel's full framed budget (as returned by `framed()` — border
563
- // rows included), supplied by the host wizard's live terminal-row callback. It can
564
- // shrink to just a couple of rows, so every part of the panel below the mandatory
565
- // content is chrome that collapses under pressure. For the roster/catalog list this
566
- // is: the frame itself drops first (there's no room for both borders and a row),
567
- // then the title/count header, then the hint/notice/empty-state line, then the
568
- // position indicator — never letting the selected row scroll out of view. The
569
- // login/rename/label/remove modals have no list to window, so they apply the same
570
- // budget via `fitTail`: their bodies always end on the actionable line (a live
571
- // input, a confirm prompt, a login dialog's current prompt), so clipping from the
572
- // head sheds chrome (title/count/older status lines) first and keeps that line.
573
- render(width, height) {
574
- const titleLine = theme.fg('accent', theme.bold('Providers'));
575
- const countLine = theme.fg('dim', `${this.rows.filter((row) => row.label === undefined).length} configured`);
576
- const useFrame = height >= FRAME_OVERHEAD + 1;
577
- const contentBudget = Math.max(1, useFrame ? height - FRAME_OVERHEAD : height);
578
- let body;
579
- if (this.mode === 'login') {
580
- body = fitTail(this.dialog?.render(Math.max(1, width - 4)) ?? [], contentBudget);
581
- }
582
- else if (this.mode === 'rename' || this.mode === 'label') {
583
- body = fitTail([titleLine, countLine, '', theme.fg('accent', this.mode === 'rename' ? 'Rename account' : `Name the ${this.loginProvider?.name ?? 'provider'} account`), ...this.renameInput.render(Math.max(1, width - 8))], contentBudget);
584
- }
585
- else if (this.mode === 'remove') {
586
- const target = this.selectedRow();
587
- const label = target?.label ?? (target?.managed ? `${target.name} · all accounts` : target?.name ?? 'account');
588
- body = fitTail([titleLine, countLine, '', theme.fg('warning', `Remove ${label}? y/enter confirm · any other key cancel`)], contentBudget);
589
- }
590
- else {
591
- // The title/count header is the first chrome to give way: only as many of its
592
- // two lines are kept as fit while still reserving at least one row for the
593
- // mandatory selected entry.
594
- const header = [titleLine, countLine].slice(0, Math.max(0, contentBudget - 1));
595
- const remaining = contentBudget - header.length; // always >= 1
596
- body = [...header];
597
- if (this.mode === 'catalog') {
598
- const items = this.catalog.length;
599
- const clipped = items > remaining;
600
- // Only trade a row for the position indicator when there's a spare row to trade;
601
- // at `remaining === 1` the sole row goes to the mandatory selected entry instead.
602
- const reserveIndicator = clipped && remaining >= 2;
603
- const rowBudget = reserveIndicator ? remaining - 1 : remaining;
604
- const { start, end } = visibleRange(items, this.catalogIndex, rowBudget);
605
- let leftover = remaining - (end - start) - (reserveIndicator ? 1 : 0);
606
- if (leftover > 0) {
607
- body.push(theme.fg('dim', 'Select a provider to add · esc back'));
608
- leftover -= 1;
609
- }
610
- for (let index = start; index < end; index++) {
611
- const provider = this.catalog[index];
612
- body.push(`${index === this.catalogIndex ? theme.fg('accent', '▶') : ' '} ${provider.name}`);
613
- }
614
- if (reserveIndicator)
615
- body.push(theme.fg('dim', `(${this.catalogIndex + 1}/${items})`));
616
- }
617
- else {
618
- const totalEntries = this.rows.length + 1; // + the trailing "Add provider" sentinel
619
- const clipped = totalEntries > remaining;
620
- const reserveIndicator = clipped && remaining >= 2;
621
- const rowBudget = reserveIndicator ? remaining - 1 : remaining;
622
- const { start, end } = visibleRange(totalEntries, this.selected, rowBudget);
623
- let leftover = remaining - (end - start) - (reserveIndicator ? 1 : 0);
624
- const showEmpty = this.rows.length === 0 && leftover > 0;
625
- if (showEmpty)
626
- leftover -= 1;
627
- const showNotice = Boolean(this.notice) && leftover > 0;
628
- if (showEmpty)
629
- body.push(theme.fg('warning', 'No provider configured yet.'));
630
- for (let index = start; index < end; index++) {
631
- if (index === this.rows.length) {
632
- body.push(`${this.selected === this.rows.length ? theme.fg('accent', '▶') : ' '} ${theme.fg('accent', '+ Add provider')}`);
633
- continue;
634
- }
635
- const row = this.rows[index];
636
- const selected = index === this.selected ? theme.fg('accent', '▶') : ' ';
637
- if (!row.label) {
638
- const state = row.managed
639
- ? `${this.rows.filter((item) => item.providerId === row.providerId && item.label).length} accounts`
640
- : row.source === 'environment' || row.source === 'runtime'
641
- ? `${row.source}${row.sourceLabel ? ` · ${row.sourceLabel}` : ''} (view only)`
642
- : 'configured';
643
- body.push(`${selected} ${theme.fg('accent', row.name)}${theme.fg('dim', ` ${state}`)}`);
644
- }
645
- else {
646
- const health = row.cooldownUntil && row.cooldownUntil > Date.now() ? `cooldown ${Math.ceil((row.cooldownUntil - Date.now()) / 60000)}m` : 'ready';
647
- const identity = row.accountId ? ` · ${row.accountId.length > 12 ? `${row.accountId.slice(0, 8)}…${row.accountId.slice(-4)}` : row.accountId}` : '';
648
- body.push(`${selected} ${row.preferred ? theme.fg('success', '●') : ' '} ${row.label}${theme.fg('dim', identity)} ${theme.fg('dim', health)}${row.preferred ? theme.fg('accent', ' preferred') : ''}`);
649
- }
650
- }
651
- if (reserveIndicator)
652
- body.push(theme.fg('dim', `(${this.selected + 1}/${totalEntries})`));
653
- if (showNotice)
654
- body.push(theme.fg('dim', this.notice));
655
- }
656
- }
657
- return useFrame ? framed(body, width) : body.map((line) => truncateToWidth(line, width));
658
- }
659
- }
660
- const TAB_LABELS = ['Providers', 'Plugins', 'System', 'Exa', 'Keybindings', 'Models', 'Broker limits', 'Settings'];
661
- const SETTINGS_TAB = 7;
662
- const STRENGTHS = ['ultra', 'strong', 'medium', 'light'];
663
- const LADDER_PROVIDER_KEYS = ['anthropic', 'openai'];
664
- const THINKING_LEVELS = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
665
- const RUNTIME_PROVIDER_ID = { anthropic: ANTHROPIC_PROVIDER_ID, openai: OPENAI_CODEX_PROVIDER_ID };
666
- const PROVIDER_LABEL = { anthropic: 'Anthropic', openai: 'OpenAI' };
667
- const KIND_COLUMNS = [
668
- { mode: 'base', dimension: 'provider' },
669
- { mode: 'base', dimension: 'strength' },
670
- { mode: 'orchestrator', dimension: 'provider' },
671
- { mode: 'orchestrator', dimension: 'strength' },
672
- ];
673
- function modelConfigTargets() {
674
- const userRoot = userScopeRoot();
675
- const profileId = process.env['CRTR_PROFILE_ID'] || null;
676
- let profileTarget;
677
- if (profileId !== null) {
678
- try {
679
- const entry = loadProfileManifest(profileId);
680
- profileTarget = { label: `Profile: ${entry.manifest.name}`, root: profileRoot(entry.profileId) };
681
- }
682
- catch {
683
- // A stale profile env should not make setup unusable; user/project
684
- // targets remain valid and launch resolution already ignores it.
685
- }
686
- }
687
- const projectRoots = findProjectScopeRoots(process.cwd(), profileId);
688
- const projectTargets = projectRoots.map((root) => ({
689
- label: `Project: ${basename(dirname(root))}`,
690
- root,
691
- }));
692
- return {
693
- targets: [{ label: 'User', root: userRoot }, ...(profileTarget ? [profileTarget] : []), ...projectTargets],
694
- precedenceRoots: [...projectRoots, ...(profileTarget ? [profileTarget.root] : []), userRoot],
695
- };
696
- }
697
- /** Parse a model ref `<provider>/<modelId>:<level>` into parts. */
698
- function parseModelRef(ref) {
699
- if (!ref)
700
- return {};
701
- const trimmed = ref.trim();
702
- const slash = trimmed.indexOf('/');
703
- const provider = slash === -1 ? undefined : trimmed.slice(0, slash);
704
- const rest = slash === -1 ? trimmed : trimmed.slice(slash + 1);
705
- const m = rest.match(/^(.*?):(off|minimal|low|medium|high|xhigh|max)$/i);
706
- return { provider, id: m ? m[1] : rest, level: m ? m[2].toLowerCase() : undefined };
707
- }
708
- /** Short display for a ladder cell value — strip the `<provider>/` prefix. */
709
- function shortRef(ref) {
710
- if (!ref)
711
- return '\u2014';
712
- const slash = ref.indexOf('/');
713
- return slash === -1 ? ref : ref.slice(slash + 1);
714
- }
715
- class ModelsPanel {
716
- tui;
717
- runtime;
718
- registry;
719
- matches;
720
- label;
721
- mode = 'list';
722
- ladders;
723
- kinds;
724
- /** The ladder providers this panel offers — only columns whose runtime
725
- * provider is signed in (all of them when nobody is, so config stays
726
- * reachable pre-login). Recomputed with every reload/refresh so a login
727
- * made on the Providers tab shows up when this tab is next entered. */
728
- providerKeys = LADDER_PROVIDER_KEYS;
729
- targets;
730
- precedenceRoots;
731
- targetIndex = 0;
732
- rawTarget;
733
- rows = [];
734
- selected = 0;
735
- column = 0;
736
- notice = '';
737
- modelSelector;
738
- thinkingSelector;
739
- pending;
740
- // The kind cell being edited: which mode/dimension, the options to choose
741
- // from, and the current mode selection so the untouched dimension survives.
742
- pendingDim;
743
- dimPickIndex = 0;
744
- onChange;
745
- constructor(tui, runtime, registry, matches, label) {
746
- this.tui = tui;
747
- this.runtime = runtime;
748
- this.registry = registry;
749
- this.matches = matches;
750
- this.label = label;
751
- const scopes = modelConfigTargets();
752
- this.targets = scopes.targets;
753
- this.precedenceRoots = scopes.precedenceRoots;
754
- this.rawTarget = readRawConfigAtRoot(this.currentTarget().root);
755
- const merged = readMergedLaunchConfig();
756
- this.ladders = merged.modelLadders;
757
- this.kinds = merged.kinds;
758
- this.providerKeys = this.offeredProviders();
759
- this.rows = this.buildRows();
760
- }
761
- /** Same signed-in rule as the broker's Alt+M cycle: pool occupancy for a
762
- * managed provider (read live), else the registry's auth status. */
763
- offeredProviders() {
764
- try {
765
- return configuredLadderProviders((providerId) => (isManagedProvider(providerId) && readSubscriptionPool(providerId).length > 0)
766
- || this.registry.getProviderAuthStatus(providerId).configured, this.ladders);
767
- }
768
- catch {
769
- return LADDER_PROVIDER_KEYS; // auth state unreadable — offer the full grid
770
- }
771
- }
772
- currentTarget() {
773
- return this.targets[this.targetIndex];
774
- }
775
- cycleTarget() {
776
- this.targetIndex = (this.targetIndex + 1) % this.targets.length;
777
- this.rawTarget = readRawConfigAtRoot(this.currentTarget().root);
778
- this.notice = '';
779
- this.notify();
780
- }
781
- shadowingPath(hasOverride) {
782
- for (const root of this.precedenceRoots) {
783
- if (root === this.currentTarget().root)
784
- break;
785
- const raw = readRawConfigAtRoot(root);
786
- if (raw && hasOverride(raw))
787
- return join(root, 'config.json');
788
- }
789
- return undefined;
790
- }
791
- savedButShadowed(path) {
792
- return `Saved to ${this.currentTarget().label}; overridden by ${path ?? 'a more specific config'}`;
793
- }
794
- buildRows() {
795
- const rows = [{ section: 'ladder', kind: 'defaultProvider' }];
796
- for (const strength of STRENGTHS)
797
- rows.push({ section: 'ladder', kind: 'strength', strength });
798
- for (const name of Object.keys(this.kinds)) {
799
- if (name.includes('/'))
800
- continue;
801
- rows.push({ section: 'kind', name });
802
- }
803
- return rows;
804
- }
805
- reload() {
806
- const merged = readMergedLaunchConfig();
807
- this.ladders = merged.modelLadders;
808
- this.kinds = merged.kinds;
809
- this.rawTarget = readRawConfigAtRoot(this.currentTarget().root);
810
- this.providerKeys = this.offeredProviders();
811
- this.rows = this.buildRows();
812
- this.selected = Math.min(this.selected, this.rows.length - 1);
813
- this.clampColumn();
814
- }
815
- /** Re-read config and auth state — called when the Models tab is entered so
816
- * a provider signed in (or out) on the Providers tab is reflected here. */
817
- refresh() {
818
- this.reload();
819
- this.notify();
820
- }
821
- notify() {
822
- this.onChange?.();
823
- this.tui.requestRender();
824
- }
825
- isPickerOpen() {
826
- return this.mode !== 'list';
827
- }
828
- currentRow() {
829
- return this.rows[Math.min(this.selected, this.rows.length - 1)];
830
- }
831
- columnsFor(row) {
832
- if (row.section === 'ladder')
833
- return row.kind === 'strength' ? this.providerKeys.length : 1;
834
- return KIND_COLUMNS.length;
835
- }
836
- clampColumn() {
837
- this.column = Math.min(this.column, this.columnsFor(this.currentRow()) - 1);
838
- }
839
- handleInput(data) {
840
- if (this.mode === 'dimension-picker') {
841
- if (this.matches('crtr.setup.models.commit', data)) {
842
- this.commitDimensionPick();
843
- return true;
844
- }
845
- if (this.matches('crtr.setup.models.cancel', data)) {
846
- this.closePicker();
847
- return true;
848
- }
849
- if (this.matches('crtr.setup.list-down', data)) {
850
- this.dimPickIndex = Math.min((this.pendingDim?.options.length ?? 1) - 1, this.dimPickIndex + 1);
851
- return true;
852
- }
853
- if (this.matches('crtr.setup.list-up', data)) {
854
- this.dimPickIndex = Math.max(0, this.dimPickIndex - 1);
855
- return true;
856
- }
857
- return true;
858
- }
859
- if (this.mode === 'model-picker') {
860
- const input = this.matches('crtr.setup.models.commit', data)
861
- ? '\r'
862
- : this.matches('crtr.setup.models.cancel', data) ? '\u001b' : data;
863
- this.modelSelector?.handleInput(input);
864
- return true;
865
- }
866
- if (this.mode === 'thinking-picker') {
867
- const input = this.matches('crtr.setup.models.commit', data)
868
- ? '\r'
869
- : this.matches('crtr.setup.models.cancel', data) ? '\u001b' : data;
870
- this.thinkingSelector?.getSelectList().handleInput(input);
871
- return true;
872
- }
873
- if (this.matches('crtr.setup.list-down', data)) {
874
- this.selected = Math.min(this.rows.length - 1, this.selected + 1);
875
- this.clampColumn();
876
- this.notice = '';
877
- return true;
878
- }
879
- if (this.matches('crtr.setup.list-up', data)) {
880
- this.selected = Math.max(0, this.selected - 1);
881
- this.clampColumn();
882
- this.notice = '';
883
- return true;
884
- }
885
- if (this.matches('crtr.setup.cell-previous', data)) {
886
- this.column = Math.max(0, this.column - 1);
887
- return true;
888
- }
889
- if (this.matches('crtr.setup.cell-next', data)) {
890
- this.column = Math.min(this.columnsFor(this.currentRow()) - 1, this.column + 1);
891
- return true;
892
- }
893
- if (this.matches('crtr.setup.models.scope-next', data)) {
894
- this.cycleTarget();
895
- return true;
896
- }
897
- if (this.matches('crtr.setup.models.open', data)) {
898
- this.activateCell();
899
- return true;
900
- }
901
- return false;
902
- }
903
- activateCell() {
904
- const row = this.currentRow();
905
- this.notice = '';
906
- if (row.section === 'ladder' && row.kind === 'defaultProvider') {
907
- const currentProvider = inferPortableModelSelection(undefined, this.ladders).provider;
908
- // Cycle within the offered providers only; with a single signed-in
909
- // provider the default has nowhere else to go.
910
- const offered = this.providerKeys;
911
- const next = offered[(Math.max(0, offered.indexOf(currentProvider)) + 1) % offered.length];
912
- if (next === currentProvider)
913
- return;
914
- try {
915
- persistModelLadders(this.currentTarget().root, { defaultProvider: next });
916
- }
917
- catch (error) {
918
- this.notice = `Could not save: ${error instanceof Error ? error.message : String(error)}`;
919
- this.notify();
920
- return;
921
- }
922
- this.reload();
923
- if ((this.ladders.defaultProvider ?? 'anthropic') !== next) {
924
- const path = this.shadowingPath((raw) => raw.modelLadders?.defaultProvider !== undefined);
925
- this.notice = this.savedButShadowed(path);
926
- }
927
- this.notify();
928
- return;
929
- }
930
- if (row.section === 'ladder' && row.kind === 'strength') {
931
- this.openModelPicker(this.providerKeys[this.column], row.strength);
932
- return;
933
- }
934
- if (row.section === 'kind') {
935
- const col = KIND_COLUMNS[this.column];
936
- if (!col)
937
- return;
938
- this.openDimensionPicker(row.name, col.mode, col.dimension);
939
- }
940
- }
941
- /** Snapshot of the mode's current provider+strength. For orchestrator this
942
- * falls back to the base `model` when no explicit override exists, so the
943
- * inherited pair is what an edit preserves. */
944
- kindSelection(name, mode) {
945
- const cfg = this.kinds[name];
946
- const value = mode === 'orchestrator' ? (cfg?.orchestratorModel ?? cfg?.model) : cfg?.model;
947
- return inferPortableModelSelection(value, this.ladders);
948
- }
949
- openDimensionPicker(name, mode, dimension) {
950
- const current = this.kindSelection(name, mode);
951
- const options = dimension === 'provider' ? this.providerKeys : STRENGTHS;
952
- const currentToken = dimension === 'provider' ? current.provider : current.strength;
953
- this.pendingDim = { name, mode, dimension, options, current };
954
- this.dimPickIndex = Math.max(0, options.indexOf(currentToken));
955
- this.mode = 'dimension-picker';
956
- this.notify();
957
- }
958
- commitDimensionPick() {
959
- const pending = this.pendingDim;
960
- const chosen = pending?.options[this.dimPickIndex];
961
- if (!pending || chosen === undefined) {
962
- this.closePicker();
963
- return;
964
- }
965
- const provider = pending.dimension === 'provider' ? chosen : pending.current.provider;
966
- const strength = pending.dimension === 'strength' ? chosen : pending.current.strength;
967
- const token = `${provider}/${strength}`;
968
- try {
969
- persistDefaultKindModel({
970
- root: this.currentTarget().root,
971
- kind: pending.name,
972
- model: token,
973
- orchestrator: pending.mode === 'orchestrator',
974
- });
975
- }
976
- catch (error) {
977
- this.notice = `Could not save: ${error instanceof Error ? error.message : String(error)}`;
978
- this.mode = 'list';
979
- this.pendingDim = undefined;
980
- this.notify();
981
- return;
982
- }
983
- this.mode = 'list';
984
- this.pendingDim = undefined;
985
- this.reload();
986
- const after = this.kinds[pending.name];
987
- const effective = pending.mode === 'orchestrator' ? after?.orchestratorModel : after?.model;
988
- if (effective !== token) {
989
- const field = pending.mode === 'orchestrator' ? 'orchestratorModel' : 'model';
990
- const path = this.shadowingPath((raw) => {
991
- const entry = raw.kinds?.[pending.name];
992
- return entry?.[field] !== undefined;
993
- });
994
- this.notice = this.savedButShadowed(path);
995
- }
996
- this.notify();
997
- }
998
- openModelPicker(providerKey, strength) {
999
- const expectedRuntime = RUNTIME_PROVIDER_ID[providerKey];
1000
- const registry = this.registry;
1001
- const parsed = parseModelRef(this.ladders[providerKey][strength]);
1002
- const currentModel = parsed.provider && parsed.id ? registry.find(parsed.provider, parsed.id) : undefined;
1003
- const scoped = registry.getAvailable().filter((m) => m.provider === expectedRuntime).map((m) => ({ model: m }));
1004
- if (scoped.length === 0) {
1005
- this.notice = `No ${providerKey} models available \u2014 sign in first.`;
1006
- this.notify();
1007
- return;
1008
- }
1009
- this.pending = { providerKey, strength, expectedRuntime };
1010
- this.modelSelector = new ModelSelectorComponent(this.tui, currentModel, SettingsManager.inMemory(), this.runtime, scoped, (model) => this.onModelPicked(model), () => this.closePicker());
1011
- this.modelSelector.focused = true;
1012
- this.mode = 'model-picker';
1013
- this.notify();
1014
- }
1015
- onModelPicked(model) {
1016
- if (!this.pending) {
1017
- this.closePicker();
1018
- return;
1019
- }
1020
- if (model.provider !== this.pending.expectedRuntime) {
1021
- this.notice = `That is a ${model.provider} model \u2014 pick an ${this.pending.providerKey} model for this column.`;
1022
- this.closePicker();
1023
- return;
1024
- }
1025
- this.pending.model = model;
1026
- if (model.reasoning) {
1027
- const currentLevel = parseModelRef(this.ladders[this.pending.providerKey][this.pending.strength]).level ?? 'medium';
1028
- this.thinkingSelector = new ThinkingSelectorComponent(currentLevel, [...THINKING_LEVELS], (level) => this.onThinkingPicked(level), () => this.closePicker());
1029
- this.modelSelector = undefined;
1030
- this.mode = 'thinking-picker';
1031
- this.notify();
1032
- return;
1033
- }
1034
- this.writeLadder(model, undefined);
1035
- this.closePicker();
1036
- }
1037
- onThinkingPicked(level) {
1038
- if (this.pending?.model)
1039
- this.writeLadder(this.pending.model, level);
1040
- this.closePicker();
1041
- }
1042
- writeLadder(model, level) {
1043
- if (!this.pending)
1044
- return;
1045
- const ref = `${model.provider}/${model.id}${level && level !== 'off' ? `:${level}` : ''}`;
1046
- const { providerKey, strength } = this.pending;
1047
- try {
1048
- persistModelLadders(this.currentTarget().root, { [providerKey]: { [strength]: ref } });
1049
- }
1050
- catch (error) {
1051
- this.notice = `Could not save: ${error instanceof Error ? error.message : String(error)}`;
1052
- return;
1053
- }
1054
- this.reload();
1055
- if (this.ladders[providerKey][strength] !== ref) {
1056
- const path = this.shadowingPath((raw) => raw.modelLadders?.[providerKey]?.[strength] !== undefined);
1057
- this.notice = this.savedButShadowed(path);
1058
- }
1059
- }
1060
- closePicker() {
1061
- this.mode = 'list';
1062
- this.modelSelector = undefined;
1063
- this.thinkingSelector = undefined;
1064
- this.pending = undefined;
1065
- this.pendingDim = undefined;
1066
- this.notify();
1067
- }
1068
- dispose() {
1069
- this.closePicker();
1070
- }
1071
- targetLadderOverride(providerKey, strength) {
1072
- const raw = this.rawTarget?.modelLadders;
1073
- return raw?.[providerKey]?.[strength] !== undefined;
1074
- }
1075
- targetKindOverride(name, orchestrator) {
1076
- const kinds = this.rawTarget?.kinds;
1077
- const field = orchestrator ? 'orchestratorModel' : 'model';
1078
- return kinds?.[name]?.[field] !== undefined;
1079
- }
1080
- footerHint() {
1081
- if (this.mode === 'dimension-picker') {
1082
- return `${this.label('crtr.setup.list-down')}/${this.label('crtr.setup.list-up')} choice \u00b7 ${this.label('crtr.setup.models.commit')} save \u00b7 ${this.label('crtr.setup.models.cancel')} cancel`;
1083
- }
1084
- if (this.mode === 'model-picker')
1085
- return `type to search \u00b7 ${this.label('crtr.setup.models.commit')} select \u00b7 tab all/scoped \u00b7 ${this.label('crtr.setup.models.cancel')} cancel`;
1086
- if (this.mode === 'thinking-picker')
1087
- return `\u2191/\u2193 level \u00b7 ${this.label('crtr.setup.models.commit')} set \u00b7 ${this.label('crtr.setup.models.cancel')} cancel`;
1088
- return '';
1089
- }
1090
- /** Body lines only — the caller frames and windows them. */
1091
- renderDimensionPicker(width) {
1092
- const pending = this.pendingDim;
1093
- if (!pending)
1094
- return [theme.fg('warning', 'No setting selected.')];
1095
- const modeLabel = pending.mode === 'orchestrator' ? 'orchestrator' : 'base';
1096
- const dimLabel = pending.dimension;
1097
- const paired = pending.dimension === 'provider'
1098
- ? `strength ${pending.current.strength}`
1099
- : `provider ${PROVIDER_LABEL[pending.current.provider]}`;
1100
- const body = [
1101
- theme.fg('accent', theme.bold(`Choose ${modeLabel} ${dimLabel}`)),
1102
- theme.fg('dim', `${pending.name} \u00b7 keeps ${modeLabel} ${paired}`),
1103
- '',
1104
- ];
1105
- for (const [index, option] of pending.options.entries()) {
1106
- const active = index === this.dimPickIndex;
1107
- const marker = active ? theme.fg('accent', '\u25b6') : ' ';
1108
- const label = pending.dimension === 'provider' ? PROVIDER_LABEL[option] : option;
1109
- body.push(`${marker} ${active ? theme.fg('accent', `[${label}]`) : theme.fg('dim', ` ${label} `)}`);
1110
- }
1111
- return body;
1112
- }
1113
- /** Fits the panel body into `budget` rows. The wizard draws header/tabs above
1114
- * and a footer below, so a body taller than its budget pushes the tab strip
1115
- * off the top of the terminal — the panel must window itself instead. Chrome
1116
- * (title/subtitle/blank) sheds first, then the remaining lines scroll just
1117
- * far enough to keep the selected row on screen. */
1118
- fitBody(lines, selected, budget) {
1119
- if (lines.length <= budget)
1120
- return lines;
1121
- const chrome = Math.min(MODELS_CHROME_LINES, lines.length - budget);
1122
- const rest = lines.slice(chrome);
1123
- if (rest.length <= budget)
1124
- return rest;
1125
- const anchor = Math.max(0, selected - chrome);
1126
- const start = Math.max(0, Math.min(anchor - budget + 2, rest.length - budget));
1127
- return rest.slice(start, start + budget);
1128
- }
1129
- render(width, height) {
1130
- const useFrame = height >= FRAME_OVERHEAD + 1;
1131
- const contentBudget = Math.max(1, useFrame ? height - FRAME_OVERHEAD : height);
1132
- const frame = (lines) => (useFrame ? framed(lines, width) : renderRows(lines, width));
1133
- if (this.mode === 'dimension-picker')
1134
- return frame(fitTail(this.renderDimensionPicker(width), contentBudget));
1135
- if (this.mode === 'model-picker')
1136
- return frame(fitTail(this.modelSelector?.render(Math.max(1, width - 4)) ?? [], contentBudget));
1137
- if (this.mode === 'thinking-picker')
1138
- return frame(fitTail(this.thinkingSelector?.render(Math.max(1, width - 4)) ?? [], contentBudget));
1139
- let selectedLine = 0;
1140
- const body = [
1141
- theme.fg('accent', theme.bold('Models')),
1142
- theme.fg('dim', 'global user defaults \u00b7 project/profile scopes can shadow these'),
1143
- '',
1144
- ];
1145
- const selectedRow = this.currentRow();
1146
- const strengthSelected = selectedRow.section === 'ladder' && selectedRow.kind === 'strength';
1147
- // Ladder grid layout. The provider columns share the width left after the
1148
- // 2-space marker gutter and the strength label, divided across as many
1149
- // columns as fit at a minimum cell width; any that don't fit scroll into a
1150
- // horizontal viewport that follows the selected column, so the grid scales
1151
- // to N providers instead of truncating the last column off-screen.
1152
- const innerWidth = Math.max(1, width - 4);
1153
- const ladderLabelWidth = Math.max(12, Math.min(20, Math.floor(innerWidth * 0.24)));
1154
- const providerCount = this.providerKeys.length;
1155
- const gridWidth = Math.max(1, innerWidth - 2 - ladderLabelWidth);
1156
- const minCellWidth = 12;
1157
- const maxVisible = Math.max(1, Math.floor((gridWidth + 1) / (minCellWidth + 1)));
1158
- const visibleCount = Math.min(providerCount, maxVisible);
1159
- const ladderCellWidth = Math.max(minCellWidth, Math.floor((gridWidth - (visibleCount - 1)) / visibleCount));
1160
- const activeColumn = strengthSelected ? Math.min(this.column, providerCount - 1) : 0;
1161
- const viewStart = Math.max(0, Math.min(activeColumn >= visibleCount ? activeColumn - visibleCount + 1 : 0, providerCount - visibleCount));
1162
- const viewEnd = viewStart + visibleCount;
1163
- const visibleCols = this.providerKeys
1164
- .map((provider, column) => ({ provider, column }))
1165
- .slice(viewStart, viewEnd);
1166
- const moreLeft = viewStart > 0;
1167
- const moreRight = viewEnd < providerCount;
1168
- const scrollHint = moreLeft || moreRight
1169
- ? ` ${theme.fg('dim', `${moreLeft ? '\u2039 ' : ''}${viewStart + 1}\u2013${viewEnd} of ${providerCount}${moreRight ? ' \u203a' : ''}`)}`
1170
- : '';
1171
- const target = this.currentTarget();
1172
- body.push(`${theme.fg('accent', 'Editing config')} ${theme.fg('accent', `[${target.label}]`)}${theme.fg('dim', ` ${this.targetIndex + 1}/${this.targets.length}`)}`);
1173
- body.push(theme.fg('muted', join(target.root, 'config.json')), '');
1174
- body.push(theme.fg('accent', 'Strength ladders') + scrollHint);
1175
- const effectiveDefaultProvider = inferPortableModelSelection(undefined, this.ladders).provider;
1176
- const defaultRow = this.rows.find((row) => row.section === 'ladder' && row.kind === 'defaultProvider');
1177
- const defaultSelected = defaultRow === selectedRow;
1178
- if (defaultSelected)
1179
- selectedLine = body.length;
1180
- body.push(`${defaultSelected ? theme.fg('accent', '\u25b6') : ' '} ${padAnsi(defaultSelected ? theme.fg('accent', 'Default provider') : 'Default provider', 20)} ${theme.fg('success', effectiveDefaultProvider)}`);
1181
- const providerHeading = (provider, column) => {
1182
- const heading = padAnsi(theme.bold(PROVIDER_LABEL[provider]), ladderCellWidth);
1183
- return strengthSelected && this.column === column ? theme.fg('accent', heading) : theme.fg('dim', heading);
1184
- };
1185
- const ladderCell = (text, active) => {
1186
- const content = padAnsi(truncateToWidth(text, Math.max(1, ladderCellWidth - 2)), Math.max(1, ladderCellWidth - 2));
1187
- return active ? theme.fg('accent', `[${content}]`) : theme.fg('dim', ` ${content} `);
1188
- };
1189
- body.push(`${' '.repeat(2)}${padAnsi(theme.fg('dim', 'Strength'), ladderLabelWidth)}${visibleCols.map(({ provider, column }) => providerHeading(provider, column)).join(' ')}`);
1190
- for (const row of this.rows) {
1191
- if (row.section !== 'ladder' || row.kind !== 'strength')
1192
- continue;
1193
- const isSel = row === selectedRow;
1194
- const marker = isSel ? theme.fg('accent', '\u25b6') : ' ';
1195
- if (isSel)
1196
- selectedLine = body.length;
1197
- const cells = visibleCols.map(({ provider, column }) => {
1198
- const over = this.targetLadderOverride(provider, row.strength) ? theme.fg('warning', '\u2022') : ' ';
1199
- return ladderCell(`${over}${shortRef(this.ladders[provider][row.strength])}`, isSel && column === this.column);
1200
- });
1201
- body.push(`${marker} ${padAnsi(row.strength, ladderLabelWidth)}${cells.join(' ')}`);
1202
- }
1203
- body.push('', theme.fg('accent', 'Kind defaults'));
1204
- body.push(theme.fg('dim', `${theme.fg('warning', '\u2022')} override in ${target.label} \u00b7 orchestrator inherits base until you set it`));
1205
- const nameW = 16;
1206
- const provCellW = 12;
1207
- const strCellW = 9;
1208
- const groupW = provCellW + 1 + strCellW;
1209
- // Fixed-width cell: `[value]` when the column is selected, faint (muted) for
1210
- // an inherited orchestrator value, plain dim otherwise. The 2 brackets/spaces
1211
- // keep every cell exactly `w` wide so the column headers line up.
1212
- const cell = (raw, w, active, faint) => {
1213
- const inner = padAnsi(truncateToWidth(raw, Math.max(1, w - 2)), Math.max(1, w - 2));
1214
- return active ? theme.fg('accent', `[${inner}]`) : faint ? theme.fg('muted', ` ${inner} `) : theme.fg('dim', ` ${inner} `);
1215
- };
1216
- body.push(`${' '.repeat(2 + nameW)}${padAnsi(theme.bold(theme.fg('dim', 'Base')), groupW)} ${padAnsi(theme.bold(theme.fg('dim', 'Orchestrator')), groupW)}`);
1217
- body.push(`${' '.repeat(2)}${padAnsi(theme.fg('dim', 'Kind'), nameW)}${padAnsi(theme.fg('dim', 'Provider'), provCellW)} ${padAnsi(theme.fg('dim', 'Strength'), strCellW)} ${padAnsi(theme.fg('dim', 'Provider'), provCellW)} ${padAnsi(theme.fg('dim', 'Strength'), strCellW)}`);
1218
- for (const row of this.rows) {
1219
- if (row.section !== 'kind')
1220
- continue;
1221
- const isSel = row === selectedRow;
1222
- const marker = isSel ? theme.fg('accent', '\u25b6') : ' ';
1223
- if (isSel)
1224
- selectedLine = body.length;
1225
- const cfg = this.kinds[row.name];
1226
- const baseSel = inferPortableModelSelection(cfg?.model, this.ladders);
1227
- const orchInherited = cfg?.orchestratorModel === undefined;
1228
- const orchSel = inferPortableModelSelection(cfg?.orchestratorModel ?? cfg?.model, this.ladders);
1229
- const baseOver = this.targetKindOverride(row.name, false) ? theme.fg('warning', '\u2022') : ' ';
1230
- const orchOver = this.targetKindOverride(row.name, true) ? theme.fg('warning', '\u2022') : ' ';
1231
- const cells = [
1232
- cell(`${baseOver}${PROVIDER_LABEL[baseSel.provider]}`, provCellW, isSel && this.column === 0, false),
1233
- cell(`${baseOver}${baseSel.strength}`, strCellW, isSel && this.column === 1, false),
1234
- cell(`${orchOver}${PROVIDER_LABEL[orchSel.provider]}`, provCellW, isSel && this.column === 2, orchInherited),
1235
- cell(`${orchOver}${orchSel.strength}`, strCellW, isSel && this.column === 3, orchInherited),
1236
- ];
1237
- body.push(`${marker} ${padAnsi(row.name, nameW)}${cells[0]} ${cells[1]} ${cells[2]} ${cells[3]}`);
1238
- }
1239
- if (this.notice)
1240
- body.push('', theme.fg('warning', this.notice));
1241
- return frame(this.fitBody(body, selectedLine, contentBudget));
1242
- }
1243
- }
1244
- /** Collapse explicitly-paired catalog definitions into one setup action row.
1245
- * Pair membership and field roles come only from catalog metadata; neither
1246
- * rendering nor installation guesses from binding ID names. */
1247
- function buildSetupKeybindingRows() {
1248
- const byId = new Map(BINDING_CATALOG.map((definition) => [definition.id, definition]));
1249
- const consumed = new Set();
1250
- const rows = [];
1251
- for (const definition of BINDING_CATALOG) {
1252
- if (consumed.has(definition.id))
1253
- continue;
1254
- if (definition.pair === undefined) {
1255
- rows.push({
1256
- group: definition.group,
1257
- name: definition.name,
1258
- description: definition.description,
1259
- fields: [{ definition }],
1260
- });
1261
- consumed.add(definition.id);
1262
- continue;
1263
- }
1264
- const paired = byId.get(definition.pair.pairedId);
1265
- if (paired?.pair?.pairedId !== definition.id || paired.pair.field === definition.pair.field) {
1266
- throw new Error(`invalid keybinding pair metadata for ${definition.id}`);
1267
- }
1268
- const members = [definition, paired];
1269
- const menu = members.find((member) => member.pair?.field === 'menu');
1270
- const second = members.find((member) => member.pair?.field !== 'menu');
1271
- if (menu === undefined || second === undefined)
1272
- throw new Error(`incomplete keybinding pair for ${definition.id}`);
1273
- const secondLabel = second.pair?.field === 'global' ? 'Global' : 'Direct';
1274
- rows.push({
1275
- group: menu.group,
1276
- name: menu.name,
1277
- description: menu.description,
1278
- fields: [
1279
- { label: 'Menu', definition: menu },
1280
- { label: secondLabel, definition: second },
1281
- ],
1282
- });
1283
- consumed.add(menu.id);
1284
- consumed.add(second.id);
1285
- }
1286
- return rows;
1287
- }
1288
- const SETUP_KEYBINDING_ROWS = buildSetupKeybindingRows();
1
+ // The `crtr sys setup` first-run installer: log in a provider, then install the
2
+ // companion packages, system dependencies and API key a fresh machine needs.
3
+ //
4
+ // Ongoing configuration — models, keybindings, display preferences, panes,
5
+ // broker limits, updates — lives in settings-shell.ts. Both hosts share the
6
+ // panels in ./panels; this one is a linear stepper that ends in a single
7
+ // confirmed install, that one is a persistent editor that saves as you go.
8
+ import { matchesKey, truncateToWidth, visibleWidth, } from "@earendil-works/pi-tui";
9
+ import { formatGesture, matchesPiTuiInput, } from "../../core/keybindings/index.js";
10
+ import { buildSetupInitialState, executeSetupSubmission, formatSetupExecutionSummary, reconcileInboxFrontDoor, renderSetupStaticInstructions, resolveExaKeyPath, } from "./setup-core.js";
11
+ import { runSetupSettingsProcess } from "./setup-settings-runner.js";
12
+ import { DependenciesPanel } from "./panels/dependencies-panel.js";
13
+ import { KeysPanel } from "./panels/keys-panel.js";
14
+ import { PAGE_CHROME_ROWS, pageFrame, theme, } from "./panels/panel.js";
15
+ import { PluginsPanel } from "./panels/plugins-panel.js";
16
+ import { ProviderPanel } from "./panels/provider-panel.js";
17
+ const STEPS = ["Providers", "Plugins", "Dependencies", "Exa key"];
18
+ /** The host owns step navigation, focus, submission and composition. Each step
19
+ * owns its own mutable UI state. */
1289
20
  export class SetupWizard {
1290
- manifest;
1291
- installedPackageIds;
1292
- systemTools;
1293
- exaInput = new Input();
1294
- keyInput = new Input();
1295
- brokerWarningInput = new Input();
1296
- brokerHardCapInput = new Input();
1297
- openingBindings;
1298
- keyDraft;
21
+ bindings;
1299
22
  terminalRows;
1300
- selectedPackageIds = new Set();
1301
- selectedSystemTools = new Set();
1302
23
  providers;
1303
- models;
1304
- activeTab = 0;
1305
- pluginIndex = 0;
1306
- systemIndex = 0;
1307
- expandedPluginId = null;
24
+ plugins;
25
+ dependencies;
26
+ keys;
27
+ step = 0;
1308
28
  confirming = false;
1309
- keyIndex = 0;
1310
- keyFieldIndex = 0;
1311
- keyScroll = 0;
1312
- keyFilter = '';
1313
- filteringKeys = false;
1314
- editingKey = null;
1315
- keyEditError;
1316
- keyDiagnostics = [];
1317
- keyConfirm = null;
1318
- keyNotice;
1319
- keyTmuxOccupancy;
1320
- brokerLimitIndex = 0;
1321
- brokerLimitNotice;
1322
- settingsIndex = 0;
1323
- settingsNotice;
1324
29
  cachedWidth;
1325
30
  cachedHeight;
1326
31
  cachedLines;
1327
32
  _focused = false;
33
+ installedPackageIds;
1328
34
  onSubmit;
1329
35
  onCancel;
1330
- constructor(manifest, installedPackageIds, systemTools, tui, agentDir, runtime, registry, openingBindings, rawKeybindings, terminalRows, rawTmuxPassthrough = undefined) {
1331
- this.manifest = manifest;
1332
- this.providers = new ProviderPanel(tui, agentDir, runtime, registry);
1333
- // The panel drives OAuth async; bust our line cache when it transitions so the panel's
1334
- // requestRender repaints the NEW frame (progress → success notice → roster) without a keypress.
1335
- this.providers.onChange = () => this.invalidate();
1336
- this.models = new ModelsPanel(tui, runtime, registry, (id, data) => this.matches(id, data), (id) => this.label(id));
1337
- this.models.onChange = () => this.invalidate();
1338
- this.installedPackageIds = installedPackageIds;
1339
- this.systemTools = systemTools;
36
+ constructor(manifest, installedPackageIds, systemPlans, tui, agentDir, runtime, registry, bindings, terminalRows) {
37
+ this.bindings = bindings;
1340
38
  this.terminalRows = terminalRows;
1341
- this.openingBindings = openingBindings;
1342
- this.keyDraft = new SetupKeybindingsDraft(rawKeybindings, rawTmuxPassthrough);
1343
- this.keyDiagnostics = this.keyDraft.resolution().diagnostics;
1344
- for (const entry of this.manifest)
1345
- this.selectedPackageIds.add(entry.id);
1346
- for (const tool of this.systemTools) {
1347
- if (tool.plan.checked)
1348
- this.selectedSystemTools.add(tool.tool);
1349
- }
1350
- const firstMissingSystem = this.systemTools.findIndex((tool) => !tool.installed);
1351
- this.systemIndex = firstMissingSystem >= 0 ? firstMissingSystem : 0;
1352
- this.exaInput.onSubmit = () => this.finalize();
1353
- this.exaInput.onEscape = () => this.cancel();
1354
- const brokerThresholds = readConfig('user').brokerThresholds;
1355
- this.brokerWarningInput.setValue(String(brokerThresholds.warning));
1356
- this.brokerWarningInput.handleInput('\x05');
1357
- this.brokerHardCapInput.setValue(String(brokerThresholds.automaticReviveCap));
1358
- this.brokerHardCapInput.handleInput('\x05');
1359
- this.brokerWarningInput.onSubmit = () => this.saveBrokerLimits();
1360
- this.brokerHardCapInput.onSubmit = () => this.saveBrokerLimits();
1361
- // Edit-mode Enter/Escape are routed through the opening snapshot's
1362
- // `crtr.setup.keybindings.commit-edit` / `.cancel-edit` bindings in
1363
- // handleKeybindingsInput (before the pi Input sees them), so the pi Input's
1364
- // own submit/escape handlers stay unset — those catalog rows are honored
1365
- // even when remapped away from Enter/Escape.
39
+ this.installedPackageIds = installedPackageIds;
40
+ const invalidate = () => this.invalidate();
41
+ this.providers = new ProviderPanel(tui, agentDir, runtime, registry);
42
+ this.providers.onChange = invalidate;
43
+ this.plugins = new PluginsPanel(manifest, installedPackageIds, invalidate);
44
+ this.dependencies = new DependenciesPanel(systemPlans, invalidate);
45
+ this.keys = new KeysPanel();
46
+ this.keys.onSubmit = () => this.finalize();
47
+ this.keys.onCancel = () => this.cancel();
1366
48
  }
1367
49
  set focused(value) {
1368
50
  this._focused = value;
1369
- this.exaInput.focused = value && this.activeTab === 3 && !this.confirming;
1370
- this.keyInput.focused = value && this.activeTab === 4 && this.editingKey !== null;
1371
- this.brokerWarningInput.focused = value && this.activeTab === 6 && this.brokerLimitIndex === 0;
1372
- this.brokerHardCapInput.focused = value && this.activeTab === 6 && this.brokerLimitIndex === 1;
51
+ this.syncFocus();
1373
52
  }
1374
53
  get focused() {
1375
54
  return this._focused;
@@ -1379,1195 +58,252 @@ export class SetupWizard {
1379
58
  this.cachedHeight = undefined;
1380
59
  this.cachedLines = undefined;
1381
60
  }
1382
- setTab(next) {
1383
- // Entering Models re-reads auth state so a provider signed in (or out) on
1384
- // the Providers tab immediately gains (or loses) its column here.
1385
- if (next === 5 && this.activeTab !== 5)
1386
- this.models.refresh();
1387
- this.activeTab = next;
1388
- this.exaInput.focused = this.focused && next === 3 && !this.confirming;
1389
- this.keyInput.focused = this.focused && next === 4 && this.editingKey !== null;
1390
- this.brokerWarningInput.focused = this.focused && next === 6 && this.brokerLimitIndex === 0;
1391
- this.brokerHardCapInput.focused = this.focused && next === 6 && this.brokerLimitIndex === 1;
1392
- this.invalidate();
1393
- }
1394
- moveTab(delta) {
1395
- const next = ((this.activeTab + delta + TAB_LABELS.length) % TAB_LABELS.length);
1396
- this.setTab(next);
1397
- }
1398
- moveSettingsRow(delta) {
1399
- this.settingsIndex = (this.settingsIndex + delta + USER_SETTINGS.length) % USER_SETTINGS.length;
1400
- this.settingsNotice = undefined;
1401
- this.invalidate();
1402
- }
1403
- /** Advance the selected preference and persist it immediately to user config.
1404
- * The shared descriptor owns cycling, rendering, validation, and applicability,
1405
- * so setup and `sys config set` cannot drift onto different setting catalogs. */
1406
- cycleSetting(delta) {
1407
- const setting = USER_SETTINGS[this.settingsIndex];
1408
- const config = updateConfig('user', (current) => setting.cycle(current, delta));
1409
- this.settingsNotice = `Saved ${setting.key} = ${setting.format(config)}. ${setting.applies}`;
1410
- this.invalidate();
1411
- }
1412
- handleSettingsInput(data) {
1413
- if (this.matches('crtr.setup.list-down', data))
1414
- this.moveSettingsRow(1);
1415
- else if (this.matches('crtr.setup.list-up', data))
1416
- this.moveSettingsRow(-1);
1417
- else if (this.matches('crtr.setup.toggle', data) || this.matches('crtr.setup.cell-next', data))
1418
- this.cycleSetting(1);
1419
- else if (this.matches('crtr.setup.cell-previous', data))
1420
- this.cycleSetting(-1);
1421
- else if (this.matches('crtr.setup.advance', data))
1422
- this.moveTab(1);
1423
- }
1424
- cancel() {
1425
- if (this.confirming) {
1426
- this.confirming = false;
1427
- this.invalidate();
1428
- return;
1429
- }
1430
- this.providers.dispose();
1431
- this.models.dispose();
1432
- this.onCancel?.();
61
+ syncFocus() {
62
+ this.keys.focused = this._focused && this.step === 3 && !this.confirming;
1433
63
  }
1434
- dispose() { this.providers.dispose(); this.models.dispose(); }
1435
64
  matches(id, data) {
1436
- return matchesPiTuiInput(this.openingBindings, id, data, (input, gesture) => matchesKey(input, gesture));
65
+ return matchesPiTuiInput(this.bindings, id, data, (input, gesture) => matchesKey(input, gesture));
1437
66
  }
1438
- /** Human hint label for an action, derived from the opening snapshot so footers
1439
- * reflect the effective (possibly remapped) gestures the wizard actually honors. */
1440
67
  label(id) {
1441
- return formatBinding(this.openingBindings, id);
68
+ const gestures = this.bindings.gestures(id);
69
+ return gestures.length === 0
70
+ ? "disabled"
71
+ : formatGesture(gestures[0].split(" > ").pop());
1442
72
  }
1443
- /** Like `label`, but drops gestures a focused pi text Input consumes as data
1444
- * (single printable chars and the arrow keys), so the Exa footer does
1445
- * not claim keys that never reach the wizard while a field owns focus. */
73
+ /** Gestures safe to advertise while a text field owns the keyboard. */
1446
74
  fieldSafeLabel(id) {
1447
- const survives = this.openingBindings.gestures(id).filter((gesture) => {
1448
- if (gesture.includes(' > '))
75
+ const gestures = this.bindings.gestures(id).filter((gesture) => {
76
+ if (gesture.includes(" > "))
1449
77
  return true;
1450
- const key = gesture.split('+').pop();
1451
- if (key === 'up' || key === 'down' || key === 'left' || key === 'right')
1452
- return false;
1453
- return key.length > 1 || gesture.includes('+');
78
+ const key = gesture.split("+").pop();
79
+ return (!(key === "up" || key === "down" || key === "left" || key === "right") &&
80
+ (key.length > 1 || gesture.includes("+")));
1454
81
  });
1455
- return survives.length === 0 ? 'Disabled' : survives.map(formatGesture).join(' / ');
1456
- }
1457
- handleGlobalTabSwitch(data) {
1458
- if (this.matches('crtr.setup.tab-next', data)) {
1459
- this.moveTab(1);
1460
- return true;
1461
- }
1462
- if (this.matches('crtr.setup.tab-previous', data)) {
1463
- this.moveTab(-1);
1464
- return true;
1465
- }
1466
- return false;
1467
- }
1468
- selectedPackage() {
1469
- return this.manifest[this.pluginIndex];
1470
- }
1471
- selectedSystem() {
1472
- return this.systemTools[this.systemIndex];
82
+ return gestures.length === 0
83
+ ? "Disabled"
84
+ : gestures.map(formatGesture).join(" / ");
1473
85
  }
1474
- movePlugin(delta) {
1475
- if (this.manifest.length === 0)
1476
- return;
1477
- this.pluginIndex = (this.pluginIndex + delta + this.manifest.length) % this.manifest.length;
86
+ setStep(next) {
87
+ this.step = next;
88
+ this.syncFocus();
1478
89
  this.invalidate();
1479
90
  }
1480
- moveSystem(delta) {
1481
- const interactives = this.systemTools
1482
- .map((tool, index) => ({ tool, index }))
1483
- .filter(({ tool }) => !tool.installed);
1484
- if (interactives.length === 0)
1485
- return;
1486
- const currentIndex = interactives.findIndex(({ index }) => index === this.systemIndex);
1487
- const nextPos = ((currentIndex >= 0 ? currentIndex : 0) + delta + interactives.length) % interactives.length;
1488
- this.systemIndex = interactives[nextPos].index;
1489
- this.invalidate();
91
+ moveStep(delta) {
92
+ this.setStep(((this.step + delta + STEPS.length) % STEPS.length));
1490
93
  }
1491
- togglePackage() {
1492
- const entry = this.selectedPackage();
1493
- if (!entry)
1494
- return;
1495
- if (this.selectedPackageIds.has(entry.id))
1496
- this.selectedPackageIds.delete(entry.id);
94
+ /** Enter on a non-final step walks forward; on the last one it finishes. */
95
+ advance() {
96
+ if (this.step === 3)
97
+ this.finalize();
1497
98
  else
1498
- this.selectedPackageIds.add(entry.id);
1499
- this.invalidate();
99
+ this.moveStep(1);
1500
100
  }
1501
- toggleSystem() {
1502
- const tool = this.selectedSystem();
1503
- if (!tool || tool.installed)
101
+ cancel() {
102
+ if (this.confirming) {
103
+ this.confirming = false;
104
+ this.syncFocus();
105
+ this.invalidate();
1504
106
  return;
1505
- if (this.selectedSystemTools.has(tool.tool))
1506
- this.selectedSystemTools.delete(tool.tool);
1507
- else
1508
- this.selectedSystemTools.add(tool.tool);
1509
- this.invalidate();
107
+ }
108
+ this.onCancel?.();
1510
109
  }
1511
- toggleDetails() {
1512
- const entry = this.selectedPackage();
1513
- if (!entry)
1514
- return;
1515
- this.expandedPluginId = this.expandedPluginId === entry.id ? null : entry.id;
1516
- this.invalidate();
110
+ dispose() {
111
+ this.providers.dispose();
112
+ this.keys.focused = false;
1517
113
  }
1518
114
  hasPendingInstalls() {
1519
- const packages = this.manifest.some((entry) => this.selectedPackageIds.has(entry.id) && !this.installedPackageIds.has(entry.id));
1520
- const system = this.systemTools.some((tool) => !tool.installed && this.selectedSystemTools.has(tool.tool));
1521
- return packages || system;
115
+ return (this.plugins.selected.some((entry) => !this.installedPackageIds.has(entry.id)) || this.dependencies.selected.length > 0);
1522
116
  }
1523
117
  buildSubmission() {
1524
118
  return {
1525
- packages: this.manifest.filter((entry) => this.selectedPackageIds.has(entry.id)),
119
+ packages: [...this.plugins.selected],
1526
120
  installedPackageIds: new Set(this.installedPackageIds),
1527
- systemDeps: this.systemTools.filter((tool) => !tool.installed && this.selectedSystemTools.has(tool.tool)).map((tool) => tool.plan),
1528
- exaKey: this.exaInput.getValue().trim(),
121
+ systemDeps: [...this.dependencies.selected],
122
+ exaKey: this.keys.value,
1529
123
  exaKeyPath: resolveExaKeyPath(),
1530
124
  };
1531
125
  }
1532
126
  finalize() {
1533
- if (this.confirming) {
127
+ if (this.confirming || !this.hasPendingInstalls()) {
1534
128
  this.onSubmit?.(this.buildSubmission());
1535
129
  return;
1536
130
  }
1537
- if (this.hasPendingInstalls()) {
1538
- this.confirming = true;
1539
- this.invalidate();
1540
- return;
1541
- }
1542
- this.onSubmit?.(this.buildSubmission());
1543
- }
1544
- filteredKeybindings() {
1545
- const resolution = this.keyDraft.resolution();
1546
- const needle = this.keyFilter.trim().toLowerCase();
1547
- if (needle === '')
1548
- return SETUP_KEYBINDING_ROWS;
1549
- return SETUP_KEYBINDING_ROWS.filter((row) => [
1550
- row.name,
1551
- row.group,
1552
- row.description,
1553
- ...row.fields.flatMap((field) => [
1554
- field.label ?? '',
1555
- field.definition.id,
1556
- field.definition.name,
1557
- field.definition.description,
1558
- formatBinding(resolution, field.definition.id),
1559
- ]),
1560
- ].some((value) => value.toLowerCase().includes(needle)));
1561
- }
1562
- selectedKeyRow() {
1563
- const rows = this.filteredKeybindings();
1564
- if (rows.length === 0)
1565
- return undefined;
1566
- this.keyIndex = Math.max(0, Math.min(this.keyIndex, rows.length - 1));
1567
- const row = rows[this.keyIndex];
1568
- this.keyFieldIndex = Math.max(0, Math.min(this.keyFieldIndex, row.fields.length - 1));
1569
- return row;
1570
- }
1571
- selectedKeybinding() {
1572
- return this.selectedKeyRow()?.fields[this.keyFieldIndex]?.definition;
1573
- }
1574
- tmuxPassthroughOffer() {
1575
- const definition = this.selectedKeybinding();
1576
- if (definition === undefined || !isAttachPaneBinding(definition))
1577
- return undefined;
1578
- const gestures = this.keyDraft.resolution().gestures(definition.id);
1579
- const expression = gestures.join('\u0000');
1580
- if (this.keyTmuxOccupancy?.id !== definition.id || this.keyTmuxOccupancy.expression !== expression) {
1581
- this.keyTmuxOccupancy = {
1582
- id: definition.id,
1583
- expression,
1584
- gestures: occupiedTmuxRootGestures(gestures),
1585
- };
1586
- }
1587
- const gesture = this.keyTmuxOccupancy.gestures[0];
1588
- return gesture === undefined ? undefined : { id: definition.id, gesture };
1589
- }
1590
- toggleTmuxPassthrough() {
1591
- const offer = this.tmuxPassthroughOffer();
1592
- if (offer === undefined)
1593
- return false;
1594
- this.keyDraft.toggleTmuxPassthrough(offer.id);
1595
- this.keyTmuxOccupancy = undefined;
1596
- this.keyNotice = undefined;
1597
- this.invalidate();
1598
- return true;
1599
- }
1600
- moveKey(delta) {
1601
- const rows = this.filteredKeybindings();
1602
- if (rows.length === 0)
1603
- return;
1604
- this.keyIndex = Math.max(0, Math.min(rows.length - 1, this.keyIndex + delta));
1605
- this.keyFieldIndex = Math.min(this.keyFieldIndex, rows[this.keyIndex].fields.length - 1);
1606
- const page = this.keyPageSize();
1607
- if (this.keyIndex < this.keyScroll)
1608
- this.keyScroll = this.keyIndex;
1609
- if (this.keyIndex >= this.keyScroll + page)
1610
- this.keyScroll = this.keyIndex - page + 1;
1611
- this.keyEditError = undefined;
1612
- this.invalidate();
1613
- }
1614
- moveKeyField(delta) {
1615
- const row = this.selectedKeyRow();
1616
- if (row === undefined)
1617
- return;
1618
- this.keyFieldIndex = Math.max(0, Math.min(row.fields.length - 1, this.keyFieldIndex + delta));
1619
- this.keyEditError = undefined;
131
+ this.confirming = true;
132
+ this.syncFocus();
1620
133
  this.invalidate();
1621
134
  }
1622
- // Max output lines the row region may emit before it starts clipping the
1623
- // confirm prompt / notice / footer. Budgets the fixed chrome around the list:
1624
- // header (3) + blank + tab header/count (2) + blank + footer (1) plus a small
1625
- // reserve for a diagnostic line and the notice/confirm block.
1626
- keyViewportLines() {
1627
- return Math.max(4, this.terminalRows() - 11);
1628
- }
1629
- // Rows moved per PageUp/PageDown and used for scroll clamping. Kept below the
1630
- // raw viewport so interleaved group-heading lines and the selected row's
1631
- // diagnostic line cannot push the selection off-screen.
1632
- keyPageSize() {
1633
- return Math.max(3, this.keyViewportLines() - 4);
1634
- }
1635
- beginKeyEdit() {
1636
- const row = this.selectedKeybinding();
1637
- if (!row)
1638
- return;
1639
- if (this.keyDraft.hasInvalidRoot()) {
1640
- this.keyNotice = `Saved keybindings root is invalid — ${this.label('crtr.setup.keybindings.reset-all')} (reset all) before editing.`;
1641
- this.invalidate();
1642
- return;
1643
- }
1644
- this.editingKey = row.id;
1645
- this.keyInput.setValue(this.keyDraft.expression(row.id));
1646
- // pi Input.setValue only clamps the cursor; seed it, then send ctrl+e
1647
- // (cursorLineEnd) so the cursor sits at end-of-value and the first keystroke
1648
- // appends after the seed instead of prepending before it.
1649
- this.keyInput.handleInput('\x05');
1650
- this.keyInput.focused = this.focused;
1651
- this.keyEditError = undefined;
1652
- this.invalidate();
1653
- }
1654
- commitKeyEdit() {
1655
- if (this.editingKey === null)
1656
- return;
1657
- const result = this.keyDraft.edit(this.editingKey, this.keyInput.getValue());
1658
- if (!result.ok) {
1659
- this.keyEditError = result.diagnostics[0]?.message ?? 'Invalid keybinding';
1660
- this.keyDiagnostics = result.diagnostics;
1661
- this.invalidate();
1662
- return;
1663
- }
1664
- this.editingKey = null;
1665
- this.keyInput.focused = false;
1666
- this.keyEditError = undefined;
1667
- this.keyDiagnostics = this.keyDraft.resolution().diagnostics;
1668
- this.keyNotice = undefined;
1669
- this.invalidate();
1670
- }
1671
- cancelKeyEdit() {
1672
- this.editingKey = null;
1673
- this.keyInput.focused = false;
1674
- this.keyEditError = undefined;
1675
- // Drop the rejected edit's collision diagnostics: the draft never took the
1676
- // offending value, so restore diagnostics from the committed draft state.
1677
- this.keyDiagnostics = this.keyDraft.resolution().diagnostics;
1678
- this.invalidate();
1679
- }
1680
- saveKeybindings() {
1681
- const resolution = this.keyDraft.resolution();
1682
- this.keyDiagnostics = resolution.diagnostics;
1683
- const errors = resolution.diagnostics.filter((diagnostic) => diagnostic.severity === 'error');
1684
- if (errors.length > 0) {
1685
- this.keyNotice = errors[0].message;
1686
- this.invalidate();
1687
- return;
1688
- }
1689
- try {
1690
- const saved = persistUserKeybindings(this.keyDraft.overrides(), {
1691
- expectedKeybindings: this.keyDraft.expectedKeybindings(),
1692
- tmuxPassthrough: this.keyDraft.tmuxPassthrough(),
1693
- expectedTmuxPassthrough: this.keyDraft.expectedTmuxPassthrough(),
1694
- });
1695
- this.keyDraft.markSaved(saved.overrides, saved.tmuxPassthrough);
1696
- this.keyNotice = 'Saved. Reload attach and reopen active views to apply changes; refreshing tmux…';
1697
- // The config is already persisted; a tmux-refresh failure (installer throw)
1698
- // must land in the notice, never as an unhandled rejection that tears down
1699
- // the wizard mid-TUI right after a good save.
1700
- void this.refreshTmuxBindingsAfterSave().catch((error) => {
1701
- this.keyNotice = `Saved, but tmux refresh failed: ${error instanceof Error ? error.message : String(error)}`;
1702
- this.invalidate();
1703
- });
1704
- }
1705
- catch (error) {
1706
- if (error instanceof KeybindingConcurrentEditError) {
1707
- this.keyNotice = 'Keybindings changed on disk. Reopen setup before saving.';
1708
- }
1709
- else if (error instanceof KeybindingValidationError) {
1710
- this.keyDiagnostics = error.diagnostics;
1711
- this.keyNotice = error.message;
1712
- }
1713
- else {
1714
- this.keyNotice = error instanceof Error ? error.message : String(error);
1715
- }
1716
- }
1717
- this.invalidate();
1718
- }
1719
- async refreshTmuxBindingsAfterSave() {
1720
- // Unit 2's frozen tmux-chrome seam: no-arg install re-reads the
1721
- // just-persisted user config and reconciles the crouter-owned bindings.
1722
- const result = installTmuxBindings();
1723
- const occupied = result.diagnostics.filter((diagnostic) => diagnostic.code === 'occupied');
1724
- if (result.ok && occupied.length === 0) {
1725
- this.keyNotice = 'Saved and applied to tmux. Reload attach and reopen active views.';
1726
- }
1727
- else if (result.ok) {
1728
- this.keyNotice = `Saved; tmux kept occupied keys: ${occupied.map((diagnostic) => diagnostic.message).join('; ')}`;
1729
- }
1730
- else {
1731
- this.keyNotice = `Saved, but tmux refresh failed: ${result.diagnostics.find((diagnostic) => diagnostic.code === 'tmux-error')?.message ?? 'tmux unavailable'}`;
1732
- }
1733
- this.invalidate();
1734
- }
1735
- confirmKeyAction() {
1736
- if (this.keyConfirm === 'reset-all') {
1737
- this.keyDraft.resetAll();
1738
- this.keyDiagnostics = [];
1739
- this.keyNotice = 'All rows reset to catalog defaults. Press save to write.';
1740
- }
1741
- else if (this.keyConfirm === 'discard') {
1742
- this.keyDraft.discard();
1743
- this.keyDiagnostics = this.keyDraft.resolution().diagnostics;
1744
- this.keyNotice = 'Unsaved keybinding changes discarded.';
1745
- }
1746
- this.keyConfirm = null;
1747
- this.invalidate();
1748
- }
1749
- selectedBrokerLimitInput() {
1750
- return this.brokerLimitIndex === 0 ? this.brokerWarningInput : this.brokerHardCapInput;
1751
- }
1752
- moveBrokerLimit(delta) {
1753
- this.brokerLimitIndex = ((this.brokerLimitIndex + delta + 2) % 2);
1754
- this.brokerWarningInput.focused = this.focused && this.brokerLimitIndex === 0;
1755
- this.brokerHardCapInput.focused = this.focused && this.brokerLimitIndex === 1;
1756
- this.brokerLimitNotice = undefined;
1757
- this.invalidate();
1758
- }
1759
- saveBrokerLimits() {
1760
- const warningThreshold = parsePositiveInteger(this.brokerWarningInput.getValue());
1761
- const automaticReviveHardCap = parsePositiveInteger(this.brokerHardCapInput.getValue());
1762
- if (warningThreshold === undefined || automaticReviveHardCap === undefined) {
1763
- this.brokerLimitNotice = 'Both broker limits must be positive integers.';
1764
- this.invalidate();
1765
- return;
1766
- }
1767
- if (warningThreshold > automaticReviveHardCap) {
1768
- this.brokerLimitNotice = 'Warning threshold cannot exceed the automatic-revive hard cap.';
1769
- this.invalidate();
1770
- return;
1771
- }
1772
- updateRawConfigAtomically('user', (latest) => ({
1773
- ...latest,
1774
- brokerThresholds: { warning: warningThreshold, automaticReviveCap: automaticReviveHardCap },
1775
- }));
1776
- this.brokerLimitNotice = `Saved to user config. Applies next tick. ${BROKER_LIMIT_OVERRIDE_PRECEDENCE}`;
1777
- this.invalidate();
1778
- }
1779
- handleBrokerLimitsInput(data) {
1780
- // Values take precedence over remappable navigation keys, so binding a
1781
- // setup action to a digit never makes that digit impossible to enter here.
1782
- if (/^\d+$/.test(data)) {
1783
- this.selectedBrokerLimitInput().handleInput(data);
1784
- this.brokerLimitNotice = undefined;
1785
- this.invalidate();
1786
- return;
1787
- }
1788
- if (this.matches('crtr.setup.list-down', data)) {
1789
- this.moveBrokerLimit(1);
1790
- return;
1791
- }
1792
- if (this.matches('crtr.setup.list-up', data)) {
1793
- this.moveBrokerLimit(-1);
1794
- return;
1795
- }
1796
- // Input receives bracketed pastes as one event. Reject a non-digit event as
1797
- // a whole so a saved broker limit is always an unambiguous positive integer.
1798
- if (data.length > 0 && [...data].every((char) => char.codePointAt(0) >= 32) && !/^\d+$/.test(data)) {
1799
- this.brokerLimitNotice = 'Broker limits accept digits only.';
1800
- this.invalidate();
1801
- return;
1802
- }
1803
- this.selectedBrokerLimitInput().handleInput(data);
1804
- // Input invokes its submit callback synchronously for Enter; preserve that
1805
- // callback's validation or saved notice instead of immediately clearing it.
1806
- if (data !== '\r' && data !== '\n')
1807
- this.brokerLimitNotice = undefined;
1808
- this.invalidate();
1809
- }
1810
- handleKeybindingsInput(data) {
1811
- if (this.keyConfirm !== null) {
1812
- if (this.matches('crtr.setup.confirm.accept', data))
1813
- this.confirmKeyAction();
1814
- else if (this.matches('crtr.setup.confirm.back', data)) {
1815
- this.keyConfirm = null;
1816
- this.invalidate();
1817
- }
1818
- return;
1819
- }
1820
- if (this.editingKey !== null) {
1821
- // Honor the catalog rows for commit/cancel BEFORE the pi Input sees the
1822
- // keystroke, so a remap of `commit-edit`/`cancel-edit` takes effect.
1823
- if (this.matches('crtr.setup.keybindings.commit-edit', data)) {
1824
- this.commitKeyEdit();
1825
- return;
1826
- }
1827
- if (this.matches('crtr.setup.keybindings.cancel-edit', data)) {
1828
- this.cancelKeyEdit();
1829
- return;
1830
- }
1831
- if (this.matches('crtr.setup.keybindings.page-up', data)) {
1832
- this.moveKey(-this.keyPageSize());
1833
- return;
1834
- }
1835
- if (this.matches('crtr.setup.keybindings.page-down', data)) {
1836
- this.moveKey(this.keyPageSize());
1837
- return;
1838
- }
1839
- this.keyInput.handleInput(data);
1840
- this.invalidate();
1841
- return;
1842
- }
1843
- if (this.filteringKeys) {
1844
- if (this.matches('crtr.setup.keybindings.filter-clear', data)) {
1845
- this.keyFilter = '';
1846
- this.filteringKeys = false;
1847
- this.keyIndex = 0;
1848
- this.keyScroll = 0;
1849
- }
1850
- else if (this.matches('crtr.setup.keybindings.edit', data)) {
1851
- // Enter commits the filter: leave filter-entry but KEEP keyFilter so the
1852
- // narrowed list can be navigated and its rows edited.
1853
- this.filteringKeys = false;
1854
- }
1855
- else if (this.matches('crtr.setup.keybindings.filter-delete', data)) {
1856
- this.keyFilter = this.keyFilter.slice(0, -1);
1857
- this.keyIndex = 0;
1858
- this.keyScroll = 0;
1859
- }
1860
- else if (data.length === 1 && data.charCodeAt(0) >= 32) {
1861
- this.keyFilter += data;
1862
- this.keyIndex = 0;
1863
- this.keyScroll = 0;
1864
- }
1865
- this.invalidate();
1866
- return;
1867
- }
1868
- if (this.matches('crtr.setup.keybindings.filter', data)) {
1869
- this.filteringKeys = true;
1870
- }
1871
- else if (this.matches('crtr.setup.list-down', data)) {
1872
- this.moveKey(1);
1873
- return;
1874
- }
1875
- else if (this.matches('crtr.setup.list-up', data)) {
1876
- this.moveKey(-1);
1877
- return;
1878
- }
1879
- else if (this.matches('crtr.setup.cell-previous', data)) {
1880
- this.moveKeyField(-1);
1881
- return;
1882
- }
1883
- else if (this.matches('crtr.setup.cell-next', data)) {
1884
- this.moveKeyField(1);
1885
- return;
1886
- }
1887
- else if (this.matches('crtr.setup.keybindings.page-up', data)) {
1888
- this.moveKey(-this.keyPageSize());
1889
- return;
1890
- }
1891
- else if (this.matches('crtr.setup.keybindings.page-down', data)) {
1892
- this.moveKey(this.keyPageSize());
1893
- return;
1894
- }
1895
- else if (this.matches('crtr.setup.toggle', data)) {
1896
- if (this.toggleTmuxPassthrough())
1897
- return;
1898
- }
1899
- else if (this.matches('crtr.setup.keybindings.edit', data)) {
1900
- this.beginKeyEdit();
1901
- return;
1902
- }
1903
- else if (this.matches('crtr.setup.keybindings.unbind', data)) {
1904
- const row = this.selectedKeybinding();
1905
- if (row) {
1906
- const result = this.keyDraft.unbind(row.id);
1907
- this.keyDiagnostics = result.ok ? this.keyDraft.resolution().diagnostics : result.diagnostics;
1908
- this.keyEditError = result.ok ? undefined : result.diagnostics[0]?.message;
1909
- }
1910
- }
1911
- else if (this.matches('crtr.setup.keybindings.reset-row', data)) {
1912
- const row = this.selectedKeybinding();
1913
- if (row)
1914
- this.keyDraft.resetRow(row.id);
1915
- this.keyDiagnostics = this.keyDraft.resolution().diagnostics;
1916
- }
1917
- else if (this.matches('crtr.setup.keybindings.reset-all', data)) {
1918
- this.keyConfirm = 'reset-all';
1919
- }
1920
- else if (this.matches('crtr.setup.keybindings.save', data)) {
1921
- this.saveKeybindings();
1922
- return;
1923
- }
1924
- else if (this.matches('crtr.setup.keybindings.discard', data)) {
1925
- // A committed (kept) filter is cleared first, before discard is considered.
1926
- if (this.keyFilter !== '') {
1927
- this.keyFilter = '';
1928
- this.keyIndex = 0;
1929
- this.keyScroll = 0;
1930
- this.keyNotice = undefined;
1931
- }
1932
- else if (this.keyDraft.dirty)
1933
- this.keyConfirm = 'discard';
1934
- else
1935
- this.keyNotice = 'No unsaved keybinding changes.';
1936
- }
1937
- else {
1938
- return;
1939
- }
1940
- this.invalidate();
135
+ currentPanel() {
136
+ return this.step === 0
137
+ ? this.providers
138
+ : this.step === 1
139
+ ? this.plugins
140
+ : this.step === 2
141
+ ? this.dependencies
142
+ : this.keys;
1941
143
  }
1942
144
  handleInput(data) {
1943
- // A nested provider login owns all input until it settles. In particular, do not let
1944
- // wizard-wide q/Tab navigation abandon the panel while its OAuth promise can still resolve.
1945
- if (this.activeTab === 0 && this.providers.isNestedFlowActive()) {
1946
- if (this.providers.handleInput(data))
1947
- this.invalidate();
1948
- return;
1949
- }
1950
- // A nested model/thinking picker owns all input until it settles, before any
1951
- // wizard-wide cancel/tab gesture can steal it.
1952
- if (this.activeTab === 5 && this.models.isPickerOpen()) {
1953
- if (this.models.handleInput(data))
145
+ const panel = this.currentPanel();
146
+ const match = (id, input) => this.matches(id, input);
147
+ const nestedInput = (this.step === 0 && this.providers.isNestedFlowActive()) ||
148
+ (this.step === 3 &&
149
+ ((data.length === 1 && data.charCodeAt(0) >= 32) ||
150
+ matchesKey(data, "left") ||
151
+ matchesKey(data, "right") ||
152
+ matchesKey(data, "up") ||
153
+ matchesKey(data, "down")));
154
+ if (nestedInput) {
155
+ if (panel.handleInput(data, match))
1954
156
  this.invalidate();
1955
157
  return;
1956
158
  }
1957
159
  if (this.confirming) {
1958
- if (this.matches('crtr.setup.confirm.accept', data)) {
160
+ if (this.matches("crtr.setup.confirm.accept", data))
1959
161
  this.onSubmit?.(this.buildSubmission());
1960
- return;
1961
- }
1962
- if (this.matches('crtr.setup.confirm.back', data)) {
162
+ else if (this.matches("crtr.setup.confirm.back", data)) {
1963
163
  this.confirming = false;
164
+ this.syncFocus();
1964
165
  this.invalidate();
1965
166
  }
1966
167
  return;
1967
168
  }
1968
- // Keybindings sub-states (editing / filtering / confirm) own input before the
1969
- // wizard-level cancel and tab gestures can steal it.
1970
- if (this.activeTab === 4 && (this.editingKey !== null || this.filteringKeys || this.keyConfirm !== null)) {
1971
- this.handleKeybindingsInput(data);
1972
- return;
1973
- }
1974
- // Printable chars and arrows on the API-key tab go to the focused field.
1975
- if (this.activeTab === 3
1976
- && ((data.length === 1 && data.charCodeAt(0) >= 32)
1977
- || matchesKey(data, 'left') || matchesKey(data, 'right') || matchesKey(data, 'up') || matchesKey(data, 'down'))) {
1978
- this.exaInput.handleInput(data);
1979
- this.invalidate();
1980
- return;
1981
- }
1982
- // Numeric broker-limit input owns digits before remappable setup navigation
1983
- // can consume them as a tab or list binding.
1984
- if (this.activeTab === 6 && /^\d+$/.test(data)) {
1985
- this.handleBrokerLimitsInput(data);
1986
- return;
1987
- }
1988
- // On the keybindings tab in list mode, a dirty draft must not be dropped
1989
- // silently by the wizard-level cancel gesture — route it through the same
1990
- // discard-confirm the discard path uses.
1991
- if (this.activeTab === 4 && this.keyDraft.dirty && this.matches('crtr.setup.cancel', data)) {
1992
- this.keyConfirm = 'discard';
1993
- this.invalidate();
1994
- return;
1995
- }
1996
- if (this.matches('crtr.setup.cancel', data)) {
169
+ if (this.matches("crtr.setup.cancel", data)) {
1997
170
  this.cancel();
1998
171
  return;
1999
172
  }
2000
- if (this.handleGlobalTabSwitch(data))
2001
- return;
2002
- if (this.activeTab === 0) {
2003
- if (this.providers.handleInput(data))
2004
- this.invalidate();
2005
- return;
2006
- }
2007
- if (this.activeTab === 1) {
2008
- if (this.matches('crtr.setup.list-down', data))
2009
- this.movePlugin(1);
2010
- else if (this.matches('crtr.setup.list-up', data))
2011
- this.movePlugin(-1);
2012
- else if (this.matches('crtr.setup.toggle', data))
2013
- this.togglePackage();
2014
- else if (this.matches('crtr.setup.details', data))
2015
- this.toggleDetails();
2016
- else if (this.matches('crtr.setup.advance', data))
2017
- this.moveTab(1);
173
+ if (this.matches("crtr.setup.tab-next", data)) {
174
+ this.moveStep(1);
2018
175
  return;
2019
176
  }
2020
- if (this.activeTab === 2) {
2021
- if (this.matches('crtr.setup.list-down', data))
2022
- this.moveSystem(1);
2023
- else if (this.matches('crtr.setup.list-up', data))
2024
- this.moveSystem(-1);
2025
- else if (this.matches('crtr.setup.toggle', data))
2026
- this.toggleSystem();
2027
- else if (this.matches('crtr.setup.advance', data))
2028
- this.moveTab(1);
177
+ if (this.matches("crtr.setup.tab-previous", data)) {
178
+ this.moveStep(-1);
2029
179
  return;
2030
180
  }
2031
- if (this.activeTab === 3) {
2032
- this.exaInput.handleInput(data);
181
+ if (panel.handleInput(data, match)) {
2033
182
  this.invalidate();
2034
183
  return;
2035
184
  }
2036
- if (this.activeTab === 4) {
2037
- this.handleKeybindingsInput(data);
2038
- return;
2039
- }
2040
- if (this.activeTab === 5) {
2041
- if (this.models.handleInput(data))
2042
- this.invalidate();
2043
- return;
2044
- }
2045
- if (this.activeTab === SETTINGS_TAB) {
2046
- this.handleSettingsInput(data);
2047
- return;
2048
- }
2049
- this.handleBrokerLimitsInput(data);
185
+ if (this.matches("crtr.setup.advance", data))
186
+ this.advance();
2050
187
  }
2051
- renderHeader(width) {
2052
- const tabs = TAB_LABELS;
2053
- const fullTabLine = tabs.slice()
2054
- .map((label, index) => tabPill(label, index === this.activeTab))
2055
- .join(theme.fg('dim', ' · '));
2056
- const tabLine = visibleWidth(fullTabLine) <= width
2057
- ? fullTabLine
2058
- : `${theme.fg('dim', `${this.activeTab + 1}/${tabs.length}`)} ${tabPill(tabs[this.activeTab], true)}`;
2059
- return [
2060
- truncateToWidth(theme.fg('accent', theme.bold('crtr sys setup')), width),
2061
- truncateToWidth(tabLine, width),
2062
- truncateToWidth(rule(width), width),
2063
- ];
2064
- }
2065
- renderFooter(width) {
2066
- const tabs = `${this.label('crtr.setup.tab-next')} next · ${this.label('crtr.setup.tab-previous')} prev`;
2067
- if (this.activeTab === 5) {
2068
- const hint = this.models.isPickerOpen()
2069
- ? this.models.footerHint()
2070
- : `${this.label('crtr.setup.list-down')}/${this.label('crtr.setup.list-up')} row · ${this.label('crtr.setup.cell-previous')}/${this.label('crtr.setup.cell-next')} column · ${this.label('crtr.setup.models.scope-next')} scope · ${this.label('crtr.setup.models.open')} edit · ${this.label('crtr.setup.tab-next')} next · ${this.label('crtr.setup.cancel')} cancel`;
2071
- return [truncateToWidth(theme.fg('dim', hint), width)];
2072
- }
2073
- if (this.activeTab === SETTINGS_TAB) {
2074
- return [truncateToWidth(theme.fg('dim', `${this.label('crtr.setup.list-down')}/${this.label('crtr.setup.list-up')} row · ${this.label('crtr.setup.toggle')} or ${this.label('crtr.setup.cell-next')}/${this.label('crtr.setup.cell-previous')} change value · ${tabs} · ${this.label('crtr.setup.cancel')} cancel`), width)];
2075
- }
2076
- if (this.activeTab === 6) {
2077
- return [truncateToWidth(theme.fg('dim', `${this.label('crtr.setup.list-down')}/${this.label('crtr.setup.list-up')} field · type value · Enter save · ${this.fieldSafeLabel('crtr.setup.tab-next')}/${this.fieldSafeLabel('crtr.setup.tab-previous')} switch tabs · ${this.label('crtr.setup.cancel')} cancel`), width)];
2078
- }
2079
- const line = this.confirming
2080
- ? `${this.label('crtr.setup.confirm.accept')} confirm · ${this.label('crtr.setup.confirm.back')} go back`
2081
- : this.activeTab === 0 && this.providers.isNestedFlowActive()
2082
- ? 'OAuth in progress · esc cancel'
2083
- : this.activeTab === 0
2084
- ? `↑/↓ select · a add · p prefer · r rename · l re-login · x remove · ${this.label('crtr.setup.tab-next')} next · ${this.label('crtr.setup.cancel')} cancel`
2085
- : this.activeTab === 1
2086
- ? `${this.label('crtr.setup.list-down')}/${this.label('crtr.setup.list-up')} move · ${this.label('crtr.setup.toggle')} toggle · ${this.label('crtr.setup.details')} details · ${this.label('crtr.setup.advance')} advance · ${tabs} · ${this.label('crtr.setup.cancel')} cancel`
2087
- : this.activeTab === 2
2088
- ? `${this.label('crtr.setup.list-down')}/${this.label('crtr.setup.list-up')} move · ${this.label('crtr.setup.toggle')} toggle · ${this.label('crtr.setup.advance')} advance · ${tabs} · ${this.label('crtr.setup.cancel')} cancel`
2089
- : this.activeTab === 3
2090
- ? `type EXA_API_KEY · Enter finish · ${this.fieldSafeLabel('crtr.setup.tab-next')}/${this.fieldSafeLabel('crtr.setup.tab-previous')} switch tabs · Esc or ${this.fieldSafeLabel('crtr.setup.cancel')} cancel`
2091
- : this.keyConfirm !== null
2092
- ? `${this.label('crtr.setup.confirm.accept')} confirm · ${this.label('crtr.setup.confirm.back')} go back`
2093
- : this.editingKey !== null
2094
- ? `type gestures separated by commas · ${this.label('crtr.setup.keybindings.commit-edit')} commit · ${this.label('crtr.setup.keybindings.cancel-edit')} cancel · ${this.label('crtr.setup.keybindings.page-up')}/${this.label('crtr.setup.keybindings.page-down')} scroll`
2095
- : this.filteringKeys
2096
- ? `filter: ${this.keyFilter || 'type to search'} · ${this.label('crtr.setup.keybindings.edit')} keep · ${this.label('crtr.setup.keybindings.filter-delete')} delete · ${this.label('crtr.setup.keybindings.filter-clear')} clear`
2097
- : `${this.label('crtr.setup.list-down')}/${this.label('crtr.setup.list-up')} move · ${this.label('crtr.setup.cell-previous')}/${this.label('crtr.setup.cell-next')} field · ${this.label('crtr.setup.keybindings.edit')} edit · ${this.tmuxPassthroughOffer() === undefined ? '' : `${this.label('crtr.setup.toggle')} tmux · `}${this.label('crtr.setup.keybindings.unbind')} disable · ${this.label('crtr.setup.keybindings.reset-row')} reset · ${this.label('crtr.setup.keybindings.reset-all')} reset all · ${this.label('crtr.setup.keybindings.filter')} filter · ${this.label('crtr.setup.keybindings.save')} save · ${this.label('crtr.setup.keybindings.discard')} ${this.keyFilter !== '' ? 'clear filter' : 'discard'}`;
2098
- return [truncateToWidth(theme.fg('dim', line), width)];
2099
- }
2100
- renderProviders(width) {
2101
- return this.providers.render(width, Math.max(1, this.terminalRows() - 6));
2102
- }
2103
- renderPlugins(width) {
2104
- const out = [];
2105
- out.push(truncateToWidth(`${theme.fg('accent', theme.bold('Plugins'))}${theme.fg('dim', ' default ON')}`, width));
2106
- for (const [index, entry] of this.manifest.entries()) {
2107
- const selected = index === this.pluginIndex;
2108
- const checked = this.selectedPackageIds.has(entry.id) ? theme.fg('success', '[x]') : theme.fg('dim', '[ ]');
2109
- const marker = selected ? theme.fg('accent', '▶') : theme.fg('dim', ' ');
2110
- const name = selected ? theme.fg('accent', entry.name) : entry.name;
2111
- const status = this.installedPackageIds.has(entry.id)
2112
- ? theme.fg('success', '✓ installed')
2113
- : theme.fg('warning', 'not installed');
2114
- const line = `${marker} ${checked} ${name} ${theme.fg('dim', '—')} ${theme.fg('muted', entry.shortDescription)} ${status}`;
2115
- out.push(truncateToWidth(line, width));
2116
- if (this.expandedPluginId === entry.id) {
2117
- for (const line of wrapText(entry.longDescription, Math.max(1, width - 4))) {
2118
- out.push(truncateToWidth(theme.fg('dim', ` ${line}`), width));
2119
- }
2120
- }
2121
- }
2122
- return out;
2123
- }
2124
- renderSystems(width) {
2125
- const out = [];
2126
- out.push(truncateToWidth(`${theme.fg('accent', theme.bold('System deps'))}${theme.fg('dim', ' tmux + nvim + Nerd Font check')}`, width));
2127
- for (const [index, tool] of this.systemTools.entries()) {
2128
- const selected = index === this.systemIndex;
2129
- const marker = selected ? theme.fg('accent', '▶') : theme.fg('dim', ' ');
2130
- if (tool.installed) {
2131
- const line = `${marker} ${theme.fg('success', '✓ installed')} ${theme.fg('muted', toolLabel(tool.tool))}`;
2132
- out.push(truncateToWidth(line, width));
2133
- continue;
2134
- }
2135
- const checked = this.selectedSystemTools.has(tool.tool) ? theme.fg('success', '[x]') : theme.fg('dim', '[ ]');
2136
- const label = tool.plan.command ?? tool.plan.hint;
2137
- const name = selected ? theme.fg('accent', `Install ${toolLabel(tool.tool)}`) : `Install ${toolLabel(tool.tool)}`;
2138
- const line = `${marker} ${checked} ${name} ${theme.fg('dim', '—')} ${theme.fg('muted', label)} ${theme.fg('warning', 'not installed')}`;
2139
- out.push(truncateToWidth(line, width));
2140
- }
2141
- return out;
188
+ /** `Providers › Plugins › Dependencies › Exa key`, active step lit, done
189
+ * steps checked — a first run should always show how much is left. */
190
+ renderTrail(width) {
191
+ const parts = STEPS.map((label, index) => {
192
+ if (index === this.step)
193
+ return theme.bg("selectedBg", ` ${index + 1} ${label} `);
194
+ const mark = index < this.step ? theme.fg("success", "\u2713") : `${index + 1}`;
195
+ return theme.fg("dim", ` ${mark} ${label} `);
196
+ });
197
+ const trail = parts.join(theme.fg("dim", "\u203a"));
198
+ return visibleWidth(trail) <= width
199
+ ? [truncateToWidth(trail, width), ""]
200
+ : [
201
+ truncateToWidth(`${theme.fg("dim", `Step ${this.step + 1} of ${STEPS.length}`)} ${theme.fg("accent", theme.bold(STEPS[this.step]))}`, width),
202
+ "",
203
+ ];
2142
204
  }
2143
- renderExa(width) {
2144
- const out = [];
2145
- out.push(truncateToWidth(`${theme.fg('accent', theme.bold('Exa key'))}${theme.fg('dim', ' optional')}${keyStatusBadge(detectExaKey(), 'EXA_API_KEY')}`, width));
2146
- out.push(truncateToWidth(`${theme.fg('mdLink', 'Create a key: ')}${theme.fg('mdLinkUrl', 'https://dashboard.exa.ai/api-keys')}`, width));
2147
- out.push(truncateToWidth(theme.fg('dim', `Will write to: ${resolveExaKeyPath()}`), width));
2148
- out.push(...framed([
2149
- theme.fg('accent', theme.bold('Paste EXA_API_KEY here')),
2150
- theme.fg('dim', detectExaKey().present ? 'Leave blank to keep the existing key; paste to replace it.' : 'Leave blank to skip.'),
2151
- ...this.exaInput.render(Math.max(1, width - 4)),
2152
- ], width));
2153
- return out;
205
+ footer(width) {
206
+ const steps = `${this.fieldSafeLabel("crtr.setup.tab-next")}/${this.fieldSafeLabel("crtr.setup.tab-previous")} step \u00b7 ${this.label("crtr.setup.cancel")} cancel`;
207
+ const move = `${this.label("crtr.setup.list-down")}/${this.label("crtr.setup.list-up")}`;
208
+ let left;
209
+ if (this.confirming)
210
+ left = `${this.label("crtr.setup.confirm.accept")} confirm \u00b7 ${this.label("crtr.setup.confirm.back")} go back`;
211
+ else if (this.step === 0)
212
+ left = this.providers.isNestedFlowActive()
213
+ ? "OAuth in progress \u00b7 esc cancel"
214
+ : `${move} select \u00b7 a add \u00b7 p prefer \u00b7 r rename \u00b7 l re-login \u00b7 x remove \u00b7 ${this.label("crtr.setup.advance")} next`;
215
+ else if (this.step === 1)
216
+ left = `${move} move \u00b7 ${this.label("crtr.setup.toggle")} toggle \u00b7 ${this.label("crtr.setup.details")} details \u00b7 ${this.label("crtr.setup.advance")} next`;
217
+ else if (this.step === 2)
218
+ left = `${move} move \u00b7 ${this.label("crtr.setup.toggle")} toggle \u00b7 ${this.label("crtr.setup.advance")} next`;
219
+ else
220
+ left = `paste EXA_API_KEY \u00b7 ${this.label("crtr.setup.advance")} finish`;
221
+ const gap = width - visibleWidth(left) - visibleWidth(steps);
222
+ return gap >= 2
223
+ ? `${theme.fg("dim", left)}${" ".repeat(gap)}${theme.fg("dim", steps)}`
224
+ : theme.fg("dim", left);
2154
225
  }
2155
226
  renderConfirm(width) {
2156
- const out = [truncateToWidth(theme.fg('warning', theme.bold('Confirm installs')), width)];
2157
227
  const submission = this.buildSubmission();
2158
- if (submission.packages.length === 0 && submission.systemDeps.length === 0 && submission.exaKey === '') {
2159
- out.push(truncateToWidth(theme.fg('success', `Nothing to install; ${this.label('crtr.setup.confirm.accept')} finish.`), width));
2160
- return out;
2161
- }
2162
- if (submission.packages.length > 0) {
2163
- out.push(truncateToWidth(theme.fg('accent', 'Packages'), width));
2164
- for (const entry of submission.packages) {
2165
- const action = this.installedPackageIds.has(entry.id)
2166
- ? theme.fg('success', 'skip (already installed)')
2167
- : theme.fg('muted', `pi install ${entry.installSource}`);
2168
- out.push(truncateToWidth(` • ${entry.name} ${theme.fg('dim', '→')} ${action}`, width));
2169
- }
2170
- }
2171
- if (submission.systemDeps.length > 0) {
2172
- out.push(truncateToWidth(theme.fg('accent', 'System deps'), width));
2173
- for (const plan of submission.systemDeps) {
2174
- out.push(truncateToWidth(` • ${plan.tool} ${theme.fg('dim', '→')} ${theme.fg('muted', plan.command ?? plan.hint)}`, width));
2175
- }
2176
- }
2177
- if (submission.exaKey !== '') {
2178
- out.push(truncateToWidth(`${theme.fg('accent', 'Exa key')} ${theme.fg('dim', '→')} ${theme.fg('warning', `write ${submission.exaKeyPath}`)}`, width));
2179
- }
2180
- out.push(truncateToWidth(theme.fg('dim', `${this.label('crtr.setup.confirm.accept')} confirm · ${this.label('crtr.setup.confirm.back')} go back`), width));
2181
- return out;
2182
- }
2183
- keyRowMessage(rows, index) {
2184
- const row = rows[index];
2185
- const selected = index === this.keyIndex;
2186
- const editing = row.fields.find((field) => field.definition.id === this.editingKey);
2187
- if (!selected && editing === undefined)
2188
- return undefined;
2189
- if (editing !== undefined && this.keyEditError)
2190
- return { label: editing.label, message: this.keyEditError };
2191
- const diagnostic = this.keyDiagnostics.find((entry) => row.fields.some((field) => entry.bindingId === field.definition.id || entry.relatedBindingIds?.includes(field.definition.id)));
2192
- if (diagnostic === undefined)
2193
- return undefined;
2194
- const responsible = row.fields.find((field) => diagnostic.bindingId === field.definition.id || diagnostic.relatedBindingIds?.includes(field.definition.id));
2195
- return { label: responsible?.label, message: diagnostic.message };
2196
- }
2197
- /** Number of lines needed to keep the active paired field legible. A narrow
2198
- * row stacks the action and field instead of sacrificing the action name,
2199
- * selected-field identity, or value to fixed columns. */
2200
- keyRowBodyLineCount(row, index, resolution, width) {
2201
- const editingFieldIndex = row.fields.findIndex((field) => field.definition.id === this.editingKey);
2202
- const active = editingFieldIndex >= 0 || index === this.keyIndex;
2203
- if (!active || row.fields.length < 2)
2204
- return 1;
2205
- const fieldIndex = editingFieldIndex >= 0 ? editingFieldIndex : Math.min(this.keyFieldIndex, row.fields.length - 1);
2206
- const field = row.fields[fieldIndex];
2207
- const value = editingFieldIndex >= 0 ? `> ${this.keyInput.getValue()}` : formatBinding(resolution, field.definition.id);
2208
- const bracketedLabel = `[${field.label}]`;
2209
- const inlineWidth = 3 + visibleWidth(row.name) + 1 + visibleWidth(bracketedLabel) + 1 + visibleWidth(value);
2210
- if (inlineWidth <= width)
2211
- return 1;
2212
- const compactFieldWidth = visibleWidth(field.label ?? '') + 1 + visibleWidth(value);
2213
- return compactFieldWidth <= width ? 2 : 3;
2214
- }
2215
- // Output lines the visible rows [start..end] cost, counting group headings,
2216
- // responsive row bodies, and diagnostics. Used to clamp the scroll so the
2217
- // anchor row always renders.
2218
- keyLineCost(rows, start, end, resolution, width) {
2219
- let lines = 0;
2220
- let previousGroup;
2221
- for (let index = start; index <= end; index += 1) {
2222
- const row = rows[index];
2223
- lines += (row.group !== previousGroup ? 1 : 0)
2224
- + this.keyRowBodyLineCount(row, index, resolution, width)
2225
- + (this.keyRowMessage(rows, index) ? 1 : 0)
2226
- + (index === this.keyIndex && this.tmuxPassthroughOffer() !== undefined ? 1 : 0);
2227
- previousGroup = row.group;
2228
- }
2229
- return lines;
2230
- }
2231
- renderKeybindings(width) {
2232
- const out = [];
2233
- const rows = this.filteredKeybindings();
2234
- const resolution = this.keyDraft.resolution();
2235
- const viewport = this.keyViewportLines();
2236
- const pageSize = this.keyPageSize();
2237
- const maxScroll = Math.max(0, rows.length - pageSize);
2238
- this.keyScroll = Math.max(0, Math.min(this.keyScroll, maxScroll));
2239
- if (this.keyIndex < this.keyScroll)
2240
- this.keyScroll = this.keyIndex;
2241
- if (this.keyIndex >= this.keyScroll + pageSize)
2242
- this.keyScroll = Math.max(0, this.keyIndex - pageSize + 1);
2243
- // Anchor that must stay on screen: the row being edited (so the editor never
2244
- // vanishes while paging moves the selection — m3), else the selection itself.
2245
- // Injected group-heading and diagnostic lines can make a pageSize-row window
2246
- // exceed the raw viewport, so instead of a fixed reserve, clamp the scroll
2247
- // dynamically until the anchor is guaranteed to render within `viewport`.
2248
- const editIndex = this.editingKey !== null
2249
- ? rows.findIndex((row) => row.fields.some((field) => field.definition.id === this.editingKey))
2250
- : -1;
2251
- const anchor = editIndex >= 0 ? editIndex : this.keyIndex;
2252
- if (anchor >= 0) {
2253
- if (anchor < this.keyScroll)
2254
- this.keyScroll = anchor;
2255
- while (this.keyScroll < anchor && this.keyLineCost(rows, this.keyScroll, anchor, resolution, width) > viewport) {
2256
- this.keyScroll += 1;
2257
- }
2258
- }
2259
- const dirty = this.keyDraft.dirty ? theme.fg('warning', 'unsaved') : theme.fg('success', 'saved');
2260
- out.push(truncateToWidth(`${theme.fg('accent', theme.bold('Keybindings'))}${theme.fg('dim', ' crouter-owned only · ')}${dirty}`, width));
2261
- out.push(truncateToWidth(theme.fg('dim', this.keyFilter === ''
2262
- ? `${rows.length} actions`
2263
- : `${rows.length} matches for “${this.keyFilter}”${this.filteringKeys ? '' : ` · ${this.label('crtr.setup.keybindings.discard')} clears filter`}`), width));
2264
- if (resolution.diagnostics.some((diagnostic) => diagnostic.code === 'invalid-keybindings')) {
2265
- out.push(truncateToWidth(theme.fg('warning', theme.bold(`Saved keybindings root is invalid — ${this.label('crtr.setup.keybindings.reset-all')} (reset all) to clear it before editing.`)), width));
2266
- }
2267
- if (rows.length === 0) {
2268
- out.push('');
2269
- out.push(truncateToWidth(theme.fg('warning', 'No matching keybindings.'), width));
2270
- return out;
2271
- }
2272
- const nameWidth = Math.max(10, Math.min(26, Math.floor(width * 0.28)));
2273
- let emitted = 0;
2274
- let previousGroup;
2275
- for (let index = this.keyScroll; index < rows.length; index += 1) {
2276
- const row = rows[index];
2277
- const selected = index === this.keyIndex;
2278
- const editingFieldIndex = row.fields.findIndex((field) => field.definition.id === this.editingKey);
2279
- const activeFieldIndex = editingFieldIndex >= 0
2280
- ? editingFieldIndex
2281
- : selected ? Math.min(this.keyFieldIndex, row.fields.length - 1) : 0;
2282
- const message = this.keyRowMessage(rows, index);
2283
- const tmuxOffer = selected ? this.tmuxPassthroughOffer() : undefined;
2284
- const bodyLines = this.keyRowBodyLineCount(row, index, resolution, width);
2285
- // Budget output lines (heading + responsive row + optional diagnostic) so
2286
- // the list never clips the confirm prompt / notice / footer below it.
2287
- const cost = (row.group !== previousGroup ? 1 : 0) + bodyLines + (message ? 1 : 0) + (tmuxOffer ? 1 : 0);
2288
- if (emitted > 0 && emitted + cost > viewport)
2289
- break;
2290
- if (row.group !== previousGroup) {
2291
- out.push(truncateToWidth(theme.fg('accent', row.group), width));
2292
- previousGroup = row.group;
2293
- }
2294
- const marker = selected ? theme.fg('accent', '▶') : ' ';
2295
- const sourceMark = row.fields.some((field) => resolution.get(field.definition.id).source === 'user')
2296
- ? theme.fg('warning', '•')
2297
- : ' ';
2298
- if (bodyLines > 1) {
2299
- const field = row.fields[activeFieldIndex];
2300
- const editing = field.definition.id === this.editingKey;
2301
- const plainValue = editing ? `> ${this.keyInput.getValue()}` : formatBinding(resolution, field.definition.id);
2302
- const bracketedLabel = `[${field.label}]`;
2303
- const compactLabel = field.label ?? '';
2304
- out.push(truncateToWidth(`${marker}${sourceMark} ${selected ? theme.fg('accent', row.name) : row.name}`, width));
2305
- if (bodyLines === 2) {
2306
- const useBracketed = visibleWidth(bracketedLabel) + 1 + visibleWidth(plainValue) <= width;
2307
- const plainLabel = useBracketed ? bracketedLabel : compactLabel;
2308
- const label = theme.fg('accent', theme.bold(plainLabel));
2309
- const indent = ' '.repeat(Math.max(0, Math.min(3, width - visibleWidth(plainLabel) - 1 - visibleWidth(plainValue))));
2310
- const valueWidth = Math.max(1, width - visibleWidth(indent) - visibleWidth(label) - 1);
2311
- const value = editing ? (this.keyInput.render(valueWidth)[0] ?? '') : plainValue;
2312
- out.push(truncateToWidth(`${indent}${label} ${value}`, width));
2313
- }
2314
- else {
2315
- const label = theme.fg('accent', theme.bold(bracketedLabel));
2316
- const labelIndent = ' '.repeat(Math.max(0, Math.min(3, width - visibleWidth(label))));
2317
- const value = editing ? (this.keyInput.render(width)[0] ?? '') : plainValue;
2318
- out.push(truncateToWidth(`${labelIndent}${label}`, width));
2319
- out.push(truncateToWidth(value, width));
2320
- }
2321
- }
2322
- else {
2323
- const name = padAnsi(selected ? theme.fg('accent', row.name) : row.name, nameWidth);
2324
- const available = Math.max(1, width - 4 - nameWidth);
2325
- const showAllFields = row.fields.length === 1 || available >= 28;
2326
- const visibleFields = showAllFields ? row.fields : [row.fields[activeFieldIndex]];
2327
- const fieldAreaWidth = row.fields.length > 1
2328
- ? Math.min(61, available)
2329
- : Math.min(34, available);
2330
- const gapWidth = Math.max(0, visibleFields.length - 1);
2331
- const fieldWidth = Math.max(1, Math.floor((fieldAreaWidth - gapWidth) / visibleFields.length));
2332
- const renderedFields = visibleFields.map((field) => {
2333
- const fieldIndex = row.fields.indexOf(field);
2334
- const editing = field.definition.id === this.editingKey;
2335
- const active = editing || (selected && fieldIndex === activeFieldIndex);
2336
- const label = field.label === undefined
2337
- ? ''
2338
- : active
2339
- ? theme.fg('accent', theme.bold(`[${field.label}]`))
2340
- : theme.fg('dim', `${field.label}:`);
2341
- const labelWidth = visibleWidth(label);
2342
- const valueWidth = Math.max(1, fieldWidth - labelWidth - (label === '' ? 0 : 1));
2343
- // Render the edit field whenever THIS underlying definition is being
2344
- // edited, even if paging moved row selection elsewhere.
2345
- const value = editing
2346
- ? (this.keyInput.render(valueWidth)[0] ?? '')
2347
- : formatBinding(resolution, field.definition.id);
2348
- const content = label === '' ? value : `${label} ${value}`;
2349
- return padAnsi(truncateToWidth(content, fieldWidth), fieldWidth);
2350
- });
2351
- const fieldsText = renderedFields.join(' ');
2352
- const descriptionWidth = available - visibleWidth(fieldsText) - 1;
2353
- const description = descriptionWidth >= 16
2354
- ? ` ${theme.fg('dim', truncateToWidth(row.description, descriptionWidth))}`
2355
- : '';
2356
- out.push(truncateToWidth(`${marker}${sourceMark} ${name} ${fieldsText}${description}`, width));
2357
- }
2358
- if (tmuxOffer) {
2359
- const checked = this.keyDraft.usesTmuxPassthrough(tmuxOffer.id)
2360
- ? theme.fg('success', '[x]')
2361
- : theme.fg('dim', '[ ]');
2362
- out.push(truncateToWidth(` ${checked} ${theme.fg('accent', `Use tmux's ${formatGesture(tmuxOffer.gesture)} here`)}`, width));
2363
- }
2364
- if (message) {
2365
- const fieldPrefix = message.label === undefined ? '' : `${message.label}: `;
2366
- out.push(truncateToWidth(theme.fg('warning', ` ${fieldPrefix}${message.message}`), width));
2367
- }
2368
- emitted += cost;
2369
- }
2370
- if (this.keyConfirm !== null) {
2371
- out.push('');
2372
- out.push(truncateToWidth(theme.fg('warning', theme.bold(this.keyConfirm === 'reset-all'
2373
- ? 'Reset every keybinding to its catalog default?'
2374
- : 'Discard every unsaved keybinding change?')), width));
2375
- }
2376
- if (this.keyNotice)
2377
- out.push(truncateToWidth(theme.fg('muted', this.keyNotice), width));
2378
- return out;
2379
- }
2380
- renderModels(width) {
2381
- return this.models.render(width, Math.max(1, this.terminalRows() - 6));
2382
- }
2383
- renderBrokerLimits(width) {
2384
- const out = [];
2385
- const inputWidth = Math.max(8, width - 28);
2386
- const renderLimit = (index, label, description, input) => {
2387
- const selected = index === this.brokerLimitIndex;
2388
- const marker = selected ? theme.fg('accent', '▶') : ' ';
2389
- const name = selected ? theme.fg('accent', label) : label;
2390
- const value = input.render(inputWidth)[0] ?? '';
2391
- out.push(truncateToWidth(`${marker} ${padAnsi(name, 24)} ${value}`, width));
2392
- out.push(truncateToWidth(theme.fg('dim', ` ${description}`), width));
2393
- };
2394
- out.push(truncateToWidth(`${theme.fg('accent', theme.bold('Broker limits'))}${theme.fg('dim', ' user scope · defaults 128 / 256')}`, width));
2395
- out.push(truncateToWidth(theme.fg('dim', 'These bounds govern daemon automatic revive; explicit node revives remain available.'), width));
2396
- out.push('');
2397
- renderLimit(0, 'Warning threshold', 'Warn when live brokers reach this count. Default: 128.', this.brokerWarningInput);
2398
- renderLimit(1, 'Automatic-revive hard cap', 'Defer automatic revives at this count. Default: 256.', this.brokerHardCapInput);
2399
- out.push('');
2400
- for (const line of wrapText(`Configured limits apply next tick. ${BROKER_LIMIT_OVERRIDE_PRECEDENCE}`, width)) {
2401
- out.push(theme.fg('dim', line));
2402
- }
2403
- if (this.brokerLimitNotice) {
2404
- const role = this.brokerLimitNotice.startsWith('Saved') ? 'success' : 'warning';
2405
- for (const line of wrapText(this.brokerLimitNotice, width))
2406
- out.push(theme.fg(role, line));
2407
- }
2408
- return out;
2409
- }
2410
- renderSettings(width) {
2411
- const out = [];
2412
- const config = readConfig('user');
2413
- const capacity = Math.max(1, Math.floor((this.terminalRows() - 13) / 2));
2414
- const range = visibleRange(USER_SETTINGS.length, this.settingsIndex, capacity);
2415
- out.push(truncateToWidth(`${theme.fg('accent', theme.bold('Settings'))}${theme.fg('dim', ' user scope')}`, width));
2416
- out.push(truncateToWidth(theme.fg('dim', 'General preferences live here; specialized settings keep their own tabs. Changes save immediately.'), width));
2417
- out.push('');
2418
- for (let index = range.start; index < range.end; index += 1) {
2419
- const setting = USER_SETTINGS[index];
2420
- const selected = index === this.settingsIndex;
2421
- const marker = selected ? theme.fg('accent', '▶') : ' ';
2422
- const name = selected ? theme.fg('accent', setting.label) : setting.label;
2423
- out.push(truncateToWidth(`${marker} ${padAnsi(name, 24)} ${theme.fg('success', setting.format(config))}`, width));
2424
- out.push(truncateToWidth(theme.fg('dim', ` ${setting.description}`), width));
2425
- }
2426
- if (range.start > 0 || range.end < USER_SETTINGS.length) {
2427
- out.push(theme.fg('dim', `(${this.settingsIndex + 1}/${USER_SETTINGS.length})`));
2428
- }
2429
- if (this.settingsNotice) {
2430
- out.push('');
2431
- for (const line of wrapText(this.settingsNotice, width))
2432
- out.push(theme.fg('success', line));
2433
- }
228
+ const out = [
229
+ truncateToWidth(theme.fg("warning", theme.bold("Confirm installs")), width),
230
+ "",
231
+ ];
232
+ if (submission.packages.length === 0 &&
233
+ submission.systemDeps.length === 0 &&
234
+ submission.exaKey === "")
235
+ return [
236
+ ...out,
237
+ truncateToWidth(theme.fg("success", `Nothing to install; ${this.label("crtr.setup.confirm.accept")} finish.`), width),
238
+ ];
239
+ if (submission.packages.length) {
240
+ out.push(truncateToWidth(theme.fg("accent", "Packages"), width));
241
+ for (const entry of submission.packages)
242
+ out.push(truncateToWidth(` \u2022 ${entry.name} ${theme.fg("dim", "\u2192")} ${this.installedPackageIds.has(entry.id) ? theme.fg("success", "skip (already installed)") : theme.fg("muted", `pi install ${entry.installSource}`)}`, width));
243
+ }
244
+ if (submission.systemDeps.length) {
245
+ out.push(truncateToWidth(theme.fg("accent", "System deps"), width));
246
+ for (const plan of submission.systemDeps)
247
+ out.push(truncateToWidth(` \u2022 ${plan.tool} ${theme.fg("dim", "\u2192")} ${theme.fg("muted", plan.command ?? plan.hint)}`, width));
248
+ }
249
+ if (submission.exaKey)
250
+ out.push(truncateToWidth(`${theme.fg("accent", "Exa key")} ${theme.fg("dim", "\u2192")} ${theme.fg("warning", `write ${submission.exaKeyPath}`)}`, width));
2434
251
  return out;
2435
252
  }
2436
253
  render(width) {
2437
- const bypass = this.activeTab === 5 && this.models.isPickerOpen();
254
+ const bypass = this.step === 0;
2438
255
  const height = this.terminalRows();
2439
- if (!bypass && this.cachedLines && this.cachedWidth === width && this.cachedHeight === height)
256
+ if (!bypass &&
257
+ this.cachedLines &&
258
+ this.cachedWidth === width &&
259
+ this.cachedHeight === height)
2440
260
  return this.cachedLines;
2441
- const lines = [
2442
- ...this.renderHeader(width),
2443
- '',
2444
- ...(this.confirming
2445
- ? this.renderConfirm(width)
2446
- : this.activeTab === 0
2447
- ? this.renderProviders(width)
2448
- : this.activeTab === 1
2449
- ? this.renderPlugins(width)
2450
- : this.activeTab === 2
2451
- ? this.renderSystems(width)
2452
- : this.activeTab === 3
2453
- ? this.renderExa(width)
2454
- : this.activeTab === 4
2455
- ? this.renderKeybindings(width)
2456
- : this.activeTab === 5
2457
- ? this.renderModels(width)
2458
- : this.activeTab === 6
2459
- ? this.renderBrokerLimits(width)
2460
- : this.renderSettings(width)),
2461
- '',
2462
- ...this.renderFooter(width),
2463
- ];
2464
- const rendered = renderRows(lines, width);
261
+ const panel = this.currentPanel();
262
+ // On a terminal too short for both, the step content wins over the trail:
263
+ // a frame that shows only "which step" and none of the step is useless.
264
+ const budget = Math.max(1, height - PAGE_CHROME_ROWS);
265
+ const trail = this.confirming || budget <= 2 ? [] : this.renderTrail(width);
266
+ const bodyHeight = Math.max(1, budget - trail.length);
267
+ const lines = pageFrame({
268
+ width,
269
+ height,
270
+ title: "crouter setup",
271
+ meta: this.confirming
272
+ ? "review"
273
+ : `step ${this.step + 1} of ${STEPS.length}`,
274
+ body: [
275
+ ...trail,
276
+ ...(this.confirming
277
+ ? this.renderConfirm(width)
278
+ : panel.render(width, bodyHeight)),
279
+ ],
280
+ footer: this.footer(width),
281
+ });
2465
282
  if (!bypass) {
2466
283
  this.cachedWidth = width;
2467
284
  this.cachedHeight = height;
2468
- this.cachedLines = rendered;
285
+ this.cachedLines = lines;
2469
286
  }
2470
- return rendered;
287
+ return lines;
2471
288
  }
2472
289
  }
2473
290
  export async function runSetup() {
2474
291
  const state = buildSetupInitialState();
2475
- // Front-door reconciliation: invoking `crtr sys setup` — TTY or not —
2476
- // refreshes crouter-owned tmux bindings and registers known nodes' interaction
2477
- // roots so resolved tickets can call back into the asking node. Best-effort +
2478
- // idempotent, so it is safe to run on every invocation regardless of whether
2479
- // the wizard is opened, canceled, or submitted (the post-submission call
2480
- // re-runs it harmlessly).
2481
292
  await reconcileInboxFrontDoor();
2482
293
  if (!process.stdout.isTTY) {
2483
294
  process.stdout.write(renderSetupStaticInstructions(state));
2484
295
  return;
2485
296
  }
2486
- applyTheme({ cwd: process.cwd() });
2487
- const agentDir = defaultAgentDir();
2488
- // The runtime reads the SAME credentials every crouter surface does, through crouter's
2489
- // own CredentialStore (pi 0.82 made credential storage app-owned): the subscription pool
2490
- // for managed providers, auth.json for the rest. `registry` is pi's synchronous facade
2491
- // over it, which the panels' sync reads need.
2492
- const runtime = await ModelRuntime.create({
2493
- credentials: new PoolCredentialStore(join(agentDir, 'auth.json')),
2494
- modelsPath: join(agentDir, 'models.json'),
2495
- });
2496
- const registry = new ModelRegistry(runtime);
2497
- const rawUserConfig = readRawScopeConfig('user');
2498
- const rawKeybindings = rawUserConfig?.keybindings;
2499
- const openingBindings = resolveKeybindings(rawKeybindings);
2500
- const terminal = new ProcessTerminal();
2501
- const tui = new TUI(terminal, true);
2502
- const wizard = new SetupWizard(state.manifest, state.installedPackageIds, state.systemTools, tui, agentDir, runtime, registry, openingBindings, rawKeybindings, () => terminal.rows, rawUserConfig?.tmux_passthrough);
2503
- tui.addChild(wizard);
2504
- tui.setFocus(wizard);
2505
- await new Promise((resolve, reject) => {
2506
- let restored = false;
2507
- const restore = () => {
2508
- if (restored)
2509
- return;
2510
- restored = true;
2511
- wizard.dispose();
2512
- try {
2513
- tui.stop();
2514
- }
2515
- catch {
2516
- /* best-effort terminal restore */
2517
- }
2518
- };
2519
- let done = false;
2520
- const finish = () => {
2521
- if (done)
2522
- return;
2523
- done = true;
2524
- restore();
2525
- resolve();
2526
- };
2527
- const fail = (error) => {
2528
- if (done)
2529
- return;
2530
- done = true;
2531
- restore();
2532
- reject(error);
2533
- };
2534
- const onSignal = () => finish();
2535
- process.once('exit', restore);
2536
- process.once('SIGTERM', onSignal);
2537
- process.once('SIGHUP', onSignal);
2538
- process.once('SIGINT', onSignal);
2539
- // Emergency cancel: the control gestures of `crtr.setup.cancel` (ctrl+c /
2540
- // ctrl+d in the shipped catalog) must always exit setup — from filter entry,
2541
- // the confirm prompts, or while a nested ProviderPanel login flow owns focus —
2542
- // where the focused component would otherwise swallow them. Matching is driven
2543
- // by the opening snapshot; the control-char guard keeps printable cancel gestures
2544
- // (e.g. q) as data for text fields and the filter buffer, which the wizard
2545
- // handles as cancel only in its own non-text states.
2546
- tui.addInputListener((data) => {
2547
- if (data.length > 0 && data.charCodeAt(0) < 32
2548
- && matchesPiTuiInput(openingBindings, 'crtr.setup.cancel', data, (input, gesture) => matchesKey(input, gesture))) {
2549
- finish();
2550
- return { consume: true };
2551
- }
2552
- return undefined;
2553
- });
2554
- wizard.onCancel = finish;
2555
- wizard.onSubmit = (submission) => {
2556
- restore();
2557
- void executeSetupSubmission(submission)
2558
- .then((summary) => {
2559
- if (done)
2560
- return;
2561
- done = true;
2562
- process.stdout.write(formatSetupExecutionSummary(summary));
2563
- resolve();
2564
- }, fail);
2565
- };
2566
- try {
2567
- tui.start();
2568
- }
2569
- catch (error) {
2570
- fail(error);
2571
- }
2572
- });
297
+ const submission = await runSetupSettingsProcess(({ tui, terminal, agentDir, runtime, registry, bindings }, control) => {
298
+ const wizard = new SetupWizard(state.manifest, state.installedPackageIds, state.systemPlans, tui, agentDir, runtime, registry, bindings, () => terminal.rows);
299
+ wizard.onCancel = control.cancel;
300
+ wizard.onSubmit = control.submit;
301
+ return wizard;
302
+ }, (data, bindings) => data.length > 0 &&
303
+ data.charCodeAt(0) < 32 &&
304
+ matchesPiTuiInput(bindings, "crtr.setup.cancel", data, (input, gesture) => matchesKey(input, gesture)));
305
+ if (submission === undefined)
306
+ return;
307
+ const summary = await executeSetupSubmission(submission);
308
+ process.stdout.write(formatSetupExecutionSummary(summary));
2573
309
  }