@quickchatai/quickreview 0.0.0-stage → 1.1.0
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/LICENSE +23 -0
- package/README.md +385 -2
- package/THIRD-PARTY-NOTICES.md +134 -0
- package/agents/pass-abstraction.md +64 -0
- package/agents/pass-ask-human.md +134 -0
- package/agents/pass-backcompat.md +67 -0
- package/agents/pass-baseline.md +61 -0
- package/agents/pass-behaviour-delta.md +64 -0
- package/agents/pass-bounds.md +36 -0
- package/agents/pass-claims.md +31 -0
- package/agents/pass-concurrency.md +168 -0
- package/agents/pass-consistency.md +57 -0
- package/agents/pass-contracts.md +30 -0
- package/agents/pass-data-safety.md +54 -0
- package/agents/pass-deferred.md +35 -0
- package/agents/pass-delivery.md +37 -0
- package/agents/pass-duplication.md +60 -0
- package/agents/pass-intent.md +63 -0
- package/agents/pass-lint-conformance.md +46 -0
- package/agents/pass-maintainability.md +67 -0
- package/agents/pass-placement.md +68 -0
- package/agents/pass-prod-risk.md +24 -0
- package/agents/pass-rollout.md +32 -0
- package/agents/pass-roundtrip.md +46 -0
- package/agents/pass-security.md +31 -0
- package/agents/pass-simplicity.md +68 -0
- package/agents/pass-statelessness.md +27 -0
- package/agents/pass-style.md +37 -0
- package/agents/pass-sweep.md +57 -0
- package/agents/pass-tests.md +58 -0
- package/agents/pass-ui.md +23 -0
- package/agents/pass-validate.md +50 -0
- package/agents/pass-widened.md +46 -0
- package/agents/scan/pass-scan.md +86 -0
- package/bin/qcr.js +11 -0
- package/config.example.toml +1067 -0
- package/deploy/ansible/README.md +356 -0
- package/deploy/ansible/group_vars/all.yml +212 -0
- package/deploy/ansible/inventory.example.ini +25 -0
- package/deploy/ansible/playbook.yml +360 -0
- package/deploy/collect-secrets.sh +289 -0
- package/deploy/collect-slack-secrets.mjs +1190 -0
- package/deploy/github-app-manifest.json +31 -0
- package/deploy/manifest.schema.json +75 -0
- package/deploy/qcr-update.sh +803 -0
- package/deploy/reviewer/AGENTS.override.md +12 -0
- package/dist/extensions/cli/bash-guard-ambient.js +42 -0
- package/dist/extensions/cli/bash-guard-ambient.js.map +1 -0
- package/dist/extensions/cli/bash-guard.js +5 -0
- package/dist/extensions/cli/bash-guard.js.map +1 -0
- package/dist/extensions/cli/companion-guard.js +5 -0
- package/dist/extensions/cli/companion-guard.js.map +1 -0
- package/dist/extensions/cli/debug-round.js +8 -0
- package/dist/extensions/cli/debug-round.js.map +1 -0
- package/dist/extensions/cli/inject-bridge.js +5 -0
- package/dist/extensions/cli/inject-bridge.js.map +1 -0
- package/dist/extensions/cli/ledger-pin.js +5 -0
- package/dist/extensions/cli/ledger-pin.js.map +1 -0
- package/dist/extensions/cli/lens.js +13 -0
- package/dist/extensions/cli/lens.js.map +1 -0
- package/dist/extensions/cli/muted-theme.js +5 -0
- package/dist/extensions/cli/muted-theme.js.map +1 -0
- package/dist/extensions/cli/onboarding.js +5 -0
- package/dist/extensions/cli/onboarding.js.map +1 -0
- package/dist/extensions/cli/setup.js +5 -0
- package/dist/extensions/cli/setup.js.map +1 -0
- package/dist/extensions/cli/tui-view.js +5 -0
- package/dist/extensions/cli/tui-view.js.map +1 -0
- package/dist/extensions/companion-guard.js +361 -0
- package/dist/extensions/companion-guard.js.map +1 -0
- package/dist/extensions/debug-round.js +204 -0
- package/dist/extensions/debug-round.js.map +1 -0
- package/dist/extensions/inject-bridge.js +603 -0
- package/dist/extensions/inject-bridge.js.map +1 -0
- package/dist/extensions/muted-theme.js +12 -0
- package/dist/extensions/muted-theme.js.map +1 -0
- package/dist/extensions/onboarding.js +175 -0
- package/dist/extensions/onboarding.js.map +1 -0
- package/dist/extensions/setup-replay.js +269 -0
- package/dist/extensions/setup-replay.js.map +1 -0
- package/dist/extensions/setup-scan.js +998 -0
- package/dist/extensions/setup-scan.js.map +1 -0
- package/dist/extensions/setup-triage.js +175 -0
- package/dist/extensions/setup-triage.js.map +1 -0
- package/dist/extensions/setup.js +1160 -0
- package/dist/extensions/setup.js.map +1 -0
- package/dist/extensions/tui-view.js +389 -0
- package/dist/extensions/tui-view.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/client.js +376 -0
- package/dist/extensions/vendor/openviking-pi/client.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/config.js +189 -0
- package/dist/extensions/vendor/openviking-pi/config.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/index.js +421 -0
- package/dist/extensions/vendor/openviking-pi/index.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/lib/capture-adapter.mjs +86 -0
- package/dist/extensions/vendor/openviking-pi/lib/capture-adapter.mjs.map +1 -0
- package/dist/extensions/vendor/openviking-pi/lib/takeover-core.mjs +408 -0
- package/dist/extensions/vendor/openviking-pi/lib/takeover-core.mjs.map +1 -0
- package/dist/extensions/vendor/openviking-pi/lib/uri-guard-adapter.mjs +41 -0
- package/dist/extensions/vendor/openviking-pi/lib/uri-guard-adapter.mjs.map +1 -0
- package/dist/extensions/vendor/openviking-pi/qcr-hint.js +290 -0
- package/dist/extensions/vendor/openviking-pi/qcr-hint.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/qcr-honest.js +148 -0
- package/dist/extensions/vendor/openviking-pi/qcr-honest.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/qcr-hooks.js +68 -0
- package/dist/extensions/vendor/openviking-pi/qcr-hooks.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/qcr-leaf.js +77 -0
- package/dist/extensions/vendor/openviking-pi/qcr-leaf.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/qcr-lesson.js +216 -0
- package/dist/extensions/vendor/openviking-pi/qcr-lesson.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/qcr-namespaces.js +154 -0
- package/dist/extensions/vendor/openviking-pi/qcr-namespaces.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/qcr-recall.js +254 -0
- package/dist/extensions/vendor/openviking-pi/qcr-recall.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/qcr-shim.js +94 -0
- package/dist/extensions/vendor/openviking-pi/qcr-shim.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/qcr-write.js +333 -0
- package/dist/extensions/vendor/openviking-pi/qcr-write.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/recall.js +108 -0
- package/dist/extensions/vendor/openviking-pi/recall.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/shared/capture-utils.mjs +529 -0
- package/dist/extensions/vendor/openviking-pi/shared/capture-utils.mjs.map +1 -0
- package/dist/extensions/vendor/openviking-pi/shared/credentials.mjs +205 -0
- package/dist/extensions/vendor/openviking-pi/shared/credentials.mjs.map +1 -0
- package/dist/extensions/vendor/openviking-pi/shared/pending-queue.mjs +442 -0
- package/dist/extensions/vendor/openviking-pi/shared/pending-queue.mjs.map +1 -0
- package/dist/extensions/vendor/openviking-pi/shared/profile-inject.mjs +286 -0
- package/dist/extensions/vendor/openviking-pi/shared/profile-inject.mjs.map +1 -0
- package/dist/extensions/vendor/openviking-pi/shared/recall-compress-core.mjs +189 -0
- package/dist/extensions/vendor/openviking-pi/shared/recall-compress-core.mjs.map +1 -0
- package/dist/extensions/vendor/openviking-pi/shared/recall-core.mjs +580 -0
- package/dist/extensions/vendor/openviking-pi/shared/recall-core.mjs.map +1 -0
- package/dist/extensions/vendor/openviking-pi/shared/retryable.mjs +11 -0
- package/dist/extensions/vendor/openviking-pi/shared/retryable.mjs.map +1 -0
- package/dist/extensions/vendor/openviking-pi/shared/session-model.mjs +63 -0
- package/dist/extensions/vendor/openviking-pi/shared/session-model.mjs.map +1 -0
- package/dist/extensions/vendor/openviking-pi/shared/uri-guard.mjs +58 -0
- package/dist/extensions/vendor/openviking-pi/shared/uri-guard.mjs.map +1 -0
- package/dist/extensions/vendor/openviking-pi/shared/workspace-peer.mjs +16 -0
- package/dist/extensions/vendor/openviking-pi/shared/workspace-peer.mjs.map +1 -0
- package/dist/extensions/vendor/openviking-pi/sync.js +127 -0
- package/dist/extensions/vendor/openviking-pi/sync.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/takeover.js +25 -0
- package/dist/extensions/vendor/openviking-pi/takeover.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/tools.js +417 -0
- package/dist/extensions/vendor/openviking-pi/tools.js.map +1 -0
- package/dist/src/anomalies.js +483 -0
- package/dist/src/anomalies.js.map +1 -0
- package/dist/src/answerworker.js +180 -0
- package/dist/src/answerworker.js.map +1 -0
- package/dist/src/appstamp.js +68 -0
- package/dist/src/appstamp.js.map +1 -0
- package/dist/src/archive/admission.js +3 -0
- package/dist/src/archive/admission.js.map +1 -0
- package/dist/src/archive/archivedb.js +474 -0
- package/dist/src/archive/archivedb.js.map +1 -0
- package/dist/src/archive/archiver.js +1770 -0
- package/dist/src/archive/archiver.js.map +1 -0
- package/dist/src/archive/archivewrite.js +198 -0
- package/dist/src/archive/archivewrite.js.map +1 -0
- package/dist/src/archive/bundle.js +266 -0
- package/dist/src/archive/bundle.js.map +1 -0
- package/dist/src/archive/dbmove.js +638 -0
- package/dist/src/archive/dbmove.js.map +1 -0
- package/dist/src/archive/duplicatecounts.js +80 -0
- package/dist/src/archive/duplicatecounts.js.map +1 -0
- package/dist/src/archive/eligibility.js +113 -0
- package/dist/src/archive/eligibility.js.map +1 -0
- package/dist/src/archive/extract.js +288 -0
- package/dist/src/archive/extract.js.map +1 -0
- package/dist/src/archive/fsutil.js +330 -0
- package/dist/src/archive/fsutil.js.map +1 -0
- package/dist/src/archive/gzfile.js +277 -0
- package/dist/src/archive/gzfile.js.map +1 -0
- package/dist/src/archive/kinds.js +22 -0
- package/dist/src/archive/kinds.js.map +1 -0
- package/dist/src/archive/livecheckpoint.js +178 -0
- package/dist/src/archive/livecheckpoint.js.map +1 -0
- package/dist/src/archive/liveretention.js +145 -0
- package/dist/src/archive/liveretention.js.map +1 -0
- package/dist/src/archive/manifest.js +284 -0
- package/dist/src/archive/manifest.js.map +1 -0
- package/dist/src/archive/overseer.js +283 -0
- package/dist/src/archive/overseer.js.map +1 -0
- package/dist/src/archive/piglobal.js +129 -0
- package/dist/src/archive/piglobal.js.map +1 -0
- package/dist/src/archive/plan.js +398 -0
- package/dist/src/archive/plan.js.map +1 -0
- package/dist/src/archive/prarchive.js +831 -0
- package/dist/src/archive/prarchive.js.map +1 -0
- package/dist/src/archive/prfiles.js +303 -0
- package/dist/src/archive/prfiles.js.map +1 -0
- package/dist/src/archive/report.js +169 -0
- package/dist/src/archive/report.js.map +1 -0
- package/dist/src/archive/restore.js +276 -0
- package/dist/src/archive/restore.js.map +1 -0
- package/dist/src/archive/rows.js +106 -0
- package/dist/src/archive/rows.js.map +1 -0
- package/dist/src/archive/seal.js +480 -0
- package/dist/src/archive/seal.js.map +1 -0
- package/dist/src/archive/status.js +528 -0
- package/dist/src/archive/status.js.map +1 -0
- package/dist/src/archive/storepass.js +409 -0
- package/dist/src/archive/storepass.js.map +1 -0
- package/dist/src/backfill.js +217 -0
- package/dist/src/backfill.js.map +1 -0
- package/dist/src/bench/cost.js +87 -0
- package/dist/src/bench/exec.js +76 -0
- package/dist/src/bench/values.js +14 -0
- package/dist/src/botcontrol.js +127 -0
- package/dist/src/botcontrol.js.map +1 -0
- package/dist/src/botworker.js +805 -0
- package/dist/src/botworker.js.map +1 -0
- package/dist/src/capture.js +234 -0
- package/dist/src/capture.js.map +1 -0
- package/dist/src/cases.js +1491 -0
- package/dist/src/cases.js.map +1 -0
- package/dist/src/channelfs.js +27 -0
- package/dist/src/channelfs.js.map +1 -0
- package/dist/src/checklist.js +312 -0
- package/dist/src/checklist.js.map +1 -0
- package/dist/src/ciretry.js +91 -0
- package/dist/src/ciretry.js.map +1 -0
- package/dist/src/cli.js +1916 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/cliops.js +333 -0
- package/dist/src/cliops.js.map +1 -0
- package/dist/src/clonedir.js +15 -0
- package/dist/src/clonedir.js.map +1 -0
- package/dist/src/commandeffects.js +43 -0
- package/dist/src/commandeffects.js.map +1 -0
- package/dist/src/commands.js +1312 -0
- package/dist/src/commands.js.map +1 -0
- package/dist/src/companioninbox.js +203 -0
- package/dist/src/companioninbox.js.map +1 -0
- package/dist/src/compare.js +673 -0
- package/dist/src/compare.js.map +1 -0
- package/dist/src/config.js +1027 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/configcheck.js +111 -0
- package/dist/src/configcheck.js.map +1 -0
- package/dist/src/coreext.js +43 -0
- package/dist/src/coreext.js.map +1 -0
- package/dist/src/daemon.js +929 -0
- package/dist/src/daemon.js.map +1 -0
- package/dist/src/debuground.js +731 -0
- package/dist/src/debuground.js.map +1 -0
- package/dist/src/debugroundpi.js +219 -0
- package/dist/src/debugroundpi.js.map +1 -0
- package/dist/src/diagnostics.js +14 -0
- package/dist/src/diagnostics.js.map +1 -0
- package/dist/src/digest.js +139 -0
- package/dist/src/digest.js.map +1 -0
- package/dist/src/dispatch.js +1726 -0
- package/dist/src/dispatch.js.map +1 -0
- package/dist/src/engine/answerguard.js +18 -0
- package/dist/src/engine/answerguard.js.map +1 -0
- package/dist/src/engine/botsdk.js +262 -0
- package/dist/src/engine/botsdk.js.map +1 -0
- package/dist/src/engine/caserunner-sdk.js +332 -0
- package/dist/src/engine/caserunner-sdk.js.map +1 -0
- package/dist/src/engine/debugpi.js +194 -0
- package/dist/src/engine/debugpi.js.map +1 -0
- package/dist/src/eod.js +473 -0
- package/dist/src/eod.js.map +1 -0
- package/dist/src/eodpinwatch.js +391 -0
- package/dist/src/eodpinwatch.js.map +1 -0
- package/dist/src/escalation.js +1009 -0
- package/dist/src/escalation.js.map +1 -0
- package/dist/src/eventloop.js +779 -0
- package/dist/src/eventloop.js.map +1 -0
- package/dist/src/execboundary.js +430 -0
- package/dist/src/execboundary.js.map +1 -0
- package/dist/src/feedback.js +915 -0
- package/dist/src/feedback.js.map +1 -0
- package/dist/src/findingscheck.js +200 -0
- package/dist/src/findingscheck.js.map +1 -0
- package/dist/src/fsdenied.js +26 -0
- package/dist/src/fsdenied.js.map +1 -0
- package/dist/src/ghread.js +181 -0
- package/dist/src/ghread.js.map +1 -0
- package/dist/src/github.js +1063 -0
- package/dist/src/github.js.map +1 -0
- package/dist/src/homes.js +103 -0
- package/dist/src/homes.js.map +1 -0
- package/dist/src/hostroute.js +24 -0
- package/dist/src/hostroute.js.map +1 -0
- package/dist/src/improvements.js +625 -0
- package/dist/src/improvements.js.map +1 -0
- package/dist/src/install.js +946 -0
- package/dist/src/install.js.map +1 -0
- package/dist/src/installkind.js +40 -0
- package/dist/src/installkind.js.map +1 -0
- package/dist/src/ipc/capability.js +286 -0
- package/dist/src/ipc/capability.js.map +1 -0
- package/dist/src/ipc/client.js +226 -0
- package/dist/src/ipc/client.js.map +1 -0
- package/dist/src/ipc/control.js +70 -0
- package/dist/src/ipc/control.js.map +1 -0
- package/dist/src/ipc/ghreadshim.js +77 -0
- package/dist/src/ipc/ghreadshim.js.map +1 -0
- package/dist/src/ipc/launcher.js +65 -0
- package/dist/src/ipc/launcher.js.map +1 -0
- package/dist/src/ipc/permits.js +293 -0
- package/dist/src/ipc/permits.js.map +1 -0
- package/dist/src/ipc/postshim.js +137 -0
- package/dist/src/ipc/postshim.js.map +1 -0
- package/dist/src/ipc/protocol.js +239 -0
- package/dist/src/ipc/protocol.js.map +1 -0
- package/dist/src/ipc/server.js +312 -0
- package/dist/src/ipc/server.js.map +1 -0
- package/dist/src/ipc/workerspec.js +148 -0
- package/dist/src/ipc/workerspec.js.map +1 -0
- package/dist/src/learner/learner.js +1272 -0
- package/dist/src/learner/learner.js.map +1 -0
- package/dist/src/learner/miner.js +948 -0
- package/dist/src/learner/miner.js.map +1 -0
- package/dist/src/learner/packet.js +216 -0
- package/dist/src/learner/packet.js.map +1 -0
- package/dist/src/learner/receipt.js +135 -0
- package/dist/src/learner/receipt.js.map +1 -0
- package/dist/src/learner/status.js +38 -0
- package/dist/src/learner/status.js.map +1 -0
- package/dist/src/ledgercheck.js +120 -0
- package/dist/src/ledgercheck.js.map +1 -0
- package/dist/src/lessonledger.js +451 -0
- package/dist/src/lessonledger.js.map +1 -0
- package/dist/src/lessonselect.js +639 -0
- package/dist/src/lessonselect.js.map +1 -0
- package/dist/src/lessonshadow.js +540 -0
- package/dist/src/lessonshadow.js.map +1 -0
- package/dist/src/levers.js +302 -0
- package/dist/src/levers.js.map +1 -0
- package/dist/src/live/projection.js +450 -0
- package/dist/src/live/projection.js.map +1 -0
- package/dist/src/live/publish.js +83 -0
- package/dist/src/live/publish.js.map +1 -0
- package/dist/src/live/read.js +60 -0
- package/dist/src/live/read.js.map +1 -0
- package/dist/src/livecli.js +288 -0
- package/dist/src/livecli.js.map +1 -0
- package/dist/src/loginhint.js +42 -0
- package/dist/src/loginhint.js.map +1 -0
- package/dist/src/loginrefresh/atomic.js +66 -0
- package/dist/src/loginrefresh/atomic.js.map +1 -0
- package/dist/src/loginrefresh/lock.js +93 -0
- package/dist/src/loginrefresh/lock.js.map +1 -0
- package/dist/src/loginrefresh/main.js +58 -0
- package/dist/src/loginrefresh/main.js.map +1 -0
- package/dist/src/loginrefresh/pirefresh.js +49 -0
- package/dist/src/loginrefresh/pirefresh.js.map +1 -0
- package/dist/src/loginrefresh/refresher.js +286 -0
- package/dist/src/loginrefresh/refresher.js.map +1 -0
- package/dist/src/loginrefresh/supervisor.js +253 -0
- package/dist/src/loginrefresh/supervisor.js.map +1 -0
- package/dist/src/loginrefresh/types.js +4 -0
- package/dist/src/loginrefresh/types.js.map +1 -0
- package/dist/src/memory/context.js +251 -0
- package/dist/src/memory/context.js.map +1 -0
- package/dist/src/memory/datafence.js +5 -0
- package/dist/src/memory/datafence.js.map +1 -0
- package/dist/src/memory/delivery.js +131 -0
- package/dist/src/memory/delivery.js.map +1 -0
- package/dist/src/memory/eventtags.js +81 -0
- package/dist/src/memory/eventtags.js.map +1 -0
- package/dist/src/memory/health.js +713 -0
- package/dist/src/memory/health.js.map +1 -0
- package/dist/src/memory/import.js +504 -0
- package/dist/src/memory/import.js.map +1 -0
- package/dist/src/memory/layout.js +252 -0
- package/dist/src/memory/layout.js.map +1 -0
- package/dist/src/memory/legacy-file.js +100 -0
- package/dist/src/memory/legacy-file.js.map +1 -0
- package/dist/src/memory/lessons.js +370 -0
- package/dist/src/memory/lessons.js.map +1 -0
- package/dist/src/memory/ovsession.js +449 -0
- package/dist/src/memory/ovsession.js.map +1 -0
- package/dist/src/memory/relayout.js +259 -0
- package/dist/src/memory/relayout.js.map +1 -0
- package/dist/src/memory/roundactivity.js +180 -0
- package/dist/src/memory/roundactivity.js.map +1 -0
- package/dist/src/memory/selection.js +690 -0
- package/dist/src/memory/selection.js.map +1 -0
- package/dist/src/memory/shadowstatus.js +69 -0
- package/dist/src/memory/shadowstatus.js.map +1 -0
- package/dist/src/memory/store.js +83 -0
- package/dist/src/memory/store.js.map +1 -0
- package/dist/src/memory/textidentity.js +8 -0
- package/dist/src/memory/textidentity.js.map +1 -0
- package/dist/src/metrics.js +773 -0
- package/dist/src/metrics.js.map +1 -0
- package/dist/src/missingconfig.js +8 -0
- package/dist/src/missingconfig.js.map +1 -0
- package/dist/src/npmverbs.js +226 -0
- package/dist/src/npmverbs.js.map +1 -0
- package/dist/src/onboard/choices.js +191 -0
- package/dist/src/onboard/choices.js.map +1 -0
- package/dist/src/onboard/engine.js +344 -0
- package/dist/src/onboard/engine.js.map +1 -0
- package/dist/src/onboard/launch.js +83 -0
- package/dist/src/onboard/launch.js.map +1 -0
- package/dist/src/onboard/steps/appslug.js +211 -0
- package/dist/src/onboard/steps/appslug.js.map +1 -0
- package/dist/src/onboard/steps/bringup.js +408 -0
- package/dist/src/onboard/steps/bringup.js.map +1 -0
- package/dist/src/onboard/steps/compose.js +343 -0
- package/dist/src/onboard/steps/compose.js.map +1 -0
- package/dist/src/onboard/steps/configdelivery.js +50 -0
- package/dist/src/onboard/steps/configdelivery.js.map +1 -0
- package/dist/src/onboard/steps/configfill.js +1148 -0
- package/dist/src/onboard/steps/configfill.js.map +1 -0
- package/dist/src/onboard/steps/deploysh.js +80 -0
- package/dist/src/onboard/steps/deploysh.js.map +1 -0
- package/dist/src/onboard/steps/embeddings.js +472 -0
- package/dist/src/onboard/steps/embeddings.js.map +1 -0
- package/dist/src/onboard/steps/escalation.js +571 -0
- package/dist/src/onboard/steps/escalation.js.map +1 -0
- package/dist/src/onboard/steps/githubapp.js +1092 -0
- package/dist/src/onboard/steps/githubapp.js.map +1 -0
- package/dist/src/onboard/steps/golive.js +506 -0
- package/dist/src/onboard/steps/golive.js.map +1 -0
- package/dist/src/onboard/steps/index.js +67 -0
- package/dist/src/onboard/steps/index.js.map +1 -0
- package/dist/src/onboard/steps/ingress.js +762 -0
- package/dist/src/onboard/steps/ingress.js.map +1 -0
- package/dist/src/onboard/steps/inventory.js +420 -0
- package/dist/src/onboard/steps/inventory.js.map +1 -0
- package/dist/src/onboard/steps/llmauth.js +893 -0
- package/dist/src/onboard/steps/llmauth.js.map +1 -0
- package/dist/src/onboard/steps/memory.js +1060 -0
- package/dist/src/onboard/steps/memory.js.map +1 -0
- package/dist/src/onboard/steps/memorykeys.js +43 -0
- package/dist/src/onboard/steps/memorykeys.js.map +1 -0
- package/dist/src/onboard/steps/preflight.js +172 -0
- package/dist/src/onboard/steps/preflight.js.map +1 -0
- package/dist/src/onboard/steps/proof.js +502 -0
- package/dist/src/onboard/steps/proof.js.map +1 -0
- package/dist/src/onboard/steps/ssh.js +300 -0
- package/dist/src/onboard/steps/ssh.js.map +1 -0
- package/dist/src/onboard/steps/topology.js +291 -0
- package/dist/src/onboard/steps/topology.js.map +1 -0
- package/dist/src/onboard/steps/transport.js +511 -0
- package/dist/src/onboard/steps/transport.js.map +1 -0
- package/dist/src/onboard/steps/verifyup.js +362 -0
- package/dist/src/onboard/steps/verifyup.js.map +1 -0
- package/dist/src/onboard/steps/vmdeploy.js +368 -0
- package/dist/src/onboard/steps/vmdeploy.js.map +1 -0
- package/dist/src/onboard/toml.js +53 -0
- package/dist/src/onboard/toml.js.map +1 -0
- package/dist/src/opencli.js +508 -0
- package/dist/src/opencli.js.map +1 -0
- package/dist/src/openpr.js +457 -0
- package/dist/src/openpr.js.map +1 -0
- package/dist/src/openviking/client.js +821 -0
- package/dist/src/openviking/client.js.map +1 -0
- package/dist/src/openviking/context.js +218 -0
- package/dist/src/openviking/context.js.map +1 -0
- package/dist/src/openviking/embedder.js +457 -0
- package/dist/src/openviking/embedder.js.map +1 -0
- package/dist/src/openviking/gapsink.js +38 -0
- package/dist/src/openviking/gapsink.js.map +1 -0
- package/dist/src/openviking/health.js +69 -0
- package/dist/src/openviking/health.js.map +1 -0
- package/dist/src/openviking/index.js +24 -0
- package/dist/src/openviking/index.js.map +1 -0
- package/dist/src/openviking/lessonfind.js +54 -0
- package/dist/src/openviking/lessonfind.js.map +1 -0
- package/dist/src/openviking/probe.js +89 -0
- package/dist/src/openviking/probe.js.map +1 -0
- package/dist/src/openviking/proc.js +95 -0
- package/dist/src/openviking/proc.js.map +1 -0
- package/dist/src/openviking/provisionreport.js +144 -0
- package/dist/src/openviking/provisionreport.js.map +1 -0
- package/dist/src/openviking/reembed.js +317 -0
- package/dist/src/openviking/reembed.js.map +1 -0
- package/dist/src/openviking/relayout.js +153 -0
- package/dist/src/openviking/relayout.js.map +1 -0
- package/dist/src/openviking/sidecar-main.js +39 -0
- package/dist/src/openviking/sidecar-main.js.map +1 -0
- package/dist/src/openviking/sidecar.js +79 -0
- package/dist/src/openviking/sidecar.js.map +1 -0
- package/dist/src/openviking/store.js +1758 -0
- package/dist/src/openviking/store.js.map +1 -0
- package/dist/src/openviking/supervisor.js +422 -0
- package/dist/src/openviking/supervisor.js.map +1 -0
- package/dist/src/openviking/types.js +5 -0
- package/dist/src/openviking/types.js.map +1 -0
- package/dist/src/operator-recovery.js +172 -0
- package/dist/src/operator-recovery.js.map +1 -0
- package/dist/src/operatormessage.js +44 -0
- package/dist/src/operatormessage.js.map +1 -0
- package/dist/src/outcomes.js +1361 -0
- package/dist/src/outcomes.js.map +1 -0
- package/dist/src/piauthcopy.js +5 -0
- package/dist/src/piauthcopy.js.map +1 -0
- package/dist/src/picli.js +45 -0
- package/dist/src/picli.js.map +1 -0
- package/dist/src/picwd.js +25 -0
- package/dist/src/picwd.js.map +1 -0
- package/dist/src/pilogin.js +35 -0
- package/dist/src/pilogin.js.map +1 -0
- package/dist/src/pingpong.js +1028 -0
- package/dist/src/pingpong.js.map +1 -0
- package/dist/src/posting-journal.js +441 -0
- package/dist/src/posting-journal.js.map +1 -0
- package/dist/src/posting-outbox.js +662 -0
- package/dist/src/posting-outbox.js.map +1 -0
- package/dist/src/posting-receipt.js +203 -0
- package/dist/src/posting-receipt.js.map +1 -0
- package/dist/src/posting.js +348 -0
- package/dist/src/posting.js.map +1 -0
- package/dist/src/postops.js +637 -0
- package/dist/src/postops.js.map +1 -0
- package/dist/src/pr-activity.js +11 -0
- package/dist/src/pr-activity.js.map +1 -0
- package/dist/src/pr-author.js +5 -0
- package/dist/src/pr-author.js.map +1 -0
- package/dist/src/pr-conversation.js +355 -0
- package/dist/src/pr-conversation.js.map +1 -0
- package/dist/src/pr-session-lifecycle.js +542 -0
- package/dist/src/pr-session-lifecycle.js.map +1 -0
- package/dist/src/preflight.js +341 -0
- package/dist/src/preflight.js.map +1 -0
- package/dist/src/priorresolve.js +275 -0
- package/dist/src/priorresolve.js.map +1 -0
- package/dist/src/qcrbin.js +180 -0
- package/dist/src/qcrbin.js.map +1 -0
- package/dist/src/recheck.js +1398 -0
- package/dist/src/recheck.js.map +1 -0
- package/dist/src/reconcile.js +820 -0
- package/dist/src/reconcile.js.map +1 -0
- package/dist/src/recordedfindings.js +15 -0
- package/dist/src/recordedfindings.js.map +1 -0
- package/dist/src/regression.js +546 -0
- package/dist/src/regression.js.map +1 -0
- package/dist/src/replay.js +598 -0
- package/dist/src/replay.js.map +1 -0
- package/dist/src/requiredpasses.js +113 -0
- package/dist/src/requiredpasses.js.map +1 -0
- package/dist/src/resumecheck.js +168 -0
- package/dist/src/resumecheck.js.map +1 -0
- package/dist/src/reviewcli.js +671 -0
- package/dist/src/reviewcli.js.map +1 -0
- package/dist/src/reviewcontext.js +22 -0
- package/dist/src/reviewcontext.js.map +1 -0
- package/dist/src/reviewdetach.js +574 -0
- package/dist/src/reviewdetach.js.map +1 -0
- package/dist/src/reviewhistory.js +284 -0
- package/dist/src/reviewhistory.js.map +1 -0
- package/dist/src/reviewhome.js +26 -0
- package/dist/src/reviewhome.js.map +1 -0
- package/dist/src/reviewmemory.js +110 -0
- package/dist/src/reviewmemory.js.map +1 -0
- package/dist/src/reviewrendercli.js +104 -0
- package/dist/src/reviewrendercli.js.map +1 -0
- package/dist/src/round-work.js +20 -0
- package/dist/src/round-work.js.map +1 -0
- package/dist/src/runhost.js +72 -0
- package/dist/src/runhost.js.map +1 -0
- package/dist/src/scanauth.js +25 -0
- package/dist/src/scanauth.js.map +1 -0
- package/dist/src/scanbox.js +846 -0
- package/dist/src/scanbox.js.map +1 -0
- package/dist/src/scancli.js +1074 -0
- package/dist/src/scancli.js.map +1 -0
- package/dist/src/scandriverlog.js +30 -0
- package/dist/src/scandriverlog.js.map +1 -0
- package/dist/src/session-bridge.js +75 -0
- package/dist/src/session-bridge.js.map +1 -0
- package/dist/src/session-command-payload.js +29 -0
- package/dist/src/session-command-payload.js.map +1 -0
- package/dist/src/session-command-supervisor.js +939 -0
- package/dist/src/session-command-supervisor.js.map +1 -0
- package/dist/src/sessionjsonl.js +85 -0
- package/dist/src/sessionjsonl.js.map +1 -0
- package/dist/src/sessions.js +541 -0
- package/dist/src/sessions.js.map +1 -0
- package/dist/src/setup/adapters.js +330 -0
- package/dist/src/setup/adapters.js.map +1 -0
- package/dist/src/setup/baselines.js +64 -0
- package/dist/src/setup/baselines.js.map +1 -0
- package/dist/src/setup/caserunner.js +554 -0
- package/dist/src/setup/caserunner.js.map +1 -0
- package/dist/src/setup/configcommit.js +358 -0
- package/dist/src/setup/configcommit.js.map +1 -0
- package/dist/src/setup/engine.js +672 -0
- package/dist/src/setup/engine.js.map +1 -0
- package/dist/src/setup/facts.js +593 -0
- package/dist/src/setup/facts.js.map +1 -0
- package/dist/src/setup/findings.js +948 -0
- package/dist/src/setup/findings.js.map +1 -0
- package/dist/src/setup/history.js +338 -0
- package/dist/src/setup/history.js.map +1 -0
- package/dist/src/setup/interview.js +185 -0
- package/dist/src/setup/interview.js.map +1 -0
- package/dist/src/setup/ledger.js +207 -0
- package/dist/src/setup/ledger.js.map +1 -0
- package/dist/src/setup/pins.js +43 -0
- package/dist/src/setup/pins.js.map +1 -0
- package/dist/src/setup/portrait.js +432 -0
- package/dist/src/setup/portrait.js.map +1 -0
- package/dist/src/setup/presentation.js +9 -0
- package/dist/src/setup/presentation.js.map +1 -0
- package/dist/src/setup/profile.js +417 -0
- package/dist/src/setup/profile.js.map +1 -0
- package/dist/src/setup/replay.js +240 -0
- package/dist/src/setup/replay.js.map +1 -0
- package/dist/src/setup/replaybuild.js +311 -0
- package/dist/src/setup/replaybuild.js.map +1 -0
- package/dist/src/setup/replayrun.js +806 -0
- package/dist/src/setup/replayrun.js.map +1 -0
- package/dist/src/setup/replaysource.js +169 -0
- package/dist/src/setup/replaysource.js.map +1 -0
- package/dist/src/setup/retune.js +333 -0
- package/dist/src/setup/retune.js.map +1 -0
- package/dist/src/setup/retunelog.js +51 -0
- package/dist/src/setup/retunelog.js.map +1 -0
- package/dist/src/setup/scan.js +725 -0
- package/dist/src/setup/scan.js.map +1 -0
- package/dist/src/setup/scanbuild.js +133 -0
- package/dist/src/setup/scanbuild.js.map +1 -0
- package/dist/src/setup/scanfacts.js +96 -0
- package/dist/src/setup/scanfacts.js.map +1 -0
- package/dist/src/setup/scanlease.js +251 -0
- package/dist/src/setup/scanlease.js.map +1 -0
- package/dist/src/setup/scanreport.js +289 -0
- package/dist/src/setup/scanreport.js.map +1 -0
- package/dist/src/setup/scanrun.js +1733 -0
- package/dist/src/setup/scanrun.js.map +1 -0
- package/dist/src/setup/scantree.js +257 -0
- package/dist/src/setup/scantree.js.map +1 -0
- package/dist/src/setup/stage2.js +617 -0
- package/dist/src/setup/stage2.js.map +1 -0
- package/dist/src/setup/stage2request.js +259 -0
- package/dist/src/setup/stage2request.js.map +1 -0
- package/dist/src/setup/survey.js +307 -0
- package/dist/src/setup/survey.js.map +1 -0
- package/dist/src/setup/surveyrun.js +549 -0
- package/dist/src/setup/surveyrun.js.map +1 -0
- package/dist/src/setup/textsniff.js +89 -0
- package/dist/src/setup/textsniff.js.map +1 -0
- package/dist/src/setup/trackersink.js +395 -0
- package/dist/src/setup/trackersink.js.map +1 -0
- package/dist/src/setup/triage.js +636 -0
- package/dist/src/setup/triage.js.map +1 -0
- package/dist/src/setup/tuning.js +585 -0
- package/dist/src/setup/tuning.js.map +1 -0
- package/dist/src/setup/unitcut.js +437 -0
- package/dist/src/setup/unitcut.js.map +1 -0
- package/dist/src/singleflight.js +72 -0
- package/dist/src/singleflight.js.map +1 -0
- package/dist/src/slack/announce.js +725 -0
- package/dist/src/slack/announce.js.map +1 -0
- package/dist/src/slack/api.js +227 -0
- package/dist/src/slack/api.js.map +1 -0
- package/dist/src/slack/messages.js +158 -0
- package/dist/src/slack/messages.js.map +1 -0
- package/dist/src/slack/origin.js +550 -0
- package/dist/src/slack/origin.js.map +1 -0
- package/dist/src/slack/policy.js +225 -0
- package/dist/src/slack/policy.js.map +1 -0
- package/dist/src/slack/reply.js +153 -0
- package/dist/src/slack/reply.js.map +1 -0
- package/dist/src/slack/router.js +189 -0
- package/dist/src/slack/router.js.map +1 -0
- package/dist/src/slack/socket.js +323 -0
- package/dist/src/slack/socket.js.map +1 -0
- package/dist/src/slack/types.js +11 -0
- package/dist/src/slack/types.js.map +1 -0
- package/dist/src/sqlitehint.js +30 -0
- package/dist/src/sqlitehint.js.map +1 -0
- package/dist/src/ssh-observation.js +25 -0
- package/dist/src/ssh-observation.js.map +1 -0
- package/dist/src/state.js +5869 -0
- package/dist/src/state.js.map +1 -0
- package/dist/src/statemachine.js +220 -0
- package/dist/src/statemachine.js.map +1 -0
- package/dist/src/stubs.js +52 -0
- package/dist/src/stubs.js.map +1 -0
- package/dist/src/takeover.js +506 -0
- package/dist/src/takeover.js.map +1 -0
- package/dist/src/takeoverpi.js +249 -0
- package/dist/src/takeoverpi.js.map +1 -0
- package/dist/src/terminal-failure.js +26 -0
- package/dist/src/terminal-failure.js.map +1 -0
- package/dist/src/thread-resolution-refusal.js +4 -0
- package/dist/src/thread-resolution-refusal.js.map +1 -0
- package/dist/src/tmux.js +126 -0
- package/dist/src/tmux.js.map +1 -0
- package/dist/src/trackerid.js +43 -0
- package/dist/src/trackerid.js.map +1 -0
- package/dist/src/transcriptindex.js +512 -0
- package/dist/src/transcriptindex.js.map +1 -0
- package/dist/src/tui/actions.js +404 -0
- package/dist/src/tui/actions.js.map +1 -0
- package/dist/src/tui/alerts.js +523 -0
- package/dist/src/tui/alerts.js.map +1 -0
- package/dist/src/tui/app.js +1835 -0
- package/dist/src/tui/app.js.map +1 -0
- package/dist/src/tui/archiveops.js +164 -0
- package/dist/src/tui/archiveops.js.map +1 -0
- package/dist/src/tui/autofile.js +191 -0
- package/dist/src/tui/autofile.js.map +1 -0
- package/dist/src/tui/board.js +1143 -0
- package/dist/src/tui/board.js.map +1 -0
- package/dist/src/tui/candidates.js +699 -0
- package/dist/src/tui/candidates.js.map +1 -0
- package/dist/src/tui/cellwidth.js +151 -0
- package/dist/src/tui/cellwidth.js.map +1 -0
- package/dist/src/tui/changelog-store.js +404 -0
- package/dist/src/tui/changelog-store.js.map +1 -0
- package/dist/src/tui/chord.js +26 -0
- package/dist/src/tui/chord.js.map +1 -0
- package/dist/src/tui/clipboard.js +81 -0
- package/dist/src/tui/clipboard.js.map +1 -0
- package/dist/src/tui/cmdbar.js +253 -0
- package/dist/src/tui/cmdbar.js.map +1 -0
- package/dist/src/tui/companion.js +1275 -0
- package/dist/src/tui/companion.js.map +1 -0
- package/dist/src/tui/companionplan.js +200 -0
- package/dist/src/tui/companionplan.js.map +1 -0
- package/dist/src/tui/companiontab.js +61 -0
- package/dist/src/tui/companiontab.js.map +1 -0
- package/dist/src/tui/confighome.js +105 -0
- package/dist/src/tui/confighome.js.map +1 -0
- package/dist/src/tui/connect.js +44 -0
- package/dist/src/tui/connect.js.map +1 -0
- package/dist/src/tui/deploy-ceremony.js +75 -0
- package/dist/src/tui/deploy-ceremony.js.map +1 -0
- package/dist/src/tui/deploy-drain.js +93 -0
- package/dist/src/tui/deploy-drain.js.map +1 -0
- package/dist/src/tui/deploy-process.js +234 -0
- package/dist/src/tui/deploy-process.js.map +1 -0
- package/dist/src/tui/deploytab.js +1263 -0
- package/dist/src/tui/deploytab.js.map +1 -0
- package/dist/src/tui/design.js +111 -0
- package/dist/src/tui/design.js.map +1 -0
- package/dist/src/tui/digesttab.js +148 -0
- package/dist/src/tui/digesttab.js.map +1 -0
- package/dist/src/tui/emptystate.js +32 -0
- package/dist/src/tui/emptystate.js.map +1 -0
- package/dist/src/tui/evidence.js +790 -0
- package/dist/src/tui/evidence.js.map +1 -0
- package/dist/src/tui/feed.js +415 -0
- package/dist/src/tui/feed.js.map +1 -0
- package/dist/src/tui/feedbacktab.js +907 -0
- package/dist/src/tui/feedbacktab.js.map +1 -0
- package/dist/src/tui/filters.js +72 -0
- package/dist/src/tui/filters.js.map +1 -0
- package/dist/src/tui/ghread.js +272 -0
- package/dist/src/tui/ghread.js.map +1 -0
- package/dist/src/tui/historypull.js +61 -0
- package/dist/src/tui/historypull.js.map +1 -0
- package/dist/src/tui/inboxpull.js +197 -0
- package/dist/src/tui/inboxpull.js.map +1 -0
- package/dist/src/tui/index.js +1425 -0
- package/dist/src/tui/index.js.map +1 -0
- package/dist/src/tui/issuesink.js +206 -0
- package/dist/src/tui/issuesink.js.map +1 -0
- package/dist/src/tui/jsonfile.js +32 -0
- package/dist/src/tui/jsonfile.js.map +1 -0
- package/dist/src/tui/keys.js +97 -0
- package/dist/src/tui/keys.js.map +1 -0
- package/dist/src/tui/layout.js +70 -0
- package/dist/src/tui/layout.js.map +1 -0
- package/dist/src/tui/learning.js +469 -0
- package/dist/src/tui/learning.js.map +1 -0
- package/dist/src/tui/lessonswitch.js +103 -0
- package/dist/src/tui/lessonswitch.js.map +1 -0
- package/dist/src/tui/livecontrol.js +167 -0
- package/dist/src/tui/livecontrol.js.map +1 -0
- package/dist/src/tui/memforward.js +310 -0
- package/dist/src/tui/memforward.js.map +1 -0
- package/dist/src/tui/memincident.js +287 -0
- package/dist/src/tui/memincident.js.map +1 -0
- package/dist/src/tui/memory.js +485 -0
- package/dist/src/tui/memory.js.map +1 -0
- package/dist/src/tui/memtab.js +513 -0
- package/dist/src/tui/memtab.js.map +1 -0
- package/dist/src/tui/metricstab.js +979 -0
- package/dist/src/tui/metricstab.js.map +1 -0
- package/dist/src/tui/model.js +104 -0
- package/dist/src/tui/model.js.map +1 -0
- package/dist/src/tui/mouse.js +83 -0
- package/dist/src/tui/mouse.js.map +1 -0
- package/dist/src/tui/msg.js +196 -0
- package/dist/src/tui/msg.js.map +1 -0
- package/dist/src/tui/nativecompanion.js +321 -0
- package/dist/src/tui/nativecompanion.js.map +1 -0
- package/dist/src/tui/openurl.js +17 -0
- package/dist/src/tui/openurl.js.map +1 -0
- package/dist/src/tui/paint.js +501 -0
- package/dist/src/tui/paint.js.map +1 -0
- package/dist/src/tui/panes.js +74 -0
- package/dist/src/tui/panes.js.map +1 -0
- package/dist/src/tui/pipane.js +166 -0
- package/dist/src/tui/pipane.js.map +1 -0
- package/dist/src/tui/poll-hooks.js +11 -0
- package/dist/src/tui/poll-hooks.js.map +1 -0
- package/dist/src/tui/poll.js +113 -0
- package/dist/src/tui/poll.js.map +1 -0
- package/dist/src/tui/proposals.js +715 -0
- package/dist/src/tui/proposals.js.map +1 -0
- package/dist/src/tui/prpause.js +132 -0
- package/dist/src/tui/prpause.js.map +1 -0
- package/dist/src/tui/reload.js +351 -0
- package/dist/src/tui/reload.js.map +1 -0
- package/dist/src/tui/remote.js +312 -0
- package/dist/src/tui/remote.js.map +1 -0
- package/dist/src/tui/rollover.js +767 -0
- package/dist/src/tui/rollover.js.map +1 -0
- package/dist/src/tui/scantab.js +1053 -0
- package/dist/src/tui/scantab.js.map +1 -0
- package/dist/src/tui/setupentry.js +81 -0
- package/dist/src/tui/setupentry.js.map +1 -0
- package/dist/src/tui/sidebar.js +602 -0
- package/dist/src/tui/sidebar.js.map +1 -0
- package/dist/src/tui/sshtriage.js +393 -0
- package/dist/src/tui/sshtriage.js.map +1 -0
- package/dist/src/tui/startup.js +172 -0
- package/dist/src/tui/startup.js.map +1 -0
- package/dist/src/tui/tabs.js +41 -0
- package/dist/src/tui/tabs.js.map +1 -0
- package/dist/src/tui/termmirror.js +217 -0
- package/dist/src/tui/termmirror.js.map +1 -0
- package/dist/src/tui/theme.js +95 -0
- package/dist/src/tui/theme.js.map +1 -0
- package/dist/src/tui/ticketstab.js +224 -0
- package/dist/src/tui/ticketstab.js.map +1 -0
- package/dist/src/tui/tmuxsocket.js +101 -0
- package/dist/src/tui/tmuxsocket.js.map +1 -0
- package/dist/src/tui/trackertickets.js +77 -0
- package/dist/src/tui/trackertickets.js.map +1 -0
- package/dist/src/tui/unwedge.js +245 -0
- package/dist/src/tui/unwedge.js.map +1 -0
- package/dist/src/tui/viewer.js +479 -0
- package/dist/src/tui/viewer.js.map +1 -0
- package/dist/src/tui/viewerinput.js +125 -0
- package/dist/src/tui/viewerinput.js.map +1 -0
- package/dist/src/updatenotice.js +143 -0
- package/dist/src/updatenotice.js.map +1 -0
- package/dist/src/usage.js +1007 -0
- package/dist/src/usage.js.map +1 -0
- package/dist/src/verdicts.js +3848 -0
- package/dist/src/verdicts.js.map +1 -0
- package/dist/src/version.js +58 -0
- package/dist/src/version.js.map +1 -0
- package/dist/src/wakes.js +411 -0
- package/dist/src/wakes.js.map +1 -0
- package/dist/src/webhook.js +725 -0
- package/dist/src/webhook.js.map +1 -0
- package/dist/src/webhookheal.js +238 -0
- package/dist/src/webhookheal.js.map +1 -0
- package/dist/src/wire.js +1801 -0
- package/dist/src/wire.js.map +1 -0
- package/dist/src/worker-session-host.js +531 -0
- package/dist/src/worker-session-host.js.map +1 -0
- package/dist/src/workerbreaker.js +112 -0
- package/dist/src/workerbreaker.js.map +1 -0
- package/dist/src/workercontrol.js +49 -0
- package/dist/src/workercontrol.js.map +1 -0
- package/dist/src/workerlease.js +351 -0
- package/dist/src/workerlease.js.map +1 -0
- package/dist/src/workerliveness.js +84 -0
- package/dist/src/workerliveness.js.map +1 -0
- package/dist/src/workerlog.js +65 -0
- package/dist/src/workerlog.js.map +1 -0
- package/dist/src/workerruntime.js +10 -0
- package/dist/src/workerruntime.js.map +1 -0
- package/dist/src/workersupervisor.js +1206 -0
- package/dist/src/workersupervisor.js.map +1 -0
- package/dist/src/workerusage.js +23 -0
- package/dist/src/workerusage.js.map +1 -0
- package/dist/src/workerworld.js +332 -0
- package/dist/src/workerworld.js.map +1 -0
- package/dist/src/worktrees.js +984 -0
- package/dist/src/worktrees.js.map +1 -0
- package/docker/Dockerfile +284 -0
- package/docker/Dockerfile.dockerignore +35 -0
- package/docker/README.md +221 -0
- package/docker/companion/Dockerfile +139 -0
- package/docker/companion/agents/evidence.md +34 -0
- package/docker/companion/agents/explore.md +26 -0
- package/docker/companion/agents/pass-survey.md +49 -0
- package/docker/companion/agents/study.md +26 -0
- package/docker/companion/entrypoint.sh +343 -0
- package/docker/companion/memory-health.mjs +269 -0
- package/docker/entrypoint.sh +782 -0
- package/docker/memory/Dockerfile +191 -0
- package/docker/memory/Dockerfile.dockerignore +20 -0
- package/docker/memory/README.md +60 -0
- package/docker/memory/entrypoint.sh +163 -0
- package/docker/setid/qcr-setid.c +109 -0
- package/docker/tunnel/Dockerfile.cloudflared +51 -0
- package/docker/tunnel/Dockerfile.ngrok +40 -0
- package/docker/tunnel/sidecar.mjs +411 -0
- package/docker/vm/tmux.conf +26 -0
- package/docker-compose.yml +348 -0
- package/extensions/cli/bash-guard-ambient.ts +71 -0
- package/extensions/cli/bash-guard.ts +12 -0
- package/extensions/cli/companion-guard.ts +8 -0
- package/extensions/cli/debug-round.ts +9 -0
- package/extensions/cli/inject-bridge.ts +8 -0
- package/extensions/cli/ledger-pin.ts +13 -0
- package/extensions/cli/lens.ts +22 -0
- package/extensions/cli/muted-theme.ts +9 -0
- package/extensions/cli/onboarding.ts +8 -0
- package/extensions/cli/setup.ts +8 -0
- package/extensions/cli/tui-view.ts +8 -0
- package/extensions/companion-guard.ts +411 -0
- package/extensions/debug-round.ts +281 -0
- package/extensions/inject-bridge.ts +630 -0
- package/extensions/inline-bash.ts +102 -0
- package/extensions/muted-theme.json +68 -0
- package/extensions/muted-theme.ts +23 -0
- package/extensions/onboarding.ts +233 -0
- package/extensions/setup-replay.ts +336 -0
- package/extensions/setup-scan.ts +1236 -0
- package/extensions/setup-triage.ts +190 -0
- package/extensions/setup.ts +1428 -0
- package/extensions/tui-view.ts +489 -0
- package/extensions/vendor/openviking-pi/LICENSE +201 -0
- package/extensions/vendor/openviking-pi/NOTICE +68 -0
- package/extensions/vendor/openviking-pi/README.md +550 -0
- package/extensions/vendor/openviking-pi/client.ts +588 -0
- package/extensions/vendor/openviking-pi/config.json +28 -0
- package/extensions/vendor/openviking-pi/config.ts +271 -0
- package/extensions/vendor/openviking-pi/index.ts +523 -0
- package/extensions/vendor/openviking-pi/lib/capture-adapter.d.mts +10 -0
- package/extensions/vendor/openviking-pi/lib/capture-adapter.mjs +88 -0
- package/extensions/vendor/openviking-pi/lib/takeover-core.d.mts +80 -0
- package/extensions/vendor/openviking-pi/lib/takeover-core.mjs +406 -0
- package/extensions/vendor/openviking-pi/lib/uri-guard-adapter.d.mts +1 -0
- package/extensions/vendor/openviking-pi/lib/uri-guard-adapter.mjs +42 -0
- package/extensions/vendor/openviking-pi/patches/0000-vendoring-and-build-conformance.patch +726 -0
- package/extensions/vendor/openviking-pi/patches/0001-remove-session-before-compact-handler.patch +101 -0
- package/extensions/vendor/openviking-pi/patches/0002-no-ambient-work-in-subagent-leaves.patch +202 -0
- package/extensions/vendor/openviking-pi/patches/0003-never-auto-create-openviking-sessions.patch +120 -0
- package/extensions/vendor/openviking-pi/patches/0004-viking-write-durable-batch-write.patch +521 -0
- package/extensions/vendor/openviking-pi/patches/0005-second-memory-namespace.patch +786 -0
- package/extensions/vendor/openviking-pi/patches/0006-writes-that-stick-and-honest-errors.patch +996 -0
- package/extensions/vendor/openviking-pi/patches/0007-bounded-recall-no-commit.patch +794 -0
- package/extensions/vendor/openviking-pi/patches/0008-pr-derived-recall.patch +379 -0
- package/extensions/vendor/openviking-pi/patches/0009-pr-session-tools-in-round-areas.patch +375 -0
- package/extensions/vendor/openviking-pi/patches/0010-typed-lessons.patch +448 -0
- package/extensions/vendor/openviking-pi/patches/0011-review-session-memory-boundary.patch +382 -0
- package/extensions/vendor/openviking-pi/patches/0012-typecheck-against-the-real-pi.patch +129 -0
- package/extensions/vendor/openviking-pi/patches/0013-createopenviking-factory.patch +1168 -0
- package/extensions/vendor/openviking-pi/qcr-hint.ts +313 -0
- package/extensions/vendor/openviking-pi/qcr-honest.ts +183 -0
- package/extensions/vendor/openviking-pi/qcr-hooks.ts +87 -0
- package/extensions/vendor/openviking-pi/qcr-leaf.ts +78 -0
- package/extensions/vendor/openviking-pi/qcr-lesson.ts +231 -0
- package/extensions/vendor/openviking-pi/qcr-namespaces.ts +198 -0
- package/extensions/vendor/openviking-pi/qcr-recall.ts +288 -0
- package/extensions/vendor/openviking-pi/qcr-shim.ts +142 -0
- package/extensions/vendor/openviking-pi/qcr-write.ts +427 -0
- package/extensions/vendor/openviking-pi/recall.ts +149 -0
- package/extensions/vendor/openviking-pi/shared/capture-utils.d.mts +9 -0
- package/extensions/vendor/openviking-pi/shared/capture-utils.mjs +515 -0
- package/extensions/vendor/openviking-pi/shared/credentials.d.mts +14 -0
- package/extensions/vendor/openviking-pi/shared/credentials.mjs +228 -0
- package/extensions/vendor/openviking-pi/shared/pending-queue.d.mts +19 -0
- package/extensions/vendor/openviking-pi/shared/pending-queue.mjs +458 -0
- package/extensions/vendor/openviking-pi/shared/profile-inject.d.mts +17 -0
- package/extensions/vendor/openviking-pi/shared/profile-inject.mjs +294 -0
- package/extensions/vendor/openviking-pi/shared/recall-compress-core.mjs +195 -0
- package/extensions/vendor/openviking-pi/shared/recall-core.d.mts +13 -0
- package/extensions/vendor/openviking-pi/shared/recall-core.mjs +609 -0
- package/extensions/vendor/openviking-pi/shared/retryable.mjs +9 -0
- package/extensions/vendor/openviking-pi/shared/session-model.d.mts +3 -0
- package/extensions/vendor/openviking-pi/shared/session-model.mjs +57 -0
- package/extensions/vendor/openviking-pi/shared/uri-guard.mjs +56 -0
- package/extensions/vendor/openviking-pi/shared/workspace-peer.d.mts +5 -0
- package/extensions/vendor/openviking-pi/shared/workspace-peer.mjs +16 -0
- package/extensions/vendor/openviking-pi/sync.ts +163 -0
- package/extensions/vendor/openviking-pi/takeover.ts +36 -0
- package/extensions/vendor/openviking-pi/tools.ts +479 -0
- package/extensions/vendor/openviking-pi/upstream.json +48 -0
- package/package.json +138 -4
- package/profiles/default/README.md +21 -0
- package/profiles/default/addenda/.gitkeep +0 -0
- package/profiles/default/profile.toml +17 -0
- package/profiles/example-django/README.md +21 -0
- package/profiles/example-django/addenda/10-migration-discipline.md +13 -0
- package/profiles/example-django/addenda/20-new-column-nullability.md +12 -0
- package/profiles/example-django/addenda/30-test-doctrine.md +12 -0
- package/profiles/example-django/addenda/40-orm-usage.md +19 -0
- package/profiles/example-django/addenda/50-declared-shape.md +10 -0
- package/profiles/example-django/addenda/60-config-placement.md +14 -0
- package/profiles/example-django/heuristics.toml +24 -0
- package/profiles/example-django/profile.toml +9 -0
- package/profiles/fast/README.md +24 -0
- package/profiles/fast/profile.toml +59 -0
- package/profiles/minimal/README.md +17 -0
- package/profiles/minimal/addenda/.gitkeep +0 -0
- package/profiles/minimal/profile.toml +8 -0
- package/profiles/snappy/README.md +24 -0
- package/profiles/snappy/addenda/10-race-as-question.md +6 -0
- package/profiles/snappy/profile.toml +46 -0
- package/prompts/companion-day-open.md +46 -0
- package/prompts/companion-first-run.md +104 -0
- package/prompts/companion.md +821 -0
- package/prompts/conversation-system.md +117 -0
- package/prompts/eod-sweep.md +120 -0
- package/prompts/onboarding.md +74 -0
- package/prompts/pass-common.md +52 -0
- package/prompts/pass-quality-common.md +22 -0
- package/prompts/round-first.md +874 -0
- package/prompts/round-follow-up.md +991 -0
- package/prompts/round-re-review.md +889 -0
- package/prompts/round-recheck.md +196 -0
- package/prompts/round-staleness.md +856 -0
- package/schemas/review-result.v1.json +391 -0
- package/schemas/scan-result.v1.json +421 -0
- package/schemas/version.v1.json +57 -0
- package/scripts/approve-pr.sh +16 -0
- package/scripts/build-stamp.mjs +67 -0
- package/scripts/commit-verdict.sh +155 -0
- package/scripts/companion-e2e.sh +250 -0
- package/scripts/companion-memory-forward.mjs +1966 -0
- package/scripts/config-fingerprint.sh +28 -0
- package/scripts/deploy-manifest.mjs +141 -0
- package/scripts/deploy.sh +1435 -0
- package/scripts/directory-lock.mjs +193 -0
- package/scripts/heartbeat-fresh.mjs +54 -0
- package/scripts/install-locator.sh +328 -0
- package/scripts/install-wrapper.sh +76 -0
- package/scripts/memory-search-check.mjs +98 -0
- package/scripts/mint-token.sh +212 -0
- package/scripts/openviking-embedder.py +392 -0
- package/scripts/openviking-ov-conf.mjs +161 -0
- package/scripts/openviking-provision.mjs +1122 -0
- package/scripts/openviking-reembed.mjs +1154 -0
- package/scripts/post-review.sh +16 -0
- package/scripts/pr-checks.sh +256 -0
- package/scripts/pr-context.py +174 -0
- package/scripts/pr-context.sh +63 -0
- package/scripts/pr-self.sh +144 -0
- package/scripts/pr-threads.sh +145 -0
- package/scripts/qcr +15 -0
- package/scripts/qcr-host.sh +769 -0
- package/scripts/render-review.sh +24 -0
- package/scripts/reply-thread.sh +16 -0
- package/scripts/required-passes.py +112 -0
- package/scripts/smoke-companion.sh +211 -0
- package/scripts/tui-deploy-runner.mjs +61 -0
- package/scripts/verify-dist.mjs +201 -0
- package/scripts/vm-ghcr-login.sh +188 -0
- package/seeds/agents/pass-baseline.md +61 -0
- package/seeds/agents/pass-contracts.md +30 -0
- package/seeds/agents/pass-data-safety.md +54 -0
- package/seeds/agents/pass-delivery.md +37 -0
- package/seeds/agents/pass-prod-risk.md +24 -0
- package/seeds/agents/pass-security.md +31 -0
- package/seeds/agents/pass-statelessness.md +27 -0
- package/seeds/agents/pass-ui.md +23 -0
- package/seeds/agents/pass-validate.md +50 -0
- package/seeds/prompts/conversation-system.md +117 -0
- package/seeds/prompts/eod-sweep.md +120 -0
- package/seeds/prompts/pass-common.md +52 -0
- package/skills/companion/adjudicate/SKILL.md +247 -0
- package/skills/companion/evidence/SKILL.md +150 -0
- package/skills/companion/land/SKILL.md +200 -0
- package/skills/companion/replay/SKILL.md +138 -0
- package/skills/companion/setup/SKILL.md +167 -0
- package/skills/companion/survey/SKILL.md +160 -0
- package/skills/selfimprove/README.md +34 -0
- package/skills/selfimprove/SKILL.md +127 -0
- package/skills/selfimprove/agents/openai.yaml +2 -0
- package/skills/selfimprove/commands/selfimprove.md +6 -0
- package/skills/selfimprove/references/fix-loop.md +49 -0
- package/skills/selfimprove/references/ledger.md +20 -0
- package/skills/selfimprove/references/setup.md +40 -0
- package/src/anomalies.ts +566 -0
- package/src/answerworker.ts +251 -0
- package/src/appstamp.ts +65 -0
- package/src/archive/admission.ts +3 -0
- package/src/archive/archivedb.ts +548 -0
- package/src/archive/archiver.ts +1963 -0
- package/src/archive/archivewrite.ts +279 -0
- package/src/archive/bundle.ts +333 -0
- package/src/archive/dbmove.ts +911 -0
- package/src/archive/duplicatecounts.ts +98 -0
- package/src/archive/eligibility.ts +181 -0
- package/src/archive/extract.ts +342 -0
- package/src/archive/fsutil.ts +415 -0
- package/src/archive/gzfile.ts +370 -0
- package/src/archive/kinds.ts +21 -0
- package/src/archive/livecheckpoint.ts +217 -0
- package/src/archive/liveretention.ts +180 -0
- package/src/archive/manifest.ts +519 -0
- package/src/archive/overseer.ts +336 -0
- package/src/archive/piglobal.ts +174 -0
- package/src/archive/plan.ts +612 -0
- package/src/archive/prarchive.ts +984 -0
- package/src/archive/prfiles.ts +406 -0
- package/src/archive/report.ts +252 -0
- package/src/archive/restore.ts +330 -0
- package/src/archive/rows.ts +187 -0
- package/src/archive/seal.ts +630 -0
- package/src/archive/status.ts +719 -0
- package/src/archive/storepass.ts +581 -0
- package/src/backfill.ts +362 -0
- package/src/bench/cost.ts +113 -0
- package/src/bench/exec.ts +90 -0
- package/src/bench/values.ts +13 -0
- package/src/botcontrol.ts +176 -0
- package/src/botworker.ts +953 -0
- package/src/capture.ts +321 -0
- package/src/cases.ts +1789 -0
- package/src/channelfs.ts +26 -0
- package/src/checklist.ts +506 -0
- package/src/ciretry.ts +141 -0
- package/src/cli.ts +2271 -0
- package/src/cliops.ts +384 -0
- package/src/clonedir.ts +15 -0
- package/src/commandeffects.ts +57 -0
- package/src/commands.ts +1678 -0
- package/src/companioninbox.ts +239 -0
- package/src/compare.ts +957 -0
- package/src/config.ts +2050 -0
- package/src/configcheck.ts +133 -0
- package/src/coreext.ts +47 -0
- package/src/daemon.ts +1302 -0
- package/src/debuground.ts +927 -0
- package/src/debugroundpi.ts +262 -0
- package/src/diagnostics.ts +14 -0
- package/src/digest.ts +183 -0
- package/src/dispatch.ts +2250 -0
- package/src/engine/answerguard.ts +27 -0
- package/src/engine/botsdk.ts +336 -0
- package/src/engine/caserunner-sdk.ts +423 -0
- package/src/engine/debugpi.ts +235 -0
- package/src/eod.ts +669 -0
- package/src/eodpinwatch.ts +556 -0
- package/src/escalation.ts +1253 -0
- package/src/eventloop.ts +1045 -0
- package/src/execboundary.ts +633 -0
- package/src/feedback.ts +1162 -0
- package/src/findingscheck.ts +298 -0
- package/src/fsdenied.ts +24 -0
- package/src/ghread.ts +242 -0
- package/src/github.ts +1497 -0
- package/src/homes.ts +175 -0
- package/src/hostroute.ts +53 -0
- package/src/improvements.ts +787 -0
- package/src/install.ts +1103 -0
- package/src/installkind.ts +50 -0
- package/src/ipc/capability.ts +333 -0
- package/src/ipc/client.ts +269 -0
- package/src/ipc/control.ts +90 -0
- package/src/ipc/ghreadshim.ts +94 -0
- package/src/ipc/launcher.ts +65 -0
- package/src/ipc/permits.ts +362 -0
- package/src/ipc/postshim.ts +164 -0
- package/src/ipc/protocol.ts +342 -0
- package/src/ipc/server.ts +371 -0
- package/src/ipc/workerspec.ts +218 -0
- package/src/learner/learner.ts +1655 -0
- package/src/learner/miner.ts +1290 -0
- package/src/learner/packet.ts +340 -0
- package/src/learner/receipt.ts +168 -0
- package/src/learner/status.ts +107 -0
- package/src/ledgercheck.ts +157 -0
- package/src/lessonledger.ts +703 -0
- package/src/lessonselect.ts +888 -0
- package/src/lessonshadow.ts +810 -0
- package/src/levers.ts +393 -0
- package/src/live/projection.ts +491 -0
- package/src/live/publish.ts +92 -0
- package/src/live/read.ts +86 -0
- package/src/livecli.ts +336 -0
- package/src/loginhint.ts +56 -0
- package/src/loginrefresh/atomic.ts +76 -0
- package/src/loginrefresh/lock.ts +104 -0
- package/src/loginrefresh/main.ts +59 -0
- package/src/loginrefresh/pirefresh.ts +57 -0
- package/src/loginrefresh/refresher.ts +326 -0
- package/src/loginrefresh/supervisor.ts +273 -0
- package/src/loginrefresh/types.ts +35 -0
- package/src/memory/context.ts +385 -0
- package/src/memory/datafence.ts +9 -0
- package/src/memory/delivery.ts +171 -0
- package/src/memory/eventtags.ts +80 -0
- package/src/memory/health.ts +953 -0
- package/src/memory/import.ts +664 -0
- package/src/memory/layout.ts +261 -0
- package/src/memory/legacy-file.ts +104 -0
- package/src/memory/lessons.ts +447 -0
- package/src/memory/ovsession.ts +601 -0
- package/src/memory/relayout.ts +335 -0
- package/src/memory/roundactivity.ts +209 -0
- package/src/memory/selection.ts +989 -0
- package/src/memory/shadowstatus.ts +125 -0
- package/src/memory/store.ts +275 -0
- package/src/memory/textidentity.ts +8 -0
- package/src/metrics.ts +1056 -0
- package/src/missingconfig.ts +7 -0
- package/src/npmverbs.ts +264 -0
- package/src/onboard/choices.ts +222 -0
- package/src/onboard/engine.ts +518 -0
- package/src/onboard/launch.ts +115 -0
- package/src/onboard/steps/appslug.ts +265 -0
- package/src/onboard/steps/bringup.ts +482 -0
- package/src/onboard/steps/compose.ts +459 -0
- package/src/onboard/steps/configdelivery.ts +58 -0
- package/src/onboard/steps/configfill.ts +1352 -0
- package/src/onboard/steps/deploysh.ts +105 -0
- package/src/onboard/steps/embeddings.ts +550 -0
- package/src/onboard/steps/escalation.ts +640 -0
- package/src/onboard/steps/githubapp.ts +1414 -0
- package/src/onboard/steps/golive.ts +577 -0
- package/src/onboard/steps/index.ts +71 -0
- package/src/onboard/steps/ingress.ts +907 -0
- package/src/onboard/steps/inventory.ts +526 -0
- package/src/onboard/steps/llmauth.ts +1095 -0
- package/src/onboard/steps/memory.ts +1290 -0
- package/src/onboard/steps/memorykeys.ts +85 -0
- package/src/onboard/steps/preflight.ts +191 -0
- package/src/onboard/steps/proof.ts +624 -0
- package/src/onboard/steps/ssh.ts +377 -0
- package/src/onboard/steps/topology.ts +337 -0
- package/src/onboard/steps/transport.ts +665 -0
- package/src/onboard/steps/verifyup.ts +448 -0
- package/src/onboard/steps/vmdeploy.ts +442 -0
- package/src/onboard/toml.ts +50 -0
- package/src/opencli.ts +591 -0
- package/src/openpr.ts +568 -0
- package/src/openviking/client.ts +1131 -0
- package/src/openviking/context.ts +263 -0
- package/src/openviking/embedder.ts +516 -0
- package/src/openviking/gapsink.ts +63 -0
- package/src/openviking/health.ts +83 -0
- package/src/openviking/index.ts +119 -0
- package/src/openviking/lessonfind.ts +92 -0
- package/src/openviking/probe.ts +129 -0
- package/src/openviking/proc.ts +110 -0
- package/src/openviking/provisionreport.ts +160 -0
- package/src/openviking/reembed.ts +412 -0
- package/src/openviking/relayout.ts +177 -0
- package/src/openviking/sidecar-main.ts +38 -0
- package/src/openviking/sidecar.ts +106 -0
- package/src/openviking/store.ts +2025 -0
- package/src/openviking/supervisor.ts +474 -0
- package/src/openviking/types.ts +174 -0
- package/src/operator-recovery.ts +237 -0
- package/src/operatormessage.ts +52 -0
- package/src/outcomes.ts +1670 -0
- package/src/piauthcopy.ts +12 -0
- package/src/picli.ts +49 -0
- package/src/picwd.ts +25 -0
- package/src/pilogin.ts +37 -0
- package/src/pingpong.ts +1339 -0
- package/src/posting-journal.ts +549 -0
- package/src/posting-outbox.ts +802 -0
- package/src/posting-receipt.ts +324 -0
- package/src/posting.ts +603 -0
- package/src/postops.ts +832 -0
- package/src/pr-activity.ts +20 -0
- package/src/pr-author.ts +6 -0
- package/src/pr-conversation.ts +432 -0
- package/src/pr-session-lifecycle.ts +631 -0
- package/src/preflight.ts +391 -0
- package/src/priorresolve.ts +343 -0
- package/src/qcrbin.ts +186 -0
- package/src/recheck.ts +1775 -0
- package/src/reconcile.ts +1030 -0
- package/src/recordedfindings.ts +27 -0
- package/src/regression.ts +833 -0
- package/src/replay.ts +816 -0
- package/src/requiredpasses.ts +148 -0
- package/src/resumecheck.ts +246 -0
- package/src/reviewcli.ts +787 -0
- package/src/reviewcontext.ts +35 -0
- package/src/reviewdetach.ts +734 -0
- package/src/reviewhistory.ts +427 -0
- package/src/reviewhome.ts +36 -0
- package/src/reviewmemory.ts +133 -0
- package/src/reviewrendercli.ts +139 -0
- package/src/round-work.ts +42 -0
- package/src/runhost.ts +108 -0
- package/src/scanauth.ts +36 -0
- package/src/scanbox.ts +1081 -0
- package/src/scancli.ts +1296 -0
- package/src/scandriverlog.ts +32 -0
- package/src/session-bridge.ts +139 -0
- package/src/session-command-payload.ts +35 -0
- package/src/session-command-supervisor.ts +1142 -0
- package/src/sessionjsonl.ts +88 -0
- package/src/sessions.ts +659 -0
- package/src/setup/adapters.ts +335 -0
- package/src/setup/baselines.ts +110 -0
- package/src/setup/caserunner.ts +611 -0
- package/src/setup/configcommit.ts +443 -0
- package/src/setup/engine.ts +1001 -0
- package/src/setup/facts.ts +627 -0
- package/src/setup/findings.ts +1264 -0
- package/src/setup/history.ts +443 -0
- package/src/setup/interview.ts +243 -0
- package/src/setup/ledger.ts +324 -0
- package/src/setup/pins.ts +66 -0
- package/src/setup/portrait.ts +635 -0
- package/src/setup/presentation.ts +18 -0
- package/src/setup/profile.ts +508 -0
- package/src/setup/replay.ts +370 -0
- package/src/setup/replaybuild.ts +392 -0
- package/src/setup/replayrun.ts +1096 -0
- package/src/setup/replaysource.ts +198 -0
- package/src/setup/retune.ts +415 -0
- package/src/setup/retunelog.ts +95 -0
- package/src/setup/scan.ts +1033 -0
- package/src/setup/scanbuild.ts +168 -0
- package/src/setup/scanfacts.ts +107 -0
- package/src/setup/scanlease.ts +297 -0
- package/src/setup/scanreport.ts +373 -0
- package/src/setup/scanrun.ts +2268 -0
- package/src/setup/scantree.ts +259 -0
- package/src/setup/stage2.ts +926 -0
- package/src/setup/stage2request.ts +347 -0
- package/src/setup/survey.ts +458 -0
- package/src/setup/surveyrun.ts +758 -0
- package/src/setup/textsniff.ts +95 -0
- package/src/setup/trackersink.ts +433 -0
- package/src/setup/triage.ts +768 -0
- package/src/setup/tuning.ts +766 -0
- package/src/setup/unitcut.ts +523 -0
- package/src/singleflight.ts +86 -0
- package/src/slack/announce.ts +911 -0
- package/src/slack/api.ts +303 -0
- package/src/slack/messages.ts +288 -0
- package/src/slack/origin.ts +599 -0
- package/src/slack/policy.ts +354 -0
- package/src/slack/reply.ts +201 -0
- package/src/slack/router.ts +285 -0
- package/src/slack/socket.ts +384 -0
- package/src/slack/types.ts +108 -0
- package/src/sqlitehint.ts +32 -0
- package/src/ssh-observation.ts +30 -0
- package/src/state.ts +8243 -0
- package/src/statemachine.ts +369 -0
- package/src/stubs.ts +68 -0
- package/src/takeover.ts +634 -0
- package/src/takeoverpi.ts +293 -0
- package/src/terminal-failure.ts +41 -0
- package/src/thread-resolution-refusal.ts +2 -0
- package/src/tmux.ts +153 -0
- package/src/trackerid.ts +41 -0
- package/src/transcriptindex.ts +665 -0
- package/src/tui/actions.ts +484 -0
- package/src/tui/alerts.ts +666 -0
- package/src/tui/app.ts +1979 -0
- package/src/tui/archiveops.ts +192 -0
- package/src/tui/autofile.ts +233 -0
- package/src/tui/board.ts +1432 -0
- package/src/tui/candidates.ts +889 -0
- package/src/tui/cellwidth.ts +148 -0
- package/src/tui/changelog-store.ts +503 -0
- package/src/tui/chord.ts +25 -0
- package/src/tui/clipboard.ts +92 -0
- package/src/tui/cmdbar.ts +296 -0
- package/src/tui/companion.ts +1427 -0
- package/src/tui/companionplan.ts +247 -0
- package/src/tui/companiontab.ts +83 -0
- package/src/tui/confighome.ts +155 -0
- package/src/tui/connect.ts +68 -0
- package/src/tui/deploy-ceremony.ts +104 -0
- package/src/tui/deploy-drain.ts +129 -0
- package/src/tui/deploy-process.ts +265 -0
- package/src/tui/deploytab.ts +1523 -0
- package/src/tui/design.ts +125 -0
- package/src/tui/digesttab.ts +214 -0
- package/src/tui/emptystate.ts +39 -0
- package/src/tui/evidence.ts +999 -0
- package/src/tui/feed.ts +604 -0
- package/src/tui/feedbacktab.ts +1077 -0
- package/src/tui/filters.ts +90 -0
- package/src/tui/ghread.ts +321 -0
- package/src/tui/historypull.ts +81 -0
- package/src/tui/inboxpull.ts +236 -0
- package/src/tui/index.ts +1565 -0
- package/src/tui/issuesink.ts +238 -0
- package/src/tui/jsonfile.ts +42 -0
- package/src/tui/keys.ts +109 -0
- package/src/tui/layout.ts +108 -0
- package/src/tui/learning.ts +627 -0
- package/src/tui/lessonswitch.ts +118 -0
- package/src/tui/livecontrol.ts +228 -0
- package/src/tui/memforward.ts +443 -0
- package/src/tui/memincident.ts +363 -0
- package/src/tui/memory.ts +620 -0
- package/src/tui/memtab.ts +606 -0
- package/src/tui/metricstab.ts +1177 -0
- package/src/tui/model.ts +131 -0
- package/src/tui/mouse.ts +98 -0
- package/src/tui/msg.ts +268 -0
- package/src/tui/nativecompanion.ts +417 -0
- package/src/tui/openurl.ts +21 -0
- package/src/tui/paint.ts +511 -0
- package/src/tui/panes.ts +98 -0
- package/src/tui/pipane.ts +219 -0
- package/src/tui/poll-hooks.ts +12 -0
- package/src/tui/poll.ts +147 -0
- package/src/tui/proposals.ts +838 -0
- package/src/tui/prpause.ts +150 -0
- package/src/tui/reload.ts +436 -0
- package/src/tui/remote.ts +413 -0
- package/src/tui/rollover.ts +969 -0
- package/src/tui/scantab.ts +1290 -0
- package/src/tui/setupentry.ts +93 -0
- package/src/tui/sidebar.ts +749 -0
- package/src/tui/sshtriage.ts +510 -0
- package/src/tui/startup.ts +231 -0
- package/src/tui/tabs.ts +45 -0
- package/src/tui/termmirror.ts +224 -0
- package/src/tui/theme.ts +95 -0
- package/src/tui/ticketstab.ts +310 -0
- package/src/tui/tmuxsocket.ts +129 -0
- package/src/tui/trackertickets.ts +84 -0
- package/src/tui/unwedge.ts +286 -0
- package/src/tui/viewer.ts +575 -0
- package/src/tui/viewerinput.ts +173 -0
- package/src/updatenotice.ts +178 -0
- package/src/usage.ts +1154 -0
- package/src/verdicts.ts +4689 -0
- package/src/version.ts +80 -0
- package/src/wakes.ts +476 -0
- package/src/webhook.ts +942 -0
- package/src/webhookheal.ts +276 -0
- package/src/wire.ts +2153 -0
- package/src/worker-session-host.ts +602 -0
- package/src/workerbreaker.ts +156 -0
- package/src/workercontrol.ts +104 -0
- package/src/workerlease.ts +441 -0
- package/src/workerliveness.ts +103 -0
- package/src/workerlog.ts +79 -0
- package/src/workerruntime.ts +30 -0
- package/src/workersupervisor.ts +1538 -0
- package/src/workerusage.ts +42 -0
- package/src/workerworld.ts +427 -0
- package/src/worktrees.ts +1138 -0
- package/tests/fixtures/regression-cases.json +795 -0
- package/tests/fixtures/seed-manifest.json +19 -0
|
@@ -0,0 +1,1835 @@
|
|
|
1
|
+
import { boardBarHeight, boardKeys, overlayInputBox, piPaneSize, renderBoard, } from "./board.js";
|
|
2
|
+
import { barHitKey, NO_LIVE_KEYS, registeredKeys } from "./cmdbar.js";
|
|
3
|
+
import { decodeInputKey, decodeKey } from "./keys.js";
|
|
4
|
+
import { visiblePrs } from "./filters.js";
|
|
5
|
+
import { formatEvent, moveSelection, reselect } from "./model.js";
|
|
6
|
+
import { TripleCtrlC } from "./chord.js";
|
|
7
|
+
import { MOUSE_OFF, MOUSE_ON, MouseDecoder, hitTest } from "./mouse.js";
|
|
8
|
+
import { ALT_SCREEN_OFF, ALT_SCREEN_ON, CLEAR, Frame, accentFrameRect, cleanCopiedBlock, cloneFrame, composeColumn, fit, sliceDisplay, spliceDisplay, stripAnsi, } from "./paint.js";
|
|
9
|
+
import { ACCENT, RESET, SELECTION } from "./design.js";
|
|
10
|
+
import { ClipboardSink } from "./clipboard.js";
|
|
11
|
+
import { PiMirror } from "./pipane.js";
|
|
12
|
+
import { TranscriptViewer, paneSurface, surfaceSession } from "./viewer.js";
|
|
13
|
+
import { newViewerInput, viewerInputKey } from "./viewerinput.js";
|
|
14
|
+
import { PANE_ESCAPE, isEndKey, keysToTmux } from "./termmirror.js";
|
|
15
|
+
import { Poller } from "./poll.js";
|
|
16
|
+
import { parseRoundProgress } from "@quickchatai/pi-quickreview/src/round-progress.js";
|
|
17
|
+
import { prUrl } from "./theme.js";
|
|
18
|
+
import { paintSidebar, sidebarHit, sidebarResizeHit, sidebarWidth, } from "./sidebar.js";
|
|
19
|
+
import { eventsArgv, progressArgv, sendKeysArgv, whyArgv, } from "./remote.js";
|
|
20
|
+
/**
|
|
21
|
+
* Decoded verbs the BOARD owns. `handleInput` refuses them off the board —
|
|
22
|
+
* see the comment at the guard.
|
|
23
|
+
*
|
|
24
|
+
* The membership test is "is this key advertised by the board's command bar"
|
|
25
|
+
* (`boardKeys`, src/tui/board.ts), NOT "does it read the selection". Most
|
|
26
|
+
* members happen to read it, but the reason they are here is that they are
|
|
27
|
+
* the board's bar, they are unregistered elsewhere, and the board is where an
|
|
28
|
+
* operator looks for them. Getting that reason wrong is how the next verb
|
|
29
|
+
* ends up in or out by accident — a missing `unwedge` would let `u` from
|
|
30
|
+
* another tab unwedge a PR nobody was looking at.
|
|
31
|
+
*/
|
|
32
|
+
const BOARD_ONLY_VERBS = new Set([
|
|
33
|
+
"attach",
|
|
34
|
+
"enter",
|
|
35
|
+
"message",
|
|
36
|
+
"github",
|
|
37
|
+
"events",
|
|
38
|
+
"unwedge",
|
|
39
|
+
]);
|
|
40
|
+
/** The frame an UNWIRED tab paints: its notice on the first body row, at the
|
|
41
|
+
* inner width, so the companion column still composes beside it. */
|
|
42
|
+
function placeholderFrame(cols, rows, notice) {
|
|
43
|
+
const frame = new Frame(cols, rows);
|
|
44
|
+
frame.set(Math.min(1, Math.max(0, rows - 1)), fit(notice, cols));
|
|
45
|
+
return frame;
|
|
46
|
+
}
|
|
47
|
+
export class App {
|
|
48
|
+
cfg;
|
|
49
|
+
ssh;
|
|
50
|
+
out;
|
|
51
|
+
input;
|
|
52
|
+
now;
|
|
53
|
+
view = {
|
|
54
|
+
health: { kind: "unreachable", sinceS: Number.POSITIVE_INFINITY },
|
|
55
|
+
// UNKNOWN until a heartbeat proves one: a default of `true` would paint a
|
|
56
|
+
// DRY-RUN badge on a live installation.
|
|
57
|
+
dryRun: undefined,
|
|
58
|
+
events: [],
|
|
59
|
+
selected: 0,
|
|
60
|
+
eventsFocus: false,
|
|
61
|
+
eventsScroll: 0,
|
|
62
|
+
help: false,
|
|
63
|
+
};
|
|
64
|
+
tab = "board";
|
|
65
|
+
hooks;
|
|
66
|
+
/** The companion sidebar — a right-hand column on every tab.
|
|
67
|
+
* Wired by index.ts; absent in bare test harnesses (no column painted). */
|
|
68
|
+
sidebar;
|
|
69
|
+
poller;
|
|
70
|
+
/** Live mirror of the pi an operator is inside for the selected PR — its
|
|
71
|
+
* debug round or its takeover (board's lower region). */
|
|
72
|
+
pi;
|
|
73
|
+
/** The same region for a worker-hosted PR: its transcript, read-only. Which
|
|
74
|
+
* of the two a PR gets is `surfaceOf` — status.json's `runtime`, no toggle. */
|
|
75
|
+
viewer;
|
|
76
|
+
/** Raw event rows behind view.events — the ambient feed writes these
|
|
77
|
+
* verbatim, so the companion sees what the daemon logged, not our prose. */
|
|
78
|
+
rawEvents = [];
|
|
79
|
+
selectedPr;
|
|
80
|
+
detailSeq = 0;
|
|
81
|
+
progressSeq = 0;
|
|
82
|
+
eventsSeq = 0;
|
|
83
|
+
/** Stale-while-revalidate detail cache: paint the last known payload
|
|
84
|
+
* instantly on selection, refresh over ssh behind it. */
|
|
85
|
+
detailCache = new Map();
|
|
86
|
+
static DETAIL_TTL_MS = 60_000;
|
|
87
|
+
/** The newest `why` read started per PR (its detailSeq), until it settles.
|
|
88
|
+
* Only that read may write the PR's cache entry — an older one resolving
|
|
89
|
+
* late must not stamp its older payload fresh — and while it is pending a
|
|
90
|
+
* poll does not stack another. Invalidation drops it, so a read begun
|
|
91
|
+
* before the invalidation cannot repopulate the cache either. */
|
|
92
|
+
detailPending = new Map();
|
|
93
|
+
/** When a PR's last `why` read failed: a poll does not retry it sooner than
|
|
94
|
+
* DETAIL_RETRY_MS (the read reaches GitHub; a 3s retry loop against a rate
|
|
95
|
+
* limit is the wrong answer). Selection and `r` are not held by it. */
|
|
96
|
+
detailFailedAt = new Map();
|
|
97
|
+
static DETAIL_RETRY_MS = 15_000;
|
|
98
|
+
quitResolve;
|
|
99
|
+
suspended = false;
|
|
100
|
+
onData = (buf) => this.handleInput(buf);
|
|
101
|
+
onResize = () => this.repaint();
|
|
102
|
+
constructor(cfg, ssh, out = process.stdout, input = process.stdin, hooks = {}, now = () => new Date()) {
|
|
103
|
+
this.cfg = cfg;
|
|
104
|
+
this.ssh = ssh;
|
|
105
|
+
this.out = out;
|
|
106
|
+
this.input = input;
|
|
107
|
+
this.now = now;
|
|
108
|
+
this.hooks = { ...hooks, tabs: hooks.tabs ?? {} };
|
|
109
|
+
this.poller = new Poller(cfg, ssh, () => this.onPoll(), now);
|
|
110
|
+
this.pi = new PiMirror(cfg, ssh, () => {
|
|
111
|
+
this.syncPiPane();
|
|
112
|
+
// The session we were typing into went away — say so and hand the
|
|
113
|
+
// keyboard back rather than swallowing keystrokes into nothing.
|
|
114
|
+
const gone = this.view.pi?.session;
|
|
115
|
+
if (this.view.piFocus && this.view.pi?.missing && gone !== undefined)
|
|
116
|
+
this.blurPiPane(`pi session ${gone} is gone — keyboard back on the board`);
|
|
117
|
+
this.repaint();
|
|
118
|
+
}, 300, (pr) => this.sessionFor(pr));
|
|
119
|
+
this.viewer = new TranscriptViewer(cfg, ssh, () => {
|
|
120
|
+
this.syncPiPane();
|
|
121
|
+
this.repaint();
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
// ---- which surface the PR pane is -----------------------------------------
|
|
125
|
+
/** What the pane shows for `pr`, from the PR's `runtime` in status.json — a
|
|
126
|
+
* pi terminal (`debug`, `takeover`) or, for every other runtime, the
|
|
127
|
+
* transcript viewer. */
|
|
128
|
+
surfaceOf(pr) {
|
|
129
|
+
return paneSurface(this.view.snapshot?.prs.find((p) => p.number === pr)?.runtime, pr);
|
|
130
|
+
}
|
|
131
|
+
/** The pi terminal a pane types into and captures; undefined for the viewer
|
|
132
|
+
* (the bot runs no `pr-<n>` tmux session). */
|
|
133
|
+
sessionFor(pr) {
|
|
134
|
+
return surfaceSession(this.surfaceOf(pr));
|
|
135
|
+
}
|
|
136
|
+
/** The PR's `runtime` in status.json, as the daemon last reported it. */
|
|
137
|
+
runtimeOf(pr) {
|
|
138
|
+
return this.view.snapshot?.prs.find((p) => p.number === pr)?.runtime;
|
|
139
|
+
}
|
|
140
|
+
/** A round (or an answer) is running for the PR: the viewer's ⏎ steers it. */
|
|
141
|
+
roundRunning(pr) {
|
|
142
|
+
const runtime = this.runtimeOf(pr);
|
|
143
|
+
return runtime === "worker" || runtime === "answer";
|
|
144
|
+
}
|
|
145
|
+
/** The viewer's input line per PR: a draft survives moving to another PR. */
|
|
146
|
+
viewerInputs = new Map();
|
|
147
|
+
viewerInputOf(pr) {
|
|
148
|
+
return this.viewerInputs.get(pr) ?? newViewerInput();
|
|
149
|
+
}
|
|
150
|
+
viewerOn(pr) {
|
|
151
|
+
return pr !== undefined && this.surfaceOf(pr).kind === "viewer";
|
|
152
|
+
}
|
|
153
|
+
/** Read the PR pane's source now: the transcript logs or the tmux capture. */
|
|
154
|
+
refreshPane(pr) {
|
|
155
|
+
if (pr === undefined)
|
|
156
|
+
return Promise.resolve();
|
|
157
|
+
return this.viewerOn(pr) ? this.viewer.refresh(pr) : this.pi.refresh(pr);
|
|
158
|
+
}
|
|
159
|
+
/** Selection moved: read the PR pane's source behind a debounce. */
|
|
160
|
+
selectPane(pr) {
|
|
161
|
+
if (this.viewerOn(pr))
|
|
162
|
+
this.viewer.select(pr);
|
|
163
|
+
else
|
|
164
|
+
this.pi.select(pr);
|
|
165
|
+
}
|
|
166
|
+
paneOffset(pr) {
|
|
167
|
+
return this.viewerOn(pr) ? this.viewer.offsetFor(pr) : this.pi.offsetFor(pr);
|
|
168
|
+
}
|
|
169
|
+
setPaneOffset(pr, n) {
|
|
170
|
+
if (this.viewerOn(pr))
|
|
171
|
+
this.viewer.setOffset(pr, n);
|
|
172
|
+
else
|
|
173
|
+
this.pi.setOffset(pr, n);
|
|
174
|
+
}
|
|
175
|
+
// ---- the pi pane as an embedded terminal --------------------------------
|
|
176
|
+
/** The session the pane types into while focused (locked at focus time). */
|
|
177
|
+
piFocusPr;
|
|
178
|
+
/** Whether that pane was the viewer when it took the keyboard: a PR whose
|
|
179
|
+
* runtime flips surface under the operator's hands gets the keyboard back. */
|
|
180
|
+
piFocusViewer = false;
|
|
181
|
+
/** While focused the capture runs far faster than the poll tick, so typing
|
|
182
|
+
* echoes back at something close to terminal speed. */
|
|
183
|
+
piTimer;
|
|
184
|
+
static PI_FOCUS_POLL_MS = 400;
|
|
185
|
+
/** Hand the keyboard to the pi pane (click, or a wheel-driven focus). */
|
|
186
|
+
focusPiPane() {
|
|
187
|
+
const pr = this.panePr();
|
|
188
|
+
if (pr === undefined)
|
|
189
|
+
return;
|
|
190
|
+
if (this.view.piFocus && this.piFocusPr === pr)
|
|
191
|
+
return;
|
|
192
|
+
this.piFocusPr = pr;
|
|
193
|
+
this.piFocusViewer = this.viewerOn(pr);
|
|
194
|
+
this.view.piFocus = true;
|
|
195
|
+
this.sidebar?.blur(); // one keyboard owner at a time
|
|
196
|
+
this.syncPiPane();
|
|
197
|
+
void this.refreshPane(pr);
|
|
198
|
+
if (!this.piTimer) {
|
|
199
|
+
const t = setInterval(() => {
|
|
200
|
+
if (this.suspended || this.tab !== "board" || !this.view.piFocus)
|
|
201
|
+
return;
|
|
202
|
+
void this.refreshPane(this.piFocusPr);
|
|
203
|
+
}, App.PI_FOCUS_POLL_MS);
|
|
204
|
+
t.unref?.();
|
|
205
|
+
this.piTimer = t;
|
|
206
|
+
}
|
|
207
|
+
this.repaint();
|
|
208
|
+
}
|
|
209
|
+
/** Ctrl-q, a click outside the pane, or a dead session: back to the list. */
|
|
210
|
+
blurPiPane(notice) {
|
|
211
|
+
if (this.piTimer)
|
|
212
|
+
clearInterval(this.piTimer);
|
|
213
|
+
this.piTimer = undefined;
|
|
214
|
+
if (!this.view.piFocus)
|
|
215
|
+
return;
|
|
216
|
+
this.view.piFocus = false;
|
|
217
|
+
this.piFocusPr = undefined;
|
|
218
|
+
this.syncPiPane();
|
|
219
|
+
if (notice !== undefined)
|
|
220
|
+
this.view.noticeLine = notice;
|
|
221
|
+
this.selectPane(this.selectedPr); // follow resumes where the list is
|
|
222
|
+
}
|
|
223
|
+
/** True while the pane owns the keyboard (tests + hot-reload read this). */
|
|
224
|
+
piFocused() {
|
|
225
|
+
return this.view.piFocus === true;
|
|
226
|
+
}
|
|
227
|
+
/** Forward one raw stdin chunk into the focused session, then re-capture so
|
|
228
|
+
* the echo lands immediately instead of on the next tick. Typing also snaps
|
|
229
|
+
* the pane back to the live tail — you cannot type into history. */
|
|
230
|
+
sendKeysToPane(buf) {
|
|
231
|
+
const pr = this.piFocusPr;
|
|
232
|
+
if (pr === undefined)
|
|
233
|
+
return;
|
|
234
|
+
const groups = keysToTmux(buf);
|
|
235
|
+
if (groups.length === 0)
|
|
236
|
+
return;
|
|
237
|
+
if (this.paneOffset(pr) !== 0) {
|
|
238
|
+
this.setPaneOffset(pr, 0);
|
|
239
|
+
this.syncPiPane();
|
|
240
|
+
this.repaint();
|
|
241
|
+
}
|
|
242
|
+
// One queue for every keystroke into a pane: each chunk is its own ssh
|
|
243
|
+
// round trip, and unqueued a slow "a" lets the Enter typed after it reach
|
|
244
|
+
// tmux first and submit pi's input without it. For the same reason a
|
|
245
|
+
// chunk that did not arrive drops everything queued behind it — an Enter
|
|
246
|
+
// must never submit input with a hole in it — and says so; the next
|
|
247
|
+
// keystroke typed once the queue has drained starts afresh.
|
|
248
|
+
if (this.paneSendsQueued === 0)
|
|
249
|
+
this.paneSendDropped = false;
|
|
250
|
+
this.paneSendsQueued++;
|
|
251
|
+
this.paneSendTail = this.paneSendTail
|
|
252
|
+
.then(async () => {
|
|
253
|
+
for (const parts of groups) {
|
|
254
|
+
if (this.paneSendDropped)
|
|
255
|
+
return;
|
|
256
|
+
// Only a pi terminal takes keys. A PR whose debug round or takeover
|
|
257
|
+
// ended under the operator's hands has none: drop, and say so.
|
|
258
|
+
const session = this.sessionFor(pr);
|
|
259
|
+
if (session === undefined) {
|
|
260
|
+
this.paneSendDropped = true;
|
|
261
|
+
this.notice(`✖ #${pr}: no pi terminal any more — input dropped`);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const res = await this.ssh(this.cfg.ssh_host, sendKeysArgv(this.cfg, session, parts), {
|
|
265
|
+
timeoutMs: 5_000,
|
|
266
|
+
});
|
|
267
|
+
if (!res.ok) {
|
|
268
|
+
this.paneSendDropped = true;
|
|
269
|
+
this.notice(`✖ ${session}: keys not delivered (${res.stderr.trim() || `exit ${res.code}`}) — input after them dropped`);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
})
|
|
274
|
+
.catch((err) => {
|
|
275
|
+
this.paneSendDropped = true;
|
|
276
|
+
this.notice(`✖ #${pr}: keys not delivered (${err instanceof Error ? err.message : String(err)}) — input after them dropped`);
|
|
277
|
+
})
|
|
278
|
+
.finally(() => {
|
|
279
|
+
this.paneSendsQueued--;
|
|
280
|
+
void this.pi.refresh(pr);
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
paneSendTail = Promise.resolve();
|
|
284
|
+
/** Sends queued and not yet finished. */
|
|
285
|
+
paneSendsQueued = 0;
|
|
286
|
+
/** A send in the current queue failed: the rest of it is not sent. */
|
|
287
|
+
paneSendDropped = false;
|
|
288
|
+
/** Wheel over the pane: move its scrollback window, re-capture with history.
|
|
289
|
+
* Wheeling back to the bottom returns it to the live tail. */
|
|
290
|
+
scrollPiPane(notches) {
|
|
291
|
+
const pr = this.panePr();
|
|
292
|
+
if (pr === undefined)
|
|
293
|
+
return;
|
|
294
|
+
if (this.viewerOn(pr))
|
|
295
|
+
this.viewer.scroll(pr, notches);
|
|
296
|
+
else
|
|
297
|
+
this.pi.scroll(pr, notches);
|
|
298
|
+
this.syncPiPane();
|
|
299
|
+
this.repaint();
|
|
300
|
+
void this.refreshPane(pr);
|
|
301
|
+
}
|
|
302
|
+
/** The PR whose session the pane is showing. While the pane has the keyboard
|
|
303
|
+
* this is LOCKED — a poll that reorders the list must never redirect the
|
|
304
|
+
* operator's typing to another PR's session. */
|
|
305
|
+
panePr() {
|
|
306
|
+
return this.view.piFocus ? this.piFocusPr : this.selectedPr;
|
|
307
|
+
}
|
|
308
|
+
/** Point the board's pane at whatever it should be showing right now (cache
|
|
309
|
+
* first, so switching back paints instantly). */
|
|
310
|
+
syncPiPane() {
|
|
311
|
+
const pr = this.panePr();
|
|
312
|
+
this.view.piPr = pr;
|
|
313
|
+
this.view.pi = this.viewerOn(pr) ? this.viewer.view(pr) : this.pi.view(pr);
|
|
314
|
+
// The input line exists on the viewer's pane only: a pi terminal gets
|
|
315
|
+
// none (the PR-pane rule).
|
|
316
|
+
this.view.viewerInput = this.viewerOn(pr)
|
|
317
|
+
? {
|
|
318
|
+
...this.viewerInputOf(pr),
|
|
319
|
+
pr,
|
|
320
|
+
running: this.roundRunning(pr),
|
|
321
|
+
focused: this.view.piFocus === true && this.piFocusPr === pr,
|
|
322
|
+
}
|
|
323
|
+
: undefined;
|
|
324
|
+
}
|
|
325
|
+
/** Selection moved: paint the cached mirror at once, capture behind it.
|
|
326
|
+
* A focused pane ignores this — follow is locked until Ctrl-q. */
|
|
327
|
+
followSelection() {
|
|
328
|
+
this.syncPiPane();
|
|
329
|
+
if (this.view.piFocus)
|
|
330
|
+
return;
|
|
331
|
+
this.selectPane(this.selectedPr);
|
|
332
|
+
}
|
|
333
|
+
// ---- lifecycle ---------------------------------------------------------
|
|
334
|
+
async run(initialSelection) {
|
|
335
|
+
if (initialSelection !== undefined)
|
|
336
|
+
this.selectedPr = initialSelection;
|
|
337
|
+
// From here the TUI owns the screen, and this is the FIRST thing that
|
|
338
|
+
// happens: nothing external — no docker call, no network call, no
|
|
339
|
+
// filesystem walk — runs before this paint.
|
|
340
|
+
//
|
|
341
|
+
// The first frame is already the final layout: the column's width is a
|
|
342
|
+
// function of the terminal alone (`sidebarWidth`), the sidebar is wired
|
|
343
|
+
// before this call, and the container merely fills a column that is
|
|
344
|
+
// already the right size. Everything slow streams in behind
|
|
345
|
+
// this frame and reports into that column (src/tui/startup.ts).
|
|
346
|
+
this.started = true;
|
|
347
|
+
this.out.write(ALT_SCREEN_ON + CLEAR + MOUSE_ON);
|
|
348
|
+
if (this.input.isTTY)
|
|
349
|
+
this.input.setRawMode(true);
|
|
350
|
+
this.input.resume();
|
|
351
|
+
this.input.on("data", this.onData);
|
|
352
|
+
this.out.on("resize", this.onResize);
|
|
353
|
+
if (this.hooks.helpSeen && !this.hooks.helpSeen.get()) {
|
|
354
|
+
this.view.help = true;
|
|
355
|
+
this.hooks.helpSeen.set();
|
|
356
|
+
}
|
|
357
|
+
this.repaint();
|
|
358
|
+
this.poller.start();
|
|
359
|
+
await new Promise((resolve) => (this.quitResolve = resolve));
|
|
360
|
+
this.teardown();
|
|
361
|
+
}
|
|
362
|
+
/** Set once teardown has restored the terminal. From then on nothing may
|
|
363
|
+
* write to the output stream: an async load that resolves after quit
|
|
364
|
+
* (memory/proposals fetches take seconds over ssh) would otherwise paint
|
|
365
|
+
* a full TUI frame onto the user's restored NORMAL screen — a ghost board
|
|
366
|
+
* that looks like the TUI is still running. */
|
|
367
|
+
stopped = false;
|
|
368
|
+
teardown() {
|
|
369
|
+
this.stopped = true;
|
|
370
|
+
this.poller.stop();
|
|
371
|
+
this.pi.stop();
|
|
372
|
+
this.viewer.stop();
|
|
373
|
+
this.sidebar?.stop();
|
|
374
|
+
if (this.piTimer)
|
|
375
|
+
clearInterval(this.piTimer);
|
|
376
|
+
this.piTimer = undefined;
|
|
377
|
+
this.input.off("data", this.onData);
|
|
378
|
+
this.out.off("resize", this.onResize);
|
|
379
|
+
if (this.input.isTTY)
|
|
380
|
+
this.input.setRawMode(false);
|
|
381
|
+
this.input.pause();
|
|
382
|
+
// MOUSE_OFF, leave the alt screen, THEN clear. The clear lands
|
|
383
|
+
// on the normal screen and is what makes the exit look clean rather than
|
|
384
|
+
// dropping the operator back onto whatever the alt screen was hiding; it
|
|
385
|
+
// also wipes any half-line a crash left behind. Order matters: clearing
|
|
386
|
+
// before ALT_SCREEN_OFF would clear the screen we are about to discard.
|
|
387
|
+
this.out.write(MOUSE_OFF + ALT_SCREEN_OFF + CLEAR);
|
|
388
|
+
}
|
|
389
|
+
quit() {
|
|
390
|
+
this.quitResolve?.();
|
|
391
|
+
}
|
|
392
|
+
/** The terminal has been restored and nothing may write to it again. Public
|
|
393
|
+
* so the orchestrator's real-terminal escape hatch can check it: re-entering
|
|
394
|
+
* the alt screen after teardown leaves the operator staring at a blank
|
|
395
|
+
* alternate screen with no TUI behind it — the trap this file exists to
|
|
396
|
+
* avoid, arrived at from the other end. */
|
|
397
|
+
isStopped() {
|
|
398
|
+
return this.stopped;
|
|
399
|
+
}
|
|
400
|
+
/** Ctrl-C taps, for the two-tap exit (distinct from the three-tap kill). */
|
|
401
|
+
ctrlCTaps = [];
|
|
402
|
+
static CTRL_C_EXIT_WINDOW_MS = 2_000;
|
|
403
|
+
/**
|
|
404
|
+
* Every route to "I want out", in one place.
|
|
405
|
+
*
|
|
406
|
+
* `q` asks first — quitting is cheap to redo but the confirmation is what
|
|
407
|
+
* stops a stray keystroke from dropping a board you were reading. Ctrl-C
|
|
408
|
+
* confirms by REPETITION instead: the second tap inside the window is the
|
|
409
|
+
* answer, so the reflex still works without a prompt in the way.
|
|
410
|
+
*/
|
|
411
|
+
requestQuit(via) {
|
|
412
|
+
if (via === "ctrl-c") {
|
|
413
|
+
const t = this.now().getTime();
|
|
414
|
+
this.ctrlCTaps = [...this.ctrlCTaps.filter((x) => t - x <= App.CTRL_C_EXIT_WINDOW_MS), t];
|
|
415
|
+
if (this.ctrlCTaps.length >= 2) {
|
|
416
|
+
this.quit();
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
this.notice("press Ctrl-C again to exit");
|
|
420
|
+
this.repaint();
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
this.view.confirmQuit = true;
|
|
424
|
+
this.repaint();
|
|
425
|
+
}
|
|
426
|
+
/** A key while the quit prompt is up. Returns true when it consumed it. */
|
|
427
|
+
handleConfirmQuit(buf) {
|
|
428
|
+
if (!this.view.confirmQuit)
|
|
429
|
+
return false;
|
|
430
|
+
const raw = buf.toString("utf8");
|
|
431
|
+
// Only an explicit yes leaves. Anything else — including a second `q`,
|
|
432
|
+
// which is what a repeated keystroke looks like — cancels, because the
|
|
433
|
+
// prompt exists to catch exactly that.
|
|
434
|
+
if (raw === "y" || raw === "Y" || raw === "\r" || raw === "\n") {
|
|
435
|
+
this.view.confirmQuit = false;
|
|
436
|
+
this.quit();
|
|
437
|
+
return true;
|
|
438
|
+
}
|
|
439
|
+
this.view.confirmQuit = false;
|
|
440
|
+
this.repaint();
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
/** Injectable so tests never kill the runner. */
|
|
444
|
+
exiter = (code) => process.exit(code);
|
|
445
|
+
/** Triple-Ctrl-C hard kill: restore the terminal no matter what, then die.
|
|
446
|
+
* A hung ssh/pty must never trap the user in the alt screen — closing the
|
|
447
|
+
* terminal window cannot stay the only way out. */
|
|
448
|
+
die() {
|
|
449
|
+
try {
|
|
450
|
+
this.suspended = false; // teardown must run even while the terminal is lent out
|
|
451
|
+
this.teardown();
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
/* a half-restored terminal still beats a trapped one */
|
|
455
|
+
}
|
|
456
|
+
this.exiter(130);
|
|
457
|
+
}
|
|
458
|
+
/** Lend the terminal out: stop painting, swallow board keys, drop our mouse
|
|
459
|
+
* reporting. The ONE remaining borrower is `s` (index.ts `onRealTerminal`):
|
|
460
|
+
* the GCP key-passphrase prompt has to run on the real terminal, outside the
|
|
461
|
+
* alt screen. It is not a TUI surface and paints no layout of its own; the
|
|
462
|
+
* board comes back exactly as it was the moment ssh returns. */
|
|
463
|
+
suspend() {
|
|
464
|
+
this.suspended = true;
|
|
465
|
+
this.input.off("data", this.onData);
|
|
466
|
+
this.out.write(MOUSE_OFF);
|
|
467
|
+
}
|
|
468
|
+
resume() {
|
|
469
|
+
if (this.stopped)
|
|
470
|
+
return; // a borrower returning after quit must not repaint
|
|
471
|
+
this.suspended = false;
|
|
472
|
+
this.input.on("data", this.onData);
|
|
473
|
+
this.out.write(CLEAR + MOUSE_ON);
|
|
474
|
+
void this.refreshDetail();
|
|
475
|
+
void this.refreshProgress();
|
|
476
|
+
void this.refreshPane(this.panePr()); // the pane caught up while attached
|
|
477
|
+
this.repaint();
|
|
478
|
+
}
|
|
479
|
+
/** True while something other than the board owns the terminal or holds
|
|
480
|
+
* unsaved user input. Used to REFUSE a remote-driven action (index.ts), so
|
|
481
|
+
* it never fights the operator for the keyboard. The hot reload deliberately
|
|
482
|
+
* does NOT consult it — every build reloads (see reload.ts). */
|
|
483
|
+
isBusy() {
|
|
484
|
+
return (this.suspended ||
|
|
485
|
+
this.view.inputBox !== undefined ||
|
|
486
|
+
this.view.piFocus === true ||
|
|
487
|
+
this.sidebar?.focused === true);
|
|
488
|
+
}
|
|
489
|
+
/** The PR the board is pointing at (survives a hot-reload). */
|
|
490
|
+
currentPr() {
|
|
491
|
+
return this.selectedPr;
|
|
492
|
+
}
|
|
493
|
+
// ---- data --------------------------------------------------------------
|
|
494
|
+
onPoll() {
|
|
495
|
+
// A tick already in flight when the operator quits resolves AFTER
|
|
496
|
+
// teardown, and this method kicks off four more remote reads. They would
|
|
497
|
+
// spawn ssh against a TUI that no longer exists — the same family as the
|
|
498
|
+
// `stopped` guard on `repaint` (an async load resolving after quit must
|
|
499
|
+
// not touch the restored terminal), one level earlier: it must not start
|
|
500
|
+
// new work either.
|
|
501
|
+
if (this.stopped)
|
|
502
|
+
return;
|
|
503
|
+
const snap = this.poller.state.snapshot;
|
|
504
|
+
if (snap) {
|
|
505
|
+
// Invalidate cached details the snapshot proves stale (state/head moved).
|
|
506
|
+
for (const pr of snap.prs) {
|
|
507
|
+
const c = this.detailCache.get(pr.number);
|
|
508
|
+
if (c && (c.payload.head_sha !== pr.head_sha || c.payload.state !== pr.state))
|
|
509
|
+
this.invalidateDetail(pr.number);
|
|
510
|
+
}
|
|
511
|
+
this.view.snapshot = snap;
|
|
512
|
+
this.view.dryRun = snap.dry_run;
|
|
513
|
+
const sorted = visiblePrs(this.view);
|
|
514
|
+
this.view.selected = reselect(sorted, this.selectedPr, this.view.selected);
|
|
515
|
+
this.selectedPr = sorted[this.view.selected]?.number;
|
|
516
|
+
}
|
|
517
|
+
this.view.health = this.poller.health();
|
|
518
|
+
void this.refreshEvents();
|
|
519
|
+
if (this.detailWanted())
|
|
520
|
+
void this.refreshDetail();
|
|
521
|
+
// Live progress is a cheap host-volume read, separate from `why` because
|
|
522
|
+
// refreshing the enriched detail would call GitHub on every poll.
|
|
523
|
+
if (this.tab === "board" && !this.suspended)
|
|
524
|
+
void this.refreshProgress();
|
|
525
|
+
// A PR whose runtime flipped the pane's surface under a focused keyboard
|
|
526
|
+
// (a worker round ended, a debug round began) hands it back: the same
|
|
527
|
+
// keys would now mean something else.
|
|
528
|
+
if (this.view.piFocus &&
|
|
529
|
+
this.piFocusPr !== undefined &&
|
|
530
|
+
this.viewerOn(this.piFocusPr) !== this.piFocusViewer)
|
|
531
|
+
this.blurPiPane(`#${this.piFocusPr}'s pane changed what it shows — keyboard back on the board`);
|
|
532
|
+
// The pane only costs an ssh while the board is the visible tab.
|
|
533
|
+
this.syncPiPane();
|
|
534
|
+
if (this.tab === "board" && !this.suspended)
|
|
535
|
+
void this.refreshPane(this.panePr());
|
|
536
|
+
try {
|
|
537
|
+
this.hooks.onPoll?.(this);
|
|
538
|
+
}
|
|
539
|
+
catch {
|
|
540
|
+
/* a throwing wrapper must never kill the poll loop */
|
|
541
|
+
}
|
|
542
|
+
this.repaint();
|
|
543
|
+
}
|
|
544
|
+
async refreshEvents() {
|
|
545
|
+
const seq = ++this.eventsSeq;
|
|
546
|
+
const res = await this.ssh(this.cfg.ssh_host, eventsArgv(this.cfg, 50));
|
|
547
|
+
if (this.stopped || !res.ok || seq !== this.eventsSeq)
|
|
548
|
+
return; // a newer read superseded this one
|
|
549
|
+
try {
|
|
550
|
+
const parsed = JSON.parse(res.stdout);
|
|
551
|
+
this.rawEvents = parsed.events;
|
|
552
|
+
const lines = parsed.events.map(formatEvent).reverse(); // newest first
|
|
553
|
+
this.view.events = this.mergeNotices(lines);
|
|
554
|
+
try {
|
|
555
|
+
this.hooks.onPoll?.(this); // feed the companion the fresh tail too
|
|
556
|
+
}
|
|
557
|
+
catch {
|
|
558
|
+
/* a throwing wrapper must never kill the poll loop */
|
|
559
|
+
}
|
|
560
|
+
this.repaint();
|
|
561
|
+
}
|
|
562
|
+
catch {
|
|
563
|
+
/* keep the previous sidebar on parse failure */
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
/** Override point to splice extra rows (e.g. quick-message pending/delivered)
|
|
567
|
+
* into the events strip; the base passes lines through. */
|
|
568
|
+
mergeNotices(lines) {
|
|
569
|
+
return lines;
|
|
570
|
+
}
|
|
571
|
+
/** A poll refreshes the detail when the selection moved, or when the
|
|
572
|
+
* selected PR's cache entry is gone (invalidated) or past its TTL and no
|
|
573
|
+
* read for it is already in flight — a PR that stays selected must follow
|
|
574
|
+
* its new rounds without the operator moving away or pressing `r`. */
|
|
575
|
+
detailWanted() {
|
|
576
|
+
const pr = this.selectedPr;
|
|
577
|
+
if (pr === undefined)
|
|
578
|
+
return false;
|
|
579
|
+
// A read already in flight for it answers the question — stacking another
|
|
580
|
+
// each tick would bump detailSeq and starve a read slower than the poll.
|
|
581
|
+
if (this.detailPending.has(pr))
|
|
582
|
+
return false;
|
|
583
|
+
const failed = this.detailFailedAt.get(pr);
|
|
584
|
+
if (failed !== undefined && Date.now() - failed < App.DETAIL_RETRY_MS)
|
|
585
|
+
return false;
|
|
586
|
+
if (this.view.detail?.pr !== pr)
|
|
587
|
+
return true;
|
|
588
|
+
const c = this.detailCache.get(pr);
|
|
589
|
+
return !c || Date.now() - c.fetched_at >= App.DETAIL_TTL_MS;
|
|
590
|
+
}
|
|
591
|
+
/** Read the selected PR's detail. Served from cache at once; the fetch is
|
|
592
|
+
* skipped while the cache is fresh, unless `force` (the operator's `r`). */
|
|
593
|
+
async refreshDetail(opts = {}) {
|
|
594
|
+
const pr = this.selectedPr;
|
|
595
|
+
if (pr === undefined)
|
|
596
|
+
return;
|
|
597
|
+
const cached = this.detailCache.get(pr);
|
|
598
|
+
const now = Date.now();
|
|
599
|
+
if (cached) {
|
|
600
|
+
this.view.detail = cached.payload;
|
|
601
|
+
this.repaint();
|
|
602
|
+
if (!opts.force && now - cached.fetched_at < App.DETAIL_TTL_MS)
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
const seq = ++this.detailSeq;
|
|
606
|
+
this.detailPending.set(pr, seq);
|
|
607
|
+
const res = await this.ssh(this.cfg.ssh_host, whyArgv(this.cfg, pr));
|
|
608
|
+
// Superseded (a newer read for this PR started) or invalidated meanwhile:
|
|
609
|
+
// this payload may be older than what the cache should hold.
|
|
610
|
+
const newest = this.detailPending.get(pr) === seq;
|
|
611
|
+
if (newest)
|
|
612
|
+
this.detailPending.delete(pr);
|
|
613
|
+
if (!res.ok) {
|
|
614
|
+
if (newest)
|
|
615
|
+
this.detailFailedAt.set(pr, Date.now());
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
try {
|
|
619
|
+
const payload = JSON.parse(res.stdout);
|
|
620
|
+
if (newest)
|
|
621
|
+
this.detailFailedAt.delete(pr);
|
|
622
|
+
if (newest && payload.pr === pr)
|
|
623
|
+
this.detailCache.set(payload.pr, { payload, fetched_at: Date.now() });
|
|
624
|
+
if (seq !== this.detailSeq || payload.pr !== this.selectedPr)
|
|
625
|
+
return;
|
|
626
|
+
this.view.detail = payload;
|
|
627
|
+
this.repaint();
|
|
628
|
+
}
|
|
629
|
+
catch {
|
|
630
|
+
if (newest)
|
|
631
|
+
this.detailFailedAt.set(pr, Date.now());
|
|
632
|
+
/* stale/garbled detail keeps the loading line */
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
async refreshProgress() {
|
|
636
|
+
const pr = this.selectedPr;
|
|
637
|
+
if (pr === undefined)
|
|
638
|
+
return;
|
|
639
|
+
const seq = ++this.progressSeq;
|
|
640
|
+
const res = await this.ssh(this.cfg.ssh_host, progressArgv(this.cfg, pr), { timeoutMs: 3_000 });
|
|
641
|
+
if (seq !== this.progressSeq || pr !== this.selectedPr)
|
|
642
|
+
return;
|
|
643
|
+
if (!res.ok) {
|
|
644
|
+
// Keep a same-PR snapshot: its age becomes the visible SILENT signal.
|
|
645
|
+
// A different PR must never inherit it while its own read is pending.
|
|
646
|
+
if (this.view.progress?.pr !== pr)
|
|
647
|
+
this.view.progress = undefined;
|
|
648
|
+
this.repaint();
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
const parsed = parseRoundProgress(res.stdout);
|
|
652
|
+
this.view.progress = parsed?.pr === pr ? parsed : undefined;
|
|
653
|
+
this.repaint();
|
|
654
|
+
}
|
|
655
|
+
/** Drop a PR's cached detail (a new verdict/round event makes it stale). */
|
|
656
|
+
invalidateDetail(pr) {
|
|
657
|
+
this.detailCache.delete(pr);
|
|
658
|
+
this.detailPending.delete(pr);
|
|
659
|
+
}
|
|
660
|
+
// ---- input -------------------------------------------------------------
|
|
661
|
+
/**
|
|
662
|
+
* Point the board at `pr` (the companion's `select_pr` action, a row click
|
|
663
|
+
* and the events overlay land here). Once the board has loaded, a PR it does not list
|
|
664
|
+
* (closed, or outside the filter) is refused with a notice and the
|
|
665
|
+
* selection stays where it is — the highlighted row, the detail and the
|
|
666
|
+
* mirrored session never point at different PRs. Before the first snapshot
|
|
667
|
+
* the request is kept (a `qcr tui <pr>` deep link), and the first poll
|
|
668
|
+
* settles it. False when refused.
|
|
669
|
+
*/
|
|
670
|
+
selectPr(pr) {
|
|
671
|
+
const sorted = visiblePrs(this.view);
|
|
672
|
+
if (this.view.snapshot !== undefined && !sorted.some((p) => p.number === pr)) {
|
|
673
|
+
this.notice(`#${pr} is not on the board (closed, or outside the filter)`);
|
|
674
|
+
return false;
|
|
675
|
+
}
|
|
676
|
+
this.view.selected = reselect(sorted, pr, this.view.selected);
|
|
677
|
+
this.selectionMoved(pr);
|
|
678
|
+
return true;
|
|
679
|
+
}
|
|
680
|
+
/** The selection now points at `pr`: everything that shows the selected PR
|
|
681
|
+
* follows it — the progress panel (another PR's is never shown), the
|
|
682
|
+
* detail, and the mirrored session. */
|
|
683
|
+
selectionMoved(pr) {
|
|
684
|
+
if (pr !== this.selectedPr)
|
|
685
|
+
this.msgDraft = ""; // a cancelled draft belongs to the PR it was typed for
|
|
686
|
+
this.selectedPr = pr;
|
|
687
|
+
if (this.view.progress?.pr !== pr)
|
|
688
|
+
this.view.progress = undefined;
|
|
689
|
+
void this.refreshDetail();
|
|
690
|
+
void this.refreshProgress();
|
|
691
|
+
this.followSelection();
|
|
692
|
+
}
|
|
693
|
+
/** Move the events overlay's scroll, clamped to its lines. */
|
|
694
|
+
scrollEvents(delta) {
|
|
695
|
+
this.view.eventsScroll = moveSelection(this.view.eventsScroll, delta, this.view.events.length);
|
|
696
|
+
}
|
|
697
|
+
/** Narrow (or, with null, clear) the board. The selection stays on the
|
|
698
|
+
* same PR while it is still listed; otherwise it lands on the nearest row
|
|
699
|
+
* of the new list — and the row, the detail and the mirrored session move
|
|
700
|
+
* together, so the next key addresses the highlighted PR. */
|
|
701
|
+
setBoardFilter(filter) {
|
|
702
|
+
this.view.boardFilter = filter;
|
|
703
|
+
const sorted = visiblePrs(this.view);
|
|
704
|
+
this.view.selected = reselect(sorted, this.selectedPr, this.view.selected);
|
|
705
|
+
const pr = sorted[this.view.selected]?.number;
|
|
706
|
+
if (pr === this.selectedPr)
|
|
707
|
+
return;
|
|
708
|
+
this.selectionMoved(pr);
|
|
709
|
+
}
|
|
710
|
+
selectedSession() {
|
|
711
|
+
const pr = this.selectedPrNumber();
|
|
712
|
+
return pr === undefined ? undefined : `pr-${pr}`;
|
|
713
|
+
}
|
|
714
|
+
notice(text) {
|
|
715
|
+
this.view.noticeLine = text;
|
|
716
|
+
// Before the TUI owns the screen, a notice is startup progress and prints
|
|
717
|
+
// as a plain line — the companion can take minutes to build, and silence
|
|
718
|
+
// there looks like a hang, and painting a frame this early would flash.
|
|
719
|
+
// The alt screen hides these the
|
|
720
|
+
// moment run() begins, so they never linger.
|
|
721
|
+
if (!this.started) {
|
|
722
|
+
this.out.write(`${text}\n`);
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
this.repaint();
|
|
726
|
+
}
|
|
727
|
+
/** The session the open input box targets; draft survives Esc. */
|
|
728
|
+
msgSession;
|
|
729
|
+
msgDraft = "";
|
|
730
|
+
/** When set, the input box belongs to a tab prompt (memory edits,
|
|
731
|
+
* proposal-reject reasons) — submit goes here, not to sendMessage. */
|
|
732
|
+
inputSubmit;
|
|
733
|
+
openMessageBox(session) {
|
|
734
|
+
this.msgSession = session;
|
|
735
|
+
this.inputSubmit = undefined;
|
|
736
|
+
const pr = /^pr-(\d+)$/.exec(session)?.[1];
|
|
737
|
+
this.view.inputBox = {
|
|
738
|
+
target: pr ? `#${pr} (a message to its session)` : session,
|
|
739
|
+
text: this.msgDraft,
|
|
740
|
+
};
|
|
741
|
+
this.repaint();
|
|
742
|
+
}
|
|
743
|
+
/** Generic one-line input over the same box UI. Esc cancels (no draft). */
|
|
744
|
+
promptInput(target, initial, onSubmit) {
|
|
745
|
+
this.inputSubmit = onSubmit;
|
|
746
|
+
this.view.inputBox = { target, text: initial };
|
|
747
|
+
this.repaint();
|
|
748
|
+
}
|
|
749
|
+
handleMessageBoxKey(buf) {
|
|
750
|
+
const box = this.view.inputBox;
|
|
751
|
+
const key = decodeInputKey(buf);
|
|
752
|
+
switch (key.kind) {
|
|
753
|
+
case "char":
|
|
754
|
+
box.text += key.ch;
|
|
755
|
+
break;
|
|
756
|
+
case "backspace":
|
|
757
|
+
box.text = box.text.slice(0, -1);
|
|
758
|
+
break;
|
|
759
|
+
case "alt-enter":
|
|
760
|
+
box.text += "\n";
|
|
761
|
+
break;
|
|
762
|
+
case "escape":
|
|
763
|
+
if (this.inputSubmit)
|
|
764
|
+
this.inputSubmit = undefined; // tab prompts keep no draft
|
|
765
|
+
else
|
|
766
|
+
this.msgDraft = box.text; // preserved until board navigation
|
|
767
|
+
this.view.inputBox = undefined;
|
|
768
|
+
break;
|
|
769
|
+
case "enter": {
|
|
770
|
+
const text = box.text.trim();
|
|
771
|
+
const submit = this.inputSubmit;
|
|
772
|
+
const session = this.msgSession;
|
|
773
|
+
this.view.inputBox = undefined;
|
|
774
|
+
this.inputSubmit = undefined;
|
|
775
|
+
if (submit) {
|
|
776
|
+
if (text.length > 0)
|
|
777
|
+
submit(text);
|
|
778
|
+
break;
|
|
779
|
+
}
|
|
780
|
+
this.msgDraft = "";
|
|
781
|
+
if (text.length > 0 && session) {
|
|
782
|
+
if (this.hooks.sendMessage)
|
|
783
|
+
void this.hooks.sendMessage(session, text, this);
|
|
784
|
+
else
|
|
785
|
+
this.notice(`✖ msg ${session} undelivered (sender not wired)`);
|
|
786
|
+
}
|
|
787
|
+
break;
|
|
788
|
+
}
|
|
789
|
+
case "quit":
|
|
790
|
+
this.requestQuit(buf.toString("utf8") === "\x03" ? "ctrl-c" : "key");
|
|
791
|
+
return;
|
|
792
|
+
default:
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
this.repaint();
|
|
796
|
+
}
|
|
797
|
+
openGithub(pr) {
|
|
798
|
+
const n = pr ?? this.selectedPrNumber();
|
|
799
|
+
if (n === undefined || !this.view.repo)
|
|
800
|
+
return;
|
|
801
|
+
this.hooks.openUrl?.(prUrl(this.view.repo, n));
|
|
802
|
+
this.notice(`opened #${n} on GitHub`);
|
|
803
|
+
}
|
|
804
|
+
/** The selected row's PR number. Public because `selectedSession()` is the
|
|
805
|
+
* session-name view of the same thing and callers want either. */
|
|
806
|
+
selectedPrNumber() {
|
|
807
|
+
const sorted = visiblePrs(this.view);
|
|
808
|
+
return sorted[this.view.selected]?.number;
|
|
809
|
+
}
|
|
810
|
+
/** Synthesize a keypress through the normal input path — the command bar's
|
|
811
|
+
* buttons and any tab code that wants "exactly what the key would do". */
|
|
812
|
+
pressKey(key) {
|
|
813
|
+
this.handleInput(Buffer.from(key, "utf8"));
|
|
814
|
+
}
|
|
815
|
+
/** The installed surface under the current tab, if any (never the board). */
|
|
816
|
+
tabHooks() {
|
|
817
|
+
return this.tab === "board" ? undefined : this.hooks.tabs[this.tab];
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* What the current frame answers to: the bar the tab paints plus
|
|
821
|
+
* its inline keys. ONE computation feeds the bar's hit-test, the companion
|
|
822
|
+
* routing (`registeredKeys`) and the paint, so the three cannot disagree.
|
|
823
|
+
* An unwired tab answers to nothing but the globals.
|
|
824
|
+
*/
|
|
825
|
+
liveKeys() {
|
|
826
|
+
if (this.tab === "board")
|
|
827
|
+
return boardKeys(this.view);
|
|
828
|
+
return this.tabHooks()?.keys(this) ?? NO_LIVE_KEYS;
|
|
829
|
+
}
|
|
830
|
+
/** Every printable the current frame claims — the companion column gets
|
|
831
|
+
* the rest. Public for the fences: a registered key must answer. */
|
|
832
|
+
registeredKeys() {
|
|
833
|
+
return registeredKeys(this.liveKeys());
|
|
834
|
+
}
|
|
835
|
+
handleMouse(ev) {
|
|
836
|
+
// The quit prompt is modal for the mouse as it is for the keyboard: a
|
|
837
|
+
// click is its "any other key" (cancel), and nothing under the prompt —
|
|
838
|
+
// a PR row, a GitHub link, the pi pane, the companion column, a tab's
|
|
839
|
+
// zones — ever sees it. Wheel, drag and release are swallowed.
|
|
840
|
+
if (this.view.confirmQuit) {
|
|
841
|
+
if (ev.kind === "click") {
|
|
842
|
+
this.view.confirmQuit = false;
|
|
843
|
+
this.repaint();
|
|
844
|
+
}
|
|
845
|
+
// A release swallowed here still ends any gesture begun before the
|
|
846
|
+
// prompt opened — never leave a seam resize armed behind it.
|
|
847
|
+
if (ev.kind === "release") {
|
|
848
|
+
this.sidebarResizeActive = false;
|
|
849
|
+
this.selAnchor = undefined;
|
|
850
|
+
}
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
const cols = this.out.columns ?? 80;
|
|
854
|
+
// The sidebar's left seam is the ONE drag surface that is chrome rather
|
|
855
|
+
// than selectable content. Everywhere else drag and release belong to the
|
|
856
|
+
// rectangular copy.
|
|
857
|
+
if (this.sidebar && ev.kind === "click") {
|
|
858
|
+
const width = this.sidebarWidth(cols);
|
|
859
|
+
if (sidebarResizeHit(ev.x, cols, width)) {
|
|
860
|
+
this.sidebarResizeActive = true;
|
|
861
|
+
this.selAnchor = undefined;
|
|
862
|
+
this.selection = undefined;
|
|
863
|
+
this.repaint();
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
if (this.sidebarResizeActive && ev.kind === "drag") {
|
|
868
|
+
this.sidebar?.resizeFromPointer(cols, ev.x);
|
|
869
|
+
this.repaint();
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
if (this.sidebarResizeActive && ev.kind === "release") {
|
|
873
|
+
this.sidebarResizeActive = false;
|
|
874
|
+
this.notice(`sidebar ${this.sidebarWidth(cols)} cols`);
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
// Selection owns every other drag and release. A drag is never
|
|
878
|
+
// a click, so nothing below sees it and no row is selected or button fired.
|
|
879
|
+
if (ev.kind === "drag") {
|
|
880
|
+
const anchor = this.selAnchor;
|
|
881
|
+
if (!anchor)
|
|
882
|
+
return;
|
|
883
|
+
// The first motion freezes the screen and fixes the pane the drag
|
|
884
|
+
// belongs to; every later motion only moves the far corner.
|
|
885
|
+
if (!this.selFrame) {
|
|
886
|
+
this.selFrame = this.baseFrame;
|
|
887
|
+
this.selPane = this.paneRectAt(anchor.x, anchor.y);
|
|
888
|
+
}
|
|
889
|
+
const p = this.selPane ?? { x1: 1, y1: 1, x2: cols, y2: this.out.rows ?? 24 };
|
|
890
|
+
const cx = (x) => Math.max(p.x1, Math.min(p.x2, x));
|
|
891
|
+
const cy = (y) => Math.max(p.y1, Math.min(p.y2, y));
|
|
892
|
+
this.selection = {
|
|
893
|
+
x1: cx(Math.min(anchor.x, ev.x)),
|
|
894
|
+
y1: cy(Math.min(anchor.y, ev.y)),
|
|
895
|
+
x2: cx(Math.max(anchor.x, ev.x)),
|
|
896
|
+
y2: cy(Math.max(anchor.y, ev.y)),
|
|
897
|
+
};
|
|
898
|
+
this.repaint();
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
if (ev.kind === "release") {
|
|
902
|
+
// A press with no drag is a plain click and has already been handled;
|
|
903
|
+
// there is nothing to copy and the anchor just goes away.
|
|
904
|
+
if (this.selection)
|
|
905
|
+
this.copySelection();
|
|
906
|
+
else
|
|
907
|
+
this.selAnchor = undefined;
|
|
908
|
+
this.selFrame = undefined;
|
|
909
|
+
this.selPane = undefined;
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
// A new press starts a possible selection and clears the last highlight.
|
|
913
|
+
if (ev.kind === "click") {
|
|
914
|
+
this.selAnchor = { x: ev.x, y: ev.y };
|
|
915
|
+
if (this.selection) {
|
|
916
|
+
this.selection = undefined;
|
|
917
|
+
this.repaint();
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
// The command bar answers first, on every tab: a click on one of its
|
|
921
|
+
// buttons IS that keypress, wherever it lands. EXCEPT while an input box
|
|
922
|
+
// is open — a synthesized key would be typed into the box, and [⏎]
|
|
923
|
+
// would SUBMIT the draft (a live memory edit or quick-message).
|
|
924
|
+
if (ev.kind === "click" && !this.view.inputBox) {
|
|
925
|
+
const rows = this.out.rows ?? 24;
|
|
926
|
+
// Hit-test at the width the bar was PAINTED at — the tab's inner width
|
|
927
|
+
// beside the companion column, not the terminal's. The bar wraps, so its
|
|
928
|
+
// row count depends on the width: tested at the full width, a click on
|
|
929
|
+
// the painted [r refresh] decoded as a nav slot one row up, and the
|
|
930
|
+
// phantom slots extended into the column (terminal x is inner-frame x
|
|
931
|
+
// because the tab is on the left).
|
|
932
|
+
const k = barHitKey(this.tab, ev.x, ev.y, this.innerWidth(cols), rows, this.liveKeys().bar);
|
|
933
|
+
if (k !== undefined) {
|
|
934
|
+
// A focused pi pane would swallow the synthesized key as text.
|
|
935
|
+
if (this.view.piFocus) {
|
|
936
|
+
this.blurPiPane();
|
|
937
|
+
this.repaint();
|
|
938
|
+
}
|
|
939
|
+
// So would a focused sidebar — and [⏎] would SUBMIT the draft. Blur
|
|
940
|
+
// first (same rule as the pane): a bar click IS that keypress.
|
|
941
|
+
if (this.sidebar?.focused) {
|
|
942
|
+
this.sidebar.blur();
|
|
943
|
+
this.repaint();
|
|
944
|
+
}
|
|
945
|
+
this.pressKey(k);
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
// The companion column is fixed chrome on every tab: clicking anywhere
|
|
949
|
+
// in it takes the keyboard (its mirror IS the companion — there is no
|
|
950
|
+
// full-screen view to open, and no collapsed state to toggle).
|
|
951
|
+
if (this.sidebar) {
|
|
952
|
+
const width = this.sidebarWidth(cols);
|
|
953
|
+
const hit = width === 0 ? undefined : sidebarHit(ev.x, ev.y, cols, rows, width);
|
|
954
|
+
if (hit !== undefined) {
|
|
955
|
+
if (this.sidebar.available()) {
|
|
956
|
+
this.focusCompanion();
|
|
957
|
+
}
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
// Zellij-style spatial focus: clicking a different pane moves focus
|
|
961
|
+
// there. Ctrl-q remains the keyboard-only escape hatch, not a tax on
|
|
962
|
+
// mouse users who already pointed at the main TUI.
|
|
963
|
+
if (this.sidebar.focused) {
|
|
964
|
+
this.sidebar.blur();
|
|
965
|
+
this.repaint();
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
// The wheel belongs to the companion column before any tab sees it. The
|
|
970
|
+
// column is chrome on EVERY tab, so a wheel notch that the tab dispatch
|
|
971
|
+
// below answers is a notch stolen from the surface the operator is
|
|
972
|
+
// actually reading: scrolling the conversation while the column has focus
|
|
973
|
+
// must never walk the board's selection (and drag the pi mirror away from
|
|
974
|
+
// the PR being read).
|
|
975
|
+
//
|
|
976
|
+
// Two ways it belongs here, and they compose: the pointer is over the
|
|
977
|
+
// column (focused or not — the same rule the pi pane already has), or the
|
|
978
|
+
// column holds the keyboard, wherever the pointer happens to rest. Focus
|
|
979
|
+
// means "this surface is the one I am working in"; the wheel is part of
|
|
980
|
+
// that, not an exception to it.
|
|
981
|
+
if (ev.kind === "wheel-up" || ev.kind === "wheel-down") {
|
|
982
|
+
if (this.sidebar && this.wheelBelongsToSidebar(ev.x, cols)) {
|
|
983
|
+
this.sidebar.scroll(ev.kind === "wheel-up" ? 1 : -1);
|
|
984
|
+
this.repaint();
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
// The KEYS overlay covers the tab, the board included (see repaint): what
|
|
989
|
+
// it hides is not on screen, so a click must not reach it (a hidden [url]
|
|
990
|
+
// or row would fire unseen) and the wheel must not move a hidden
|
|
991
|
+
// selection. A click dismisses help, as any key does; the wheel is
|
|
992
|
+
// swallowed.
|
|
993
|
+
if (this.view.help) {
|
|
994
|
+
if (ev.kind === "click") {
|
|
995
|
+
this.view.help = false;
|
|
996
|
+
this.view.helpScroll = 0;
|
|
997
|
+
this.repaint();
|
|
998
|
+
}
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
// The tab's input overlay owns its painted cells. Its renderer supplies
|
|
1002
|
+
// the bounds, so resize/column layout cannot expose hidden tab actions.
|
|
1003
|
+
if (this.view.inputBox) {
|
|
1004
|
+
const meta = this.lastFrame?.meta;
|
|
1005
|
+
const top = meta?.["inputBoxTop"];
|
|
1006
|
+
const bottom = meta?.["inputBoxBottom"];
|
|
1007
|
+
const width = meta?.["inputBoxCols"];
|
|
1008
|
+
if (top !== undefined &&
|
|
1009
|
+
bottom !== undefined &&
|
|
1010
|
+
width !== undefined &&
|
|
1011
|
+
ev.x >= 1 &&
|
|
1012
|
+
ev.x <= width &&
|
|
1013
|
+
ev.y >= top + 1 &&
|
|
1014
|
+
ev.y <= bottom + 1)
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
const tab = this.tabHooks();
|
|
1018
|
+
if (this.tab !== "board") {
|
|
1019
|
+
// An installed surface: clicks hit its own zones (rows select, buttons
|
|
1020
|
+
// act); the wheel is handed over as a key that says it was a wheel.
|
|
1021
|
+
if (ev.kind === "click")
|
|
1022
|
+
tab?.click?.(this, ev.x, ev.y);
|
|
1023
|
+
else
|
|
1024
|
+
tab?.key(this, { kind: ev.kind === "wheel-down" ? "down" : "up", wheel: true });
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
const zoneAt = (x, y) => this.lastFrame ? hitTest(this.lastFrame.zones, x, y) : undefined;
|
|
1028
|
+
if (ev.kind === "wheel-up" || ev.kind === "wheel-down") {
|
|
1029
|
+
const delta = ev.kind === "wheel-down" ? 1 : -1;
|
|
1030
|
+
// Pointer over the pi pane (focused or not) = that session's scrollback,
|
|
1031
|
+
// in place. Everything else keeps the wheel's board meaning.
|
|
1032
|
+
if (!this.view.eventsFocus && zoneAt(ev.x, ev.y)?.t === "pi-focus") {
|
|
1033
|
+
this.scrollPiPane(-delta);
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
// The events overlay owns the wheel while it is up; otherwise the wheel
|
|
1037
|
+
// moves the selection (and the mirror follows it).
|
|
1038
|
+
if (this.view.eventsFocus) {
|
|
1039
|
+
this.scrollEvents(delta);
|
|
1040
|
+
}
|
|
1041
|
+
else {
|
|
1042
|
+
const sorted = visiblePrs(this.view);
|
|
1043
|
+
this.view.selected = moveSelection(this.view.selected, delta, sorted.length);
|
|
1044
|
+
this.selectionMoved(sorted[this.view.selected]?.number);
|
|
1045
|
+
}
|
|
1046
|
+
this.repaint();
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
const action = zoneAt(ev.x, ev.y);
|
|
1050
|
+
// Any click outside the pane hands the keyboard back to the board — that
|
|
1051
|
+
// includes clicks on nothing in particular in the list/detail region.
|
|
1052
|
+
const paneWasFocused = this.view.piFocus;
|
|
1053
|
+
if (action?.t !== "pi-focus" && paneWasFocused) {
|
|
1054
|
+
this.blurPiPane();
|
|
1055
|
+
this.repaint();
|
|
1056
|
+
}
|
|
1057
|
+
if (!action)
|
|
1058
|
+
return;
|
|
1059
|
+
switch (action.t) {
|
|
1060
|
+
case "select-pr": {
|
|
1061
|
+
if (this.selectedPr === action.pr) {
|
|
1062
|
+
// Second click on the selected row = go to its pi, the same thing
|
|
1063
|
+
// ⏎ does. If the pane WAS focused, the click above already handed the
|
|
1064
|
+
// keyboard back, and that is the whole meaning of it.
|
|
1065
|
+
if (paneWasFocused)
|
|
1066
|
+
return;
|
|
1067
|
+
this.focusPiPane();
|
|
1068
|
+
this.repaint();
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
this.selectPr(action.pr);
|
|
1072
|
+
break;
|
|
1073
|
+
}
|
|
1074
|
+
case "open-github":
|
|
1075
|
+
this.openGithub(action.pr);
|
|
1076
|
+
return;
|
|
1077
|
+
case "event-jump":
|
|
1078
|
+
this.selectPr(action.pr);
|
|
1079
|
+
break;
|
|
1080
|
+
case "pi-focus":
|
|
1081
|
+
// Clicking the pane types into it in place — no full-screen takeover.
|
|
1082
|
+
this.focusPiPane();
|
|
1083
|
+
return;
|
|
1084
|
+
case "viewer-lens": {
|
|
1085
|
+
// A lens row of the progress panel, with the transcript viewer in the pane.
|
|
1086
|
+
const pr = this.selectedPr;
|
|
1087
|
+
if (!this.viewerOn(pr))
|
|
1088
|
+
return;
|
|
1089
|
+
if (this.viewer.selectLens(pr, action.lens, action.nth)) {
|
|
1090
|
+
this.syncPiPane();
|
|
1091
|
+
this.repaint();
|
|
1092
|
+
void this.viewer.refresh(pr);
|
|
1093
|
+
}
|
|
1094
|
+
else
|
|
1095
|
+
this.notice(`${action.lens}: no transcript yet — its lens has not started logging`);
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
case "companion":
|
|
1099
|
+
// The top-bar alert badge: hand the keyboard to the companion column.
|
|
1100
|
+
this.openCompanion();
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
this.repaint();
|
|
1104
|
+
}
|
|
1105
|
+
killChord = new TripleCtrlC();
|
|
1106
|
+
mouse = new MouseDecoder();
|
|
1107
|
+
handleInput(buf) {
|
|
1108
|
+
if (this.killChord.feed(buf)) {
|
|
1109
|
+
this.die();
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
if (this.suspended)
|
|
1113
|
+
return;
|
|
1114
|
+
const { events, rest } = this.mouse.feed(buf);
|
|
1115
|
+
for (const ev of events)
|
|
1116
|
+
this.handleMouse(ev);
|
|
1117
|
+
if (rest.length === 0)
|
|
1118
|
+
return;
|
|
1119
|
+
buf = rest;
|
|
1120
|
+
if (this.view.inputBox) {
|
|
1121
|
+
this.handleMessageBoxKey(buf);
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
// The quit prompt outranks every surface, focused sidebar included: it is
|
|
1125
|
+
// modal, and a keystroke meant for it must never reach a pi session.
|
|
1126
|
+
if (this.handleConfirmQuit(buf))
|
|
1127
|
+
return;
|
|
1128
|
+
const key = decodeKey(buf);
|
|
1129
|
+
if (this.view.help) {
|
|
1130
|
+
if ((key.kind === "down" || key.kind === "up") && key.arrow) {
|
|
1131
|
+
const max = this.lastFrame?.meta["helpMax"] ?? 0;
|
|
1132
|
+
const next = (this.view.helpScroll ?? 0) + (key.kind === "down" ? 1 : -1);
|
|
1133
|
+
this.view.helpScroll = Math.min(Math.max(0, next), max);
|
|
1134
|
+
}
|
|
1135
|
+
else {
|
|
1136
|
+
this.view.help = false;
|
|
1137
|
+
this.view.helpScroll = 0;
|
|
1138
|
+
}
|
|
1139
|
+
this.repaint();
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
// The pi pane owns the keyboard while it is focused: j/k/q/1-8/? and every
|
|
1143
|
+
// other board binding is just text as far as the session is concerned.
|
|
1144
|
+
// Ctrl-q — and only Ctrl-q — comes back to the board.
|
|
1145
|
+
if (this.tab === "board" && this.view.piFocus) {
|
|
1146
|
+
if (buf.length === 1 && buf[0] === PANE_ESCAPE) {
|
|
1147
|
+
this.blurPiPane();
|
|
1148
|
+
this.repaint();
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
// The transcript viewer is not a terminal: its keys are its own, and
|
|
1152
|
+
// nothing else goes anywhere.
|
|
1153
|
+
if (this.piFocusViewer && this.piFocusPr !== undefined) {
|
|
1154
|
+
this.handleViewerKey(buf, this.piFocusPr);
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
// End, while parked in scrollback, is the "back to live" key the header
|
|
1158
|
+
// advertises — it snaps the window to the tail instead of travelling.
|
|
1159
|
+
if (isEndKey(buf.toString("utf8")) && this.pi.offsetFor(this.piFocusPr) > 0) {
|
|
1160
|
+
this.pi.setOffset(this.piFocusPr, 0);
|
|
1161
|
+
this.syncPiPane();
|
|
1162
|
+
this.repaint();
|
|
1163
|
+
void this.pi.refresh(this.piFocusPr);
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
this.sendKeysToPane(buf);
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
// A focused sidebar owns the keyboard, exactly as the pi pane does above:
|
|
1170
|
+
// the companion is a live terminal, so j/k/1-8/? are just text to it.
|
|
1171
|
+
// Ctrl-q releases — the same chord, so there is ONE way out of every
|
|
1172
|
+
// embedded surface; Esc belongs to pi.
|
|
1173
|
+
if (this.sidebar?.focused) {
|
|
1174
|
+
this.handleSidebarKey(buf);
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
const raw = buf.toString("utf8");
|
|
1178
|
+
// `/` always focuses the companion, on every tab — and TRAVELS.
|
|
1179
|
+
// It is the key that opens pi's command menu, so swallowing it as a pure
|
|
1180
|
+
// focus key is the worst possible choice: you press `/`, the column takes
|
|
1181
|
+
// the keyboard, and the menu you actually wanted never opens. Seeding it
|
|
1182
|
+
// makes `/` behave like every other printable — focus, then reach pi.
|
|
1183
|
+
if (raw === "/" && this.sidebar?.available()) {
|
|
1184
|
+
this.sidebar.focus(raw);
|
|
1185
|
+
this.repaint();
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
// `\` is plain text for pi like any other unclaimed printable: the column
|
|
1189
|
+
// is always at its full width and the layout never changes shape.
|
|
1190
|
+
// The installed surface gets first crack at every key — including the
|
|
1191
|
+
// nav digits, which the metrics tab reads as TEXT inside its `# <pr> ⏎`
|
|
1192
|
+
// box (`keyChar` maps a tab kind back to its digit). Outside that box no
|
|
1193
|
+
// tab answers to a digit, so the hook returns false and the key falls
|
|
1194
|
+
// through to the switch below.
|
|
1195
|
+
if (this.tabHooks()?.key(this, key))
|
|
1196
|
+
return;
|
|
1197
|
+
// An unregistered printable is a message to the companion, not a dead
|
|
1198
|
+
// key: focus the sidebar seeded with it (paste seeds whole). Registered
|
|
1199
|
+
// means "this frame answers to it" — the set is derived from
|
|
1200
|
+
// the bar the tab is painting right now, never from a table.
|
|
1201
|
+
if (this.sidebar?.available() &&
|
|
1202
|
+
raw.length > 0 &&
|
|
1203
|
+
!raw.includes("\x1b") &&
|
|
1204
|
+
raw >= " " &&
|
|
1205
|
+
raw !== "\x7f" && // backspace (DEL) is an edit, never draft text
|
|
1206
|
+
raw !== "\b" &&
|
|
1207
|
+
!(raw.length === 1 && this.registeredKeys().has(raw))) {
|
|
1208
|
+
this.sidebar.focus(raw);
|
|
1209
|
+
this.repaint();
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
// The verbs below belong to the BOARD's command bar, so they only answer
|
|
1213
|
+
// on the board: off it, `a` would focus the board's pi pane behind your
|
|
1214
|
+
// back, and `u` would unwedge a PR nobody was looking at. A verb with no
|
|
1215
|
+
// meaning on the current surface must do nothing; the printable
|
|
1216
|
+
// spellings are unregistered off the board, so they reach the companion
|
|
1217
|
+
// column instead of dying here. The one exception is a live
|
|
1218
|
+
// alert, which owns ⏎ from wherever you are.
|
|
1219
|
+
if (this.tab !== "board" &&
|
|
1220
|
+
BOARD_ONLY_VERBS.has(key.kind) &&
|
|
1221
|
+
!(key.kind === "enter" && this.view.alertFocus))
|
|
1222
|
+
return;
|
|
1223
|
+
switch (key.kind) {
|
|
1224
|
+
case "quit":
|
|
1225
|
+
this.requestQuit(buf.toString("utf8") === "\x03" ? "ctrl-c" : "key");
|
|
1226
|
+
return;
|
|
1227
|
+
case "help":
|
|
1228
|
+
this.view.help = true;
|
|
1229
|
+
break;
|
|
1230
|
+
case "tab":
|
|
1231
|
+
this.tab = key.tab;
|
|
1232
|
+
break;
|
|
1233
|
+
case "up":
|
|
1234
|
+
case "down": {
|
|
1235
|
+
const delta = key.kind === "down" ? 1 : -1;
|
|
1236
|
+
if (this.view.eventsFocus) {
|
|
1237
|
+
this.scrollEvents(delta);
|
|
1238
|
+
break;
|
|
1239
|
+
}
|
|
1240
|
+
const sorted = visiblePrs(this.view);
|
|
1241
|
+
this.view.selected = moveSelection(this.view.selected, delta, sorted.length);
|
|
1242
|
+
this.msgDraft = ""; // board navigation drops a cancelled draft
|
|
1243
|
+
this.selectionMoved(sorted[this.view.selected]?.number);
|
|
1244
|
+
break;
|
|
1245
|
+
}
|
|
1246
|
+
case "escape":
|
|
1247
|
+
this.view.eventsFocus = false;
|
|
1248
|
+
this.view.alertFocus = false; // acknowledged: ⏎ goes back to the pi pane
|
|
1249
|
+
break;
|
|
1250
|
+
case "events":
|
|
1251
|
+
this.view.eventsFocus = !this.view.eventsFocus;
|
|
1252
|
+
this.view.eventsScroll = 0;
|
|
1253
|
+
break;
|
|
1254
|
+
case "refresh":
|
|
1255
|
+
void this.poller.tick();
|
|
1256
|
+
void this.refreshDetail({ force: true });
|
|
1257
|
+
void this.refreshPane(this.panePr());
|
|
1258
|
+
break;
|
|
1259
|
+
case "reconnect":
|
|
1260
|
+
if (this.hooks.reconnect)
|
|
1261
|
+
this.hooks.reconnect(this);
|
|
1262
|
+
else
|
|
1263
|
+
this.notice("reconnect not wired (test harness?)");
|
|
1264
|
+
void this.poller.tick();
|
|
1265
|
+
break;
|
|
1266
|
+
case "attach":
|
|
1267
|
+
case "enter": {
|
|
1268
|
+
// A live alert owns ⏎ until it is followed or dismissed — the point of
|
|
1269
|
+
// an alert is that the next thing you do is talk to the companion.
|
|
1270
|
+
if (key.kind === "enter" && this.view.alertFocus) {
|
|
1271
|
+
this.openCompanion();
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
// ⏎ on a PR row focuses the board's EMBEDDED pane. Nothing may take
|
|
1275
|
+
// the whole TUI (AGENTS.md invariant 10, the fixed layout). With no PR
|
|
1276
|
+
// selected there is nothing to focus, and nothing else to open.
|
|
1277
|
+
if (this.selectedPr !== undefined) {
|
|
1278
|
+
this.focusPiPane();
|
|
1279
|
+
this.repaint();
|
|
1280
|
+
}
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
case "github":
|
|
1284
|
+
this.openGithub();
|
|
1285
|
+
return;
|
|
1286
|
+
case "message": {
|
|
1287
|
+
const session = this.selectedSession();
|
|
1288
|
+
if (!session)
|
|
1289
|
+
break;
|
|
1290
|
+
if (this.hooks.message)
|
|
1291
|
+
this.hooks.message(session, this);
|
|
1292
|
+
else
|
|
1293
|
+
this.openMessageBox(session);
|
|
1294
|
+
break;
|
|
1295
|
+
}
|
|
1296
|
+
case "unwedge": {
|
|
1297
|
+
// Cancel the selected PR's running round and re-queue it. Safe
|
|
1298
|
+
// and recoverable by construction — the round re-dispatches through
|
|
1299
|
+
// the normal pump, nothing is posted and no GitHub state is touched —
|
|
1300
|
+
// so it acts immediately and reports through the events strip, the
|
|
1301
|
+
// same shape `m` uses. The harsher `restart-session` (which also kills
|
|
1302
|
+
// the conversation) is deliberately NOT bound to a key: it stays an
|
|
1303
|
+
// overseer command, where the extra deliberation is the point.
|
|
1304
|
+
const pr = this.selectedPrNumber();
|
|
1305
|
+
if (pr === undefined)
|
|
1306
|
+
break;
|
|
1307
|
+
if (this.hooks.unwedge)
|
|
1308
|
+
this.hooks.unwedge(pr, this);
|
|
1309
|
+
else
|
|
1310
|
+
this.notice("unwedge: not wired (no remote runner)");
|
|
1311
|
+
break;
|
|
1312
|
+
}
|
|
1313
|
+
case "older":
|
|
1314
|
+
case "newer":
|
|
1315
|
+
// `[ ]` step the viewer's streams — registered only while it is up.
|
|
1316
|
+
if (this.tab === "board" && this.viewerOn(this.selectedPr))
|
|
1317
|
+
this.stepViewer(this.selectedPr, "stream", key.kind === "older" ? -1 : 1);
|
|
1318
|
+
return;
|
|
1319
|
+
case "char":
|
|
1320
|
+
// `< >` step the viewer's rounds.
|
|
1321
|
+
if ((key.ch === "<" || key.ch === ">") &&
|
|
1322
|
+
this.tab === "board" &&
|
|
1323
|
+
this.viewerOn(this.selectedPr)) {
|
|
1324
|
+
this.stepViewer(this.selectedPr, "round", key.ch === "<" ? -1 : 1);
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
// F clears the board filter — the indicator promises it.
|
|
1328
|
+
if (key.ch === "F" && this.tab === "board" && this.view.boardFilter != null) {
|
|
1329
|
+
this.setBoardFilter(null);
|
|
1330
|
+
break;
|
|
1331
|
+
}
|
|
1332
|
+
// I starts setup — the companion takes it from there.
|
|
1333
|
+
if (key.ch === "I" && this.tab === "board") {
|
|
1334
|
+
if (this.hooks.setup)
|
|
1335
|
+
this.hooks.setup(this);
|
|
1336
|
+
else
|
|
1337
|
+
this.notice("setup: not wired (no companion in this TUI)");
|
|
1338
|
+
break;
|
|
1339
|
+
}
|
|
1340
|
+
return;
|
|
1341
|
+
default:
|
|
1342
|
+
return; // unknown keys repaint nothing
|
|
1343
|
+
}
|
|
1344
|
+
this.repaint();
|
|
1345
|
+
}
|
|
1346
|
+
/** Do what the input line asked: send a control, or say why not. */
|
|
1347
|
+
applyViewerEffect(pr, effect) {
|
|
1348
|
+
if (effect.t === "notice") {
|
|
1349
|
+
this.view.noticeLine = effect.text;
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
if (effect.t === "none")
|
|
1353
|
+
return;
|
|
1354
|
+
const req = effect.t === "abort"
|
|
1355
|
+
? { kind: "abort-round", pr }
|
|
1356
|
+
: effect.kind === "message"
|
|
1357
|
+
? { kind: "message", pr, text: effect.text, may_post: effect.may_post }
|
|
1358
|
+
: { kind: effect.kind, pr, text: effect.text };
|
|
1359
|
+
const send = this.hooks.liveControl;
|
|
1360
|
+
if (!send) {
|
|
1361
|
+
this.view.noticeLine = `✖ ${req.kind} #${pr} not sent (no control route wired)`;
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
void send(req, this).then(() => this.repaint());
|
|
1365
|
+
}
|
|
1366
|
+
/** Step the viewer's stream (`[ ]`) or round (`< >`) and read what it now shows. */
|
|
1367
|
+
stepViewer(pr, what, dir) {
|
|
1368
|
+
if (what === "stream")
|
|
1369
|
+
this.viewer.stepStream(pr, dir);
|
|
1370
|
+
else
|
|
1371
|
+
this.viewer.stepRound(pr, dir);
|
|
1372
|
+
this.syncPiPane();
|
|
1373
|
+
this.repaint();
|
|
1374
|
+
void this.viewer.refresh(pr);
|
|
1375
|
+
}
|
|
1376
|
+
/**
|
|
1377
|
+
* Keys while the transcript viewer holds the keyboard. It is not a terminal
|
|
1378
|
+
* — there is no pi behind it — so nothing is forwarded, and what it does not
|
|
1379
|
+
* answer to is dropped. Ctrl-q (handled by the caller) releases it.
|
|
1380
|
+
*/
|
|
1381
|
+
handleViewerKey(buf, pr) {
|
|
1382
|
+
const raw = buf.toString("utf8");
|
|
1383
|
+
// The input line first: it takes text, ⏎, Alt-⏎, Alt-p and the abort
|
|
1384
|
+
// prompt. The navigation keys below are the line's only when it is empty.
|
|
1385
|
+
const typed = viewerInputKey(this.viewerInputOf(pr), raw, this.roundRunning(pr));
|
|
1386
|
+
if (typed.handled) {
|
|
1387
|
+
this.viewerInputs.set(pr, typed.state);
|
|
1388
|
+
this.applyViewerEffect(pr, typed.effect);
|
|
1389
|
+
this.syncPiPane();
|
|
1390
|
+
this.repaint();
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1393
|
+
if (raw === "[" || raw === "]")
|
|
1394
|
+
return this.stepViewer(pr, "stream", raw === "[" ? -1 : 1);
|
|
1395
|
+
if (raw === "<" || raw === ">")
|
|
1396
|
+
return this.stepViewer(pr, "round", raw === "<" ? -1 : 1);
|
|
1397
|
+
if (raw === "\x1b[5~")
|
|
1398
|
+
this.viewer.page(pr, 1);
|
|
1399
|
+
else if (raw === "\x1b[6~")
|
|
1400
|
+
this.viewer.page(pr, -1);
|
|
1401
|
+
else if (raw === "\x1b[A")
|
|
1402
|
+
this.viewer.setOffset(pr, this.viewer.offsetFor(pr) + 1);
|
|
1403
|
+
else if (raw === "\x1b[B")
|
|
1404
|
+
this.viewer.setOffset(pr, this.viewer.offsetFor(pr) - 1);
|
|
1405
|
+
else if (isEndKey(raw))
|
|
1406
|
+
this.viewer.setOffset(pr, 0);
|
|
1407
|
+
else
|
|
1408
|
+
return;
|
|
1409
|
+
this.syncPiPane();
|
|
1410
|
+
this.repaint();
|
|
1411
|
+
void this.viewer.refresh(pr);
|
|
1412
|
+
}
|
|
1413
|
+
/**
|
|
1414
|
+
* Go to the companion (the sidebar's rows, the alert badge, an alert-armed
|
|
1415
|
+
* ⏎). Following an alert clears its focus — it has been acted on.
|
|
1416
|
+
*
|
|
1417
|
+
* The companion is always on screen, at full width, so "open" means take
|
|
1418
|
+
* the keyboard. Nothing in the TUI covers the whole terminal, and the
|
|
1419
|
+
* column never collapses.
|
|
1420
|
+
*/
|
|
1421
|
+
openCompanion() {
|
|
1422
|
+
this.view.alertFocus = false;
|
|
1423
|
+
if (this.sidebar?.available()) {
|
|
1424
|
+
this.focusCompanion();
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
// Down, but a failed startup can heal: hand it to the ensure hook, which
|
|
1428
|
+
// re-runs the startup and focuses on success (companiontab.ts).
|
|
1429
|
+
if (this.hooks.companionEnsure) {
|
|
1430
|
+
void this.hooks.companionEnsure(this);
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
this.notice("companion unavailable (started with --no-companion?)");
|
|
1434
|
+
}
|
|
1435
|
+
/** Give the companion column the keyboard — taking it from the PR pane
|
|
1436
|
+
* first: one keyboard owner, whoever asked (a retry that heals can land
|
|
1437
|
+
* here after the operator focused a pane). */
|
|
1438
|
+
focusCompanion() {
|
|
1439
|
+
if (!this.sidebar)
|
|
1440
|
+
return;
|
|
1441
|
+
if (this.view.piFocus)
|
|
1442
|
+
this.blurPiPane();
|
|
1443
|
+
this.sidebar.focus();
|
|
1444
|
+
this.repaint();
|
|
1445
|
+
}
|
|
1446
|
+
/**
|
|
1447
|
+
* Keys while the companion column has focus: RAW PASSTHROUGH to pi.
|
|
1448
|
+
*
|
|
1449
|
+
* The column is an embedded terminal, exactly like the board's pi pane, and
|
|
1450
|
+
* that is the whole point of running pi in tmux — `/` commands, the
|
|
1451
|
+
* completion menu, history, multi-line editing, Ctrl-C to interrupt a turn
|
|
1452
|
+
* are all pi's, and they only work if pi actually receives the bytes.
|
|
1453
|
+
*
|
|
1454
|
+
* DO NOT reintroduce a TUI-side draft line here: it would be a second, worse
|
|
1455
|
+
* input box in front of the real one, with no `/` commands and nothing
|
|
1456
|
+
* clickable. The rule is in
|
|
1457
|
+
* docs/tui.md; `tests/tui-sidebar.test.ts` pins it.
|
|
1458
|
+
*
|
|
1459
|
+
* Ctrl-q is the ONE byte that never travels — the same release chord the pi
|
|
1460
|
+
* pane uses, so there is one way out of every embedded surface. Everything
|
|
1461
|
+
* else, including Esc and Ctrl-C, belongs to pi.
|
|
1462
|
+
*/
|
|
1463
|
+
handleSidebarKey(buf) {
|
|
1464
|
+
const sidebar = this.sidebar;
|
|
1465
|
+
if (buf.length === 1 && buf[0] === PANE_ESCAPE) {
|
|
1466
|
+
sidebar.blur();
|
|
1467
|
+
this.repaint();
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
// Scrollback is the MIRROR's, not pi's: pi cannot scroll a pane it does
|
|
1471
|
+
// not know is being mirrored. PgUp always enters scrollback; PgDn is the
|
|
1472
|
+
// mirror's only while parked (offset > 0), otherwise it reaches pi.
|
|
1473
|
+
const raw = buf.toString("utf8");
|
|
1474
|
+
if ((raw === "\x1b[5~" || raw === "\x1b[6~") && (raw === "\x1b[5~" || sidebar.offset > 0)) {
|
|
1475
|
+
sidebar.scroll(raw === "\x1b[5~" ? 1 : -1);
|
|
1476
|
+
this.repaint();
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
void sidebar
|
|
1480
|
+
.sendKeys(buf)
|
|
1481
|
+
.catch((err) => this.notice(`✖ ${String(err instanceof Error ? err.message : err)}`));
|
|
1482
|
+
}
|
|
1483
|
+
/** The alert that last armed `alertFocus`. A folded repeat keeps the
|
|
1484
|
+
* consumer's `latest` object, so it updates the counters without re-arming
|
|
1485
|
+
* an alert the operator already acknowledged. */
|
|
1486
|
+
armedAlert;
|
|
1487
|
+
/** Alert consumer → top bar. A NEW `alert`-tier alert also grabs ⏎ (see
|
|
1488
|
+
* handleInput); a folded repeat of it does not. */
|
|
1489
|
+
setAlertBadge(badge) {
|
|
1490
|
+
this.view.alerts = badge;
|
|
1491
|
+
if (badge.latest?.tier !== "alert") {
|
|
1492
|
+
this.view.alertFocus = false;
|
|
1493
|
+
this.armedAlert = undefined;
|
|
1494
|
+
}
|
|
1495
|
+
if (badge.latest?.tier === "alert" && badge.latest !== this.armedAlert) {
|
|
1496
|
+
this.armedAlert = badge.latest;
|
|
1497
|
+
this.view.alertFocus = true;
|
|
1498
|
+
}
|
|
1499
|
+
this.repaint();
|
|
1500
|
+
}
|
|
1501
|
+
// ---- paint -------------------------------------------------------------
|
|
1502
|
+
lastFrame;
|
|
1503
|
+
/**
|
|
1504
|
+
* The quit prompt, painted over the finished frame.
|
|
1505
|
+
*
|
|
1506
|
+
* Over the COMPOSED frame, not inside a renderer: it must appear on every
|
|
1507
|
+
* tab and over the companion column alike, and no tab should have to know
|
|
1508
|
+
* it exists. Three rows in the middle, so it cannot be mistaken for the
|
|
1509
|
+
* command bar's own `[q quit]` button.
|
|
1510
|
+
*/
|
|
1511
|
+
paintQuitPrompt(frame, cols, rows) {
|
|
1512
|
+
if (!this.view.confirmQuit)
|
|
1513
|
+
return;
|
|
1514
|
+
const text = " Quit qcr tui? [y] yes [any other key] no ";
|
|
1515
|
+
const w = Math.max(2, Math.min(cols - 4, Math.max(text.length + 2, 24)));
|
|
1516
|
+
const x = Math.max(0, Math.floor((cols - w) / 2));
|
|
1517
|
+
const y = Math.max(0, Math.floor(rows / 2) - 1);
|
|
1518
|
+
const box = [`┌${"─".repeat(w - 2)}┐`, `│${fit(text, w - 2)}│`, `└${"─".repeat(w - 2)}┘`];
|
|
1519
|
+
for (let i = 0; i < box.length; i++) {
|
|
1520
|
+
const row = y + i;
|
|
1521
|
+
if (row < 0 || row >= rows)
|
|
1522
|
+
continue;
|
|
1523
|
+
const line = frame.lines[row] ?? "";
|
|
1524
|
+
// Splice by DISPLAY cells: the underlying row is full of SGR runs, and
|
|
1525
|
+
// slicing by code units would cut one in half and bleed colour.
|
|
1526
|
+
frame.set(row, spliceDisplay(line, x, box[i]));
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
/**
|
|
1530
|
+
* Does this wheel notch belong to the companion column's scrollback?
|
|
1531
|
+
*
|
|
1532
|
+
* True when the pointer is inside the column, or when the column holds the
|
|
1533
|
+
* keyboard and the pointer is elsewhere.
|
|
1534
|
+
*/
|
|
1535
|
+
wheelBelongsToSidebar(x, cols) {
|
|
1536
|
+
const sb = this.sidebar;
|
|
1537
|
+
if (!sb)
|
|
1538
|
+
return false;
|
|
1539
|
+
const width = this.sidebarWidth(cols);
|
|
1540
|
+
if (width === 0)
|
|
1541
|
+
return false;
|
|
1542
|
+
if (x > cols - width && x <= cols)
|
|
1543
|
+
return true;
|
|
1544
|
+
return sb.focused;
|
|
1545
|
+
}
|
|
1546
|
+
/**
|
|
1547
|
+
* Width the companion column takes on THIS paint.
|
|
1548
|
+
*
|
|
1549
|
+
* A pure function of the terminal width and the operator's seam drag —
|
|
1550
|
+
* never of config, docker or the container's state: the column is part of
|
|
1551
|
+
* the layout whenever a sidebar exists at all, so the first frame is the final shape and nothing that
|
|
1552
|
+
* happens later can move it. `0` only for a harness that built no sidebar.
|
|
1553
|
+
*/
|
|
1554
|
+
sidebarWidth(cols) {
|
|
1555
|
+
if (!this.sidebar)
|
|
1556
|
+
return 0;
|
|
1557
|
+
return sidebarWidth(cols, { preferredWidth: this.sidebar.preferredWidth });
|
|
1558
|
+
}
|
|
1559
|
+
/** The tab's own width: everything left of the companion column, the
|
|
1560
|
+
* shared border cell included — the width every tab renders and lays its
|
|
1561
|
+
* command bar out at. */
|
|
1562
|
+
innerWidth(cols) {
|
|
1563
|
+
const sw = this.sidebarWidth(cols);
|
|
1564
|
+
return sw === 0 ? cols : cols - sw + 1;
|
|
1565
|
+
}
|
|
1566
|
+
/** Compose the companion column onto a tab's frame. The tab was rendered at
|
|
1567
|
+
* the REDUCED width, so this only ever appends — no coordinate inside the
|
|
1568
|
+
* frame moves, and the tab renderers stay ignorant of the sidebar. */
|
|
1569
|
+
withSidebar(inner, cols, rows) {
|
|
1570
|
+
const width = this.sidebarWidth(cols);
|
|
1571
|
+
if (width === 0 || !this.sidebar)
|
|
1572
|
+
return inner;
|
|
1573
|
+
// joinSeam: the tab's right border and the column's left border are the
|
|
1574
|
+
// same wall, so the inner frame gives up its last cell — which is why the
|
|
1575
|
+
// tab is rendered one column WIDER than `cols - width` (see repaint).
|
|
1576
|
+
return composeColumn(inner, paintSidebar(this.sidebar.view(), width, rows), width, true);
|
|
1577
|
+
}
|
|
1578
|
+
/** Set by run(): the TUI owns the screen and may paint. */
|
|
1579
|
+
started = false;
|
|
1580
|
+
// ---- mouse selection → clipboard -----------------------------------------
|
|
1581
|
+
//
|
|
1582
|
+
// Turning mouse reporting on takes the terminal's own selection away, so the
|
|
1583
|
+
// TUI owes one back. A selection is almost always made to copy it, so there
|
|
1584
|
+
// is no copy key and no copy mode: release IS the copy.
|
|
1585
|
+
//
|
|
1586
|
+
// RECTANGULAR, not linear. The screen is columns side by side — list next to
|
|
1587
|
+
// detail next to the companion — and a linear selection across a row would
|
|
1588
|
+
// splice text from panes the user never pointed at. A block keeps a drag
|
|
1589
|
+
// inside the pane it started in, which is what "select this bit of pi's
|
|
1590
|
+
// output" means beside the companion conversation.
|
|
1591
|
+
sidebarResizeActive = false;
|
|
1592
|
+
selAnchor;
|
|
1593
|
+
selection;
|
|
1594
|
+
/** The last composed frame WITHOUT the highlight — what a drag freezes. */
|
|
1595
|
+
baseFrame;
|
|
1596
|
+
/**
|
|
1597
|
+
* The screen as it was when the drag began, held until release.
|
|
1598
|
+
*
|
|
1599
|
+
* The mirrors refresh under the pointer several times a second while pi
|
|
1600
|
+
* streams: without the freeze the text would scroll out from under a
|
|
1601
|
+
* half-finished drag, and release would copy whatever had arrived in its
|
|
1602
|
+
* place rather than what the operator had highlighted.
|
|
1603
|
+
*/
|
|
1604
|
+
selFrame;
|
|
1605
|
+
/** The pane the drag started in (1-based, inclusive); the block never leaves it. */
|
|
1606
|
+
selPane;
|
|
1607
|
+
/** Injectable so tests never touch the real clipboard. */
|
|
1608
|
+
clipboard = new ClipboardSink();
|
|
1609
|
+
/** The selected block as plain text, exactly as it appears on screen. */
|
|
1610
|
+
selectedText() {
|
|
1611
|
+
const sel = this.selection;
|
|
1612
|
+
const frame = this.selFrame ?? this.baseFrame ?? this.lastFrame;
|
|
1613
|
+
if (!sel || !frame)
|
|
1614
|
+
return "";
|
|
1615
|
+
const rows = [];
|
|
1616
|
+
for (let y = sel.y1; y <= sel.y2; y++) {
|
|
1617
|
+
const line = frame.lines[y - 1];
|
|
1618
|
+
if (line === undefined)
|
|
1619
|
+
continue;
|
|
1620
|
+
// BY DISPLAY CELL, not code unit: what is copied must be what was
|
|
1621
|
+
// highlighted, and a single wide glyph earlier in the row would put
|
|
1622
|
+
// those two out of step.
|
|
1623
|
+
rows.push(sliceDisplay(line, sel.x1 - 1, sel.x2));
|
|
1624
|
+
}
|
|
1625
|
+
return cleanCopiedBlock(rows);
|
|
1626
|
+
}
|
|
1627
|
+
/**
|
|
1628
|
+
* The pane under a point, as the content rectangle a selection may cover
|
|
1629
|
+
* (1-based, inclusive, borders excluded).
|
|
1630
|
+
*
|
|
1631
|
+
* A rectangular block already keeps a drag in its COLUMN band, but a drag
|
|
1632
|
+
* that wandered a cell too far picked up the neighbour's border and first
|
|
1633
|
+
* letters — the board's list beside its detail, the companion column beside
|
|
1634
|
+
* the tab. Each pane is known from the layout, not guessed from glyphs: the
|
|
1635
|
+
* companion column, and on the board the list, the detail and the pi pane
|
|
1636
|
+
* (`frame.meta`, written by renderBoard). Any other tab is one pane.
|
|
1637
|
+
*/
|
|
1638
|
+
paneRectAt(x, y) {
|
|
1639
|
+
const frame = this.baseFrame;
|
|
1640
|
+
if (!frame)
|
|
1641
|
+
return undefined;
|
|
1642
|
+
const cols = frame.cols;
|
|
1643
|
+
const rows = frame.rows;
|
|
1644
|
+
const sw = this.sidebarWidth(cols);
|
|
1645
|
+
// The seam: with a column, the tab's right border IS the column's left one.
|
|
1646
|
+
const seam = sw === 0 ? cols : this.innerWidth(cols);
|
|
1647
|
+
if (sw > 0 && x > seam)
|
|
1648
|
+
return { x1: seam + 1, y1: 2, x2: cols - 1, y2: rows - 1 };
|
|
1649
|
+
const half = { x1: 2, y1: 1, x2: seam - 1, y2: rows };
|
|
1650
|
+
const m = frame.meta;
|
|
1651
|
+
if (this.tab !== "board" || this.view.help || m["listW"] === undefined)
|
|
1652
|
+
return half;
|
|
1653
|
+
const listW = m["listW"];
|
|
1654
|
+
const bodyRows = m["bodyRows"] ?? 0;
|
|
1655
|
+
const piTop = m["piTop"] ?? rows; // 0-based divider row
|
|
1656
|
+
const piRows = m["piRows"] ?? 0;
|
|
1657
|
+
// Body rows are 0-based 1..bodyRows: 1-based 2..bodyRows+1.
|
|
1658
|
+
if (y >= 2 && y <= bodyRows + 1) {
|
|
1659
|
+
return x <= listW + 1
|
|
1660
|
+
? { x1: 2, y1: 2, x2: listW + 1, y2: bodyRows + 1 }
|
|
1661
|
+
: { x1: listW + 3, y1: 2, x2: half.x2, y2: bodyRows + 1 };
|
|
1662
|
+
}
|
|
1663
|
+
// The pi pane: its header row and mirror lines, below the divider.
|
|
1664
|
+
if (piRows > 0 && y >= piTop + 2 && y <= piTop + piRows) {
|
|
1665
|
+
return { x1: 2, y1: piTop + 2, x2: half.x2, y2: piTop + piRows };
|
|
1666
|
+
}
|
|
1667
|
+
return half;
|
|
1668
|
+
}
|
|
1669
|
+
/**
|
|
1670
|
+
* The WHOLE last painted frame as plain text: exactly what is on
|
|
1671
|
+
* screen, ANSI stripped, trailing blank rows dropped. Same source as
|
|
1672
|
+
* `selectedText()` (the last painted frame), for the same reason: what the
|
|
1673
|
+
* companion is shown must be what the operator is looking at, not a re-render of
|
|
1674
|
+
* the model behind it.
|
|
1675
|
+
*
|
|
1676
|
+
* `undefined` before the first frame is painted. Never a fetch: this is a
|
|
1677
|
+
* getter over state already painted.
|
|
1678
|
+
*/
|
|
1679
|
+
screenText() {
|
|
1680
|
+
const frame = this.lastFrame;
|
|
1681
|
+
if (!frame)
|
|
1682
|
+
return undefined;
|
|
1683
|
+
const rows = frame.lines.map((line) => stripAnsi(line).replace(/\s+$/, ""));
|
|
1684
|
+
while (rows.length > 0 && rows[rows.length - 1] === "")
|
|
1685
|
+
rows.pop();
|
|
1686
|
+
return rows.join("\n");
|
|
1687
|
+
}
|
|
1688
|
+
/**
|
|
1689
|
+
* Called at the end of every repaint, so the published view tracks
|
|
1690
|
+
* keystrokes and not just the 3 s poll. Installed by index.ts; the callee
|
|
1691
|
+
* throttles. It must never repaint (that would recurse) and never throw.
|
|
1692
|
+
*/
|
|
1693
|
+
afterRepaint;
|
|
1694
|
+
copySelection() {
|
|
1695
|
+
const text = this.selectedText();
|
|
1696
|
+
this.selAnchor = undefined;
|
|
1697
|
+
if (text.length === 0) {
|
|
1698
|
+
this.selection = undefined;
|
|
1699
|
+
this.repaint();
|
|
1700
|
+
return;
|
|
1701
|
+
}
|
|
1702
|
+
void this.copyText(text);
|
|
1703
|
+
// The highlight stays until the next click, so it is visible at the moment
|
|
1704
|
+
// the confirmation appears — proof of WHAT was copied, not just that
|
|
1705
|
+
// something was.
|
|
1706
|
+
this.repaint();
|
|
1707
|
+
}
|
|
1708
|
+
/**
|
|
1709
|
+
* Put `text` on the system clipboard and say so on the notice line — the
|
|
1710
|
+
* ONE route for every copy the TUI makes: a released drag, and a copy made
|
|
1711
|
+
* inside the companion's container (`from`, e.g. pi's `/copy`), which the
|
|
1712
|
+
* column hands over because nothing else could deliver it (sidebar.ts
|
|
1713
|
+
* `onCopy`).
|
|
1714
|
+
*/
|
|
1715
|
+
async copyText(text, from) {
|
|
1716
|
+
if (text.length === 0)
|
|
1717
|
+
return true;
|
|
1718
|
+
const lines = text.split("\n").length;
|
|
1719
|
+
const what = `${text.length} chars${lines > 1 ? ` · ${lines} lines` : ""}`;
|
|
1720
|
+
try {
|
|
1721
|
+
const ok = await this.clipboard.write(text);
|
|
1722
|
+
this.notice(ok
|
|
1723
|
+
? `✓ copied ${what}${from ? ` from ${from}` : ""}`
|
|
1724
|
+
: "✖ no clipboard tool (wl-copy / xclip / klipper)");
|
|
1725
|
+
return ok;
|
|
1726
|
+
}
|
|
1727
|
+
catch {
|
|
1728
|
+
this.notice("✖ clipboard write failed");
|
|
1729
|
+
return false;
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
/** Paint the selection highlight over the finished frame. */
|
|
1733
|
+
paintSelection(frame, cols, rows) {
|
|
1734
|
+
const sel = this.selection;
|
|
1735
|
+
if (!sel)
|
|
1736
|
+
return;
|
|
1737
|
+
for (let y = sel.y1; y <= sel.y2; y++) {
|
|
1738
|
+
if (y < 1 || y > rows)
|
|
1739
|
+
continue;
|
|
1740
|
+
const line = frame.lines[y - 1];
|
|
1741
|
+
if (line === undefined)
|
|
1742
|
+
continue;
|
|
1743
|
+
const x1 = Math.max(1, Math.min(sel.x1, cols));
|
|
1744
|
+
const x2 = Math.max(1, Math.min(sel.x2, cols));
|
|
1745
|
+
const span = sliceDisplay(line, x1 - 1, x2);
|
|
1746
|
+
if (span.length === 0)
|
|
1747
|
+
continue;
|
|
1748
|
+
frame.set(y - 1, spliceDisplay(line, x1 - 1, `${SELECTION}${span}${RESET}`));
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
/** Fire `afterRepaint` — never let a publisher's failure reach the paint
|
|
1752
|
+
* path. The screen is the product; publishing it is a side effect. */
|
|
1753
|
+
notifyRepaint() {
|
|
1754
|
+
try {
|
|
1755
|
+
this.afterRepaint?.();
|
|
1756
|
+
}
|
|
1757
|
+
catch {
|
|
1758
|
+
/* a failed publish is never worth a broken frame */
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
repaint() {
|
|
1762
|
+
if (!this.started || this.suspended || this.stopped)
|
|
1763
|
+
return;
|
|
1764
|
+
const cols = this.out.columns ?? 80;
|
|
1765
|
+
const rows = this.out.rows ?? 24;
|
|
1766
|
+
// +1 when a column is shown: the tab and the column share one border, and
|
|
1767
|
+
// `withSidebar` drops the tab's last cell to join them (composeColumn's
|
|
1768
|
+
// joinSeam). Without the +1 every tab would lose a usable column.
|
|
1769
|
+
const sw = this.sidebarWidth(cols);
|
|
1770
|
+
// The companion runs in a detached tmux window, whose implicit 80×24 size
|
|
1771
|
+
// otherwise survives forever. Feed the exact painted rectangle back to
|
|
1772
|
+
// the mirror; its next refresh atomically resizes then captures the pane.
|
|
1773
|
+
this.sidebar?.setViewport(sw, rows);
|
|
1774
|
+
const inner = this.innerWidth(cols);
|
|
1775
|
+
const paint = (screen, fallback) => {
|
|
1776
|
+
// An unwired tab (a harness, or a hook that failed to install) paints
|
|
1777
|
+
// its one-line notice INSIDE the layout, never as a bare full-width
|
|
1778
|
+
// string: the column is part of every frame, wired tab or not.
|
|
1779
|
+
// Mid-drag the screen holds still (see selFrame): repaint the frozen
|
|
1780
|
+
// frame with the moving highlight, and nothing new underneath it.
|
|
1781
|
+
const frozen = this.selFrame;
|
|
1782
|
+
const frame = frozen
|
|
1783
|
+
? cloneFrame(frozen)
|
|
1784
|
+
: this.withSidebar(screen ?? placeholderFrame(inner, rows, fallback), cols, rows);
|
|
1785
|
+
// Zellij-style spatial focus: exactly one pane owns the accent square.
|
|
1786
|
+
// The sidebar and board pi are terminals; otherwise the main TUI pane
|
|
1787
|
+
// owns focus. Colour existing frame glyphs only, never the content.
|
|
1788
|
+
const mainRight = sw === 0 ? frame.cols - 1 : inner - 1;
|
|
1789
|
+
if (frozen) {
|
|
1790
|
+
// already composed and accented
|
|
1791
|
+
}
|
|
1792
|
+
else if (sw > 0 && this.sidebar?.focused) {
|
|
1793
|
+
accentFrameRect(frame, inner - 1, 0, frame.cols - 1, rows - 1, ACCENT, RESET);
|
|
1794
|
+
}
|
|
1795
|
+
else if (this.tab === "board" && this.view.piFocus && frame.meta["piTop"] !== undefined) {
|
|
1796
|
+
accentFrameRect(frame, 0, frame.meta["piTop"], mainRight, rows - 1, ACCENT, RESET);
|
|
1797
|
+
}
|
|
1798
|
+
else {
|
|
1799
|
+
accentFrameRect(frame, 0, 0, mainRight, rows - 1, ACCENT, RESET);
|
|
1800
|
+
}
|
|
1801
|
+
if (!frozen)
|
|
1802
|
+
this.baseFrame = cloneFrame(frame);
|
|
1803
|
+
// Selection first, prompt second: the modal must sit ON TOP of a
|
|
1804
|
+
// highlight, never under it.
|
|
1805
|
+
this.paintSelection(frame, cols, rows);
|
|
1806
|
+
this.paintQuitPrompt(frame, cols, rows);
|
|
1807
|
+
this.lastFrame = frame;
|
|
1808
|
+
this.out.write(frame.render());
|
|
1809
|
+
this.notifyRepaint();
|
|
1810
|
+
};
|
|
1811
|
+
// `?` is a global key advertised on every tab's bar, and the KEYS overlay
|
|
1812
|
+
// (renderBoard's help branch) is the only thing that paints it — so while
|
|
1813
|
+
// help is up, every tab shows it. A tab screen here would leave the flag
|
|
1814
|
+
// set with nothing on screen, and the next keystroke silently eaten.
|
|
1815
|
+
if (this.tab !== "board" && !this.view.help) {
|
|
1816
|
+
const screen = this.tabHooks()?.screen(this, inner, rows);
|
|
1817
|
+
// A prompt the tab opened types into the input box: show it there.
|
|
1818
|
+
if (screen && this.view.inputBox)
|
|
1819
|
+
overlayInputBox(screen, this.view.inputBox);
|
|
1820
|
+
paint(screen, `${this.tab} tab not wired — [1] back to board`);
|
|
1821
|
+
return;
|
|
1822
|
+
}
|
|
1823
|
+
this.view.now = this.now();
|
|
1824
|
+
// Same reason as the sidebar above: a debug round's or a takeover's pi runs
|
|
1825
|
+
// in a detached tmux window stuck at 80×24, so without this it wraps at 80
|
|
1826
|
+
// columns however wide we paint the mirror. Fed here, applied by the next capture — and
|
|
1827
|
+
// sized against the bar rows THIS frame's board actually paints.
|
|
1828
|
+
const paneSize = piPaneSize(inner, rows, boardBarHeight(this.view, inner));
|
|
1829
|
+
this.pi.setViewport(paneSize);
|
|
1830
|
+
// The viewer keeps its last pane row for the input line.
|
|
1831
|
+
this.viewer.setViewport(paneSize && { ...paneSize, rows: Math.max(1, paneSize.rows - 1) });
|
|
1832
|
+
paint(renderBoard(this.view, inner, rows), "");
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
//# sourceMappingURL=app.js.map
|