@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.
- package/bin/runtime-selector.mjs +8 -1
- package/dist/api/__tests__/{client.test.js → serial/client.test.js} +2 -135
- package/dist/api/client.d.ts +12 -21
- package/dist/api/client.js +22 -31
- package/dist/api/dto/broker-ops.d.ts +158 -0
- package/dist/api/dto/broker-ops.js +6 -0
- package/dist/api/dto/crons.d.ts +6 -0
- package/dist/api/dto/human.d.ts +7 -1
- package/dist/api/dto/inbox.d.ts +1 -71
- package/dist/api/dto/inbox.js +1 -9
- package/dist/api/dto/nodes.d.ts +6 -7
- package/dist/api/dto/reviews.d.ts +1 -11
- package/dist/api/routes.d.ts +7 -4
- package/dist/api/routes.js +8 -5
- package/dist/build-root.js +3 -3
- package/dist/builtin-memory/04-base-worker.md +1 -4
- package/dist/builtin-memory/05-kinds/advisor/00-base.md +1 -5
- package/dist/builtin-memory/05-kinds/advisor/01-orchestrator.md +2 -7
- package/dist/builtin-memory/05-kinds/design/00-base.md +1 -6
- package/dist/builtin-memory/05-kinds/developer/00-base.md +1 -6
- package/dist/builtin-memory/05-kinds/explore/00-base.md +1 -4
- package/dist/builtin-memory/05-kinds/explore/01-orchestrator.md +1 -2
- package/dist/builtin-memory/05-kinds/general/00-base.md +1 -3
- package/dist/builtin-memory/05-kinds/plan/00-base.md +1 -6
- package/dist/builtin-memory/05-kinds/plan/reviewers/00-base.md +1 -2
- package/dist/builtin-memory/05-kinds/plan/reviewers/architecture-fit.md +1 -3
- package/dist/builtin-memory/05-kinds/plan/reviewers/code-smells.md +1 -3
- package/dist/builtin-memory/05-kinds/plan/reviewers/pattern-consistency.md +1 -3
- package/dist/builtin-memory/05-kinds/plan/reviewers/requirements-coverage.md +1 -3
- package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +1 -4
- package/dist/builtin-memory/05-kinds/review/companion/00-base.md +2 -3
- package/dist/builtin-memory/05-kinds/spec/00-base.md +1 -2
- package/dist/builtin-memory/advisor/council.md +1 -6
- package/dist/builtin-memory/design.md +2 -7
- package/dist/builtin-memory/development.md +9 -14
- package/dist/builtin-memory/internal/marketplaces.md +2 -6
- package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
- package/dist/builtin-memory/internal/plugins.md +2 -8
- package/dist/builtin-memory/internal/storage-tiers.md +1 -2
- package/dist/builtin-memory/testing.md +41 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/{provider-rotation.test.ts → serial/provider-rotation.test.ts} +106 -177
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/claude-plugin-commands.ts +5 -15
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/frontmatter-rules/index.ts +1 -28
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +43 -88
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +55 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +60 -0
- package/dist/cli.js +2 -2
- package/dist/clients/attach/__tests__/attach-remote-readonly.test.js +37 -131
- package/dist/clients/attach/__tests__/chat-view-snapshot-ordering.test.js +71 -152
- package/dist/clients/attach/__tests__/context-message.test.js +16 -174
- package/dist/clients/attach/__tests__/crtr-output.test.js +36 -2069
- package/dist/clients/attach/__tests__/diagram.test.js +2 -2
- package/dist/clients/attach/__tests__/edit-diff.test.js +0 -4
- package/dist/clients/attach/__tests__/file-review-focus.test.js +2 -1
- package/dist/clients/attach/__tests__/group-activity.test.js +40 -229
- package/dist/clients/attach/__tests__/oauth-dialog-lifecycle.test.js +26 -76
- package/dist/clients/attach/__tests__/prompt-preview.test.js +4 -70
- package/dist/clients/attach/__tests__/ref-autocomplete.test.js +11 -125
- package/dist/clients/attach/__tests__/titled-editor-preview.test.js +14 -205
- package/dist/clients/attach/command.d.ts +2 -1
- package/dist/clients/attach/command.js +81 -76
- package/dist/clients/attach/input/capabilities.d.ts +27 -0
- package/dist/clients/attach/input/capabilities.js +96 -0
- package/dist/clients/attach/input/controller.d.ts +20 -182
- package/dist/clients/attach/input/controller.js +128 -537
- package/dist/clients/attach/input/overlay-owner.d.ts +41 -0
- package/dist/clients/attach/input/overlay-owner.js +199 -0
- package/dist/clients/attach/overlays/file-review.d.ts +0 -4
- package/dist/clients/attach/overlays/file-review.js +13 -5
- package/dist/clients/attach/overlays/graph.js +2 -10
- package/dist/clients/attach/render/chat-view.d.ts +4 -3
- package/dist/clients/attach/render/chat-view.js +39 -33
- package/dist/clients/attach/render/group-activity.d.ts +1 -0
- package/dist/clients/attach/render/group-activity.js +20 -13
- package/dist/clients/attach/render/group-recap.js +12 -3
- package/dist/clients/attach/session/connection.js +9 -15
- package/dist/clients/attach/session/editor-inventory.d.ts +6 -5
- package/dist/clients/attach/session/editor-inventory.js +7 -8
- package/dist/clients/attach/session/file-links.d.ts +13 -0
- package/dist/clients/attach/session/file-links.js +107 -0
- package/dist/clients/attach/session/frame.d.ts +3 -0
- package/dist/clients/attach/session/frame.js +10 -1
- package/dist/clients/attach/session/frames.js +2 -3
- package/dist/clients/attach/session/input-wiring.d.ts +4 -1
- package/dist/clients/attach/session/input-wiring.js +15 -14
- package/dist/clients/attach/session/mouse.d.ts +6 -1
- package/dist/clients/attach/session/mouse.js +8 -5
- package/dist/clients/attach/session/profile-files.js +18 -12
- package/dist/clients/attach/session/reconnect.js +1 -1
- package/dist/clients/attach/session/state-sync.js +1 -2
- package/dist/clients/attach/session/surface-host.d.ts +3 -2
- package/dist/clients/attach/session/surface-host.js +4 -4
- package/dist/clients/attach/slash/dispatch.d.ts +8 -31
- package/dist/clients/attach/slash/dispatch.js +34 -81
- package/dist/clients/attach/viewer.js +820 -860
- package/dist/clients/conversation/projection.d.ts +79 -0
- package/dist/clients/conversation/projection.js +341 -0
- package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +159 -0
- package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +151 -0
- package/dist/clients/inbox/controller.d.ts +0 -4
- package/dist/clients/inbox/controller.js +0 -2
- package/dist/clients/inbox/review/__tests__/review-core.test.js +4 -293
- package/dist/clients/inbox/review/__tests__/teardown-notice.test.js +100 -0
- package/dist/clients/inbox/review/comments-client.d.ts +3 -2
- package/dist/clients/inbox/review/comments-client.js +12 -12
- package/dist/clients/inbox/review/companion-pane.d.ts +30 -0
- package/dist/clients/inbox/review/companion-pane.js +86 -0
- package/dist/clients/inbox/review/document-surface.d.ts +182 -0
- package/dist/clients/inbox/review/{document-region.js → document-surface.js} +252 -48
- package/dist/clients/inbox/review/frame.js +5 -4
- package/dist/clients/inbox/review/launch.d.ts +8 -8
- package/dist/clients/inbox/review/launch.js +52 -92
- package/dist/clients/inbox/review/nvim.d.ts +2 -0
- package/dist/clients/inbox/review/nvim.js +14 -12
- package/dist/clients/inbox/review/review-client.d.ts +12 -8
- package/dist/clients/inbox/review/review-client.js +46 -35
- package/dist/clients/inbox/review-adapter.d.ts +3 -10
- package/dist/clients/inbox/review-adapter.js +27 -19
- package/dist/clients/inbox/surface.d.ts +0 -4
- package/dist/clients/inbox/surface.js +0 -2
- package/dist/clients/surfaces/host.d.ts +11 -3
- package/dist/commands/__tests__/api-client-cold-start-diagnostic.test.js +7 -34
- package/dist/commands/__tests__/human.test.js +11 -322
- package/dist/commands/api-client.d.ts +5 -5
- package/dist/commands/api-client.js +6 -85
- package/dist/commands/canvas-browse.js +3 -29
- package/dist/commands/canvas-config.js +1 -1
- package/dist/commands/canvas-use.js +3 -3
- package/dist/commands/canvas.js +1 -1
- package/dist/commands/cron.js +5 -1
- package/dist/commands/human/queue.d.ts +0 -4
- package/dist/commands/human/queue.js +7 -40
- package/dist/commands/memory/list.d.ts +14 -0
- package/dist/commands/memory/list.js +35 -31
- package/dist/commands/memory/read.d.ts +7 -0
- package/dist/commands/memory/read.js +14 -8
- package/dist/commands/node/bash.d.ts +1 -0
- package/dist/commands/node/bash.js +198 -0
- package/dist/commands/node/create.d.ts +23 -0
- package/dist/commands/node/create.js +207 -0
- package/dist/commands/node/inspect.d.ts +2 -0
- package/dist/commands/node/inspect.js +302 -0
- package/dist/commands/node/lifecycle.d.ts +4 -0
- package/dist/commands/node/lifecycle.js +415 -0
- package/dist/commands/node/message.d.ts +1 -0
- package/dist/commands/node/message.js +164 -0
- package/dist/commands/node/pyramid.d.ts +2 -0
- package/dist/commands/node/pyramid.js +79 -0
- package/dist/commands/node/subscription.d.ts +1 -0
- package/dist/commands/node/subscription.js +82 -0
- package/dist/commands/node/wait.d.ts +1 -0
- package/dist/commands/node/wait.js +159 -0
- package/dist/commands/node-lifecycle-revive.js +10 -57
- package/dist/commands/node.d.ts +0 -42
- package/dist/commands/node.js +11 -2065
- package/dist/commands/search.js +1 -1
- package/dist/commands/surface/node/cycle.d.ts +1 -0
- package/dist/commands/surface/node/cycle.js +83 -0
- package/dist/commands/surface/node/focus.d.ts +1 -0
- package/dist/commands/surface/node/focus.js +67 -0
- package/dist/commands/surface/node/id.d.ts +1 -0
- package/dist/commands/surface/node/id.js +38 -0
- package/dist/commands/surface/node/pin.d.ts +1 -0
- package/dist/commands/surface/node/pin.js +42 -0
- package/dist/commands/surface/node/placement.d.ts +46 -0
- package/dist/commands/surface/node/placement.js +124 -0
- package/dist/commands/surface-node.js +4 -1
- package/dist/commands/surface-reload.js +1 -2
- package/dist/commands/surface-tmux-spread.js +1 -1
- package/dist/commands/surface.js +5 -20
- package/dist/commands/sys/__tests__/setup-core.test.js +18 -271
- package/dist/commands/sys/config.js +6 -6
- package/dist/commands/sys/logs.js +1 -1
- package/dist/commands/sys/panels/broker-limits-panel.d.ts +17 -0
- package/dist/commands/sys/panels/broker-limits-panel.js +104 -0
- package/dist/commands/sys/panels/dependencies-panel.d.ts +14 -0
- package/dist/commands/sys/panels/dependencies-panel.js +80 -0
- package/dist/commands/sys/panels/keybindings-panel.d.ts +56 -0
- package/dist/commands/sys/panels/keybindings-panel.js +703 -0
- package/dist/commands/sys/panels/keys-panel.d.ts +16 -0
- package/dist/commands/sys/panels/keys-panel.js +60 -0
- package/dist/commands/sys/panels/models-panel.d.ts +77 -0
- package/dist/commands/sys/panels/models-panel.js +647 -0
- package/dist/commands/sys/panels/panel.d.ts +45 -0
- package/dist/commands/sys/panels/panel.js +137 -0
- package/dist/commands/sys/panels/plugins-panel.d.ts +16 -0
- package/dist/commands/sys/panels/plugins-panel.js +80 -0
- package/dist/commands/sys/panels/prefs-panel.d.ts +21 -0
- package/dist/commands/sys/panels/prefs-panel.js +85 -0
- package/dist/commands/sys/panels/provider-panel.d.ts +62 -0
- package/dist/commands/sys/panels/provider-panel.js +538 -0
- package/dist/commands/sys/settings-shell.d.ts +55 -0
- package/dist/commands/sys/settings-shell.js +400 -0
- package/dist/commands/sys/settings.d.ts +2 -0
- package/dist/commands/sys/settings.js +31 -0
- package/dist/commands/sys/setup-core.d.ts +3 -33
- package/dist/commands/sys/setup-core.js +27 -78
- package/dist/commands/sys/setup-settings-runner.d.ts +24 -0
- package/dist/commands/sys/setup-settings-runner.js +63 -0
- package/dist/commands/sys/setup-wizard.d.ts +26 -125
- package/dist/commands/sys/setup-wizard.js +209 -2473
- package/dist/commands/sys/setup.js +5 -7
- package/dist/commands/sys.js +3 -2
- package/dist/core/__tests__/broker-extension-canvas-db-boundary.test.js +50 -0
- package/dist/core/__tests__/broker-launch-candidates.test.js +20 -9
- package/dist/core/__tests__/broker-preflight.test.js +1 -1
- package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +108 -27
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +14 -0
- package/dist/core/__tests__/canvas.test.js +2 -36
- package/dist/core/__tests__/close.test.js +14 -3
- package/dist/core/__tests__/context-intro.test.js +32 -143
- package/dist/core/__tests__/cron-broker-capacity.test.js +22 -3
- package/dist/core/__tests__/daemon-boot.test.js +2 -20
- package/dist/core/__tests__/daemon-wedge.test.js +1 -1
- package/dist/core/__tests__/dead-node-policy-table.test.js +1 -18
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +1 -1
- package/dist/core/__tests__/fixtures/fake-engine.js +2 -2
- package/dist/core/__tests__/fixtures/fake-pi-host.js +2 -2
- package/dist/core/__tests__/focuses.test.js +4 -98
- package/dist/core/__tests__/helpers/broker-clients.d.ts +0 -3
- package/dist/core/__tests__/helpers/broker-clients.js +0 -14
- package/dist/core/__tests__/helpers/harness.d.ts +0 -8
- package/dist/core/__tests__/helpers/harness.js +7 -87
- package/dist/core/__tests__/helpers/viewer-panes.js +1 -1
- package/dist/core/__tests__/inline-memory-refs.test.js +5 -134
- package/dist/core/__tests__/kickoff.test.js +1 -1
- package/dist/core/__tests__/memory-resolver-precedence.test.js +29 -1
- package/dist/core/__tests__/migration.test.js +35 -48
- package/dist/core/__tests__/on-read-dedup-resume.test.js +22 -12
- package/dist/core/__tests__/passive-subscription.test.js +26 -49
- package/dist/core/__tests__/preview-registry-sync.test.js +1 -2
- package/dist/core/__tests__/prune.test.js +94 -1
- package/dist/core/__tests__/review-model-floor.test.js +0 -32
- package/dist/core/__tests__/seam/broker-attach-multiclient.test.js +255 -0
- package/dist/core/__tests__/{full → seam}/broker-attach-stream.test.js +39 -7
- package/dist/core/__tests__/{full → seam}/broker-crash-teardown.test.js +30 -29
- package/dist/core/__tests__/{full → seam}/broker-pane-resolution.test.js +54 -5
- package/dist/core/__tests__/{full → seam}/broker-provider-retry.test.js +134 -82
- package/dist/core/__tests__/{full → seam}/broker-whip-interrupt.test.js +15 -4
- package/dist/core/__tests__/{full/idle-release-settlement.test.js → seam/dormancy-release.test.js} +69 -17
- package/dist/core/__tests__/{full/yield-message-transaction.test.js → seam/yield-refresh-transaction.test.js} +31 -9
- package/dist/core/__tests__/secrets.test.js +1 -7
- package/dist/core/__tests__/{broker-fork-seam.test.js → serial/broker-fork-seam.test.js} +58 -7
- package/dist/core/__tests__/{broker-sdk-wiring.test.js → serial/broker-sdk-wiring.test.js} +18 -162
- package/dist/core/__tests__/{broker-snapshot-history.test.js → serial/broker-snapshot-history.test.js} +9 -9
- package/dist/core/__tests__/{command-plugins.test.js → serial/command-plugins.test.js} +105 -15
- package/dist/core/__tests__/{deferred-no-wake.test.js → serial/deferred-no-wake.test.js} +23 -20
- package/dist/core/__tests__/{flagship-lifecycle.test.js → serial/flagship-lifecycle.test.js} +7 -7
- package/dist/core/__tests__/{host-teardown-process-group.test.js → serial/host-teardown-process-group.test.js} +6 -6
- package/dist/core/__tests__/{human-deliver-e2e.test.js → serial/human-deliver-e2e.test.js} +12 -12
- package/dist/core/__tests__/{live-mutation.test.js → serial/live-mutation.test.js} +5 -5
- package/dist/core/__tests__/{refresh-stall-recycle.test.js → serial/refresh-stall-recycle.test.js} +5 -14
- package/dist/core/__tests__/{revive.test.js → serial/revive.test.js} +15 -100
- package/dist/core/__tests__/{spawn-root.test.js → serial/spawn-root.test.js} +32 -15
- package/dist/core/__tests__/serial/subscription-delivery.test.js +126 -0
- package/dist/core/__tests__/serial/tmux-surface.test.js +256 -0
- package/dist/core/__tests__/{worktree.test.js → serial/worktree.test.js} +15 -15
- package/dist/core/__tests__/session-model.test.js +0 -18
- package/dist/core/__tests__/spawn-worktree-id-conflict.test.js +9 -0
- package/dist/core/__tests__/watchdog-abort-arms-retry.test.js +50 -0
- package/dist/core/broker-client/__tests__/transport-relay.test.js +15 -16
- package/dist/core/broker-client/__tests__/transport-socket.test.js +28 -0
- package/dist/core/broker-client/client.d.ts +2 -2
- package/dist/core/broker-client/client.js +4 -7
- package/dist/core/broker-client/transport-relay.d.ts +13 -19
- package/dist/core/broker-client/transport-relay.js +19 -34
- package/dist/core/broker-client/transport-socket.d.ts +2 -2
- package/dist/core/broker-client/transport-socket.js +9 -2
- package/dist/core/broker-client/transport.d.ts +7 -3
- package/dist/core/broker-client/transport.js +4 -4
- package/dist/core/canvas/__tests__/attention.test.js +1 -18
- package/dist/core/canvas/__tests__/remote-canvas-source.test.js +7 -64
- package/dist/core/canvas/__tests__/remote-transport.test.js +2 -3
- package/dist/core/canvas/__tests__/render-remote.test.js +7 -52
- package/dist/core/canvas/browse/app.d.ts +2 -17
- package/dist/core/canvas/browse/app.js +16 -88
- package/dist/core/canvas/browse/model.d.ts +0 -4
- package/dist/core/canvas/browse/model.js +1 -12
- package/dist/core/canvas/browse/render.js +6 -3
- package/dist/core/canvas/crons.d.ts +8 -5
- package/dist/core/canvas/crons.js +7 -6
- package/dist/core/canvas/extensions.d.ts +7 -3
- package/dist/core/canvas/extensions.js +16 -4
- package/dist/core/canvas/nav-model.d.ts +0 -4
- package/dist/core/canvas/nav-model.js +2 -14
- package/dist/core/canvas/node-order.d.ts +3 -0
- package/dist/core/canvas/node-order.js +12 -0
- package/dist/core/canvas/remote-canvas-source.d.ts +3 -6
- package/dist/core/canvas/remote-canvas-source.js +8 -15
- package/dist/core/command.js +11 -4
- package/dist/core/config.js +3 -1
- package/dist/core/events/envelope.js +1 -1
- package/dist/core/events/read.js +7 -21
- package/dist/core/events/source.d.ts +0 -2
- package/dist/core/events/source.js +0 -16
- package/dist/core/events/types.d.ts +1 -1
- package/dist/core/fault-classifier.d.ts +1 -1
- package/dist/core/feed/passive.d.ts +0 -11
- package/dist/core/feed/passive.js +1 -62
- package/dist/core/human/__tests__/{inbox-core.test.js → serial/inbox-core.test.js} +6 -6
- package/dist/core/keybindings/__tests__/resolve.test.js +1 -96
- package/dist/core/keybindings/background-bash.js +3 -4
- package/dist/core/keybindings/catalog.js +1 -1
- package/dist/core/memory/inline-ref-grammar.js +3 -3
- package/dist/core/memory-resolver.d.ts +11 -25
- package/dist/core/memory-resolver.js +51 -33
- package/dist/core/model-routes.d.ts +15 -0
- package/dist/core/model-routes.js +12 -0
- package/dist/core/pool-credential-store.js +1 -1
- package/dist/core/preview-registry.js +8 -9
- package/dist/core/profiles/__tests__/root-profile.test.js +4 -14
- package/dist/core/review/realize.d.ts +0 -13
- package/dist/core/review/realize.js +0 -40
- package/dist/core/review/types.d.ts +1 -1
- package/dist/core/runtime/__tests__/engine-route.test.js +2 -2
- package/dist/core/runtime/bearings.js +1 -11
- package/dist/core/runtime/broker/client-registry.d.ts +33 -0
- package/dist/core/runtime/broker/client-registry.js +164 -0
- package/dist/core/runtime/broker/daemon-ops.d.ts +19 -0
- package/dist/core/runtime/broker/daemon-ops.js +49 -0
- package/dist/core/runtime/broker/event-projection.d.ts +42 -0
- package/dist/core/runtime/broker/event-projection.js +178 -0
- package/dist/core/runtime/broker/fault-retry.d.ts +39 -0
- package/dist/core/runtime/broker/fault-retry.js +277 -0
- package/dist/core/runtime/broker/frame-dispatch.d.ts +238 -0
- package/dist/core/runtime/broker/frame-dispatch.js +1537 -0
- package/dist/core/runtime/broker/inbox.d.ts +22 -0
- package/dist/core/runtime/broker/inbox.js +167 -0
- package/dist/core/runtime/broker/passive.d.ts +16 -0
- package/dist/core/runtime/broker/passive.js +44 -0
- package/dist/core/runtime/broker/read-ops.d.ts +23 -0
- package/dist/core/runtime/broker/read-ops.js +150 -0
- package/dist/core/runtime/broker/rebind.d.ts +74 -0
- package/dist/core/runtime/broker/rebind.js +211 -0
- package/dist/core/runtime/broker/tool-groups.d.ts +39 -0
- package/dist/core/runtime/broker/tool-groups.js +244 -0
- package/dist/core/runtime/broker-extension-render.d.ts +11 -0
- package/dist/core/runtime/broker-extension-render.js +181 -0
- package/dist/core/runtime/broker-persona-guidance.d.ts +13 -0
- package/dist/core/runtime/broker-persona-guidance.js +58 -0
- package/dist/core/runtime/broker-protocol.d.ts +1 -1
- package/dist/core/runtime/broker-protocol.js +1 -1
- package/dist/core/runtime/broker.d.ts +5 -196
- package/dist/core/runtime/broker.js +276 -3059
- package/dist/core/runtime/canvas-extensions.js +11 -2
- package/dist/core/runtime/fault.js +1 -1
- package/dist/core/runtime/model-selection.d.ts +9 -0
- package/dist/core/runtime/model-selection.js +12 -0
- package/dist/core/runtime/model-swap.d.ts +3 -3
- package/dist/core/runtime/model-swap.js +33 -25
- package/dist/core/runtime/placement-tmux.d.ts +3 -14
- package/dist/core/runtime/placement-tmux.js +11 -80
- package/dist/core/runtime/placement.d.ts +4 -2
- package/dist/core/runtime/placement.js +7 -12
- package/dist/core/runtime/promote.d.ts +14 -1
- package/dist/core/runtime/promote.js +1 -1
- package/dist/core/runtime/reset.d.ts +1 -1
- package/dist/core/runtime/roadmap.js +1 -1
- package/dist/core/runtime/spawn.d.ts +8 -0
- package/dist/core/runtime/spawn.js +36 -6
- package/dist/core/runtime/stop-guard.d.ts +4 -3
- package/dist/core/runtime/stop-guard.js +36 -18
- package/dist/core/runtime/stop-signals.d.ts +4 -1
- package/dist/core/runtime/stream-watchdog.js +4 -5
- package/dist/core/runtime/structured-output.d.ts +15 -2
- package/dist/core/runtime/structured-output.js +35 -12
- package/dist/core/runtime/tmux-bindings.d.ts +25 -0
- package/dist/core/runtime/tmux-bindings.js +682 -0
- package/dist/core/runtime/tmux-chrome.d.ts +2 -1
- package/dist/core/runtime/tmux-chrome.js +3 -2
- package/dist/core/runtime/{tmux.d.ts → tmux-driver.d.ts} +18 -26
- package/dist/core/runtime/tmux-driver.js +598 -0
- package/dist/core/runtime/view-socket.d.ts +14 -0
- package/dist/core/runtime/view-socket.js +83 -0
- package/dist/core/session-model/session-state.d.ts +1 -2
- package/dist/core/session-model/session-state.js +5 -8
- package/dist/core/spawn.d.ts +1 -1
- package/dist/core/spawn.js +2 -2
- package/dist/core/substrate/gate.d.ts +1 -1
- package/dist/core/substrate/on-read-pure.d.ts +7 -0
- package/dist/core/substrate/on-read-pure.js +272 -0
- package/dist/core/termrender/termrender.js +32 -2
- package/dist/core/user-settings.d.ts +23 -1
- package/dist/core/user-settings.js +18 -1
- package/dist/daemon/api/__tests__/node-model-validation.test.js +134 -0
- package/dist/daemon/api/__tests__/review-terminal-conflict.test.js +98 -0
- package/dist/daemon/api/__tests__/{full → seam}/api-server.test.js +6 -48
- package/dist/daemon/api/__tests__/{full → seam}/b10-attach-modelauth.test.js +17 -44
- package/dist/daemon/api/__tests__/{full/push-parity.test.js → seam/leaf-api-parity.test.js} +61 -42
- package/dist/daemon/api/bridge.js +40 -81
- package/dist/daemon/api/handlers/broker-ops.d.ts +2 -0
- package/dist/daemon/api/handlers/broker-ops.js +383 -0
- package/dist/daemon/api/handlers/crons.js +9 -4
- package/dist/daemon/api/handlers/files.js +6 -7
- package/dist/daemon/api/handlers/inbox.js +2 -340
- package/dist/daemon/api/handlers/nodes.d.ts +10 -1
- package/dist/daemon/api/handlers/nodes.js +74 -39
- package/dist/daemon/api/handlers/review-comments.js +5 -1
- package/dist/daemon/api/handlers/reviews.js +0 -19
- package/dist/daemon/api/map.js +25 -4
- package/dist/daemon/api/router.js +2 -3
- package/dist/daemon/api/server.js +5 -7
- package/dist/daemon/cron-run.d.ts +4 -0
- package/dist/daemon/cron-run.js +30 -13
- package/dist/daemon/crtrd.d.ts +3 -58
- package/dist/daemon/crtrd.js +28 -1244
- package/dist/daemon/manage.js +0 -1
- package/dist/daemon/reconcilers/broker-supervision.d.ts +50 -0
- package/dist/daemon/reconcilers/broker-supervision.js +549 -0
- package/dist/daemon/reconcilers/cron-lane.d.ts +10 -0
- package/dist/daemon/reconcilers/cron-lane.js +32 -0
- package/dist/daemon/reconcilers/dormant-inbox.d.ts +18 -0
- package/dist/daemon/reconcilers/dormant-inbox.js +143 -0
- package/dist/daemon/reconcilers/storage-maintenance.d.ts +22 -0
- package/dist/daemon/reconcilers/storage-maintenance.js +213 -0
- package/dist/daemon/review/finish.js +2 -5
- package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +34 -53
- package/dist/pi-extensions/__tests__/canvas-recap.test.js +16 -113
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +72 -99
- package/dist/pi-extensions/__tests__/canvas-stophook-context-nudge.test.js +29 -12
- package/dist/pi-extensions/broker-local.d.ts +20 -0
- package/dist/pi-extensions/broker-local.js +89 -0
- package/dist/pi-extensions/canvas-context-intro.d.ts +3 -3
- package/dist/pi-extensions/canvas-context-intro.js +22 -11
- package/dist/pi-extensions/canvas-doc-substrate.js +17 -34
- package/dist/pi-extensions/canvas-goal-capture.js +7 -12
- package/dist/pi-extensions/canvas-inbox-watcher.js +124 -139
- package/dist/pi-extensions/canvas-passive-context.d.ts +3 -3
- package/dist/pi-extensions/canvas-passive-context.js +22 -18
- package/dist/pi-extensions/canvas-recap.d.ts +2 -2
- package/dist/pi-extensions/canvas-recap.js +48 -21
- package/dist/pi-extensions/canvas-review-boundary.js +10 -4
- package/dist/pi-extensions/canvas-stophook.d.ts +0 -5
- package/dist/pi-extensions/canvas-stophook.js +149 -431
- package/dist/pi-extensions/canvas-structured-output.js +52 -30
- package/dist/pi-extensions/envelope-merge.d.ts +13 -0
- package/dist/pi-extensions/envelope-merge.js +29 -0
- package/dist/shared/working-activity.d.ts +3 -2
- package/dist/shared/working-activity.js +3 -2
- package/dist/types.d.ts +6 -5
- package/dist/types.js +2 -2
- package/docs/compat/hearth-crtr-v1.md +4 -12
- package/docs/compat/hearth-crtr-v2.md +3 -11
- package/docs/compat/hearth-crtr-v3.md +4 -12
- package/docs/compat/hearth-crtr-v4.md +3 -11
- package/docs/compat/hearth-crtr-v5.md +3 -11
- package/docs/public-api.md +3 -7
- package/package.json +7 -49
- package/runtime.lock.json +2766 -14533
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/claude-plugin-commands.test.ts +0 -49
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/memory-slash-commands.test.ts +0 -194
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/statusline.test.ts +0 -68
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/frontmatter-rules/test.ts +0 -119
- package/dist/clients/attach/__tests__/attach-chrome-remote.test.js +0 -439
- package/dist/clients/attach/__tests__/attach-keybindings.test.js +0 -170
- package/dist/clients/attach/__tests__/crtr-output-coverage.test.js +0 -57
- package/dist/clients/attach/__tests__/editor-frame-title.test.js +0 -19
- package/dist/clients/attach/__tests__/input-controller-extension-command.test.js +0 -32
- package/dist/clients/attach/__tests__/markdown-headings.test.js +0 -35
- package/dist/clients/attach/__tests__/session-identity-refresh.test.js +0 -32
- package/dist/clients/conversation/conversation.d.ts +0 -65
- package/dist/clients/conversation/conversation.js +0 -318
- package/dist/clients/conversation/frames.d.ts +0 -30
- package/dist/clients/conversation/frames.js +0 -52
- package/dist/clients/conversation/region.d.ts +0 -51
- package/dist/clients/conversation/region.js +0 -265
- package/dist/clients/inbox/__tests__/inbox-controller.test.js +0 -247
- package/dist/clients/inbox/__tests__/mount-panel.test.js +0 -776
- package/dist/clients/inbox/review/__tests__/remap.test.js +0 -196
- package/dist/clients/inbox/review/columns.d.ts +0 -22
- package/dist/clients/inbox/review/columns.js +0 -40
- package/dist/clients/inbox/review/composition.d.ts +0 -60
- package/dist/clients/inbox/review/composition.js +0 -278
- package/dist/clients/inbox/review/document-region.d.ts +0 -120
- package/dist/clients/surfaces/region.d.ts +0 -16
- package/dist/clients/web/__tests__/push-engine.test.js +0 -158
- package/dist/clients/web/__tests__/push-registry.test.d.ts +0 -1
- package/dist/clients/web/__tests__/push-registry.test.js +0 -177
- package/dist/clients/web/dev-server.d.ts +0 -7
- package/dist/clients/web/dev-server.js +0 -46
- package/dist/clients/web/events.d.ts +0 -19
- package/dist/clients/web/events.js +0 -163
- package/dist/clients/web/push-engine.d.ts +0 -44
- package/dist/clients/web/push-engine.js +0 -125
- package/dist/clients/web/push-registry.d.ts +0 -85
- package/dist/clients/web/push-registry.js +0 -373
- package/dist/clients/web/server.d.ts +0 -19
- package/dist/clients/web/server.js +0 -661
- package/dist/clients/web/web-client/shared/protocol.d.ts +0 -214
- package/dist/clients/web/web-client/shared/protocol.js +0 -8
- package/dist/clients/web/web-cmd.d.ts +0 -2
- package/dist/clients/web/web-cmd.js +0 -141
- package/dist/commands/__tests__/api-canvas-source.test.d.ts +0 -1
- package/dist/commands/__tests__/api-canvas-source.test.js +0 -37
- package/dist/commands/__tests__/canvas-config.test.d.ts +0 -1
- package/dist/commands/__tests__/canvas-config.test.js +0 -107
- package/dist/commands/__tests__/node-no-kickoff-flag.test.d.ts +0 -1
- package/dist/commands/__tests__/node-no-kickoff-flag.test.js +0 -73
- package/dist/commands/__tests__/node-worktree-flag.test.d.ts +0 -1
- package/dist/commands/__tests__/node-worktree-flag.test.js +0 -49
- package/dist/commands/__tests__/search-contents.test.d.ts +0 -1
- package/dist/commands/__tests__/search-contents.test.js +0 -52
- package/dist/commands/memory/__tests__/lint.test.d.ts +0 -1
- package/dist/commands/memory/__tests__/lint.test.js +0 -15
- package/dist/commands/sys/__tests__/config-broker-thresholds.test.d.ts +0 -1
- package/dist/commands/sys/__tests__/config-broker-thresholds.test.js +0 -95
- package/dist/commands/sys/__tests__/config-keybindings.test.d.ts +0 -1
- package/dist/commands/sys/__tests__/config-keybindings.test.js +0 -55
- package/dist/commands/sys/__tests__/helpers/setup-runtime.d.ts +0 -5
- package/dist/commands/sys/__tests__/helpers/setup-runtime.js +0 -19
- package/dist/commands/sys/__tests__/setup-front-door.test.d.ts +0 -1
- package/dist/commands/sys/__tests__/setup-front-door.test.js +0 -102
- package/dist/commands/sys/__tests__/setup-models.test.d.ts +0 -1
- package/dist/commands/sys/__tests__/setup-models.test.js +0 -205
- package/dist/commands/sys/__tests__/setup-providers-viewport.test.d.ts +0 -1
- package/dist/commands/sys/__tests__/setup-providers-viewport.test.js +0 -155
- package/dist/commands/sys/__tests__/sync-deps.test.d.ts +0 -1
- package/dist/commands/sys/__tests__/sync-deps.test.js +0 -179
- package/dist/commands/sys/__tests__/sync-import.test.d.ts +0 -1
- package/dist/commands/sys/__tests__/sync-import.test.js +0 -315
- package/dist/core/__tests__/base-worker-prompt.test.d.ts +0 -1
- package/dist/core/__tests__/base-worker-prompt.test.js +0 -57
- package/dist/core/__tests__/command-plugins-surfaces.test.d.ts +0 -1
- package/dist/core/__tests__/command-plugins-surfaces.test.js +0 -359
- package/dist/core/__tests__/connection-reconnect-resume.test.d.ts +0 -1
- package/dist/core/__tests__/connection-reconnect-resume.test.js +0 -36
- package/dist/core/__tests__/finalization-migration.test.d.ts +0 -1
- package/dist/core/__tests__/finalization-migration.test.js +0 -38
- package/dist/core/__tests__/fixtures/extra-extension.d.ts +0 -1
- package/dist/core/__tests__/fixtures/extra-extension.js +0 -3
- package/dist/core/__tests__/fixtures/spawn-proof-target.test.d.ts +0 -1
- package/dist/core/__tests__/fixtures/spawn-proof-target.test.js +0 -15
- package/dist/core/__tests__/fork.test.d.ts +0 -1
- package/dist/core/__tests__/fork.test.js +0 -108
- package/dist/core/__tests__/full/broker-attach-limits.test.d.ts +0 -1
- package/dist/core/__tests__/full/broker-attach-limits.test.js +0 -147
- package/dist/core/__tests__/full/broker-deliver-ack.test.d.ts +0 -1
- package/dist/core/__tests__/full/broker-deliver-ack.test.js +0 -73
- package/dist/core/__tests__/full/broker-dialogs.test.d.ts +0 -1
- package/dist/core/__tests__/full/broker-dialogs.test.js +0 -180
- package/dist/core/__tests__/full/broker-display-snapshot.test.d.ts +0 -1
- package/dist/core/__tests__/full/broker-display-snapshot.test.js +0 -75
- package/dist/core/__tests__/full/broker-dormant-wake.test.d.ts +0 -1
- package/dist/core/__tests__/full/broker-dormant-wake.test.js +0 -130
- package/dist/core/__tests__/full/broker-model-changed-broadcast.test.d.ts +0 -1
- package/dist/core/__tests__/full/broker-model-changed-broadcast.test.js +0 -57
- package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.d.ts +0 -1
- package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.js +0 -80
- package/dist/core/__tests__/full/cascade-close.test.d.ts +0 -1
- package/dist/core/__tests__/full/cascade-close.test.js +0 -240
- package/dist/core/__tests__/full/detach-focus.test.d.ts +0 -1
- package/dist/core/__tests__/full/detach-focus.test.js +0 -126
- package/dist/core/__tests__/full/idle-release-settlement.test.d.ts +0 -1
- package/dist/core/__tests__/full/issue-146-yield-spawn-focus.test.d.ts +0 -1
- package/dist/core/__tests__/full/issue-146-yield-spawn-focus.test.js +0 -107
- package/dist/core/__tests__/full/park-clock-release.test.d.ts +0 -1
- package/dist/core/__tests__/full/park-clock-release.test.js +0 -56
- package/dist/core/__tests__/full/yield-message-transaction.test.d.ts +0 -1
- package/dist/core/__tests__/memory-index-render.test.d.ts +0 -1
- package/dist/core/__tests__/memory-index-render.test.js +0 -60
- package/dist/core/__tests__/model-routes.test.d.ts +0 -1
- package/dist/core/__tests__/model-routes.test.js +0 -41
- package/dist/core/__tests__/on-read-identity.test.d.ts +0 -1
- package/dist/core/__tests__/on-read-identity.test.js +0 -61
- package/dist/core/__tests__/phase4-review-migration.test.d.ts +0 -1
- package/dist/core/__tests__/phase4-review-migration.test.js +0 -100
- package/dist/core/__tests__/prune-to-limit.test.d.ts +0 -1
- package/dist/core/__tests__/prune-to-limit.test.js +0 -126
- package/dist/core/__tests__/reset.test.d.ts +0 -1
- package/dist/core/__tests__/reset.test.js +0 -69
- package/dist/core/__tests__/spawn-no-kickoff.test.d.ts +0 -1
- package/dist/core/__tests__/spawn-no-kickoff.test.js +0 -115
- package/dist/core/__tests__/steer-note.test.d.ts +0 -1
- package/dist/core/__tests__/steer-note.test.js +0 -41
- package/dist/core/__tests__/subcommand-tier.test.d.ts +0 -1
- package/dist/core/__tests__/subcommand-tier.test.js +0 -99
- package/dist/core/__tests__/subscription-delivery.test.js +0 -211
- package/dist/core/__tests__/test-runner-cheap-sharding.test.d.ts +0 -1
- package/dist/core/__tests__/test-runner-cheap-sharding.test.js +0 -97
- package/dist/core/__tests__/tmux-surface.test.js +0 -484
- package/dist/core/__tests__/wake-bearings.test.d.ts +0 -1
- package/dist/core/__tests__/wake-bearings.test.js +0 -102
- package/dist/core/canvas/__tests__/remote-event-stream.test.d.ts +0 -1
- package/dist/core/canvas/__tests__/remote-event-stream.test.js +0 -146
- package/dist/core/canvas/__tests__/source-resolve.test.d.ts +0 -1
- package/dist/core/canvas/__tests__/source-resolve.test.js +0 -77
- package/dist/core/canvas/browse/__tests__/model.test.d.ts +0 -1
- package/dist/core/canvas/browse/__tests__/model.test.js +0 -290
- package/dist/core/canvas/browse/__tests__/rebuild-coalescer.test.d.ts +0 -1
- package/dist/core/canvas/browse/__tests__/rebuild-coalescer.test.js +0 -134
- package/dist/core/canvas/browse/__tests__/render.test.d.ts +0 -1
- package/dist/core/canvas/browse/__tests__/render.test.js +0 -111
- package/dist/core/canvas/browse/rebuild-coalescer.d.ts +0 -7
- package/dist/core/canvas/browse/rebuild-coalescer.js +0 -61
- package/dist/core/canvas/remote-event-stream.d.ts +0 -24
- package/dist/core/canvas/remote-event-stream.js +0 -94
- package/dist/core/feed/__tests__/coalesce-human.test.d.ts +0 -1
- package/dist/core/feed/__tests__/coalesce-human.test.js +0 -36
- package/dist/core/feed/__tests__/empty-node-guard.test.d.ts +0 -1
- package/dist/core/feed/__tests__/empty-node-guard.test.js +0 -45
- package/dist/core/host-exports/__tests__/export-prunes-boot-skill.test.d.ts +0 -1
- package/dist/core/host-exports/__tests__/export-prunes-boot-skill.test.js +0 -84
- package/dist/core/human/__tests__/visible-paths.test.d.ts +0 -1
- package/dist/core/human/__tests__/visible-paths.test.js +0 -28
- package/dist/core/keybindings/__tests__/inbox-affordance.test.d.ts +0 -1
- package/dist/core/keybindings/__tests__/inbox-affordance.test.js +0 -38
- package/dist/core/review/__tests__/project.test.d.ts +0 -1
- package/dist/core/review/__tests__/project.test.js +0 -71
- package/dist/core/runtime/__tests__/command-expansion.test.d.ts +0 -1
- package/dist/core/runtime/__tests__/command-expansion.test.js +0 -25
- package/dist/core/runtime/__tests__/session-visibility.test.d.ts +0 -1
- package/dist/core/runtime/__tests__/session-visibility.test.js +0 -119
- package/dist/core/runtime/__tests__/spawn-at-given-id.test.d.ts +0 -1
- package/dist/core/runtime/__tests__/spawn-at-given-id.test.js +0 -34
- package/dist/core/runtime/tmux.js +0 -1314
- package/dist/daemon/api/__tests__/full/push-parity.test.d.ts +0 -1
- package/dist/daemon/api/__tests__/full/worktree-parity.test.d.ts +0 -1
- package/dist/daemon/api/__tests__/full/worktree-parity.test.js +0 -160
- package/dist/daemon/api/__tests__/inbox.test.d.ts +0 -1
- package/dist/daemon/api/__tests__/inbox.test.js +0 -489
- package/dist/web-client/apple-touch-icon.png +0 -0
- package/dist/web-client/assets/fragment-mono-latin-400-normal-BYwT3kSJ.woff +0 -0
- package/dist/web-client/assets/fragment-mono-latin-400-normal-yxdJ5AmL.woff2 +0 -0
- package/dist/web-client/assets/fragment-mono-latin-ext-400-normal-BbKYyvR9.woff2 +0 -0
- package/dist/web-client/assets/fragment-mono-latin-ext-400-normal-CT4YFKeK.woff +0 -0
- package/dist/web-client/assets/index-CCPaLeW7.css +0 -2
- package/dist/web-client/assets/index-kyOWsnZK.js +0 -86
- package/dist/web-client/assets/martian-mono-cyrillic-wght-normal-B84CD5C_.woff2 +0 -0
- package/dist/web-client/assets/martian-mono-latin-ext-wght-normal-DlL6xMw5.woff2 +0 -0
- package/dist/web-client/assets/martian-mono-latin-wght-normal-5W32yIyr.woff2 +0 -0
- package/dist/web-client/assets/workbox-window.prod.es5-Bd17z0YL.js +0 -2
- package/dist/web-client/icon-192.png +0 -0
- package/dist/web-client/icon-512.png +0 -0
- package/dist/web-client/icon-maskable-512.png +0 -0
- package/dist/web-client/index.html +0 -17
- package/dist/web-client/manifest.webmanifest +0 -1
- package/dist/web-client/sw.js +0 -1
- /package/dist/api/__tests__/{client.test.d.ts → serial/client.test.d.ts} +0 -0
- /package/dist/clients/inbox/__tests__/{inbox-controller.test.d.ts → serial/inbox-controller.test.d.ts} +0 -0
- /package/dist/clients/inbox/__tests__/{mount-panel.test.d.ts → serial/mount-panel.test.d.ts} +0 -0
- /package/dist/clients/{attach/__tests__/attach-chrome-remote.test.d.ts → inbox/review/__tests__/teardown-notice.test.d.ts} +0 -0
- /package/dist/{clients/attach/__tests__/attach-keybindings.test.d.ts → core/__tests__/broker-extension-canvas-db-boundary.test.d.ts} +0 -0
- /package/dist/{clients/attach/__tests__/crtr-output-coverage.test.d.ts → core/__tests__/seam/broker-attach-multiclient.test.d.ts} +0 -0
- /package/dist/core/__tests__/{full → seam}/broker-attach-stream.test.d.ts +0 -0
- /package/dist/core/__tests__/{full → seam}/broker-crash-teardown.test.d.ts +0 -0
- /package/dist/core/__tests__/{full → seam}/broker-pane-resolution.test.d.ts +0 -0
- /package/dist/core/__tests__/{full → seam}/broker-provider-retry.test.d.ts +0 -0
- /package/dist/core/__tests__/{full → seam}/broker-whip-interrupt.test.d.ts +0 -0
- /package/dist/{clients/attach/__tests__/editor-frame-title.test.d.ts → core/__tests__/seam/dormancy-release.test.d.ts} +0 -0
- /package/dist/{clients/attach/__tests__/input-controller-extension-command.test.d.ts → core/__tests__/seam/yield-refresh-transaction.test.d.ts} +0 -0
- /package/dist/core/__tests__/{broker-fork-seam.test.d.ts → serial/broker-fork-seam.test.d.ts} +0 -0
- /package/dist/core/__tests__/{broker-sdk-wiring.test.d.ts → serial/broker-sdk-wiring.test.d.ts} +0 -0
- /package/dist/core/__tests__/{broker-snapshot-history.test.d.ts → serial/broker-snapshot-history.test.d.ts} +0 -0
- /package/dist/core/__tests__/{command-plugins.test.d.ts → serial/command-plugins.test.d.ts} +0 -0
- /package/dist/core/__tests__/{deferred-no-wake.test.d.ts → serial/deferred-no-wake.test.d.ts} +0 -0
- /package/dist/core/__tests__/{flagship-lifecycle.test.d.ts → serial/flagship-lifecycle.test.d.ts} +0 -0
- /package/dist/core/__tests__/{host-teardown-process-group.test.d.ts → serial/host-teardown-process-group.test.d.ts} +0 -0
- /package/dist/core/__tests__/{human-deliver-e2e.test.d.ts → serial/human-deliver-e2e.test.d.ts} +0 -0
- /package/dist/core/__tests__/{live-mutation.test.d.ts → serial/live-mutation.test.d.ts} +0 -0
- /package/dist/core/__tests__/{refresh-stall-recycle.test.d.ts → serial/refresh-stall-recycle.test.d.ts} +0 -0
- /package/dist/core/__tests__/{revive.test.d.ts → serial/revive.test.d.ts} +0 -0
- /package/dist/core/__tests__/{spawn-root.test.d.ts → serial/spawn-root.test.d.ts} +0 -0
- /package/dist/core/__tests__/{subscription-delivery.test.d.ts → serial/subscription-delivery.test.d.ts} +0 -0
- /package/dist/core/__tests__/{tmux-surface.test.d.ts → serial/tmux-surface.test.d.ts} +0 -0
- /package/dist/core/__tests__/{worktree.test.d.ts → serial/worktree.test.d.ts} +0 -0
- /package/dist/{clients/attach/__tests__/markdown-headings.test.d.ts → core/__tests__/watchdog-abort-arms-retry.test.d.ts} +0 -0
- /package/dist/{clients/attach/__tests__/session-identity-refresh.test.d.ts → core/broker-client/__tests__/transport-socket.test.d.ts} +0 -0
- /package/dist/core/human/__tests__/{inbox-core.test.d.ts → serial/inbox-core.test.d.ts} +0 -0
- /package/dist/{clients/inbox/review/__tests__/remap.test.d.ts → daemon/api/__tests__/node-model-validation.test.d.ts} +0 -0
- /package/dist/{clients/surfaces/region.js → daemon/api/__tests__/review-terminal-conflict.test.d.ts} +0 -0
- /package/dist/daemon/api/__tests__/{full → seam}/api-server.test.d.ts +0 -0
- /package/dist/daemon/api/__tests__/{full → seam}/b10-attach-modelauth.test.d.ts +0 -0
- /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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
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,
|
|
1331
|
-
this.
|
|
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.
|
|
1342
|
-
|
|
1343
|
-
this.
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
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.
|
|
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
|
-
|
|
1383
|
-
|
|
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.
|
|
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
|
-
|
|
68
|
+
const gestures = this.bindings.gestures(id);
|
|
69
|
+
return gestures.length === 0
|
|
70
|
+
? "disabled"
|
|
71
|
+
: formatGesture(gestures[0].split(" > ").pop());
|
|
1442
72
|
}
|
|
1443
|
-
/**
|
|
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
|
|
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(
|
|
1451
|
-
|
|
1452
|
-
|
|
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
|
|
1456
|
-
|
|
1457
|
-
|
|
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
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
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
|
-
|
|
1481
|
-
|
|
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
|
-
|
|
1492
|
-
|
|
1493
|
-
if (
|
|
1494
|
-
|
|
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.
|
|
1499
|
-
this.invalidate();
|
|
99
|
+
this.moveStep(1);
|
|
1500
100
|
}
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
101
|
+
cancel() {
|
|
102
|
+
if (this.confirming) {
|
|
103
|
+
this.confirming = false;
|
|
104
|
+
this.syncFocus();
|
|
105
|
+
this.invalidate();
|
|
1504
106
|
return;
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
else
|
|
1508
|
-
this.selectedSystemTools.add(tool.tool);
|
|
1509
|
-
this.invalidate();
|
|
107
|
+
}
|
|
108
|
+
this.onCancel?.();
|
|
1510
109
|
}
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
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
|
-
|
|
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.
|
|
119
|
+
packages: [...this.plugins.selected],
|
|
1526
120
|
installedPackageIds: new Set(this.installedPackageIds),
|
|
1527
|
-
systemDeps: this.
|
|
1528
|
-
exaKey: this.
|
|
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
|
-
|
|
1538
|
-
|
|
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
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
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
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
if (
|
|
1953
|
-
if (
|
|
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(
|
|
160
|
+
if (this.matches("crtr.setup.confirm.accept", data))
|
|
1959
161
|
this.onSubmit?.(this.buildSubmission());
|
|
1960
|
-
|
|
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
|
-
|
|
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.
|
|
2001
|
-
|
|
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.
|
|
2021
|
-
|
|
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 (
|
|
2032
|
-
this.exaInput.handleInput(data);
|
|
181
|
+
if (panel.handleInput(data, match)) {
|
|
2033
182
|
this.invalidate();
|
|
2034
183
|
return;
|
|
2035
184
|
}
|
|
2036
|
-
if (this.
|
|
2037
|
-
this.
|
|
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
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
?
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
truncateToWidth(
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
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
|
-
|
|
2144
|
-
const
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
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
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
if (submission.packages.length
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
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.
|
|
254
|
+
const bypass = this.step === 0;
|
|
2438
255
|
const height = this.terminalRows();
|
|
2439
|
-
if (!bypass &&
|
|
256
|
+
if (!bypass &&
|
|
257
|
+
this.cachedLines &&
|
|
258
|
+
this.cachedWidth === width &&
|
|
259
|
+
this.cachedHeight === height)
|
|
2440
260
|
return this.cachedLines;
|
|
2441
|
-
const
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
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 =
|
|
285
|
+
this.cachedLines = lines;
|
|
2469
286
|
}
|
|
2470
|
-
return
|
|
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
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
const
|
|
2497
|
-
|
|
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
|
}
|