@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,1314 +0,0 @@
|
|
|
1
|
-
// tmux placement — one window per active node.
|
|
2
|
-
//
|
|
3
|
-
// session = a root window = a node window 0 = optional dashboard
|
|
4
|
-
//
|
|
5
|
-
// Background windows run but don't render — only the current window draws. That
|
|
6
|
-
// is the "detached but switchable" model: nothing tiles, you never see a node's
|
|
7
|
-
// UI unless you switch to it. Bring one forefront with select-window (within a
|
|
8
|
-
// root) or switch-client + select-window (across roots). done/dead nodes close
|
|
9
|
-
// their window; reviving opens a fresh one.
|
|
10
|
-
import { execFile, spawnSync } from 'node:child_process';
|
|
11
|
-
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
12
|
-
import { homedir, tmpdir } from 'node:os';
|
|
13
|
-
import { join } from 'node:path';
|
|
14
|
-
import { ATTACH_CONTROL_BINDINGS, BINDING_CATALOG, BINDING_IDS, encodeAttachControlInput, formatGesture, isAttachPaneBinding, resolveUserKeybindingSettings, } from '../keybindings/index.js';
|
|
15
|
-
import { normalizeTmuxPassthrough } from '../config.js';
|
|
16
|
-
import { withExclusiveDirectoryLock } from '../human/convention.js';
|
|
17
|
-
import { bundledPiCommandForShell } from './pi-cli.js';
|
|
18
|
-
import { surfaceTmuxStyleArgs } from './surface-bg.js';
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
// Shell quoting + tmux invocation
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
22
|
-
/** POSIX single-quote escaping for one shell word. */
|
|
23
|
-
export function shellQuote(s) {
|
|
24
|
-
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
25
|
-
}
|
|
26
|
-
function tmux(args) {
|
|
27
|
-
const r = spawnSync('tmux', args, { encoding: 'utf8' });
|
|
28
|
-
return {
|
|
29
|
-
ok: r.status === 0,
|
|
30
|
-
stdout: (r.stdout ?? '').trim(),
|
|
31
|
-
stderr: (r.stderr ?? '').trim(),
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
function tmuxOnSocket(socket, args) {
|
|
35
|
-
return socket === undefined || socket === '' ? tmux(args) : tmux(['-S', socket, ...args]);
|
|
36
|
-
}
|
|
37
|
-
export function inTmux() {
|
|
38
|
-
return process.env['TMUX'] !== undefined && process.env['TMUX'] !== '';
|
|
39
|
-
}
|
|
40
|
-
/** The pane currently displaying `nodeId`, found by the authoritative `@crtr_node`
|
|
41
|
-
* pane tag — set by EVERY viewer (a bare `surface attach`, a focus split, a
|
|
42
|
-
* spawned viewer) on connect. Scans all panes (`list-panes -a`); the reverse of
|
|
43
|
-
* `nodeInPane`. Unlike the focus TABLE (which only `registerViewerFocus` writes,
|
|
44
|
-
* so it misses attach-viewed nodes), the tag is on any pane showing the node.
|
|
45
|
-
* Returns the pane id, or null when no pane shows it. Works headless (no $TMUX). */
|
|
46
|
-
export function paneOfNode(nodeId) {
|
|
47
|
-
if (nodeId === '')
|
|
48
|
-
return null;
|
|
49
|
-
const r = tmux(['list-panes', '-a', '-F', '#{pane_id}\t#{@crtr_node}']);
|
|
50
|
-
if (!r.ok)
|
|
51
|
-
return null;
|
|
52
|
-
for (const line of r.stdout.split('\n')) {
|
|
53
|
-
const tab = line.indexOf('\t');
|
|
54
|
-
if (tab < 0)
|
|
55
|
-
continue;
|
|
56
|
-
if (line.slice(tab + 1) === nodeId) {
|
|
57
|
-
const pane = line.slice(0, tab);
|
|
58
|
-
if (pane !== '')
|
|
59
|
-
return pane;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
64
|
-
/** Where the caller currently is, or null if not inside tmux. */
|
|
65
|
-
export function currentTmux() {
|
|
66
|
-
if (!inTmux())
|
|
67
|
-
return null;
|
|
68
|
-
const r = tmux([
|
|
69
|
-
'display-message',
|
|
70
|
-
'-p',
|
|
71
|
-
'#{session_name}\t#{window_id}\t#{pane_id}',
|
|
72
|
-
]);
|
|
73
|
-
if (!r.ok)
|
|
74
|
-
return null;
|
|
75
|
-
const [session, window, pane] = r.stdout.split('\t');
|
|
76
|
-
return { session, window, pane };
|
|
77
|
-
}
|
|
78
|
-
// ---------------------------------------------------------------------------
|
|
79
|
-
// Sessions + windows
|
|
80
|
-
// ---------------------------------------------------------------------------
|
|
81
|
-
export function sessionExists(name) {
|
|
82
|
-
return tmux(['has-session', '-t', name]).ok;
|
|
83
|
-
}
|
|
84
|
-
/** Force bracketed-paste capabilities on for every terminal on this tmux server.
|
|
85
|
-
*
|
|
86
|
-
* Many terminfo entries lack the Enbp/Dsbp (enable/disable bracketed paste)
|
|
87
|
-
* capabilities. Without them tmux never asks the outer terminal to wrap pastes,
|
|
88
|
-
* so a multi-line paste arrives as raw `\r` and the inner app (pi, zsh, …)
|
|
89
|
-
* submits on the first newline — i.e. one paste lands as several messages.
|
|
90
|
-
* Declaring the caps for `*` restores bracketed paste so pastes stay atomic.
|
|
91
|
-
*
|
|
92
|
-
* `terminal-overrides` is a server option, so this is set once per server and
|
|
93
|
-
* is idempotent when a valid escaped Enbp capability is already present,
|
|
94
|
-
* including a user's own override. Must run only once a server exists —
|
|
95
|
-
* set-option errors out with no server running — so callers invoke it AFTER
|
|
96
|
-
* ensuring a session. */
|
|
97
|
-
function ensureBracketedPaste() {
|
|
98
|
-
const current = tmux(['show-options', '-s', 'terminal-overrides']).stdout;
|
|
99
|
-
if (/Enbp=\\+E/.test(current))
|
|
100
|
-
return;
|
|
101
|
-
// Emit a LITERAL backslash-E (`\\E`) so tmux's option parser resolves it to ESC.
|
|
102
|
-
// A single `\E` in this JS source collapses to the letter `E`, which stores a
|
|
103
|
-
// broken `Enbp=E[?2004h` (no ESC) and silently disables bracketed paste.
|
|
104
|
-
tmux(['set-option', '-sga', 'terminal-overrides', '*:Enbp=\\E[?2004h:Dsbp=\\E[?2004l']);
|
|
105
|
-
}
|
|
106
|
-
/** Server-level tmux prerequisites for crtr's viewer surfaces, applied once a
|
|
107
|
-
* server exists (callers invoke via ensureSession). Currently: bracketed paste
|
|
108
|
-
* capability (above) and `focus-events on`, which forwards terminal focus
|
|
109
|
-
* in/out (DECSET 1004) to the program in each pane — the attach viewer uses it
|
|
110
|
-
* to hollow out the editor cursor in blurred panes. */
|
|
111
|
-
function ensureServerOptions() {
|
|
112
|
-
ensureBracketedPaste();
|
|
113
|
-
tmux(['set-option', '-s', 'focus-events', 'on']);
|
|
114
|
-
}
|
|
115
|
-
/** Create a detached session rooted at `cwd` if it doesn't exist. The session
|
|
116
|
-
* name is a root's tmux home; every node under that root is a window in it. */
|
|
117
|
-
export function ensureSession(name, cwd) {
|
|
118
|
-
if (!sessionExists(name)) {
|
|
119
|
-
tmux(['new-session', '-d', '-s', name, '-c', cwd]);
|
|
120
|
-
}
|
|
121
|
-
ensureServerOptions();
|
|
122
|
-
}
|
|
123
|
-
function envFlags(env) {
|
|
124
|
-
const out = [];
|
|
125
|
-
for (const [k, v] of Object.entries(env))
|
|
126
|
-
out.push('-e', `${k}=${v}`);
|
|
127
|
-
return out;
|
|
128
|
-
}
|
|
129
|
-
/** Open a background window for a node and run `command` in it. `-d` keeps it
|
|
130
|
-
* detached so it doesn't steal focus or become the current window. Returns the
|
|
131
|
-
* new window id AND the pane id it created (the durable `%pane_id`, LOCATION's
|
|
132
|
-
* anchor) — callers that only need the window destructure `.window`.
|
|
133
|
-
*
|
|
134
|
-
* Target is `${session}:` (trailing colon = the session, no window index) plus
|
|
135
|
-
* `-a` (insert after the current window) so tmux allocates the next free index.
|
|
136
|
-
* Passing a bare session name resolves to the session's *active window*, which
|
|
137
|
-
* makes new-window try to create AT that occupied index and fail with
|
|
138
|
-
* "create window failed: index N in use" whenever the active window is not the
|
|
139
|
-
* last one (common when base-index is 0 but the live window sits at index 1).
|
|
140
|
-
* `-a` also keeps node windows off index 0, which is reserved for the optional
|
|
141
|
-
* dashboard. The explicit `-t ${session}:` target is the §2.2 HARD DRIVER
|
|
142
|
-
* INVARIANT — never let new-window fall back to tmux's global current session. */
|
|
143
|
-
export function openNodeWindow(opts) {
|
|
144
|
-
const r = tmux([
|
|
145
|
-
'new-window',
|
|
146
|
-
'-d',
|
|
147
|
-
'-a',
|
|
148
|
-
'-P',
|
|
149
|
-
'-F',
|
|
150
|
-
'#{window_id}\t#{pane_id}',
|
|
151
|
-
'-t',
|
|
152
|
-
`${opts.session}:`,
|
|
153
|
-
'-n',
|
|
154
|
-
opts.name,
|
|
155
|
-
'-c',
|
|
156
|
-
opts.cwd,
|
|
157
|
-
...envFlags(opts.env),
|
|
158
|
-
opts.command,
|
|
159
|
-
]);
|
|
160
|
-
if (!r.ok)
|
|
161
|
-
return null;
|
|
162
|
-
const [window, pane] = r.stdout.split('\t');
|
|
163
|
-
if (window === undefined || window === '' || pane === undefined || pane === '') {
|
|
164
|
-
return null;
|
|
165
|
-
}
|
|
166
|
-
return { window, pane };
|
|
167
|
-
}
|
|
168
|
-
/** Split `targetPane`'s window, opening a NEW pane beside it running `command`,
|
|
169
|
-
* and return the new pane id (the durable `%id`). The ONLY new-pane-beside verb
|
|
170
|
-
* (Q3: a focus opened side-by-side). `-d` keeps the caller's pane active; `-h`
|
|
171
|
-
* makes the split side-by-side (left/right), the default for a focus viewport.
|
|
172
|
-
*
|
|
173
|
-
* §2.2 HARD DRIVER INVARIANT: `targetPane` is REQUIRED — a bare `split-window`
|
|
174
|
-
* would split tmux's global current pane, which can leak a pane into an
|
|
175
|
-
* unrelated user session (the exact bug this design kills). The explicit
|
|
176
|
-
* `-t <targetPane>` makes the destination structurally un-leakable. Returns
|
|
177
|
-
* null if tmux fails. */
|
|
178
|
-
export function splitWindow(targetPane, opts) {
|
|
179
|
-
const r = tmux([
|
|
180
|
-
'split-window',
|
|
181
|
-
'-d',
|
|
182
|
-
...(opts.vertical === true ? [] : ['-h']),
|
|
183
|
-
...(opts.before === true ? ['-b'] : []),
|
|
184
|
-
...(opts.size !== undefined ? ['-l', String(opts.size)] : []),
|
|
185
|
-
'-P',
|
|
186
|
-
'-F',
|
|
187
|
-
'#{pane_id}',
|
|
188
|
-
'-t',
|
|
189
|
-
targetPane,
|
|
190
|
-
'-c',
|
|
191
|
-
opts.cwd,
|
|
192
|
-
...envFlags(opts.env),
|
|
193
|
-
opts.command,
|
|
194
|
-
]);
|
|
195
|
-
return r.ok && r.stdout !== '' ? r.stdout : null;
|
|
196
|
-
}
|
|
197
|
-
/** Bring a node's window forefront. Switches client across roots when needed. */
|
|
198
|
-
export function focusWindow(session, window) {
|
|
199
|
-
const here = currentTmux();
|
|
200
|
-
const sameRoot = here?.session === session;
|
|
201
|
-
if (!sameRoot) {
|
|
202
|
-
if (!tmux(['switch-client', '-t', session]).ok)
|
|
203
|
-
return false;
|
|
204
|
-
}
|
|
205
|
-
return tmux(['select-window', '-t', window]).ok;
|
|
206
|
-
}
|
|
207
|
-
/** Close a node's window (drop it from the UI). */
|
|
208
|
-
export function closeWindow(window) {
|
|
209
|
-
return tmux(['kill-window', '-t', window]).ok;
|
|
210
|
-
}
|
|
211
|
-
/** Rename a window (`tmux rename-window -t <window> <name>`). Used by the
|
|
212
|
-
* root relaunch to re-title the viewer window when its pane is re-pointed at
|
|
213
|
-
* the freshly-minted node. Best-effort; false if tmux fails. */
|
|
214
|
-
export function renameWindow(window, name) {
|
|
215
|
-
return tmux(['rename-window', '-t', window, name]).ok;
|
|
216
|
-
}
|
|
217
|
-
/** Break a single PANE out into a BRAND-NEW window of its own (`tmux break-pane
|
|
218
|
-
* -d`). `-d` keeps the new window in the background (does not switch the client
|
|
219
|
-
* to it); `-s <pane>` names the source pane — the §2.2 HARD DRIVER INVARIANT, so
|
|
220
|
-
* the break never falls back to tmux's global current pane. The pane keeps its
|
|
221
|
-
* durable `%id` across the move (only its window changes), so callers can keep
|
|
222
|
-
* using the same pane handle. Returns the new `{window, pane}` location, or null
|
|
223
|
-
* if tmux fails. Used by `surface tmux-spread` to lift the caller's viewer into a
|
|
224
|
-
* fresh window before tiling sibling viewers beside it. */
|
|
225
|
-
export function breakPane(pane) {
|
|
226
|
-
const r = tmux([
|
|
227
|
-
'break-pane',
|
|
228
|
-
'-d',
|
|
229
|
-
'-P',
|
|
230
|
-
'-F',
|
|
231
|
-
'#{window_id}\t#{pane_id}',
|
|
232
|
-
'-s',
|
|
233
|
-
pane,
|
|
234
|
-
]);
|
|
235
|
-
if (!r.ok)
|
|
236
|
-
return null;
|
|
237
|
-
const [window, p] = r.stdout.split('\t');
|
|
238
|
-
if (window === undefined || window === '' || p === undefined || p === '')
|
|
239
|
-
return null;
|
|
240
|
-
return { window, pane: p };
|
|
241
|
-
}
|
|
242
|
-
/** Close a single PANE. Its window closes automatically once this was the last
|
|
243
|
-
* pane, but sibling panes survive — so co-located nodes (several agents sharing
|
|
244
|
-
* one window via swap-pane focus) are torn down one at a time instead of all
|
|
245
|
-
* at once by a window kill. Pane ids are the stable vehicle handle; windows
|
|
246
|
-
* shift under swap-pane focus, so pane-granular teardown is the correct unit. */
|
|
247
|
-
export function closePane(pane) {
|
|
248
|
-
return tmux(['kill-pane', '-t', pane]).ok;
|
|
249
|
-
}
|
|
250
|
-
/** Open a command in a tmux popup anchored to a pane. The command is a shell
|
|
251
|
-
* string because tmux executes popup payloads through the user's shell. */
|
|
252
|
-
export function displayPopup(pane, cwd, command) {
|
|
253
|
-
return tmux(['display-popup', '-E', '-t', pane, '-d', cwd, '-w', '90%', '-h', '90%', command]).ok;
|
|
254
|
-
}
|
|
255
|
-
/** The active pane id of a window. Node windows are single-pane, so this is the
|
|
256
|
-
* node's pane. Returns null if the window is gone or tmux fails. */
|
|
257
|
-
export function paneOfWindow(session, window) {
|
|
258
|
-
const r = tmux(['display-message', '-p', '-t', `${session}:${window}`, '#{pane_id}']);
|
|
259
|
-
return r.ok && r.stdout !== '' ? r.stdout : null;
|
|
260
|
-
}
|
|
261
|
-
/** The window a pane currently lives in. Used after a swap-pane to learn which
|
|
262
|
-
* slot the caller's pane occupied — pane ids are stable across swaps, windows
|
|
263
|
-
* are not, so the node→window mapping must be re-derived from the pane. Returns
|
|
264
|
-
* null if the pane is gone or tmux fails. */
|
|
265
|
-
export function windowOfPane(pane) {
|
|
266
|
-
const r = tmux(['display-message', '-p', '-t', pane, '#{window_id}']);
|
|
267
|
-
return r.ok && r.stdout !== '' ? r.stdout : null;
|
|
268
|
-
}
|
|
269
|
-
/** The session + window a pane currently lives in (`display-message -p -t %id`).
|
|
270
|
-
* The §2.4 reconciliation read-back: resolve a node's/focus's CURRENT
|
|
271
|
-
* window/session from its durable pane id before any act, so crtr follows a
|
|
272
|
-
* manual `move-pane`/`join-pane`/`break-pane` instead of fighting it. Null if
|
|
273
|
-
* the pane is gone or tmux fails. */
|
|
274
|
-
export function paneLocation(pane) {
|
|
275
|
-
const r = tmux(['display-message', '-p', '-t', pane, '#{session_name}\t#{window_id}']);
|
|
276
|
-
if (!r.ok)
|
|
277
|
-
return null;
|
|
278
|
-
const [session, window] = r.stdout.split('\t');
|
|
279
|
-
if (session === undefined || session === '' || window === undefined || window === '')
|
|
280
|
-
return null;
|
|
281
|
-
return { session, window };
|
|
282
|
-
}
|
|
283
|
-
/** Does this pane id still exist? A `display-message` probe on the `%id` — the
|
|
284
|
-
* v3 PRIMARY liveness probe (§1.2/§2.2), replacing window-existence so a user
|
|
285
|
-
* moving a pane to another window/session never reads as "gone". True iff tmux
|
|
286
|
-
* knows the pane.
|
|
287
|
-
*
|
|
288
|
-
* NOTE: `display-message -p -t <gone-pane>` EXITS 0 with EMPTY output (it does
|
|
289
|
-
* not error on an unresolvable pane target) — so an `.ok` check alone would
|
|
290
|
-
* report a dead pane as alive, defeating the whole point of pane-existence
|
|
291
|
-
* liveness. We therefore require the echoed `#{pane_id}` to equal the requested
|
|
292
|
-
* pane: a live pane echoes its own id, a gone/bogus one yields empty. */
|
|
293
|
-
export function paneExists(pane) {
|
|
294
|
-
const r = tmux(['display-message', '-p', '-t', pane, '#{pane_id}']);
|
|
295
|
-
return r.ok && r.stdout === pane;
|
|
296
|
-
}
|
|
297
|
-
/** Does this pane exist AND have its command still RUNNING (`#{pane_dead}` = 0)?
|
|
298
|
-
* Distinguishes a pane genuinely hosting a live process from a remain-on-exit
|
|
299
|
-
* corpse frozen after exit (`pane_dead` = 1). Node viewer panes run `crtr surface attach`
|
|
300
|
-
* as the pane command, so pane-running ⟹ a live viewer occupies it. */
|
|
301
|
-
export function paneRunning(pane) {
|
|
302
|
-
const r = tmux(['display-message', '-p', '-t', pane, '#{pane_id}\t#{pane_dead}']);
|
|
303
|
-
return r.ok && r.stdout === `${pane}\t0`;
|
|
304
|
-
}
|
|
305
|
-
/** Every live pane id on the server (`list-panes -a`), as a Set for membership
|
|
306
|
-
* probes. Returns null when tmux is unreachable (no server / transient failure)
|
|
307
|
-
* so callers can tell "no panes" apart from "can't tell" — a GC pass must skip,
|
|
308
|
-
* never mass-delete, on a failed probe. One subprocess call total, so batch
|
|
309
|
-
* liveness sweeps (e.g. the daemon's stale-focus GC) don't pay a per-pane
|
|
310
|
-
* display-message each. */
|
|
311
|
-
export function listLivePanes() {
|
|
312
|
-
const r = tmux(['list-panes', '-a', '-F', '#{pane_id}']);
|
|
313
|
-
if (!r.ok)
|
|
314
|
-
return null;
|
|
315
|
-
return new Set(r.stdout.split('\n').filter((p) => p !== ''));
|
|
316
|
-
}
|
|
317
|
-
/** The working directory of a pane (`display-message -p -t <pane>
|
|
318
|
-
* '#{pane_current_path}'`). Null if tmux fails. */
|
|
319
|
-
export function paneCurrentPath(pane) {
|
|
320
|
-
const r = tmux(['display-message', '-p', '-t', pane, '#{pane_current_path}']);
|
|
321
|
-
return r.ok && r.stdout !== '' ? r.stdout : null;
|
|
322
|
-
}
|
|
323
|
-
/** Flash a transient status-line toast on the client showing `pane` (or the
|
|
324
|
-
* current client when `pane` is omitted). `ms` sets the display duration. Used
|
|
325
|
-
* by `crtr canvas issue new` to confirm a capture from inside its popup.
|
|
326
|
-
* Best-effort; never throws. */
|
|
327
|
-
export function displayMessage(message, pane, ms = 4000) {
|
|
328
|
-
const args = ['display-message', '-d', String(ms)];
|
|
329
|
-
if (pane !== undefined && pane !== '')
|
|
330
|
-
args.push('-t', pane);
|
|
331
|
-
args.push(message);
|
|
332
|
-
return tmux(args).ok;
|
|
333
|
-
}
|
|
334
|
-
/** Set a PANE-scoped tmux option (`tmux set-option -p -t <pane> <name> <value>`).
|
|
335
|
-
* Best-effort; never throws. */
|
|
336
|
-
export function setPaneOption(pane, name, value) {
|
|
337
|
-
return tmux(['set-option', '-p', '-t', pane, name, value]).ok;
|
|
338
|
-
}
|
|
339
|
-
/** Read a PANE-scoped tmux option value (`tmux show-options -p -t <pane> -q -v
|
|
340
|
-
* <name>`): `-v` prints only the value, `-q` suppresses the unknown-option
|
|
341
|
-
* error so an unset option yields an empty string. undefined if tmux fails. */
|
|
342
|
-
export function getPaneOption(pane, name) {
|
|
343
|
-
const r = tmux(['show-options', '-p', '-t', pane, '-q', '-v', name]);
|
|
344
|
-
return r.ok ? r.stdout : undefined;
|
|
345
|
-
}
|
|
346
|
-
/** Pane option recording when an attach viewer was last started or focused.
|
|
347
|
-
* `liveViewerPanes` uses it to select the most recently used viewer. */
|
|
348
|
-
export const VIEWER_STAMP_OPTION = '@crtr_viewer_at';
|
|
349
|
-
let lastViewerStampMs = 0;
|
|
350
|
-
let viewerStampQueue = Promise.resolve();
|
|
351
|
-
/** Stamp a viewer pane without blocking the input path that observed focus.
|
|
352
|
-
* Stamps are monotonically increasing and their tmux writes are serialized, so
|
|
353
|
-
* two rapid focus events cannot leave an older pane with the newer stamp. */
|
|
354
|
-
export function stampViewerPaneAsync(pane) {
|
|
355
|
-
if (pane === '')
|
|
356
|
-
return;
|
|
357
|
-
const stampMs = Math.max(Date.now(), lastViewerStampMs + 1);
|
|
358
|
-
lastViewerStampMs = stampMs;
|
|
359
|
-
viewerStampQueue = viewerStampQueue.then(() => new Promise((resolve) => {
|
|
360
|
-
try {
|
|
361
|
-
execFile('tmux', ['set-option', '-p', '-t', pane, VIEWER_STAMP_OPTION, String(stampMs)], () => resolve());
|
|
362
|
-
}
|
|
363
|
-
catch {
|
|
364
|
-
resolve();
|
|
365
|
-
}
|
|
366
|
-
}));
|
|
367
|
-
}
|
|
368
|
-
function escapeRegex(text) {
|
|
369
|
-
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
370
|
-
}
|
|
371
|
-
function looksLikeViewerCommand(command, nodeId) {
|
|
372
|
-
const unquoted = command.replace(/^"|"$/g, '');
|
|
373
|
-
return new RegExp(`(?:^|\\s)surface\\s+attach\\s+to\\s+${escapeRegex(nodeId)}(?:\\s|$)`).test(unquoted);
|
|
374
|
-
}
|
|
375
|
-
function readProcessTree() {
|
|
376
|
-
const result = spawnSync('ps', ['-ax', '-o', 'pid=,ppid=,command='], { encoding: 'utf8' });
|
|
377
|
-
if (result.status !== 0 || typeof result.stdout !== 'string')
|
|
378
|
-
return null;
|
|
379
|
-
const childrenOf = new Map();
|
|
380
|
-
const commandOf = new Map();
|
|
381
|
-
for (const line of result.stdout.split('\n')) {
|
|
382
|
-
const match = /^\s*(\d+)\s+(\d+)\s+(.*)$/.exec(line);
|
|
383
|
-
if (match === null)
|
|
384
|
-
continue;
|
|
385
|
-
const pid = Number(match[1]);
|
|
386
|
-
const parentPid = Number(match[2]);
|
|
387
|
-
commandOf.set(pid, match[3]);
|
|
388
|
-
const children = childrenOf.get(parentPid) ?? [];
|
|
389
|
-
children.push(pid);
|
|
390
|
-
childrenOf.set(parentPid, children);
|
|
391
|
-
}
|
|
392
|
-
return { childrenOf, commandOf };
|
|
393
|
-
}
|
|
394
|
-
function processTreeHasViewer(tree, rootPid, nodeId) {
|
|
395
|
-
const pending = [rootPid];
|
|
396
|
-
while (pending.length > 0) {
|
|
397
|
-
const pid = pending.pop();
|
|
398
|
-
if (looksLikeViewerCommand(tree.commandOf.get(pid) ?? '', nodeId))
|
|
399
|
-
return true;
|
|
400
|
-
const children = tree.childrenOf.get(pid);
|
|
401
|
-
if (children !== undefined)
|
|
402
|
-
pending.push(...children);
|
|
403
|
-
}
|
|
404
|
-
return false;
|
|
405
|
-
}
|
|
406
|
-
/** List viewer panes tagged by a running local attach viewer. Pane tags can
|
|
407
|
-
* survive a killed process, so every tag must match a viewer for the same node.
|
|
408
|
-
* Shell-hosted viewers are recognized through their live child process. Unstamped
|
|
409
|
-
* valid viewers remain eligible at stamp zero while an older viewer rolls forward. */
|
|
410
|
-
export function liveViewerPanes(socket) {
|
|
411
|
-
const listed = tmuxOnSocket(socket, [
|
|
412
|
-
'list-panes', '-a', '-F',
|
|
413
|
-
'#{pane_id}\t#{session_name}\t#{@crtr_node}\t#{@crtr_viewer_at}',
|
|
414
|
-
]);
|
|
415
|
-
if (!listed.ok || listed.stdout === '')
|
|
416
|
-
return [];
|
|
417
|
-
const viewers = [];
|
|
418
|
-
let processTree;
|
|
419
|
-
for (const line of listed.stdout.split('\n')) {
|
|
420
|
-
const [pane, session, nodeId, stamp] = line.split('\t');
|
|
421
|
-
if (pane === undefined || pane === '' || session === undefined || session === '' || nodeId === undefined || nodeId === '')
|
|
422
|
-
continue;
|
|
423
|
-
const command = tmuxOnSocket(socket, [
|
|
424
|
-
'display-message', '-p', '-t', pane,
|
|
425
|
-
'#{pane_current_command}\t#{pane_start_command}\t#{pane_pid}',
|
|
426
|
-
]);
|
|
427
|
-
if (!command.ok)
|
|
428
|
-
continue;
|
|
429
|
-
const [currentCommand = '', startCommand = '', pid] = command.stdout.split('\t');
|
|
430
|
-
const directViewer = looksLikeViewerCommand(currentCommand, nodeId) || looksLikeViewerCommand(startCommand, nodeId);
|
|
431
|
-
if (!directViewer) {
|
|
432
|
-
processTree ??= readProcessTree();
|
|
433
|
-
const rootPid = Number.parseInt(pid, 10);
|
|
434
|
-
if (processTree === null || !Number.isInteger(rootPid) || rootPid <= 0 || !processTreeHasViewer(processTree, rootPid, nodeId))
|
|
435
|
-
continue;
|
|
436
|
-
}
|
|
437
|
-
const parsedStamp = Number.parseInt(stamp ?? '', 10);
|
|
438
|
-
viewers.push({
|
|
439
|
-
pane,
|
|
440
|
-
session,
|
|
441
|
-
nodeId,
|
|
442
|
-
stampMs: Number.isSafeInteger(parsedStamp) && parsedStamp >= 0 ? parsedStamp : 0,
|
|
443
|
-
});
|
|
444
|
-
}
|
|
445
|
-
return viewers;
|
|
446
|
-
}
|
|
447
|
-
/** Deliver one private attach-control sequence literally into a viewer pane.
|
|
448
|
-
* `-l` bypasses tmux key-table lookup, so delivery cannot re-trigger a root
|
|
449
|
-
* binding that initiated it. */
|
|
450
|
-
export function sendAttachControl(socket, pane, actionId) {
|
|
451
|
-
return tmuxOnSocket(socket, ['send-keys', '-l', '-t', pane, encodeAttachControlInput(actionId)]).ok;
|
|
452
|
-
}
|
|
453
|
-
/** Show a tmux status-line message on a specific client when one is known. */
|
|
454
|
-
export function displayClientMessage(socket, client, text) {
|
|
455
|
-
const args = ['display-message'];
|
|
456
|
-
if (client !== undefined && client !== '')
|
|
457
|
-
args.push('-c', client);
|
|
458
|
-
args.push(text);
|
|
459
|
-
return tmuxOnSocket(socket, args).ok;
|
|
460
|
-
}
|
|
461
|
-
/** The `respawn-pane -k` argv for `opts`. `-k` kills the pane's current process
|
|
462
|
-
* (e.g. a yielding pi) and re-execs `command` in the SAME pane, preserving its
|
|
463
|
-
* `%id` (§1.5 F3: a frozen focus pane resumes in place, no new window). The
|
|
464
|
-
* explicit `-t opts.pane` is the §2.2 HARD DRIVER INVARIANT — respawn must name
|
|
465
|
-
* its target pane, never tmux's global current pane. */
|
|
466
|
-
function respawnPaneArgs(opts) {
|
|
467
|
-
return [
|
|
468
|
-
'respawn-pane',
|
|
469
|
-
'-k',
|
|
470
|
-
'-c',
|
|
471
|
-
opts.cwd,
|
|
472
|
-
...envFlags(opts.env),
|
|
473
|
-
'-t',
|
|
474
|
-
opts.pane,
|
|
475
|
-
opts.command,
|
|
476
|
-
];
|
|
477
|
-
}
|
|
478
|
-
/** Re-exec a command in an EXISTING pane, in place — SYNCHRONOUS. Runs the
|
|
479
|
-
* `respawn-pane` to completion and reports the real exit status. Used when the
|
|
480
|
-
* caller is NOT the pane being respawned (e.g. the daemon resuming a frozen
|
|
481
|
-
* focus pane), so it can confirm the respawn landed. Returns true on success. */
|
|
482
|
-
export function respawnPaneSync(opts) {
|
|
483
|
-
return tmux(respawnPaneArgs(opts)).ok;
|
|
484
|
-
}
|
|
485
|
-
/** The command line a pane was originally started with (`#{pane_start_command}`)
|
|
486
|
-
* — the exact command `respawn-pane` with no argument re-runs. undefined when
|
|
487
|
-
* tmux is unreachable or the pane is gone. */
|
|
488
|
-
export function paneStartCommand(pane) {
|
|
489
|
-
const r = tmux(['display-message', '-p', '-t', pane, '#{pane_start_command}']);
|
|
490
|
-
return r.ok ? r.stdout : undefined;
|
|
491
|
-
}
|
|
492
|
-
/** The pane's root process pid (`#{pane_pid}`) — the process tmux launched in
|
|
493
|
-
* the pane (the viewer itself, or the login shell hosting it). undefined when
|
|
494
|
-
* tmux is unreachable or the pane is gone. */
|
|
495
|
-
export function panePid(pane) {
|
|
496
|
-
const r = tmux(['display-message', '-p', '-t', pane, '#{pane_pid}']);
|
|
497
|
-
if (!r.ok)
|
|
498
|
-
return undefined;
|
|
499
|
-
const pid = Number.parseInt(r.stdout, 10);
|
|
500
|
-
return Number.isInteger(pid) && pid > 0 ? pid : undefined;
|
|
501
|
-
}
|
|
502
|
-
/** Kill a pane's current process and re-run the SAME command line it was
|
|
503
|
-
* started with (`respawn-pane -k` with no command reuses the pane's original
|
|
504
|
-
* command). Used to hot-reload a viewer pane in place: the fresh process
|
|
505
|
-
* re-resolves `crtr` and picks up the currently selected runtime generation,
|
|
506
|
-
* while the pane id, geometry, and the node's broker are untouched. */
|
|
507
|
-
export function respawnPaneInPlace(pane) {
|
|
508
|
-
return tmux(['respawn-pane', '-k', '-t', pane]).ok;
|
|
509
|
-
}
|
|
510
|
-
// ---------------------------------------------------------------------------
|
|
511
|
-
// pi command assembly
|
|
512
|
-
// ---------------------------------------------------------------------------
|
|
513
|
-
/** Turn a pi argv array into a single shell command string.
|
|
514
|
-
*
|
|
515
|
-
* The binary defaults to `CRTR_PI_BINARY` when that env var is set, else the
|
|
516
|
-
* bundled pi CLI launched with the current Node binary. CRTR_PI_BINARY is a
|
|
517
|
-
* TEST-ONLY substitution seam: the integration-test harness points it at a
|
|
518
|
-
* deterministic fake-pi vehicle so a real `crtr node new` reaches the fake
|
|
519
|
-
* instead of the LLM pi, without depending on tmux/shell PATH inheritance. An
|
|
520
|
-
* explicit `binary` arg still overrides the env. The value may be a multi-word
|
|
521
|
-
* launcher (e.g. `node --import tsx/esm host.ts`); only argv entries are
|
|
522
|
-
* shell-quoted, so a multi-word binary is spliced verbatim ahead of them. */
|
|
523
|
-
export function piCommand(argv, binary = process.env['CRTR_PI_BINARY'] ?? bundledPiCommandForShell(shellQuote)) {
|
|
524
|
-
return [binary, ...argv.map(shellQuote)].join(' ');
|
|
525
|
-
}
|
|
526
|
-
// ---------------------------------------------------------------------------
|
|
527
|
-
// Window liveness helpers (used by the supervisor daemon)
|
|
528
|
-
// ---------------------------------------------------------------------------
|
|
529
|
-
/** List all window ids present in `session`. Returns [] if the session does
|
|
530
|
-
* not exist or tmux fails for any reason. Each entry is the raw window id
|
|
531
|
-
* string reported by tmux (e.g. `@1`, `@2`, …). */
|
|
532
|
-
export function listWindowIds(session) {
|
|
533
|
-
const r = tmux(['list-windows', '-t', session, '-F', '#{window_id}']);
|
|
534
|
-
if (!r.ok || r.stdout === '')
|
|
535
|
-
return [];
|
|
536
|
-
return r.stdout.split('\n').filter((s) => s !== '');
|
|
537
|
-
}
|
|
538
|
-
/** True when both `session` and `window` are present (non-null/undefined) and
|
|
539
|
-
* the window currently exists inside the session. False whenever either arg
|
|
540
|
-
* is absent, the session is gone, or tmux does not know the window. */
|
|
541
|
-
export function windowAlive(session, window) {
|
|
542
|
-
if (session == null || window == null)
|
|
543
|
-
return false;
|
|
544
|
-
return listWindowIds(session).includes(window);
|
|
545
|
-
}
|
|
546
|
-
// ---------------------------------------------------------------------------
|
|
547
|
-
// Focus helpers (used by the placement layer)
|
|
548
|
-
// ---------------------------------------------------------------------------
|
|
549
|
-
/** Activate a window within its session (same-session navigation). Equivalent
|
|
550
|
-
* to `tmux select-window -t <session>:<window>`. Best-effort; never throws. */
|
|
551
|
-
export function selectWindow(session, window) {
|
|
552
|
-
return tmux(['select-window', '-t', `${session}:${window}`]).ok;
|
|
553
|
-
}
|
|
554
|
-
/** Apply a named tmux layout to a window (`tmux select-layout -t <window>
|
|
555
|
-
* <layout>`). `surface tmux-spread` calls it with `tiled` to evenly grid every
|
|
556
|
-
* viewer pane in the spread window (and between splits, to redistribute space so
|
|
557
|
-
* the next split has room). Best-effort; false if tmux fails. */
|
|
558
|
-
export function selectLayout(window, layout) {
|
|
559
|
-
return tmux(['select-layout', '-t', window, layout]).ok;
|
|
560
|
-
}
|
|
561
|
-
/** Make a pane the ACTIVE pane in its window (`tmux select-pane -t <pane>`). A
|
|
562
|
-
* `split-window -d` keeps the CALLER active, so a freshly-opened viewer pane is
|
|
563
|
-
* not focused until this runs — `focus` calls it so picking a node (e.g. from the
|
|
564
|
-
* alt+g graph) lands the keyboard on the new viewer. Best-effort; false if tmux
|
|
565
|
-
* fails. */
|
|
566
|
-
export function selectPane(pane) {
|
|
567
|
-
return tmux(['select-pane', '-t', pane]).ok;
|
|
568
|
-
}
|
|
569
|
-
/** Switch the tmux client to a different session (cross-session focus). Runs
|
|
570
|
-
* `tmux switch-client -t <session>`. Best-effort; never throws. The caller is
|
|
571
|
-
* responsible for following up with selectWindow to land on the right window. */
|
|
572
|
-
export function switchClient(session) {
|
|
573
|
-
return tmux(['switch-client', '-t', session]).ok;
|
|
574
|
-
}
|
|
575
|
-
// ---------------------------------------------------------------------------
|
|
576
|
-
// send-keys (chrome — used by the prefix menu + nav bindings)
|
|
577
|
-
// ---------------------------------------------------------------------------
|
|
578
|
-
/** Type a literal (e.g. a `/graph` slash command) into a pane and press Enter
|
|
579
|
-
* (`tmux send-keys -t <pane> '<text>' Enter`). Requires the pane's editor be
|
|
580
|
-
* empty, same limitation as the menu's `/promote` item. Best-effort. */
|
|
581
|
-
export function sendKeysEnter(pane, text) {
|
|
582
|
-
return tmux(['send-keys', '-t', pane, text, 'Enter']).ok;
|
|
583
|
-
}
|
|
584
|
-
// ---------------------------------------------------------------------------
|
|
585
|
-
// Crouter-owned tmux bindings
|
|
586
|
-
// ---------------------------------------------------------------------------
|
|
587
|
-
const TMUX_BINDING_MANIFEST = '@crtr_keybindings_manifest';
|
|
588
|
-
const ROOT_TABLE = 'root';
|
|
589
|
-
const LEAF_MENU_ACTIONS = {
|
|
590
|
-
'crtr.tmux.menu.promote': { description: 'promote to orchestrator', action: { kind: 'keys', keys: '/promote' } },
|
|
591
|
-
'crtr.tmux.menu.resume': { description: 'find node', action: { kind: 'keys', keys: '/resume-node' } },
|
|
592
|
-
'crtr.tmux.menu.fork': { description: 'fork into idle root', action: { kind: 'direct', run: 'node fork --pane {pane}' } },
|
|
593
|
-
'crtr.tmux.menu.demote': { description: 'demote (finish when done)', action: { kind: 'command', run: 'node lifecycle demote --pane {pane}' } },
|
|
594
|
-
'crtr.tmux.menu.detach': { description: 'demote + close viewer', action: { kind: 'command', run: 'node lifecycle demote --pane {pane} --detach' } },
|
|
595
|
-
'crtr.tmux.menu.close-subtree': { description: 'close agent + subtree', action: { kind: 'command', run: 'node lifecycle close --pane {pane}' } },
|
|
596
|
-
'crtr.tmux.menu.background-bash': { description: 'background running bash', action: { kind: 'direct', run: 'node bash background --pane {pane}' } },
|
|
597
|
-
// M-S-r, not M-R: with extended-keys csi-u tmux encodes M-R as alt+uppercase-R
|
|
598
|
-
// WITHOUT the shift modifier bit, which pi-tui's `alt+shift+r` matcher rejects;
|
|
599
|
-
// M-S-r carries shift+alt and lands as the same chord the keyboard produces.
|
|
600
|
-
'crtr.tmux.menu.file-review': { description: 'review a file', action: { kind: 'raw-keys', keys: 'M-S-r' } },
|
|
601
|
-
'crtr.tmux.menu.metadata': { description: 'metadata', action: { kind: 'keys', keys: '/node-metadata' } },
|
|
602
|
-
'crtr.tmux.menu.fold-tools': { description: 'fold finished tool calls', action: { kind: 'keys', keys: '/fold-tools' } },
|
|
603
|
-
'crtr.tmux.menu.context': { description: 'context + reports', action: { kind: 'keys', keys: '/context' } },
|
|
604
|
-
'crtr.tmux.menu.providers': { description: 'settings', action: { kind: 'popup', run: 'sys setup' } },
|
|
605
|
-
'crtr.tmux.menu.graph': { description: 'canvas graph', action: { kind: 'keys', keys: '/graph' } },
|
|
606
|
-
'crtr.tmux.menu.focus-manager': { description: 'go to manager', action: { kind: 'command', run: 'surface node focus {manager}' } },
|
|
607
|
-
'crtr.tmux.menu.copy-id': { description: 'copy node id', action: { kind: 'command', run: 'surface node id --pane {pane}' } },
|
|
608
|
-
'crtr.tmux.menu.reload-viewer': { description: 'reload viewer', action: { kind: 'direct', run: 'surface reload --pane {pane}' } },
|
|
609
|
-
'crtr.tmux.menu.recap': { description: 'generate recap', action: { kind: 'keys', keys: '/recap' } },
|
|
610
|
-
'crtr.tmux.menu.pin': { description: 'pin conversation', action: { kind: 'direct', run: 'surface node pin --pane {pane}' } },
|
|
611
|
-
'crtr.tmux.menu.issue.idea': { description: 'idea', action: { kind: 'popup', run: 'canvas issue new --type idea' } },
|
|
612
|
-
'crtr.tmux.menu.issue.bug': { description: 'bug', action: { kind: 'popup', run: 'canvas issue new --type bug' } },
|
|
613
|
-
};
|
|
614
|
-
function escapeTmuxFormats(value) {
|
|
615
|
-
return value.replace(/#/g, '##');
|
|
616
|
-
}
|
|
617
|
-
function tmuxCommand(args) {
|
|
618
|
-
return args.map(shellQuote).join(' ');
|
|
619
|
-
}
|
|
620
|
-
function chordCommand(run, interactive = false) {
|
|
621
|
-
const command = `crtr canvas chord --pane '#{pane_id}' --run ${shellQuote(escapeTmuxFormats(run))}${interactive ? ' --interactive' : ''}`;
|
|
622
|
-
return interactive
|
|
623
|
-
? {
|
|
624
|
-
argv: ['display-popup', '-E', '-w', '60%', '-h', '50%', '-d', '#{pane_current_path}', command],
|
|
625
|
-
menu: `display-popup -E -w 60% -h 50% -d "#{pane_current_path}" ${shellQuote(command)}`,
|
|
626
|
-
}
|
|
627
|
-
: {
|
|
628
|
-
argv: ['run-shell', `${command} >/dev/null 2>&1`],
|
|
629
|
-
menu: `run-shell ${shellQuote(`${command} >/dev/null 2>&1`)}`,
|
|
630
|
-
};
|
|
631
|
-
}
|
|
632
|
-
/** Dispatch an action straight to a `crtr` invocation, skipping the
|
|
633
|
-
* `canvas chord` hop entirely. Only sound for an action whose sole
|
|
634
|
-
* interpolation need is `{pane}` — tmux expands it natively as `#{pane_id}` at
|
|
635
|
-
* keypress time (a real tmux format, substituted AFTER escaping the static
|
|
636
|
-
* run text so the `#` here is never doubled), so chord's node-resolution
|
|
637
|
-
* round trip buys nothing. `-b` keeps dispatch non-blocking. */
|
|
638
|
-
function directCommand(run) {
|
|
639
|
-
const argv = escapeTmuxFormats(run).replace('{pane}', '#{pane_id}');
|
|
640
|
-
const command = `crtr ${argv} >/dev/null 2>&1`;
|
|
641
|
-
return {
|
|
642
|
-
argv: ['run-shell', '-b', command],
|
|
643
|
-
menu: `run-shell -b ${shellQuote(command)}`,
|
|
644
|
-
};
|
|
645
|
-
}
|
|
646
|
-
/** One command constructor serves both display-menu selections and direct root
|
|
647
|
-
* shortcuts. Each result carries the argv bind-key needs plus the stable menu
|
|
648
|
-
* serialization existing Alt+C installs already use. */
|
|
649
|
-
function menuActionCommand(action) {
|
|
650
|
-
switch (action.kind) {
|
|
651
|
-
case 'disabled': return null;
|
|
652
|
-
case 'separator': return null;
|
|
653
|
-
// Choosing any menu item closes it; refresh-client is the deliberately
|
|
654
|
-
// inert command that lets the prefix shortcut choose this close row.
|
|
655
|
-
case 'dismiss': return { argv: ['refresh-client'], menu: 'refresh-client' };
|
|
656
|
-
case 'keys': {
|
|
657
|
-
const keys = escapeTmuxFormats(action.keys);
|
|
658
|
-
return {
|
|
659
|
-
// A root binding already runs in the pressed key's pane context. Its
|
|
660
|
-
// target must stay implicit: bind-key does not expand `#{pane_id}` in
|
|
661
|
-
// send-keys' target argument. A menu command is serialized later and
|
|
662
|
-
// does need the pane captured when the menu opened.
|
|
663
|
-
argv: ['send-keys', keys, 'Enter'],
|
|
664
|
-
menu: `send-keys -t '#{pane_id}' ${shellQuote(keys)} Enter`,
|
|
665
|
-
};
|
|
666
|
-
}
|
|
667
|
-
case 'raw-keys': {
|
|
668
|
-
const keys = escapeTmuxFormats(action.keys);
|
|
669
|
-
return {
|
|
670
|
-
argv: ['send-keys', keys],
|
|
671
|
-
menu: `send-keys -t '#{pane_id}' ${shellQuote(keys)}`,
|
|
672
|
-
};
|
|
673
|
-
}
|
|
674
|
-
case 'literal-input': {
|
|
675
|
-
const input = escapeTmuxFormats(action.input);
|
|
676
|
-
return {
|
|
677
|
-
argv: ['send-keys', '-l', input],
|
|
678
|
-
menu: `send-keys -l -t '#{pane_id}' ${shellQuote(input)}`,
|
|
679
|
-
};
|
|
680
|
-
}
|
|
681
|
-
case 'command': return chordCommand(action.run);
|
|
682
|
-
case 'direct': return directCommand(action.run);
|
|
683
|
-
case 'popup': return chordCommand(action.run, true);
|
|
684
|
-
case 'submenu': {
|
|
685
|
-
const argv = buildMenuArgs(action.title, action.items);
|
|
686
|
-
return { argv, menu: tmuxCommand(argv) };
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
function renderMenuAction(action) {
|
|
691
|
-
return menuActionCommand(action)?.menu ?? '';
|
|
692
|
-
}
|
|
693
|
-
/** Visible width of a menu item name: style markup (`#[...]`) takes no cells. */
|
|
694
|
-
function menuItemWidth(description) {
|
|
695
|
-
return description.replace(/#\[[^\]]*\]/g, '').length;
|
|
696
|
-
}
|
|
697
|
-
function buildMenuArgs(title, items) {
|
|
698
|
-
const safeTitle = escapeTmuxFormats(title);
|
|
699
|
-
const widestItem = items.reduce((width, item) => Math.max(width, menuItemWidth(item.description)), 0);
|
|
700
|
-
const args = ['display-menu', '-T', `#[align=centre]${safeTitle}`, ...surfaceTmuxStyleArgs()];
|
|
701
|
-
args.push('-x', `#{e|-:#{pane_right},${Math.max(safeTitle.length, widestItem) + 7}}`, '-y', '#{e|+:#{pane_top},3}', '--');
|
|
702
|
-
for (const item of items) {
|
|
703
|
-
// A separator is a single empty name with key and command omitted; tmux
|
|
704
|
-
// draws it as a horizontal rule. A header (disabled) name is a crouter-owned
|
|
705
|
-
// literal carrying `#[...]` style markup, so it skips format escaping.
|
|
706
|
-
if (item.action.kind === 'separator') {
|
|
707
|
-
args.push('');
|
|
708
|
-
continue;
|
|
709
|
-
}
|
|
710
|
-
const name = item.action.kind === 'disabled' ? item.description : escapeTmuxFormats(item.description);
|
|
711
|
-
args.push(name, item.selector, renderMenuAction(item.action));
|
|
712
|
-
}
|
|
713
|
-
args.push('', '-esc / q to close', '', '');
|
|
714
|
-
return args;
|
|
715
|
-
}
|
|
716
|
-
const TMUX_NAMED_KEYS = {
|
|
717
|
-
enter: 'Enter', escape: 'Escape', tab: 'Tab', backspace: 'BSpace', delete: 'DC',
|
|
718
|
-
space: 'Space', up: 'Up', down: 'Down', left: 'Left', right: 'Right',
|
|
719
|
-
pageup: 'PPage', pagedown: 'NPage', home: 'Home', end: 'End',
|
|
720
|
-
};
|
|
721
|
-
function normalizedStrokeParts(stroke) {
|
|
722
|
-
if (stroke === '+')
|
|
723
|
-
return { key: '+', modifiers: [] };
|
|
724
|
-
if (stroke.endsWith('++'))
|
|
725
|
-
return { key: '+', modifiers: stroke.slice(0, -2).split('+') };
|
|
726
|
-
const parts = stroke.split('+');
|
|
727
|
-
return { key: parts.pop(), modifiers: parts };
|
|
728
|
-
}
|
|
729
|
-
function tmuxKey(stroke) {
|
|
730
|
-
let { key, modifiers: modifierParts } = normalizedStrokeParts(stroke);
|
|
731
|
-
const modifiers = new Set(modifierParts);
|
|
732
|
-
if (modifiers.has('shift') && key.length === 1 && /[a-z]/.test(key)) {
|
|
733
|
-
// tmux names a shifted printable with its uppercase character even when an
|
|
734
|
-
// extended-key terminal also preserves Shift as a modifier: Alt+Shift+F is
|
|
735
|
-
// `M-S-F`. Shift by itself needs only the uppercase character (`F`).
|
|
736
|
-
key = key.toUpperCase();
|
|
737
|
-
if (!modifiers.has('ctrl') && !modifiers.has('alt'))
|
|
738
|
-
modifiers.delete('shift');
|
|
739
|
-
}
|
|
740
|
-
key = TMUX_NAMED_KEYS[key] ?? key;
|
|
741
|
-
const prefix = [
|
|
742
|
-
modifiers.has('ctrl') ? 'C-' : '',
|
|
743
|
-
modifiers.has('alt') ? 'M-' : '',
|
|
744
|
-
modifiers.has('shift') ? 'S-' : '',
|
|
745
|
-
].join('');
|
|
746
|
-
return `${prefix}${key}`;
|
|
747
|
-
}
|
|
748
|
-
function menuSelector(stroke) {
|
|
749
|
-
const { key, modifiers } = normalizedStrokeParts(stroke);
|
|
750
|
-
return modifiers.includes('shift') && /[a-z]/.test(key) ? key.toUpperCase() : key === 'space' ? ' ' : key;
|
|
751
|
-
}
|
|
752
|
-
function boundMenuItems(ids, entries, bindings) {
|
|
753
|
-
const items = [];
|
|
754
|
-
for (const id of ids) {
|
|
755
|
-
const entry = entries.get(id);
|
|
756
|
-
if (entry === undefined)
|
|
757
|
-
continue;
|
|
758
|
-
for (const gesture of bindings.gestures(id)) {
|
|
759
|
-
items.push({ description: entry.description, selector: menuSelector(gesture), action: entry.action });
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
return items;
|
|
763
|
-
}
|
|
764
|
-
/** Resolve the complete action descriptor set once. Submenu openers point at
|
|
765
|
-
* the same child MenuItems the prefix menu renders, so their direct global
|
|
766
|
-
* shortcuts cannot drift from the menu path. */
|
|
767
|
-
function resolvedMenuActions(bindings) {
|
|
768
|
-
const entries = new Map();
|
|
769
|
-
for (const [id, entry] of Object.entries(LEAF_MENU_ACTIONS)) {
|
|
770
|
-
if (entry !== undefined)
|
|
771
|
-
entries.set(id, entry);
|
|
772
|
-
}
|
|
773
|
-
for (const { menuId, actionId } of ATTACH_CONTROL_BINDINGS) {
|
|
774
|
-
const definition = BINDING_CATALOG.find((candidate) => candidate.id === actionId);
|
|
775
|
-
if (definition === undefined)
|
|
776
|
-
throw new Error(`missing attach binding definition ${actionId}`);
|
|
777
|
-
entries.set(menuId, {
|
|
778
|
-
description: definition.name.toLowerCase(),
|
|
779
|
-
action: { kind: 'literal-input', input: encodeAttachControlInput(actionId) },
|
|
780
|
-
});
|
|
781
|
-
}
|
|
782
|
-
const attachItems = boundMenuItems(ATTACH_CONTROL_BINDINGS.map(({ menuId }) => menuId), entries, bindings);
|
|
783
|
-
if (attachItems.length > 0) {
|
|
784
|
-
entries.set('crtr.tmux.menu.attach', {
|
|
785
|
-
description: 'attach viewer ›',
|
|
786
|
-
action: { kind: 'submenu', title: ' attach viewer ', items: attachItems },
|
|
787
|
-
});
|
|
788
|
-
}
|
|
789
|
-
const showItems = boundMenuItems([
|
|
790
|
-
'crtr.tmux.menu.metadata',
|
|
791
|
-
'crtr.tmux.menu.context',
|
|
792
|
-
], entries, bindings);
|
|
793
|
-
entries.set('crtr.tmux.menu.show', {
|
|
794
|
-
description: 'inspect ›',
|
|
795
|
-
action: { kind: 'submenu', title: ' inspect node ', items: showItems },
|
|
796
|
-
});
|
|
797
|
-
// How this viewer PAINTS the transcript, as opposed to what it shows. Each item
|
|
798
|
-
// is a toggle the viewer applies to its own render state (a `/`-command sent to
|
|
799
|
-
// the pane); a tmux menu is built once and cannot show live state, so the item
|
|
800
|
-
// reads as the action and the viewer answers with a notice.
|
|
801
|
-
const viewSettingsItems = boundMenuItems(['crtr.tmux.menu.fold-tools'], entries, bindings);
|
|
802
|
-
entries.set('crtr.tmux.menu.view-settings', {
|
|
803
|
-
description: 'display ›',
|
|
804
|
-
action: { kind: 'submenu', title: ' display ', items: viewSettingsItems },
|
|
805
|
-
});
|
|
806
|
-
const issueItems = boundMenuItems([
|
|
807
|
-
'crtr.tmux.menu.issue.idea',
|
|
808
|
-
'crtr.tmux.menu.issue.bug',
|
|
809
|
-
], entries, bindings);
|
|
810
|
-
entries.set('crtr.tmux.menu.issues', {
|
|
811
|
-
description: 'create ›',
|
|
812
|
-
action: { kind: 'submenu', title: ' create ', items: issueItems },
|
|
813
|
-
});
|
|
814
|
-
return entries;
|
|
815
|
-
}
|
|
816
|
-
function menuItems(bindings, entries) {
|
|
817
|
-
const boundItems = (ids) => boundMenuItems(ids, entries, bindings);
|
|
818
|
-
const items = [];
|
|
819
|
-
const addGroup = (heading, groupItems) => {
|
|
820
|
-
if (groupItems.length === 0)
|
|
821
|
-
return;
|
|
822
|
-
if (items.length > 0)
|
|
823
|
-
items.push({ description: '', selector: '', action: { kind: 'separator' } });
|
|
824
|
-
items.push({ description: `-#[bold]${heading}`, selector: '', action: { kind: 'disabled' } }, ...groupItems);
|
|
825
|
-
};
|
|
826
|
-
// Explore changes what is in view or opens a canvas-level browser. Current
|
|
827
|
-
// node acts on the agent in this pane without changing its lifecycle.
|
|
828
|
-
addGroup('Explore', boundItems([
|
|
829
|
-
'crtr.tmux.menu.resume',
|
|
830
|
-
'crtr.tmux.menu.graph',
|
|
831
|
-
'crtr.tmux.menu.focus-manager',
|
|
832
|
-
'crtr.tmux.menu.show',
|
|
833
|
-
'crtr.tmux.menu.view-settings',
|
|
834
|
-
]));
|
|
835
|
-
addGroup('Current node', boundItems([
|
|
836
|
-
'crtr.tmux.menu.attach',
|
|
837
|
-
'crtr.tmux.menu.background-bash',
|
|
838
|
-
'crtr.tmux.menu.file-review',
|
|
839
|
-
'crtr.tmux.menu.pin',
|
|
840
|
-
'crtr.tmux.menu.copy-id',
|
|
841
|
-
'crtr.tmux.menu.reload-viewer',
|
|
842
|
-
'crtr.tmux.menu.recap',
|
|
843
|
-
]));
|
|
844
|
-
addGroup('Lifecycle', boundItems([
|
|
845
|
-
'crtr.tmux.menu.fork',
|
|
846
|
-
'crtr.tmux.menu.promote',
|
|
847
|
-
'crtr.tmux.menu.demote',
|
|
848
|
-
'crtr.tmux.menu.detach',
|
|
849
|
-
'crtr.tmux.menu.close-subtree',
|
|
850
|
-
]));
|
|
851
|
-
addGroup('System', boundItems([
|
|
852
|
-
'crtr.tmux.menu.providers',
|
|
853
|
-
'crtr.tmux.menu.issues',
|
|
854
|
-
]));
|
|
855
|
-
const prefixGestures = bindings.gestures('crtr.tmux.menu.open');
|
|
856
|
-
if (prefixGestures.length > 0 && items.length > 0) {
|
|
857
|
-
items.push({ description: '', selector: '', action: { kind: 'separator' } });
|
|
858
|
-
for (const gesture of prefixGestures) {
|
|
859
|
-
items.push({
|
|
860
|
-
description: `${formatGesture(gesture)} to close`,
|
|
861
|
-
// Menu selectors accept tmux key notation, including modifiers. This
|
|
862
|
-
// is intentionally the exact binding that opened the menu.
|
|
863
|
-
selector: tmuxKey(gesture),
|
|
864
|
-
action: { kind: 'dismiss' },
|
|
865
|
-
});
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
return items;
|
|
869
|
-
}
|
|
870
|
-
/** Read one token from tmux's sourceable `list-keys` output. Key names that
|
|
871
|
-
* contain punctuation are quoted, so splitting on whitespace would make their
|
|
872
|
-
* displayed token differ from the key name accepted by bind/unbind commands. */
|
|
873
|
-
function readTmuxToken(line, from) {
|
|
874
|
-
let index = from;
|
|
875
|
-
while (index < line.length && /\s/.test(line[index]))
|
|
876
|
-
index += 1;
|
|
877
|
-
if (index >= line.length)
|
|
878
|
-
return null;
|
|
879
|
-
let quote = null;
|
|
880
|
-
let value = '';
|
|
881
|
-
while (index < line.length) {
|
|
882
|
-
const char = line[index];
|
|
883
|
-
if (quote === null && /\s/.test(char))
|
|
884
|
-
break;
|
|
885
|
-
if (quote === null && (char === "'" || char === '"')) {
|
|
886
|
-
quote = char;
|
|
887
|
-
index += 1;
|
|
888
|
-
continue;
|
|
889
|
-
}
|
|
890
|
-
if (quote === char) {
|
|
891
|
-
quote = null;
|
|
892
|
-
index += 1;
|
|
893
|
-
continue;
|
|
894
|
-
}
|
|
895
|
-
if (char === '\\' && quote !== "'" && index + 1 < line.length) {
|
|
896
|
-
value += line[index + 1];
|
|
897
|
-
index += 2;
|
|
898
|
-
continue;
|
|
899
|
-
}
|
|
900
|
-
value += char;
|
|
901
|
-
index += 1;
|
|
902
|
-
}
|
|
903
|
-
return { value, end: index };
|
|
904
|
-
}
|
|
905
|
-
function parseTmuxBinding(line) {
|
|
906
|
-
const commandName = readTmuxToken(line, 0);
|
|
907
|
-
if (commandName?.value !== 'bind-key')
|
|
908
|
-
return null;
|
|
909
|
-
let cursor = commandName.end;
|
|
910
|
-
let repeat = false;
|
|
911
|
-
while (true) {
|
|
912
|
-
const token = readTmuxToken(line, cursor);
|
|
913
|
-
if (token === null)
|
|
914
|
-
return null;
|
|
915
|
-
cursor = token.end;
|
|
916
|
-
if (token.value === '-r') {
|
|
917
|
-
repeat = true;
|
|
918
|
-
continue;
|
|
919
|
-
}
|
|
920
|
-
if (token.value !== '-T')
|
|
921
|
-
continue;
|
|
922
|
-
const table = readTmuxToken(line, cursor);
|
|
923
|
-
if (table === null)
|
|
924
|
-
return null;
|
|
925
|
-
const key = readTmuxToken(line, table.end);
|
|
926
|
-
if (key === null)
|
|
927
|
-
return null;
|
|
928
|
-
return {
|
|
929
|
-
table: table.value,
|
|
930
|
-
key: key.value,
|
|
931
|
-
command: line.slice(key.end).trimStart(),
|
|
932
|
-
repeat,
|
|
933
|
-
};
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
function bindingAt(table, key) {
|
|
937
|
-
const result = tmux(['list-keys', '-T', table]);
|
|
938
|
-
if (!result.ok)
|
|
939
|
-
return null;
|
|
940
|
-
return result.stdout.split('\n').find((line) => {
|
|
941
|
-
const parsed = parseTmuxBinding(line);
|
|
942
|
-
return parsed?.table === table && parsed.key === key;
|
|
943
|
-
}) ?? null;
|
|
944
|
-
}
|
|
945
|
-
/** Effective crouter gestures that tmux's root table currently consumes before
|
|
946
|
-
* pane input can see them. One list-keys snapshot serves the whole query. */
|
|
947
|
-
export function occupiedTmuxRootGestures(gestures) {
|
|
948
|
-
const result = tmux(['list-keys', '-T', ROOT_TABLE]);
|
|
949
|
-
if (!result.ok)
|
|
950
|
-
return [];
|
|
951
|
-
const occupied = new Set(result.stdout.split('\n').flatMap((line) => {
|
|
952
|
-
const parsed = parseTmuxBinding(line);
|
|
953
|
-
return parsed?.table === ROOT_TABLE ? [parsed.key] : [];
|
|
954
|
-
}));
|
|
955
|
-
return gestures.filter((gesture) => !gesture.includes(' > ') && occupied.has(tmuxKey(gesture)));
|
|
956
|
-
}
|
|
957
|
-
function isInstalledPair(value) {
|
|
958
|
-
return typeof value === 'object' && value !== null
|
|
959
|
-
&& typeof value.table === 'string'
|
|
960
|
-
&& typeof value.key === 'string';
|
|
961
|
-
}
|
|
962
|
-
function isWrappedRootBinding(value) {
|
|
963
|
-
if (typeof value !== 'object' || value === null)
|
|
964
|
-
return false;
|
|
965
|
-
const wrapper = value;
|
|
966
|
-
return typeof wrapper.bindingId === 'string'
|
|
967
|
-
&& BINDING_IDS.has(wrapper.bindingId)
|
|
968
|
-
&& typeof wrapper.gesture === 'string'
|
|
969
|
-
&& typeof wrapper.key === 'string'
|
|
970
|
-
&& (typeof wrapper.original === 'string' || wrapper.original === null)
|
|
971
|
-
&& typeof wrapper.wrapper === 'string'
|
|
972
|
-
&& (wrapper.state === 'owned' || wrapper.state === 'relinquished');
|
|
973
|
-
}
|
|
974
|
-
function readManifest() {
|
|
975
|
-
const option = tmux(['show-options', '-s', '-q', TMUX_BINDING_MANIFEST]);
|
|
976
|
-
if (!option.ok)
|
|
977
|
-
return null;
|
|
978
|
-
if (option.stdout === '')
|
|
979
|
-
return { pairs: [], wrappers: [] };
|
|
980
|
-
const result = tmux(['show-options', '-s', '-qv', TMUX_BINDING_MANIFEST]);
|
|
981
|
-
if (!result.ok)
|
|
982
|
-
return null;
|
|
983
|
-
try {
|
|
984
|
-
const value = JSON.parse(result.stdout);
|
|
985
|
-
// Before passthrough wrappers, the manifest was the pair array itself.
|
|
986
|
-
if (Array.isArray(value))
|
|
987
|
-
return value.every(isInstalledPair) ? { pairs: value, wrappers: [] } : null;
|
|
988
|
-
if (typeof value !== 'object' || value === null)
|
|
989
|
-
return null;
|
|
990
|
-
const manifest = value;
|
|
991
|
-
if (!Array.isArray(manifest.pairs) || !manifest.pairs.every(isInstalledPair)
|
|
992
|
-
|| !Array.isArray(manifest.wrappers) || !manifest.wrappers.every(isWrappedRootBinding))
|
|
993
|
-
return null;
|
|
994
|
-
return { pairs: manifest.pairs, wrappers: manifest.wrappers };
|
|
995
|
-
}
|
|
996
|
-
catch {
|
|
997
|
-
return null;
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
function writeManifest(manifest) {
|
|
1001
|
-
return tmux(['set-option', '-s', TMUX_BINDING_MANIFEST, JSON.stringify(manifest)]);
|
|
1002
|
-
}
|
|
1003
|
-
/** Tmux parses semicolons once more even when argv already isolated them. */
|
|
1004
|
-
function tmuxKeyArgument(key) {
|
|
1005
|
-
return key.replace(/\\/g, '\\\\').replace(/;/g, '\\;');
|
|
1006
|
-
}
|
|
1007
|
-
function tmuxCommandToken(value) {
|
|
1008
|
-
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/;/g, '\\;')}"`;
|
|
1009
|
-
}
|
|
1010
|
-
/** A top-level separator is printed as `\;` in a bind-key line, but the same
|
|
1011
|
-
* sequence is literal after the command tail becomes one if-shell branch. */
|
|
1012
|
-
function bindingCommandForBranch(command) {
|
|
1013
|
-
let quote = null;
|
|
1014
|
-
let out = '';
|
|
1015
|
-
for (let index = 0; index < command.length; index += 1) {
|
|
1016
|
-
const char = command[index];
|
|
1017
|
-
if (quote === null && (char === "'" || char === '"')) {
|
|
1018
|
-
quote = char;
|
|
1019
|
-
out += char;
|
|
1020
|
-
continue;
|
|
1021
|
-
}
|
|
1022
|
-
if (quote === char) {
|
|
1023
|
-
quote = null;
|
|
1024
|
-
out += char;
|
|
1025
|
-
continue;
|
|
1026
|
-
}
|
|
1027
|
-
if (char === '\\' && index + 1 < command.length) {
|
|
1028
|
-
const next = command[index + 1];
|
|
1029
|
-
if (quote === null && next === ';') {
|
|
1030
|
-
out += ';';
|
|
1031
|
-
}
|
|
1032
|
-
else {
|
|
1033
|
-
out += char + next;
|
|
1034
|
-
}
|
|
1035
|
-
index += 1;
|
|
1036
|
-
continue;
|
|
1037
|
-
}
|
|
1038
|
-
out += char;
|
|
1039
|
-
}
|
|
1040
|
-
return out;
|
|
1041
|
-
}
|
|
1042
|
-
function sourceBindingLine(line) {
|
|
1043
|
-
const dir = mkdtempSync(join(tmpdir(), 'crtr-tmux-binding-'));
|
|
1044
|
-
const file = join(dir, 'binding.conf');
|
|
1045
|
-
try {
|
|
1046
|
-
writeFileSync(file, `${line}\n`, 'utf8');
|
|
1047
|
-
return tmux(['source-file', file]);
|
|
1048
|
-
}
|
|
1049
|
-
finally {
|
|
1050
|
-
rmSync(dir, { recursive: true, force: true });
|
|
1051
|
-
}
|
|
1052
|
-
}
|
|
1053
|
-
function restoreWrappedBinding(wrapper) {
|
|
1054
|
-
return wrapper.original === null
|
|
1055
|
-
? tmux(['unbind-key', '-T', ROOT_TABLE, tmuxKeyArgument(wrapper.key)])
|
|
1056
|
-
: sourceBindingLine(wrapper.original);
|
|
1057
|
-
}
|
|
1058
|
-
// Crouter's own pre-manifest root bindings. Reconciliation always adopts an
|
|
1059
|
-
// untracked binding matching these crouter-specific markers, even when a partial
|
|
1060
|
-
// ownership manifest already exists. Older installs could leave M-c outside a
|
|
1061
|
-
// later manifest that tracked only newly-added bindings; treating that stale
|
|
1062
|
-
// menu as foreign made every future menu update silently skip it.
|
|
1063
|
-
const LEGACY_ROOT_BINDINGS = [
|
|
1064
|
-
{ key: 'M-c', markers: ['display-menu', 'promote to orchestrator', 'crtr canvas chord', 'new issue / todo'] },
|
|
1065
|
-
// Later pre-manifest menu generations dropped the 'new issue / todo' row, so
|
|
1066
|
-
// they need their own marker set (still crouter-specific via the chord verb).
|
|
1067
|
-
{ key: 'M-c', markers: ['display-menu', 'promote to orchestrator', 'crtr canvas chord'] },
|
|
1068
|
-
{ key: 'M-]', markers: ['run-shell', '--dir next', "--pane '#{pane_id}'"] },
|
|
1069
|
-
{ key: 'M-[', markers: ['run-shell', '--dir prev', "--pane '#{pane_id}'"] },
|
|
1070
|
-
{ key: 'M-v', markers: ['switch-client', '-T crtr-view'] },
|
|
1071
|
-
];
|
|
1072
|
-
// The inbox toggle is a crouter-owned root binding, resolved from keybindings.json
|
|
1073
|
-
// and installed through the manifest sweep like every sibling. Root bindings
|
|
1074
|
-
// matching these markers that are NOT tracked in the manifest are hl-owned
|
|
1075
|
-
// strays; reconciliation removes them (and the state file naming their key) so
|
|
1076
|
-
// the manifest stays the sole owner of the inbox command.
|
|
1077
|
-
const LEGACY_INBOX_MARKERS = ['run-shell', 'hl inbox toggle'];
|
|
1078
|
-
const LEGACY_INBOX_DEFAULT_KEY = 'M-i';
|
|
1079
|
-
export function legacyInboxKeyFile() {
|
|
1080
|
-
const stateHome = process.env['XDG_STATE_HOME'] || join(homedir(), '.local', 'state');
|
|
1081
|
-
return join(stateHome, 'humanloop', 'inbox-key');
|
|
1082
|
-
}
|
|
1083
|
-
function adoptLegacyBindings() {
|
|
1084
|
-
for (const legacy of LEGACY_ROOT_BINDINGS) {
|
|
1085
|
-
const existing = bindingAt(ROOT_TABLE, legacy.key);
|
|
1086
|
-
if (existing !== null && legacy.markers.every((marker) => existing.includes(marker))) {
|
|
1087
|
-
tmux(['unbind-key', '-T', ROOT_TABLE, legacy.key]);
|
|
1088
|
-
}
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
// Enforce the manifest's sole ownership of the inbox command: any hl-owned
|
|
1092
|
-
// inbox binding not tracked in the manifest is unbound, and the humanloop
|
|
1093
|
-
// state file is discarded. Runs on every reconciliation regardless of whether
|
|
1094
|
-
// a manifest already exists. Checks the default key plus the key the state
|
|
1095
|
-
// file records (the file's value only locates a stray binding — it is never
|
|
1096
|
-
// imported into crouter config). A key tracked in the manifest is crouter's
|
|
1097
|
-
// own and left alone; a foreign non-hl binding never matches the markers and
|
|
1098
|
-
// is preserved. Idempotent: with no file and no untracked hl binding, a no-op.
|
|
1099
|
-
function removeLegacyInboxBindings(manifestPairs) {
|
|
1100
|
-
const owned = new Set(manifestPairs.filter((pair) => pair.table === ROOT_TABLE).map((pair) => pair.key));
|
|
1101
|
-
const keys = new Set([LEGACY_INBOX_DEFAULT_KEY]);
|
|
1102
|
-
const file = legacyInboxKeyFile();
|
|
1103
|
-
try {
|
|
1104
|
-
const configured = readFileSync(file, 'utf8').trim();
|
|
1105
|
-
if (configured !== '')
|
|
1106
|
-
keys.add(configured);
|
|
1107
|
-
}
|
|
1108
|
-
catch { /* no state file: only the default key can hold a stale binding */ }
|
|
1109
|
-
for (const key of keys) {
|
|
1110
|
-
if (owned.has(key))
|
|
1111
|
-
continue;
|
|
1112
|
-
const existing = bindingAt(ROOT_TABLE, key);
|
|
1113
|
-
if (existing !== null && LEGACY_INBOX_MARKERS.every((marker) => existing.includes(marker))) {
|
|
1114
|
-
tmux(['unbind-key', '-T', ROOT_TABLE, key]);
|
|
1115
|
-
}
|
|
1116
|
-
}
|
|
1117
|
-
try {
|
|
1118
|
-
rmSync(file, { force: true });
|
|
1119
|
-
}
|
|
1120
|
-
catch { /* best-effort */ }
|
|
1121
|
-
}
|
|
1122
|
-
// --quiet keeps the background `run-shell -b` binding from printing its result JSON, which
|
|
1123
|
-
// tmux would otherwise surface as a view-mode overlay on the active pane. The popup opening
|
|
1124
|
-
// is the feedback; a genuine failure still prints through the CLI toggle action. This deliberately
|
|
1125
|
-
// invokes bare `crtr` from PATH: it is installed once and must resolve to the selected generation,
|
|
1126
|
-
// not the sealed generation that installed it. The neighboring `crtr surface node cycle` builders
|
|
1127
|
-
// share that property.
|
|
1128
|
-
export function inboxToggleTmuxCommand() {
|
|
1129
|
-
return ['run-shell', '-b', 'crtr human inbox toggle --quiet --tmux-socket "#{socket_path}" --tmux-client "#{client_name}" --target-pane "#{pane_id}"'];
|
|
1130
|
-
}
|
|
1131
|
-
function actionCommand(id) {
|
|
1132
|
-
switch (id) {
|
|
1133
|
-
case 'crtr.tmux.node.next': return ['run-shell', `crtr surface node cycle --dir next --pane '#{pane_id}' >/dev/null 2>&1`];
|
|
1134
|
-
case 'crtr.tmux.node.previous': return ['run-shell', `crtr surface node cycle --dir prev --pane '#{pane_id}' >/dev/null 2>&1`];
|
|
1135
|
-
case 'crtr.tmux.inbox.toggle': return inboxToggleTmuxCommand();
|
|
1136
|
-
default: throw new Error(`no tmux command for ${id}`);
|
|
1137
|
-
}
|
|
1138
|
-
}
|
|
1139
|
-
/** A directory beside the server socket serializes every process driving that
|
|
1140
|
-
* exact tmux server. Outside tmux, ask the default server for its socket first;
|
|
1141
|
-
* an unavailable server uses one per-user fallback lock for the error path. */
|
|
1142
|
-
function tmuxBindingLockPath() {
|
|
1143
|
-
const envSocket = process.env['TMUX']?.split(',')[0]?.trim();
|
|
1144
|
-
if (envSocket !== undefined && envSocket !== '')
|
|
1145
|
-
return `${envSocket}.crtr-keybindings.lock`;
|
|
1146
|
-
const result = tmux(['display-message', '-p', '#{socket_path}']);
|
|
1147
|
-
if (result.ok && result.stdout !== '')
|
|
1148
|
-
return `${result.stdout}.crtr-keybindings.lock`;
|
|
1149
|
-
const user = typeof process.getuid === 'function' ? process.getuid() : 'user';
|
|
1150
|
-
return join(tmpdir(), `crtr-tmux-keybindings-${user}.lock`);
|
|
1151
|
-
}
|
|
1152
|
-
/** Reconcile the tmux server with one immutable crouter settings snapshot.
|
|
1153
|
-
* Passing snapshots is hermetic; the no-argument front door reads both user
|
|
1154
|
-
* settings from the same config read. The server lock covers the entire
|
|
1155
|
-
* manifest read, restore, install, fingerprint, and write transition. */
|
|
1156
|
-
export function installTmuxBindings(bindingSnapshot, passthroughSnapshot = []) {
|
|
1157
|
-
return withExclusiveDirectoryLock(tmuxBindingLockPath(), () => reconcileTmuxBindings(bindingSnapshot, passthroughSnapshot));
|
|
1158
|
-
}
|
|
1159
|
-
function reconcileTmuxBindings(bindingSnapshot, passthroughSnapshot = []) {
|
|
1160
|
-
const userSettings = bindingSnapshot === undefined ? resolveUserKeybindingSettings() : undefined;
|
|
1161
|
-
const bindings = bindingSnapshot ?? userSettings.bindings;
|
|
1162
|
-
const passthrough = bindingSnapshot === undefined
|
|
1163
|
-
? userSettings.tmuxPassthrough
|
|
1164
|
-
: normalizeTmuxPassthrough(passthroughSnapshot);
|
|
1165
|
-
const diagnostics = [];
|
|
1166
|
-
const installed = [];
|
|
1167
|
-
const manifest = readManifest();
|
|
1168
|
-
if (manifest === null) {
|
|
1169
|
-
diagnostics.push({ code: 'tmux-error', message: `tmux is unavailable or ${TMUX_BINDING_MANIFEST} is malformed` });
|
|
1170
|
-
return { ok: false, installed, diagnostics };
|
|
1171
|
-
}
|
|
1172
|
-
const desiredWrappers = new Map();
|
|
1173
|
-
for (const bindingId of passthrough) {
|
|
1174
|
-
const definition = BINDING_CATALOG.find((candidate) => candidate.id === bindingId);
|
|
1175
|
-
if (definition === undefined || !isAttachPaneBinding(definition))
|
|
1176
|
-
continue;
|
|
1177
|
-
for (const gesture of bindings.gestures(bindingId)) {
|
|
1178
|
-
if (gesture.includes(' > '))
|
|
1179
|
-
continue;
|
|
1180
|
-
const key = tmuxKey(gesture);
|
|
1181
|
-
if (!desiredWrappers.has(key))
|
|
1182
|
-
desiredWrappers.set(key, { bindingId, gesture });
|
|
1183
|
-
}
|
|
1184
|
-
}
|
|
1185
|
-
// Restore every wrapper crouter still owns before the ordinary binding sweep.
|
|
1186
|
-
// A live line that differs from the fingerprint is a user's later rebind: it
|
|
1187
|
-
// is protected from this and every later reconciliation until they toggle the
|
|
1188
|
-
// passthrough preference off and on again.
|
|
1189
|
-
const protectedRootKeys = new Set();
|
|
1190
|
-
const wrappers = [];
|
|
1191
|
-
for (const prior of manifest.wrappers) {
|
|
1192
|
-
const desired = desiredWrappers.get(prior.key);
|
|
1193
|
-
const live = bindingAt(ROOT_TABLE, prior.key);
|
|
1194
|
-
if (prior.state === 'relinquished' || live !== prior.wrapper) {
|
|
1195
|
-
protectedRootKeys.add(prior.key);
|
|
1196
|
-
if (desired !== undefined) {
|
|
1197
|
-
wrappers.push({ ...prior, ...desired, state: 'relinquished' });
|
|
1198
|
-
}
|
|
1199
|
-
continue;
|
|
1200
|
-
}
|
|
1201
|
-
const restored = restoreWrappedBinding(prior);
|
|
1202
|
-
if (!restored.ok) {
|
|
1203
|
-
protectedRootKeys.add(prior.key);
|
|
1204
|
-
wrappers.push(prior);
|
|
1205
|
-
diagnostics.push({
|
|
1206
|
-
code: 'tmux-error',
|
|
1207
|
-
bindingId: prior.bindingId,
|
|
1208
|
-
gesture: prior.gesture,
|
|
1209
|
-
message: `could not restore tmux binding ${ROOT_TABLE}:${prior.key}: ${restored.stderr}`,
|
|
1210
|
-
});
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
adoptLegacyBindings();
|
|
1214
|
-
removeLegacyInboxBindings(manifest.pairs);
|
|
1215
|
-
for (const pair of manifest.pairs) {
|
|
1216
|
-
if (pair.table === ROOT_TABLE && protectedRootKeys.has(pair.key))
|
|
1217
|
-
continue;
|
|
1218
|
-
const result = tmux(['unbind-key', '-T', pair.table, tmuxKeyArgument(pair.key)]);
|
|
1219
|
-
if (!result.ok && bindingAt(pair.table, pair.key) !== null) {
|
|
1220
|
-
diagnostics.push({ code: 'tmux-error', message: `could not remove crouter binding ${pair.table}:${pair.key}: ${result.stderr}` });
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
if (diagnostics.some((diagnostic) => diagnostic.code === 'tmux-error')) {
|
|
1224
|
-
const save = writeManifest({ pairs: manifest.pairs, wrappers });
|
|
1225
|
-
if (!save.ok)
|
|
1226
|
-
diagnostics.push({ code: 'tmux-error', message: `could not save crouter tmux ownership manifest: ${save.stderr}` });
|
|
1227
|
-
return { ok: false, installed, diagnostics };
|
|
1228
|
-
}
|
|
1229
|
-
const install = (table, key, command, id, gesture) => {
|
|
1230
|
-
if (bindingAt(table, key) !== null) {
|
|
1231
|
-
diagnostics.push({ code: 'occupied', bindingId: id, gesture, message: `${table}:${key} is already bound by tmux; ${id} was not installed` });
|
|
1232
|
-
return false;
|
|
1233
|
-
}
|
|
1234
|
-
const result = tmux(['bind-key', '-T', table, tmuxKeyArgument(key), ...command]);
|
|
1235
|
-
if (!result.ok) {
|
|
1236
|
-
diagnostics.push({ code: 'tmux-error', bindingId: id, gesture, message: `could not install ${id}: ${result.stderr}` });
|
|
1237
|
-
return false;
|
|
1238
|
-
}
|
|
1239
|
-
installed.push({ table, key });
|
|
1240
|
-
return true;
|
|
1241
|
-
};
|
|
1242
|
-
const menuActions = resolvedMenuActions(bindings);
|
|
1243
|
-
const menuCommand = buildMenuArgs(' crtr ', menuItems(bindings, menuActions));
|
|
1244
|
-
for (const gesture of bindings.gestures('crtr.tmux.menu.open')) {
|
|
1245
|
-
install(ROOT_TABLE, tmuxKey(gesture), menuCommand, 'crtr.tmux.menu.open', gesture);
|
|
1246
|
-
}
|
|
1247
|
-
for (const definition of BINDING_CATALOG) {
|
|
1248
|
-
if (definition.pair?.field !== 'menu')
|
|
1249
|
-
continue;
|
|
1250
|
-
const paired = BINDING_CATALOG.find((candidate) => candidate.id === definition.pair?.pairedId);
|
|
1251
|
-
if (paired?.pair?.field !== 'global')
|
|
1252
|
-
continue;
|
|
1253
|
-
const menuId = definition.id;
|
|
1254
|
-
const globalId = paired.id;
|
|
1255
|
-
const entry = menuActions.get(menuId);
|
|
1256
|
-
const command = entry === undefined ? null : menuActionCommand(entry.action);
|
|
1257
|
-
if (command === null)
|
|
1258
|
-
throw new Error(`no direct tmux action for paired menu binding ${menuId}`);
|
|
1259
|
-
for (const gesture of bindings.gestures(globalId)) {
|
|
1260
|
-
install(ROOT_TABLE, tmuxKey(gesture), command.argv, globalId, gesture);
|
|
1261
|
-
}
|
|
1262
|
-
}
|
|
1263
|
-
for (const id of ['crtr.tmux.node.next', 'crtr.tmux.node.previous', 'crtr.tmux.inbox.toggle']) {
|
|
1264
|
-
for (const gesture of bindings.gestures(id))
|
|
1265
|
-
install(ROOT_TABLE, tmuxKey(gesture), actionCommand(id), id, gesture);
|
|
1266
|
-
}
|
|
1267
|
-
// Wrap only occupied roots. Unoccupied gestures already reach the pane and do
|
|
1268
|
-
// not need crouter ownership. Re-read the installed line as the fingerprint;
|
|
1269
|
-
// tmux canonicalizes quoting and braces in ways hand serialization cannot.
|
|
1270
|
-
const newlyWrapped = [];
|
|
1271
|
-
for (const [key, desired] of desiredWrappers) {
|
|
1272
|
-
if (protectedRootKeys.has(key))
|
|
1273
|
-
continue;
|
|
1274
|
-
const original = bindingAt(ROOT_TABLE, key);
|
|
1275
|
-
if (original === null)
|
|
1276
|
-
continue;
|
|
1277
|
-
const parsed = parseTmuxBinding(original);
|
|
1278
|
-
if (parsed === null || parsed.command === '') {
|
|
1279
|
-
diagnostics.push({ code: 'tmux-error', bindingId: desired.bindingId, gesture: desired.gesture, message: `could not parse occupied tmux binding ${ROOT_TABLE}:${key}` });
|
|
1280
|
-
continue;
|
|
1281
|
-
}
|
|
1282
|
-
const result = tmux([
|
|
1283
|
-
'bind-key',
|
|
1284
|
-
...(parsed.repeat ? ['-r'] : []),
|
|
1285
|
-
'-T', ROOT_TABLE, tmuxKeyArgument(key),
|
|
1286
|
-
'if-shell', '-F', '#{!=:#{@crtr_attach},}',
|
|
1287
|
-
`send-keys ${tmuxCommandToken(key)}`,
|
|
1288
|
-
bindingCommandForBranch(parsed.command),
|
|
1289
|
-
]);
|
|
1290
|
-
if (!result.ok) {
|
|
1291
|
-
diagnostics.push({ code: 'tmux-error', bindingId: desired.bindingId, gesture: desired.gesture, message: `could not wrap ${ROOT_TABLE}:${key}: ${result.stderr}` });
|
|
1292
|
-
continue;
|
|
1293
|
-
}
|
|
1294
|
-
const wrapper = bindingAt(ROOT_TABLE, key);
|
|
1295
|
-
if (wrapper === null || !wrapper.includes('@crtr_attach')) {
|
|
1296
|
-
diagnostics.push({ code: 'tmux-error', bindingId: desired.bindingId, gesture: desired.gesture, message: `could not fingerprint wrapped tmux binding ${ROOT_TABLE}:${key}` });
|
|
1297
|
-
continue;
|
|
1298
|
-
}
|
|
1299
|
-
const record = { ...desired, key, original, wrapper, state: 'owned' };
|
|
1300
|
-
wrappers.push(record);
|
|
1301
|
-
newlyWrapped.push(record);
|
|
1302
|
-
}
|
|
1303
|
-
const save = writeManifest({ pairs: installed, wrappers });
|
|
1304
|
-
if (!save.ok) {
|
|
1305
|
-
diagnostics.push({ code: 'tmux-error', message: `could not save crouter tmux ownership manifest: ${save.stderr}` });
|
|
1306
|
-
// A wrapper without restoration data is unsafe. Put each newly wrapped key
|
|
1307
|
-
// back immediately when manifest persistence fails.
|
|
1308
|
-
for (const wrapper of newlyWrapped) {
|
|
1309
|
-
if (bindingAt(ROOT_TABLE, wrapper.key) === wrapper.wrapper)
|
|
1310
|
-
restoreWrappedBinding(wrapper);
|
|
1311
|
-
}
|
|
1312
|
-
}
|
|
1313
|
-
return { ok: diagnostics.every((diagnostic) => diagnostic.code !== 'tmux-error'), installed, diagnostics };
|
|
1314
|
-
}
|