talos-code 0.0.1 → 0.2.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/CHANGELOG.md +87 -0
- package/LICENSE +661 -0
- package/README.md +116 -3
- package/THIRD_PARTY_NOTICES.md +40 -0
- package/dist/archive/zip.js +73 -0
- package/dist/args.js +100 -0
- package/dist/automations/history-store.js +34 -0
- package/dist/automations/policy-store.js +43 -0
- package/dist/automations/runner.js +107 -0
- package/dist/automations/schedule.js +85 -0
- package/dist/automations/windows-task.js +53 -0
- package/dist/commands/advanced-cli.js +233 -0
- package/dist/commands/checkpoint-cli.js +45 -0
- package/dist/commands/config-cli.js +107 -0
- package/dist/commands/context.js +17 -0
- package/dist/commands/extensions-cli.js +207 -0
- package/dist/commands/project-cli.js +74 -0
- package/dist/commands/project-commands.js +143 -0
- package/dist/commands/provider-cli.js +244 -0
- package/dist/commands/services-cli.js +248 -0
- package/dist/commands/session-cli.js +171 -0
- package/dist/commands/system-cli.js +405 -0
- package/dist/config/commands.js +60 -0
- package/dist/config/load.js +382 -0
- package/dist/config/migrations.js +48 -0
- package/dist/config/types.js +11 -0
- package/dist/diagnostics/development-log.js +154 -0
- package/dist/diagnostics/doctor.js +104 -0
- package/dist/diagnostics/redact.js +80 -0
- package/dist/diagnostics/zip.js +52 -0
- package/dist/errors.js +156 -0
- package/dist/events/bridge.js +130 -0
- package/dist/extensions/installer.js +178 -0
- package/dist/extensions/package-schema.js +24 -0
- package/dist/headless/result.js +84 -0
- package/dist/headless/run.js +230 -0
- package/dist/i18n/en/approval.js +46 -0
- package/dist/i18n/en/common.js +9 -0
- package/dist/i18n/en/credentials.js +92 -0
- package/dist/i18n/en/errors.js +267 -0
- package/dist/i18n/en/firstrun.js +101 -0
- package/dist/i18n/en/providers.js +37 -0
- package/dist/i18n/en/screen.js +774 -0
- package/dist/i18n/en/tools.js +127 -0
- package/dist/i18n/en.js +14 -0
- package/dist/i18n/error-view.js +307 -0
- package/dist/i18n/index.js +19 -0
- package/dist/i18n/kernel-map.js +139 -0
- package/dist/io.js +45 -0
- package/dist/main.js +331 -0
- package/dist/paths.js +41 -0
- package/dist/protocol/v2/codec.js +9 -0
- package/dist/protocol/v2/events.js +555 -0
- package/dist/protocol/v2/index.js +4 -0
- package/dist/protocol/v2/replay.js +28 -0
- package/dist/protocol/v2/types.js +1 -0
- package/dist/provider/control-plane.js +135 -0
- package/dist/provider/environment-keys.js +275 -0
- package/dist/provider/health.js +110 -0
- package/dist/provider/missing-key.js +48 -0
- package/dist/provider/model-catalog.js +168 -0
- package/dist/provider/openrouter-login.js +172 -0
- package/dist/provider/provider-text.js +13 -0
- package/dist/provider/store.js +95 -0
- package/dist/provider/system-keyring.js +214 -0
- package/dist/runtime/active-run.js +63 -0
- package/dist/runtime/agent-tree.js +74 -0
- package/dist/runtime/attachments.js +84 -0
- package/dist/runtime/brokered-executor.js +395 -0
- package/dist/runtime/context-archive.js +69 -0
- package/dist/runtime/context-status.js +76 -0
- package/dist/runtime/create-runtime.js +111 -0
- package/dist/runtime/model-profile.js +361 -0
- package/dist/runtime/output-store.js +137 -0
- package/dist/runtime/provider-attempts.js +185 -0
- package/dist/runtime/replay-buffer.js +153 -0
- package/dist/runtime/repo.js +95 -0
- package/dist/runtime/session-facade.js +135 -0
- package/dist/runtime/session-summary.js +125 -0
- package/dist/runtime/supervisor.js +265 -0
- package/dist/runtime/talos-composition.js +1011 -0
- package/dist/runtime/types.js +5 -0
- package/dist/runtime/usage-snapshot.js +82 -0
- package/dist/security/auto-classifier.js +38 -0
- package/dist/security/credential-free-environment.js +54 -0
- package/dist/security/evaluate.js +243 -0
- package/dist/security/execution-backends.js +236 -0
- package/dist/security/execution-broker.js +102 -0
- package/dist/security/forge-scan.js +74 -0
- package/dist/security/from-approval.js +39 -0
- package/dist/security/mxc-execution-backend.js +281 -0
- package/dist/security/permission-engine.js +138 -0
- package/dist/security/permission-explanation.js +54 -0
- package/dist/security/persist.js +179 -0
- package/dist/security/plugin-guard.js +230 -0
- package/dist/security/process-tree-evidence-store.js +74 -0
- package/dist/security/process-tree-probe.js +554 -0
- package/dist/security/project-resource-inventory.js +186 -0
- package/dist/security/project-trust-gate.js +38 -0
- package/dist/security/project-trust.js +367 -0
- package/dist/security/rule-parser.js +201 -0
- package/dist/security/shell-segmentation.js +68 -0
- package/dist/security/trust-authority.js +324 -0
- package/dist/security/types.js +1 -0
- package/dist/security/workspace-identity.js +102 -0
- package/dist/services/automation-facade.js +59 -0
- package/dist/services/forge-facade.js +77 -0
- package/dist/services/hook-facade.js +240 -0
- package/dist/services/index.js +16 -0
- package/dist/services/library-facade.js +174 -0
- package/dist/services/mcp-facade.js +348 -0
- package/dist/services/memory-facade.js +126 -0
- package/dist/services/notes-facade.js +157 -0
- package/dist/services/plugin-facade.js +308 -0
- package/dist/services/research-facade.js +110 -0
- package/dist/services/task-board-facade.js +236 -0
- package/dist/services/workflow-catalog.js +246 -0
- package/dist/sessions/export-format.js +98 -0
- package/dist/sessions/metadata-store.js +160 -0
- package/dist/sessions/share.js +122 -0
- package/dist/sessions/transfer.js +16 -0
- package/dist/subcommands.js +62 -0
- package/dist/tui/agent-roster.js +36 -0
- package/dist/tui/agent-view.js +51 -0
- package/dist/tui/app.js +4098 -0
- package/dist/tui/approval.js +36 -0
- package/dist/tui/boot/boot-sequence.js +76 -0
- package/dist/tui/boot/cinematic.js +243 -0
- package/dist/tui/boot/desktop-logo.js +82 -0
- package/dist/tui/boot.js +3 -0
- package/dist/tui/busy-input.js +58 -0
- package/dist/tui/catalog-service.js +560 -0
- package/dist/tui/components/assistant-stream.js +1 -0
- package/dist/tui/components/command-menu.js +68 -0
- package/dist/tui/components/composer.js +219 -0
- package/dist/tui/components/diff.js +35 -0
- package/dist/tui/components/footer.js +48 -0
- package/dist/tui/components/header.js +6 -0
- package/dist/tui/components/markdown.js +305 -0
- package/dist/tui/components/status-indicator.js +38 -0
- package/dist/tui/components/terminal-shell.js +214 -0
- package/dist/tui/components/thinking-row.js +19 -0
- package/dist/tui/components/tool-row.js +97 -0
- package/dist/tui/components/transcript-virtualizer.js +165 -0
- package/dist/tui/components/transcript.js +43 -0
- package/dist/tui/descendant-approvals.js +104 -0
- package/dist/tui/diff-model.js +77 -0
- package/dist/tui/editor-history.js +21 -0
- package/dist/tui/editor.js +210 -0
- package/dist/tui/event-adapter.js +436 -0
- package/dist/tui/exit-output.js +58 -0
- package/dist/tui/external-editor.js +53 -0
- package/dist/tui/file-completion.js +89 -0
- package/dist/tui/focus-manager.js +5 -0
- package/dist/tui/highlight.js +30 -0
- package/dist/tui/input-router.js +18 -0
- package/dist/tui/interrupt.js +99 -0
- package/dist/tui/keybindings.js +194 -0
- package/dist/tui/keymap-resolver.js +91 -0
- package/dist/tui/launch-state.js +148 -0
- package/dist/tui/line-diff.js +57 -0
- package/dist/tui/live-activity.js +56 -0
- package/dist/tui/metrics.js +123 -0
- package/dist/tui/onboarding.js +36 -0
- package/dist/tui/overlays/agent-tree.js +43 -0
- package/dist/tui/overlays/approval-dialog.js +642 -0
- package/dist/tui/overlays/automation-center.js +51 -0
- package/dist/tui/overlays/checkpoint-picker.js +47 -0
- package/dist/tui/overlays/context-inspector.js +36 -0
- package/dist/tui/overlays/effort-line.js +110 -0
- package/dist/tui/overlays/forge-center.js +46 -0
- package/dist/tui/overlays/help-dialog.js +16 -0
- package/dist/tui/overlays/history-picker.js +37 -0
- package/dist/tui/overlays/hook-center.js +70 -0
- package/dist/tui/overlays/library-center.js +55 -0
- package/dist/tui/overlays/mcp-center.js +60 -0
- package/dist/tui/overlays/memory-center.js +63 -0
- package/dist/tui/overlays/model-picker.js +77 -0
- package/dist/tui/overlays/notes-center.js +53 -0
- package/dist/tui/overlays/overlay-host.js +8 -0
- package/dist/tui/overlays/plugin-center.js +76 -0
- package/dist/tui/overlays/provider-picker.js +171 -0
- package/dist/tui/overlays/queue-editor.js +32 -0
- package/dist/tui/overlays/research-center.js +59 -0
- package/dist/tui/overlays/scroll-window.js +234 -0
- package/dist/tui/overlays/session-picker.js +115 -0
- package/dist/tui/overlays/tasks-center.js +83 -0
- package/dist/tui/overlays/theme-picker.js +21 -0
- package/dist/tui/overlays/transcript-search.js +58 -0
- package/dist/tui/overlays/trust-center.js +29 -0
- package/dist/tui/overlays/workflow-center.js +46 -0
- package/dist/tui/project-file-index.js +214 -0
- package/dist/tui/project-references.js +85 -0
- package/dist/tui/project-trust-prompt.js +289 -0
- package/dist/tui/prompt-history-store.js +116 -0
- package/dist/tui/queue-store.js +110 -0
- package/dist/tui/regions/budget.js +59 -0
- package/dist/tui/regions/views.js +96 -0
- package/dist/tui/render-coordinator.js +148 -0
- package/dist/tui/render-scheduler.js +4 -0
- package/dist/tui/run.js +69 -0
- package/dist/tui/selection-list.js +29 -0
- package/dist/tui/session-controller.js +791 -0
- package/dist/tui/session-export.js +54 -0
- package/dist/tui/setup-wizard.js +46 -0
- package/dist/tui/shell-input.js +35 -0
- package/dist/tui/shell-layout.js +48 -0
- package/dist/tui/shell-model.js +98 -0
- package/dist/tui/slash-commands.js +82 -0
- package/dist/tui/state.js +151 -0
- package/dist/tui/status-bar.js +125 -0
- package/dist/tui/status-view.js +40 -0
- package/dist/tui/terminal-capabilities.js +9 -0
- package/dist/tui/terminal-session.js +11 -0
- package/dist/tui/text-width.js +66 -0
- package/dist/tui/theme-catalog.js +27 -0
- package/dist/tui/theme-store.js +23 -0
- package/dist/tui/theme.js +23 -0
- package/dist/tui/tool-display.js +135 -0
- package/dist/tui/tool-facts.js +1 -0
- package/dist/tui/tools/bash-renderer.js +32 -0
- package/dist/tui/tools/change.js +61 -0
- package/dist/tui/tools/edit-renderer.js +17 -0
- package/dist/tui/tools/generic-renderer.js +13 -0
- package/dist/tui/tools/list-renderer.js +14 -0
- package/dist/tui/tools/read-renderer.js +13 -0
- package/dist/tui/tools/registry.js +20 -0
- package/dist/tui/tools/row-format.js +187 -0
- package/dist/tui/tools/search-renderer.js +38 -0
- package/dist/tui/tools/shared.js +97 -0
- package/dist/tui/tools/write-renderer.js +15 -0
- package/dist/tui/transcript-model.js +441 -0
- package/dist/tui/ui-preferences.js +79 -0
- package/dist/tui/usage-view.js +77 -0
- package/dist/tui/vim-mode.js +99 -0
- package/dist/update/check.js +15 -0
- package/dist/update/npm.js +51 -0
- package/dist/update/run.js +129 -0
- package/dist/version.js +2 -0
- package/dist/workspace/checkpoint-store.js +210 -0
- package/dist/workspace/checkpoint.js +413 -0
- package/dist/workspace/restore.js +232 -0
- package/package.json +64 -5
- package/vendor/context-engine/package.json +11 -0
- package/vendor/context-engine/src/compaction-planner.mjs +57 -0
- package/vendor/context-engine/src/contracts.mjs +48 -0
- package/vendor/context-engine/src/engine.mjs +445 -0
- package/vendor/context-engine/src/node/context-export.mjs +164 -0
- package/vendor/context-engine/src/node/legacy-import.mjs +92 -0
- package/vendor/context-engine/src/node/migrations/001-context.sql +126 -0
- package/vendor/context-engine/src/node/sqlite-store.mjs +85 -0
- package/vendor/context-engine/src/node/sqlite-worker.mjs +559 -0
- package/vendor/context-engine/src/profiles.mjs +10 -0
- package/vendor/context-engine/src/retrieval.mjs +104 -0
- package/vendor/context-engine/src/summary.mjs +97 -0
- package/vendor/context-engine/src/usage.mjs +34 -0
- package/vendor/harness-ui/package.json +38 -0
- package/vendor/harness-ui/src/acp-agent.mjs +350 -0
- package/vendor/harness-ui/src/agent-service.mjs +2188 -0
- package/vendor/harness-ui/src/agui-events.mjs +452 -0
- package/vendor/harness-ui/src/ambiente-solo-server.mjs +121 -0
- package/vendor/harness-ui/src/artifact-store.mjs +39 -0
- package/vendor/harness-ui/src/assistenza.mjs +123 -0
- package/vendor/harness-ui/src/automation-scheduler.mjs +69 -0
- package/vendor/harness-ui/src/automation-store.mjs +145 -0
- package/vendor/harness-ui/src/browser-annota.mjs +639 -0
- package/vendor/harness-ui/src/browser-frame.mjs +211 -0
- package/vendor/harness-ui/src/browser-proxy-universale.mjs +519 -0
- package/vendor/harness-ui/src/browser-proxy.mjs +87 -0
- package/vendor/harness-ui/src/browser-sessione-viva.mjs +329 -0
- package/vendor/harness-ui/src/browser-stream.mjs +445 -0
- package/vendor/harness-ui/src/browser-vivo.mjs +694 -0
- package/vendor/harness-ui/src/chat-image-attachments.mjs +72 -0
- package/vendor/harness-ui/src/config.mjs +697 -0
- package/vendor/harness-ui/src/contesto-del-progetto.mjs +385 -0
- package/vendor/harness-ui/src/context-asset-adapter.mjs +72 -0
- package/vendor/harness-ui/src/context-desktop-service.mjs +253 -0
- package/vendor/harness-ui/src/context-embedding-runtime.mjs +252 -0
- package/vendor/harness-ui/src/context-inference-scheduler.mjs +81 -0
- package/vendor/harness-ui/src/context-native-compaction.mjs +75 -0
- package/vendor/harness-ui/src/context-provider-adapter.mjs +141 -0
- package/vendor/harness-ui/src/context-runtime.mjs +118 -0
- package/vendor/harness-ui/src/context-token-counters.mjs +184 -0
- package/vendor/harness-ui/src/context-tool-catalog.mjs +86 -0
- package/vendor/harness-ui/src/context-tool-output.mjs +72 -0
- package/vendor/harness-ui/src/costo-elenco.mjs +252 -0
- package/vendor/harness-ui/src/custom-task.mjs +171 -0
- package/vendor/harness-ui/src/doctor.mjs +142 -0
- package/vendor/harness-ui/src/document-filename.mjs +97 -0
- package/vendor/harness-ui/src/document-generator.mjs +493 -0
- package/vendor/harness-ui/src/document-report.mjs +331 -0
- package/vendor/harness-ui/src/duckduckgo-search.mjs +155 -0
- package/vendor/harness-ui/src/elenco-profondo.mjs +337 -0
- package/vendor/harness-ui/src/favicon-proxy.mjs +113 -0
- package/vendor/harness-ui/src/forge-contract.mjs +221 -0
- package/vendor/harness-ui/src/frequent-dirs.mjs +134 -0
- package/vendor/harness-ui/src/generated-image-store.mjs +147 -0
- package/vendor/harness-ui/src/generation-idle.mjs +332 -0
- package/vendor/harness-ui/src/gguf-header.mjs +207 -0
- package/vendor/harness-ui/src/git-service.mjs +626 -0
- package/vendor/harness-ui/src/gitignore-elenco.mjs +604 -0
- package/vendor/harness-ui/src/harness-receipt-keypair.mjs +207 -0
- package/vendor/harness-ui/src/hf-direct-transfer.mjs +170 -0
- package/vendor/harness-ui/src/hf-hub-client.mjs +106 -0
- package/vendor/harness-ui/src/hf-image-proxy.mjs +105 -0
- package/vendor/harness-ui/src/hf-model-transfer.mjs +245 -0
- package/vendor/harness-ui/src/hook-registry.mjs +186 -0
- package/vendor/harness-ui/src/http-app.mjs +6259 -0
- package/vendor/harness-ui/src/http-lifecycle.mjs +132 -0
- package/vendor/harness-ui/src/id-archivio.mjs +27 -0
- package/vendor/harness-ui/src/image-generator.mjs +143 -0
- package/vendor/harness-ui/src/istruzioni-di-progetto.mjs +234 -0
- package/vendor/harness-ui/src/kernel/dist/kernelPerIlBanco.js +518 -0
- package/vendor/harness-ui/src/kernel/talosHarness.mjs +10437 -0
- package/vendor/harness-ui/src/library-policy-store.mjs +175 -0
- package/vendor/harness-ui/src/library-store.mjs +652 -0
- package/vendor/harness-ui/src/llama-server-supervisor.mjs +629 -0
- package/vendor/harness-ui/src/local-model-store.mjs +339 -0
- package/vendor/harness-ui/src/local-runtime-contract.mjs +66 -0
- package/vendor/harness-ui/src/local-runtime-events.mjs +44 -0
- package/vendor/harness-ui/src/local-runtime-llama-server.mjs +244 -0
- package/vendor/harness-ui/src/local-runtime-probe.mjs +401 -0
- package/vendor/harness-ui/src/machine-capacity.mjs +66 -0
- package/vendor/harness-ui/src/mappa-cartelle.mjs +491 -0
- package/vendor/harness-ui/src/mcp-client.mjs +98 -0
- package/vendor/harness-ui/src/mcp-registry.mjs +157 -0
- package/vendor/harness-ui/src/mcp-session.mjs +177 -0
- package/vendor/harness-ui/src/memory-store.mjs +227 -0
- package/vendor/harness-ui/src/model-catalog-models-dev.mjs +276 -0
- package/vendor/harness-ui/src/model-catalog.mjs +129 -0
- package/vendor/harness-ui/src/model-destination.mjs +189 -0
- package/vendor/harness-ui/src/modifica-ancorata.mjs +177 -0
- package/vendor/harness-ui/src/native-provider-adapter.mjs +205 -0
- package/vendor/harness-ui/src/notes-store.mjs +250 -0
- package/vendor/harness-ui/src/openai-compatible-runtime.mjs +428 -0
- package/vendor/harness-ui/src/openrouter-oauth.mjs +339 -0
- package/vendor/harness-ui/src/path-policy.mjs +442 -0
- package/vendor/harness-ui/src/plugin-registry.mjs +780 -0
- package/vendor/harness-ui/src/plugin-session.mjs +180 -0
- package/vendor/harness-ui/src/process-policy.mjs +345 -0
- package/vendor/harness-ui/src/prompt-enhancer-provider.mjs +94 -0
- package/vendor/harness-ui/src/provider-auth-cloud.mjs +95 -0
- package/vendor/harness-ui/src/provider-credential-store.mjs +541 -0
- package/vendor/harness-ui/src/provider-probe.mjs +582 -0
- package/vendor/harness-ui/src/provider-registry.mjs +1633 -0
- package/vendor/harness-ui/src/pty-terminal.mjs +312 -0
- package/vendor/harness-ui/src/public-problem.mjs +109 -0
- package/vendor/harness-ui/src/research/card.mjs +235 -0
- package/vendor/harness-ui/src/research/citations.mjs +142 -0
- package/vendor/harness-ui/src/research/collector.mjs +275 -0
- package/vendor/harness-ui/src/research/deposito-a-pezzi.mjs +139 -0
- package/vendor/harness-ui/src/research/dossier.mjs +114 -0
- package/vendor/harness-ui/src/research/esportazioni.mjs +560 -0
- package/vendor/harness-ui/src/research/fetch-cache.mjs +465 -0
- package/vendor/harness-ui/src/research/fidelity.mjs +122 -0
- package/vendor/harness-ui/src/research/independence.mjs +159 -0
- package/vendor/harness-ui/src/research/ledger.mjs +166 -0
- package/vendor/harness-ui/src/research/markdown-server.mjs +565 -0
- package/vendor/harness-ui/src/research/narration.mjs +181 -0
- package/vendor/harness-ui/src/research/open-cards.mjs +131 -0
- package/vendor/harness-ui/src/research/opposing.mjs +305 -0
- package/vendor/harness-ui/src/research/outline.mjs +111 -0
- package/vendor/harness-ui/src/research/page-budget.mjs +209 -0
- package/vendor/harness-ui/src/research/pdf.mjs +291 -0
- package/vendor/harness-ui/src/research/plan.mjs +301 -0
- package/vendor/harness-ui/src/research/raccolta-viva.mjs +452 -0
- package/vendor/harness-ui/src/research/recheck-document.mjs +69 -0
- package/vendor/harness-ui/src/research/recheck-history.mjs +192 -0
- package/vendor/harness-ui/src/research/recheck.mjs +194 -0
- package/vendor/harness-ui/src/research/report.mjs +203 -0
- package/vendor/harness-ui/src/research/run.mjs +527 -0
- package/vendor/harness-ui/src/research/synthesis.mjs +318 -0
- package/vendor/harness-ui/src/research/verification.mjs +572 -0
- package/vendor/harness-ui/src/research-orchestrator.mjs +2679 -0
- package/vendor/harness-ui/src/research-store.mjs +1133 -0
- package/vendor/harness-ui/src/runtime-build-manifest.mjs +26 -0
- package/vendor/harness-ui/src/runtime-contract.mjs +59 -0
- package/vendor/harness-ui/src/runtime-owner-adapter.mjs +1348 -0
- package/vendor/harness-ui/src/runtime-owner-contract.mjs +32 -0
- package/vendor/harness-ui/src/scheda-di-lavoro.mjs +249 -0
- package/vendor/harness-ui/src/search-source-store.mjs +172 -0
- package/vendor/harness-ui/src/session-registry.mjs +6095 -0
- package/vendor/harness-ui/src/session-store.mjs +220 -0
- package/vendor/harness-ui/src/sessione-pronta.mjs +73 -0
- package/vendor/harness-ui/src/setup-stato.mjs +31 -0
- package/vendor/harness-ui/src/sezioni-istruzioni.mjs +204 -0
- package/vendor/harness-ui/src/skill-registry.mjs +120 -0
- package/vendor/harness-ui/src/sse-replay-coalescente.mjs +0 -0
- package/vendor/harness-ui/src/static-files.mjs +96 -0
- package/vendor/harness-ui/src/stream-partition.mjs +123 -0
- package/vendor/harness-ui/src/subagent-orchestrator.mjs +453 -0
- package/vendor/harness-ui/src/task-catalog.mjs +65 -0
- package/vendor/harness-ui/src/tasks-store.mjs +220 -0
- package/vendor/harness-ui/src/terminal-registry.mjs +312 -0
- package/vendor/harness-ui/src/terminal-ws.mjs +170 -0
- package/vendor/harness-ui/src/tool-forge-store.mjs +299 -0
- package/vendor/harness-ui/src/tool-schema-normalize.mjs +100 -0
- package/vendor/harness-ui/src/usage-cache.mjs +315 -0
- package/vendor/harness-ui/src/workspace-browser.mjs +213 -0
- package/vendor/harness-ui/src/workspace-context.mjs +124 -0
- package/vendor/harness-ui/src/workspace-disk.mjs +62 -0
- package/vendor/harness-ui/src/workspace-files.mjs +589 -0
- package/vendor/harness-ui/src/workspace-info.mjs +189 -0
- package/vendor/harness-ui/src/workspace-launch-store.mjs +150 -0
- package/vendor/harness-ui/src/workspace-tree.mjs +67 -0
- package/vendor/harness-ui/src/workspace-watcher.mjs +161 -0
- package/vendor/manifest.json +170 -0
package/dist/tui/app.js
ADDED
|
@@ -0,0 +1,4098 @@
|
|
|
1
|
+
import { keySavedText } from "../provider/store.js";
|
|
2
|
+
import { developmentLog, developmentLogError, developmentTextEvidence } from "../diagnostics/development-log.js";
|
|
3
|
+
import { describeError, errorViewLines } from "../i18n/error-view.js";
|
|
4
|
+
import { t, tn } from "../i18n/index.js";
|
|
5
|
+
import { execFile } from 'node:child_process';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { promisify } from 'node:util';
|
|
8
|
+
import { benchedKeyText, unusableKeyRefusal } from "./catalog-service.js";
|
|
9
|
+
import { providerOfModel, screenChildEnvironment } from "../provider/environment-keys.js";
|
|
10
|
+
import { createBootSequenceComponent } from "./boot/boot-sequence.js";
|
|
11
|
+
import { completeBoot, createBootState, markBootError, markBootReady, shouldRenderBootLogo, skipBoot } from "./boot/boot-sequence.js";
|
|
12
|
+
import { commandMenuFastTextInput, commandMenuHighlight, commandMenuItems, commandMenuOverlay, completeCommandSelection, createCommandMenuSelectionStore } from "./components/command-menu.js";
|
|
13
|
+
import { applyComposerFastEditorAction, applyComposerFastPasteInput, composerFastEditorAction, composerFastPasteInput, composerFastTextInput, composerOwnsPaste, createComposerStore, planComposerHistoryAction, reverseHistoryMatch } from "./components/composer.js";
|
|
14
|
+
import { headerLine } from "./components/header.js";
|
|
15
|
+
import { createMarkdownParseMemo, markdownBlockLines, markdownRenderPropsEqual, markdownSafeText, parseMarkdownInline } from "./components/markdown.js";
|
|
16
|
+
import { renderDiffModel } from "./components/diff.js";
|
|
17
|
+
import { parseUnifiedDiff } from "./diff-model.js";
|
|
18
|
+
import { activityWord, busyIndicatorText } from "./components/status-indicator.js";
|
|
19
|
+
import { isAnimatedActivity, retryCountdownEnd } from "./live-activity.js";
|
|
20
|
+
import { smallWindowNotice, usageFacts, usageViewText } from "./usage-view.js";
|
|
21
|
+
import { exportSession } from "./session-export.js";
|
|
22
|
+
import { secretValuesFromEnvironment } from "../diagnostics/redact.js";
|
|
23
|
+
import { busyCommand, busyEnterAction, busyTip, tipsOnEnter } from "./busy-input.js";
|
|
24
|
+
import { saveUiPreference, useUiPreferences } from "./ui-preferences.js";
|
|
25
|
+
import { thoughtLine, visibleTranscriptItems as visibleItemsOf } from "./components/thinking-row.js";
|
|
26
|
+
import { agentRosterLine, frameRowsFor } from "./components/terminal-shell.js";
|
|
27
|
+
import { toolRowView } from "./components/tool-row.js";
|
|
28
|
+
/* Lane RD contract (R3): toolRowView(row,width,expanded,now?) draws `running 12 s` from `now`; a renderer without the
|
|
29
|
+
parameter ignores it. */
|
|
30
|
+
const toolRowWithClock = toolRowView;
|
|
31
|
+
import { createTranscriptMeasurementStore, estimateTranscriptItemRows, planTranscriptVirtualWindow, transcriptAnnouncement, transcriptMeasurementKey } from "./components/transcript-virtualizer.js";
|
|
32
|
+
import { copySelection, createEditorState, deleteBackward, deleteForward, draftOf, editInsert, editorFromDraft, insertPaste, killToEnd, killToStart, killWordBackward, moveCursor, moveVertical, moveWord, previousGraphemeBoundary, redoEditor, replaceEditorText, undoEditor, yank } from "./editor.js";
|
|
33
|
+
import { editExternally } from "./external-editor.js";
|
|
34
|
+
import { FILE_COMPLETION_MAX_ROWS, completeProjectFiles, fileCompletionOverlay } from "./file-completion.js";
|
|
35
|
+
import { createProjectFileIndexCache } from "./project-file-index.js";
|
|
36
|
+
import { permissionRuleCounts, usageLine } from "./status-view.js";
|
|
37
|
+
import { filterHistory, historyPickerOverlay } from "./overlays/history-picker.js";
|
|
38
|
+
import { closeFocus, createFocusState, openFocus } from "./focus-manager.js";
|
|
39
|
+
import { createInterruptController, EXIT_CONFIRM_WINDOW_MS, exitHintText } from "./interrupt.js";
|
|
40
|
+
import { bootPlan, currentBuildId } from "./launch-state.js";
|
|
41
|
+
import { CLI_VERSION, TALOS_BASE_COMMIT } from "../version.js";
|
|
42
|
+
import { KEYBINDINGS, cyclePermissionMode, resolveKeybinding, sanitizeInputText, splitInputChunk } from "./keybindings.js";
|
|
43
|
+
import { APPROVAL_DEFAULT_SELECTED, approvalDialogFit, approvalDialogModel, approvalInputGate, approvalKeyOutcome, renderApprovalDialog } from "./overlays/approval-dialog.js";
|
|
44
|
+
import { fitOverlay, renderFittedOverlay, renderOverlay } from "./overlays/scroll-window.js";
|
|
45
|
+
import { commitModelSelection, createModelPickerModel, modelPickerOverlay } from "./overlays/model-picker.js";
|
|
46
|
+
import { effortLineOverlay, orderedEfforts } from "./overlays/effort-line.js";
|
|
47
|
+
import { consentQuestion, createProviderPickerModel, maskedSecret, providerConsentOverlay, providerKeyOverlay, providerKeySourceLabel, providerPickerEnter, providerPickerItems, providerPickerOverlay, providerLoginChoiceOverlay, providerOAuthOverlay } from "./overlays/provider-picker.js";
|
|
48
|
+
import { createSessionPickerModel, filterSessions, sessionPickerOverlay } from "./overlays/session-picker.js";
|
|
49
|
+
import { agentTreeOverlay as agentTreeOverlayContent, createAgentTreeOverlayModel, moveAgentTreeSelection, selectedAgentRow } from "./overlays/agent-tree.js";
|
|
50
|
+
import { createAgentView } from "./agent-view.js";
|
|
51
|
+
import { agentRosterRows } from "./agent-roster.js";
|
|
52
|
+
import { helpDialogOverlay } from "./overlays/help-dialog.js";
|
|
53
|
+
import { queueEditorOverlay, queueEditorSelected } from "./overlays/queue-editor.js";
|
|
54
|
+
import { contextInspectorOverlay } from "./overlays/context-inspector.js";
|
|
55
|
+
import { createTranscriptSearchState, moveTranscriptSearchSelection, selectedTranscriptMatch, transcriptSearchMatches, transcriptSearchOverlay } from "./overlays/transcript-search.js";
|
|
56
|
+
import { trustCenterModel, trustCenterOverlay } from "./overlays/trust-center.js";
|
|
57
|
+
import { createMcpCenterModel, moveMcpCenterSelection, mcpCenterOverlay, replaceMcpCenterRow, selectedMcpServer } from "./overlays/mcp-center.js";
|
|
58
|
+
import { createMcpFacade } from "../services/mcp-facade.js";
|
|
59
|
+
import { createHookCenterModel, moveHookCenterSelection, hookCenterOverlay, replaceHookCenterRow, selectedHook } from "./overlays/hook-center.js";
|
|
60
|
+
import { createHookFacade } from "../services/hook-facade.js";
|
|
61
|
+
import { createPluginCenterModel, movePluginCenterSelection, pluginCenterOverlay, selectedPlugin } from "./overlays/plugin-center.js";
|
|
62
|
+
import { createPluginFacade } from "../services/plugin-facade.js";
|
|
63
|
+
import { createMemoryCenterModel, moveMemoryCenterSelection, memoryCenterOverlay, selectedMemory } from "./overlays/memory-center.js";
|
|
64
|
+
import { createMemoryFacade } from "../services/memory-facade.js";
|
|
65
|
+
import { createNotesCenterModel, moveNotesCenterSelection, notesCenterOverlay, selectedNote } from "./overlays/notes-center.js";
|
|
66
|
+
import { createNotesFacade } from "../services/notes-facade.js";
|
|
67
|
+
import { createTasksCenterModel, moveTasksCenterSelection, tasksCenterOverlay, selectedTask } from "./overlays/tasks-center.js";
|
|
68
|
+
import { createTaskBoardFacade } from "../services/task-board-facade.js";
|
|
69
|
+
import { createLibraryCenterModel, moveLibraryCenterSelection, libraryCenterOverlay, selectedLibraryEntry, setLibraryCenterDuplicateScan, setLibraryCenterPreview } from "./overlays/library-center.js";
|
|
70
|
+
import { attachLibraryContextToken, createLibraryFacade, detachLibraryContextToken } from "../services/library-facade.js";
|
|
71
|
+
import { createResearchCenterModel, moveResearchCenterSelection, researchCenterOverlay, selectedResearch, setResearchCenterDetail } from "./overlays/research-center.js";
|
|
72
|
+
import { createResearchFacade } from "../services/research-facade.js";
|
|
73
|
+
import { createAutomationCenterModel, moveAutomationCenterSelection, automationCenterOverlay, selectedAutomation, setAutomationCenterDetail } from "./overlays/automation-center.js";
|
|
74
|
+
import { createAutomationFacade } from "../services/automation-facade.js";
|
|
75
|
+
import { createForgeCenterModel, moveForgeCenterSelection, forgeCenterOverlay, selectedForgeTool, setForgeCenterDetail } from "./overlays/forge-center.js";
|
|
76
|
+
import { createForgeFacade } from "../services/forge-facade.js";
|
|
77
|
+
import { createTrustAuthority } from "../security/trust-authority.js";
|
|
78
|
+
import { createSessionMetadataStore, sessionMetadataRoot } from "../sessions/metadata-store.js";
|
|
79
|
+
import { moveSelection } from "./selection-list.js";
|
|
80
|
+
import { createTuiSessionController, TuiNotReadyError } from "./session-controller.js";
|
|
81
|
+
import { parseSlashCommand, SLASH_COMMANDS } from "./slash-commands.js";
|
|
82
|
+
import { initialTuiState, reasoningUsageLabel, reduceTuiEvent } from "./state.js";
|
|
83
|
+
import { appendTranscriptMessage, appendTranscriptWarning, createTranscriptModel, transcriptItemRaw, transcriptItemText } from "./transcript-model.js";
|
|
84
|
+
import { createTuiTheme, themeColor } from "./theme.js";
|
|
85
|
+
import { DEFAULT_THEME_ACCENT } from "./theme-catalog.js";
|
|
86
|
+
import { THEME_CHOICES, createThemeStore, parseThemeChoice } from "./theme-store.js";
|
|
87
|
+
import { themeLabel, themePickerOverlay } from "./overlays/theme-picker.js";
|
|
88
|
+
import { checkpointFilesText, rangeFiles, undoPickerOverlay, undoRange } from "./overlays/checkpoint-picker.js";
|
|
89
|
+
import { checkpointChanges, createCheckpointStore, sessionCheckpoints } from "../workspace/checkpoint-store.js";
|
|
90
|
+
import { reapplyToCheckpoint, rewindToCheckpoint } from "../workspace/restore.js";
|
|
91
|
+
import { hasActiveRuns } from "../runtime/active-run.js";
|
|
92
|
+
import { PROMPT_HISTORY_LIMIT, createPromptHistoryStore, promptHistoryEnabled } from "./prompt-history-store.js";
|
|
93
|
+
import { configSet, configUnset } from "../config/commands.js";
|
|
94
|
+
import { osc52CopySequence, reduceViewport, transcriptEventAddsItem } from "./components/transcript.js";
|
|
95
|
+
import { MODEL_ID, NO_MODEL, NO_PROVIDER_MODEL, closeOverlay, createTuiAppState, lastProjectToken, modelItems, openOverlay, permissionDryRunSlash, pickerMove, providerItems, queuedActionPreview, reduceAppAction, replaceLastProjectToken, sessionStatusText, slashCommandIntent, slashCommandToCliArgs, togglePlanMode, } from "./shell-model.js";
|
|
96
|
+
import { deriveShellLayout, pickerWindow } from "./shell-layout.js";
|
|
97
|
+
import { budget as layoutBudget, MIN_COLUMNS, MIN_ROWS } from "./regions/budget.js";
|
|
98
|
+
import { composerMeasure, createRegionViews } from "./regions/views.js";
|
|
99
|
+
import { displayWidth, truncateDisplay, truncateMiddle } from "./text-width.js";
|
|
100
|
+
import { decideShellInput } from "./shell-input.js";
|
|
101
|
+
import { createRenderCoordinator } from "./render-coordinator.js";
|
|
102
|
+
import { MOTION_FRAME_MS, frameIndex } from "./render-scheduler.js";
|
|
103
|
+
import { onboardingSteps } from "./onboarding.js";
|
|
104
|
+
import { setupEndLines, setupOverlay, setupStart } from "./setup-wizard.js";
|
|
105
|
+
import { createVimState, handleVimInput, toggleVim, vimModeHint } from "./vim-mode.js";
|
|
106
|
+
export { closeOverlay, createTuiAppState, permissionDryRunSlash, queuedActionPreview, reduceAppAction, slashCommandIntent, slashCommandToCliArgs, togglePlanMode } from "./shell-model.js";
|
|
107
|
+
const execFileAsync = promisify(execFile);
|
|
108
|
+
/* R5b D5 (Q-R5-15): the last 1,000 prompts, the same bound in memory and on disk. */
|
|
109
|
+
const MAX_HISTORY = PROMPT_HISTORY_LIMIT;
|
|
110
|
+
/* R1 lane I (E5): a refresh after a run ends returns equal data in new objects; keeping the old object keeps React from
|
|
111
|
+
committing an identical frame on an idle screen. */
|
|
112
|
+
function sameValue(a, b) { if (a === b)
|
|
113
|
+
return true; try {
|
|
114
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return false;
|
|
118
|
+
} }
|
|
119
|
+
function errorCode(error) { const code = error?.code; return typeof code === 'string' && /^[A-Z0-9_]{1,64}$/u.test(code) ? code : 'UNAVAILABLE'; }
|
|
120
|
+
/* The provider, catalog and key paths printed only the error's code before R2; the error anatomy keeps it that way (it
|
|
121
|
+
gets the code and nothing else), so no message text of those paths reaches the screen. */
|
|
122
|
+
function codeOnly(error) { return { code: errorCode(error) }; }
|
|
123
|
+
/*
|
|
124
|
+
* R2 (Q-R2-4, gate E6): every error the app shows is lane E's anatomy — headline · what to do · dim "Details: … (CODE)
|
|
125
|
+
* · where to look" — with the action that failed in front of the headline; `note` is a fact the person must also read
|
|
126
|
+
* ("The key was not saved."); `rejected` says the action was refused, not broken. The raw message reaches only the
|
|
127
|
+
* details line, through describeError.
|
|
128
|
+
*/
|
|
129
|
+
export function screenErrorText(error, context, { note, rejected = false } = {}) {
|
|
130
|
+
const view = describeError(error);
|
|
131
|
+
const what = note ? t('screen.error.withNote', { what: view.what, note }) : view.what;
|
|
132
|
+
return errorViewLines({ ...view, what: context ? t(rejected ? 'screen.error.rejected' : 'screen.error.context', { context, what }) : what }).join('\n');
|
|
133
|
+
}
|
|
134
|
+
/* One row (a notice inside an overlay): the same parts, joined. */
|
|
135
|
+
function screenErrorLine(error, context) { return screenErrorText(error, context).split('\n').join(' · '); }
|
|
136
|
+
/* The action that failed: a dictionary label, or a slash command with its operation. */
|
|
137
|
+
const act = (id, params) => t(id, params);
|
|
138
|
+
export function reasoningEffortChoices(model) {
|
|
139
|
+
if (model.reasoning !== true || !Array.isArray(model.reasoningEfforts) || model.reasoningEfforts.length === 0)
|
|
140
|
+
return [];
|
|
141
|
+
/* R5a (owner 2026-09-24): always in TALOS's order, default first, never in the order the model declares them. */
|
|
142
|
+
return ['default', ...orderedEfforts(model.reasoningEfforts)];
|
|
143
|
+
}
|
|
144
|
+
export async function selectExplicitModel(id, catalog, setControllerModel) { await commitModelSelection({ id, catalog, setControllerModel }); }
|
|
145
|
+
export async function runControllerSlashAction(name, controller) { if (name !== 'compact')
|
|
146
|
+
return null; const result = await controller.compact(); return result?.compattato === false ? t('screen.compact.already') : t('screen.compact.done'); }
|
|
147
|
+
/* P13: what still works while an agent is watched (the rest of the keys do nothing; Esc goes back). */
|
|
148
|
+
const AGENT_VIEW_ACTIONS = new Set(['interrupt', 'exit', 'agent-tree', 'page-up', 'page-down', 'transcript-top', 'transcript-bottom', 'redraw', 'reasoning-toggle', 'tool-details', 'help']);
|
|
149
|
+
function localMessage(state, text) { const id = `local-${Date.now()}-${Math.random()}`; const base = state.messages.length >= 511 ? state.messages.slice(-511) : state.messages; return { ...state, messages: [...base, { id, role: 'assistant', text }], transcript: appendTranscriptWarning(state.transcript, text) }; }
|
|
150
|
+
function userMessage(state, text, id = `user-${Date.now()}-${Math.random()}`) { const base = state.messages.length >= 511 ? state.messages.slice(-511) : state.messages; return { ...state, messages: [...base, { id, role: 'user', text }], transcript: appendTranscriptMessage(state.transcript, { role: 'user', text }) }; }
|
|
151
|
+
/* R5b D4 (Q-R5-14): the theme is live. run.ts hands the app the store it paints the background from; without one (tests,
|
|
152
|
+
embedders) the app keeps its own, seeded from `capabilities` and `accentId` as before. The colours below follow the store
|
|
153
|
+
at every render, and a change of theme remounts the main tree (its key), so memoised rows are drawn again. */
|
|
154
|
+
export function createTuiAppComponent(React, Ink, baseCapabilities = { interactive: true, color: true, motion: true, unicode: true }, accentId, givenThemeStore) {
|
|
155
|
+
const h = React.createElement;
|
|
156
|
+
const themeStore = givenThemeStore ?? createThemeStore(baseCapabilities, accentId ?? DEFAULT_THEME_ACCENT);
|
|
157
|
+
let capabilities = baseCapabilities, theme = createTuiTheme(capabilities, accentId);
|
|
158
|
+
let bootColor, transcriptColor, pickerColor, approvalColor, toolColor, footerColor;
|
|
159
|
+
const applyLive = (live) => {
|
|
160
|
+
capabilities = live.capabilities;
|
|
161
|
+
theme = live.theme;
|
|
162
|
+
bootColor = themeColor(theme, 'boot');
|
|
163
|
+
transcriptColor = themeColor(theme, 'transcript');
|
|
164
|
+
pickerColor = themeColor(theme, 'picker');
|
|
165
|
+
approvalColor = themeColor(theme, 'approval');
|
|
166
|
+
toolColor = themeColor(theme, 'tool');
|
|
167
|
+
footerColor = themeColor(theme, 'footer');
|
|
168
|
+
};
|
|
169
|
+
applyLive(themeStore.getSnapshot());
|
|
170
|
+
const BootSequence = createBootSequenceComponent(React, Ink);
|
|
171
|
+
const Regions = createRegionViews(React, Ink);
|
|
172
|
+
/* The composer region (regions/views.ts): subscribes to the composer store itself, capped by the budget. */
|
|
173
|
+
const ComposerView = Regions.ComposerRegion;
|
|
174
|
+
const renderInline = (text, keyPrefix) => parseMarkdownInline(text).map((token, index) => {
|
|
175
|
+
const key = `${keyPrefix}-${index}`;
|
|
176
|
+
if (token.kind === 'strong')
|
|
177
|
+
return h(Ink.Text, { key, bold: true }, token.text);
|
|
178
|
+
if (token.kind === 'emphasis')
|
|
179
|
+
return h(Ink.Text, { key, italic: true }, token.text);
|
|
180
|
+
if (token.kind === 'code')
|
|
181
|
+
return h(Ink.Text, { key, color: toolColor }, token.text);
|
|
182
|
+
if (token.kind === 'strike')
|
|
183
|
+
return h(Ink.Text, { key, strikethrough: true, dimColor: true }, token.text);
|
|
184
|
+
if (token.kind === 'link')
|
|
185
|
+
return h(React.Fragment, { key }, h(Ink.Text, { underline: true, color: pickerColor }, token.text), h(Ink.Text, { dimColor: true }, ` (${token.href})`));
|
|
186
|
+
return h(Ink.Text, { key }, token.text);
|
|
187
|
+
});
|
|
188
|
+
const MarkdownView = React.memo(function MarkdownView({ text, width }) {
|
|
189
|
+
const parseMemoRef = React.useRef(null);
|
|
190
|
+
if (!parseMemoRef.current)
|
|
191
|
+
parseMemoRef.current = createMarkdownParseMemo();
|
|
192
|
+
const blocks = parseMemoRef.current.get(text);
|
|
193
|
+
return h(React.Fragment, null, ...blocks.map((block, index) => {
|
|
194
|
+
if (block.kind === 'heading')
|
|
195
|
+
return h(Ink.Text, { key: index, bold: true }, ...renderInline(block.text, `h-${index}`));
|
|
196
|
+
if (block.kind === 'code')
|
|
197
|
+
return h(Ink.Box, { key: index, flexDirection: 'column' }, ...block.tokens.map((line, lineIndex) => h(Ink.Text, { key: lineIndex }, ...line.map((token, tokenIndex) => h(Ink.Text, { key: tokenIndex, dimColor: token.kind === 'comment', bold: token.kind === 'keyword' || token.kind === 'key' }, markdownSafeText(token.text))))));
|
|
198
|
+
if (block.kind === 'blockquote')
|
|
199
|
+
return h(Ink.Box, { key: index, flexDirection: 'column', paddingLeft: 1 }, ...block.text.split('\n').map((line, lineIndex) => h(Ink.Text, { key: lineIndex, dimColor: true }, h(Ink.Text, { bold: true }, '│ '), ...renderInline(line, `q-${index}-${lineIndex}`))));
|
|
200
|
+
if (block.kind === 'list')
|
|
201
|
+
return h(Ink.Box, { key: index, flexDirection: 'column' }, ...block.items.map((item, itemIndex) => h(Ink.Text, { key: itemIndex }, ' '.repeat(block.depths[itemIndex] ?? 0) + (block.ordered ? String(block.start + itemIndex) + '. ' : '• '), ...renderInline(item, `l-${index}-${itemIndex}`))));
|
|
202
|
+
if (block.kind === 'table') {
|
|
203
|
+
const layout = markdownBlockLines(block, width);
|
|
204
|
+
return h(Ink.Box, { key: index, flexDirection: 'column' }, ...layout.lines.map((line, lineIndex) => h(Ink.Text, { key: lineIndex, bold: layout.mode === 'wide' && lineIndex === 0, dimColor: lineIndex === 1 && layout.mode === 'wide' }, line || ' ')));
|
|
205
|
+
}
|
|
206
|
+
return h(Ink.Text, { key: index }, ...renderInline(block.text, `p-${index}`));
|
|
207
|
+
}));
|
|
208
|
+
}, markdownRenderPropsEqual);
|
|
209
|
+
/* The command menu (lane O builder, fitted to its budget like every overlay, Q-R1-3). It subscribes to the composer and
|
|
210
|
+
selection stores itself, so typing a query re-renders the menu, not the root. */
|
|
211
|
+
const CommandMenuView = React.memo(function CommandMenuView({ composerStore, selectionStore, maxRows, columns }) {
|
|
212
|
+
const editor = React.useSyncExternalStore(composerStore.subscribe, composerStore.getSnapshot, composerStore.getSnapshot);
|
|
213
|
+
const selection = React.useSyncExternalStore(selectionStore.subscribe, selectionStore.getSnapshot, selectionStore.getSnapshot);
|
|
214
|
+
/* R4 Q-R4-5: the highlight is the row Enter runs; none after a typo (Enter then sends the text as typed). */
|
|
215
|
+
return renderOverlay(h, Ink, commandMenuOverlay({ text: editor.text, selected: commandMenuHighlight(editor.text, selection) }), { maxRows, columns, accentColor: pickerColor });
|
|
216
|
+
}, (previous, next) => previous.composerStore === next.composerStore && previous.selectionStore === next.selectionStore && previous.maxRows === next.maxRows && previous.columns === next.columns);
|
|
217
|
+
const MeasuredTranscriptRow = React.memo(function MeasuredTranscriptRow({ item, measurementKey, measurementStore, renderItem }) {
|
|
218
|
+
const ref = React.useRef(null);
|
|
219
|
+
/* R1 lane I measurement (E5): `useBoxMetrics` also tracks left/top and sets state when a row only MOVES (an overlay
|
|
220
|
+
opens, the terminal resizes), which cost one identical full repaint. The height is read after layout instead, on
|
|
221
|
+
every render of this row, and the store publishes only a changed height; a row that only moved commits nothing. */
|
|
222
|
+
React.useLayoutEffect(() => {
|
|
223
|
+
const node = ref.current;
|
|
224
|
+
if (!node)
|
|
225
|
+
return;
|
|
226
|
+
const { height } = Ink.measureElement(node);
|
|
227
|
+
if (height > 0)
|
|
228
|
+
measurementStore.measure(measurementKey, height);
|
|
229
|
+
});
|
|
230
|
+
return h(Ink.Box, { ref, flexDirection: 'column' }, renderItem(item));
|
|
231
|
+
}, (previous, next) => previous.item === next.item && previous.measurementKey === next.measurementKey && previous.measurementStore === next.measurementStore && previous.renderItem === next.renderItem);
|
|
232
|
+
/*
|
|
233
|
+
* R1 (Q-R1-2, E4, E9): the transcript inside its region. The window is planned in ROWS with measured heights
|
|
234
|
+
* (`useBoxMetrics` per mounted item; an item never mounted is counted, not estimated), the region is bottom-anchored
|
|
235
|
+
* and clipped, and the one announcement row keeps the person's latest prompt in sight (D-04). `onPlan` reports the
|
|
236
|
+
* geometry the key handlers need (PageUp/PageDown, Ctrl+Home) and the rows added while the person reads above the
|
|
237
|
+
* tail, so the view stays where they left it.
|
|
238
|
+
*/
|
|
239
|
+
const TranscriptView = React.memo(function TranscriptView({ items, scrollRows, rowBudget, width, raw, expandedTools, reasoningVisible = true, revealId, measurementStore, renderItem, onPlan, dimColor, now }) {
|
|
240
|
+
React.useSyncExternalStore(measurementStore.subscribe, measurementStore.getSnapshot, measurementStore.getSnapshot);
|
|
241
|
+
/* R5a S8: a collapsed reasoning block is one row, a shown one a box: the choice is part of every measured height. */
|
|
242
|
+
const variant = (raw ? 'raw' : expandedTools ? 'pretty-expanded' : 'pretty-compact') + (reasoningVisible ? '' : '-thought');
|
|
243
|
+
const revealIndex = revealId ? items.findIndex((item) => item.id === revealId) : -1;
|
|
244
|
+
/* Lane RD contract (R3): the estimate may read the clock of a running row; an estimator without it ignores it. */
|
|
245
|
+
const estimateOptions = { raw, expandedTools, reasoningVisible, ...(typeof now === 'number' ? { now } : {}) };
|
|
246
|
+
const heightOf = (item) => measurementStore.height(transcriptMeasurementKey(item.id, width, variant)) ?? estimateTranscriptItemRows(item, width, estimateOptions);
|
|
247
|
+
const plan = (budgetRows) => planTranscriptVirtualWindow({ items, offset: 0, rowBudget: budgetRows, scrollRows, revealIndex, heightOf });
|
|
248
|
+
let planned = plan(rowBudget), announcement = null;
|
|
249
|
+
const latestUser = (() => { for (let index = items.length - 1; index >= 0; index--) {
|
|
250
|
+
const item = items[index];
|
|
251
|
+
if (item.kind === 'message' && item.role === 'user')
|
|
252
|
+
return index;
|
|
253
|
+
} return -1; })();
|
|
254
|
+
const announce = (window) => {
|
|
255
|
+
const hidden = window.hiddenAbove ?? 0;
|
|
256
|
+
const promptAboveView = latestUser >= 0 && window.firstRowFromBottom !== undefined && window.firstRowFromBottom(latestUser) >= (window.scrollRows ?? 0) + Math.max(0, rowBudget - 1);
|
|
257
|
+
const messagesAbove = items.slice(0, hidden).filter((item) => item.kind === 'message').length;
|
|
258
|
+
const latest = latestUser >= 0 ? items[latestUser] : null;
|
|
259
|
+
return transcriptAnnouncement({ hiddenAbove: hidden, hiddenMessagesAbove: messagesAbove, latestPrompt: latest && latest.kind === 'message' ? latest.text : null, promptAboveView, width });
|
|
260
|
+
};
|
|
261
|
+
if (rowBudget > 1) {
|
|
262
|
+
announcement = announce(planned);
|
|
263
|
+
if (announcement)
|
|
264
|
+
planned = plan(rowBudget - 1);
|
|
265
|
+
}
|
|
266
|
+
const anchorIndex = planned.end > 0 ? planned.end - 1 : -1;
|
|
267
|
+
const indexById = (id) => items.findIndex((item) => item.id === id);
|
|
268
|
+
const report = {
|
|
269
|
+
totalRows: planned.totalRows ?? 0, maxScroll: planned.maxScroll ?? 0, scrollRows: planned.scrollRows ?? 0, itemRows: announcement ? rowBudget - 1 : rowBudget,
|
|
270
|
+
anchorId: anchorIndex >= 0 ? items[anchorIndex].id : null, anchorBelow: anchorIndex >= 0 && planned.rowsBelow ? planned.rowsBelow(anchorIndex) : 0,
|
|
271
|
+
belowOf: (id) => { const index = indexById(id); return index >= 0 && planned.rowsBelow ? planned.rowsBelow(index) : null; },
|
|
272
|
+
};
|
|
273
|
+
React.useLayoutEffect(() => { onPlan(report); }, [onPlan, report.totalRows, report.maxScroll, report.scrollRows, report.itemRows, report.anchorId, report.anchorBelow]);
|
|
274
|
+
return h(Regions.TranscriptRegion, { rows: rowBudget, announcement, clipBelow: planned.clipBelow ?? 0, color: dimColor }, ...planned.rows.map((item) => {
|
|
275
|
+
const measurementKey = transcriptMeasurementKey(item.id, width, variant);
|
|
276
|
+
return h(MeasuredTranscriptRow, { key: item.id, item, measurementKey, measurementStore, renderItem });
|
|
277
|
+
}));
|
|
278
|
+
}, (previous, next) => previous.items === next.items && previous.scrollRows === next.scrollRows && previous.rowBudget === next.rowBudget && previous.width === next.width && previous.raw === next.raw && previous.expandedTools === next.expandedTools && previous.reasoningVisible === next.reasoningVisible && previous.revealId === next.revealId && previous.measurementStore === next.measurementStore && previous.renderItem === next.renderItem && previous.onPlan === next.onPlan && previous.dimColor === next.dimColor && previous.now === next.now);
|
|
279
|
+
return function TuiApp(props) {
|
|
280
|
+
const live = React.useSyncExternalStore(themeStore.subscribe, themeStore.getSnapshot, themeStore.getSnapshot);
|
|
281
|
+
applyLive(live);
|
|
282
|
+
const { runtime, registry, catalog, invocation, projectRoot, paths, permissionRules, autoClassifier, initialPrompt, initialReasoningEffort = null, renderCoordinator, keymap: effectiveKeymap = KEYBINDINGS, terminalSize } = props;
|
|
283
|
+
const inkApp = Ink.useApp();
|
|
284
|
+
const inkStdout = Ink.useStdout?.();
|
|
285
|
+
const stdout = inkStdout?.stdout ?? process.stdout;
|
|
286
|
+
const baseModel = invocation.model ?? 'openai:gpt-5-mini';
|
|
287
|
+
const baseMode = (invocation.permissionMode ?? 'default');
|
|
288
|
+
const [state, setState] = React.useState(() => initialTuiState({ model: NO_PROVIDER_MODEL, permissionMode: baseMode }));
|
|
289
|
+
const [appState, setAppState] = React.useState(() => ({ ...createTuiAppState(), reasoningVisible: props.reasoning === 'visible' }));
|
|
290
|
+
const composerStoreRef = React.useRef(null);
|
|
291
|
+
if (!composerStoreRef.current)
|
|
292
|
+
composerStoreRef.current = createComposerStore(appState.editor);
|
|
293
|
+
const composerStore = composerStoreRef.current;
|
|
294
|
+
const currentEditor = () => composerStore.getSnapshot();
|
|
295
|
+
const commandMenuSelectionStoreRef = React.useRef(null);
|
|
296
|
+
if (!commandMenuSelectionStoreRef.current)
|
|
297
|
+
commandMenuSelectionStoreRef.current = createCommandMenuSelectionStore();
|
|
298
|
+
const commandMenuSelectionStore = commandMenuSelectionStoreRef.current;
|
|
299
|
+
const transcriptMeasurementStoreRef = React.useRef(null);
|
|
300
|
+
if (!transcriptMeasurementStoreRef.current)
|
|
301
|
+
transcriptMeasurementStoreRef.current = createTranscriptMeasurementStore();
|
|
302
|
+
const transcriptMeasurementStore = transcriptMeasurementStoreRef.current;
|
|
303
|
+
const [vim, setVim] = React.useState(() => createVimState());
|
|
304
|
+
/* R4 contract (lane F owns launch-state.ts and BootSequence): the boot plays full only on the first launch and after an
|
|
305
|
+
update, short otherwise (Q-R4-4). Asked once per mount; a plan that cannot be read is a full boot. */
|
|
306
|
+
const bootPlanRef = React.useRef(null);
|
|
307
|
+
if (!bootPlanRef.current) {
|
|
308
|
+
try {
|
|
309
|
+
bootPlanRef.current = bootPlan({ dataRoot: paths.dataRoot, version: CLI_VERSION, build: currentBuildId() ?? TALOS_BASE_COMMIT, now: Date.now() });
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
bootPlanRef.current = { mode: 'full', firstLaunch: true };
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
const [boot, setBoot] = React.useState(() => createBootState(Date.now()));
|
|
316
|
+
/* R4 Q-R4-9 / E9: each entry keeps its paste blocks, so a recalled paste stays collapsed; the text is sent as it was. */
|
|
317
|
+
const [history, setHistory] = React.useState([]);
|
|
318
|
+
const historyIndexRef = React.useRef(-1);
|
|
319
|
+
const reverseIndexRef = React.useRef(0);
|
|
320
|
+
const [viewport, setViewport] = React.useState(() => ({ offset: 0, pageSize: 12, unseen: 0, followingTail: true }));
|
|
321
|
+
const [pending, setPending] = React.useState(null);
|
|
322
|
+
const [approvalExpanded, setApprovalExpanded] = React.useState(false);
|
|
323
|
+
const [approvalScroll, setApprovalScroll] = React.useState(0);
|
|
324
|
+
/* R3 gate E11 (Q-R3-6): the highlighted choice of the approval dialog, Deny by default. */
|
|
325
|
+
const [approvalSelected, setApprovalSelected] = React.useState(APPROVAL_DEFAULT_SELECTED);
|
|
326
|
+
/*
|
|
327
|
+
* R3 approval key race, owner decision (binding): for APPROVAL_GRACE_MS after the dialog is first shown for a request,
|
|
328
|
+
* keys keep going to the composer and never answer it; after that the dialog takes every key. What the key handler
|
|
329
|
+
* needs is read from refs set synchronously when the approval arrives, never from the last render (Ink's useInput
|
|
330
|
+
* closes over the previous render, so routing by render state made a key's target depend on paint timing).
|
|
331
|
+
*/
|
|
332
|
+
const clockRef = React.useRef(props.clock ?? Date.now);
|
|
333
|
+
const approvalShownRef = React.useRef(null);
|
|
334
|
+
const pendingRef = React.useRef(null);
|
|
335
|
+
const approvalUiRef = React.useRef({ expanded: false, selected: APPROVAL_DEFAULT_SELECTED });
|
|
336
|
+
/* R3 Q-R3-13: null while unknown (no probe, or not answered yet). */
|
|
337
|
+
const [sandboxVerified, setSandboxVerified] = React.useState(null);
|
|
338
|
+
const sandboxAskedRef = React.useRef(false);
|
|
339
|
+
const askSandboxRef = React.useRef(() => { });
|
|
340
|
+
askSandboxRef.current = () => {
|
|
341
|
+
const probe = props.shellIsolation;
|
|
342
|
+
if (!probe || sandboxAskedRef.current)
|
|
343
|
+
return;
|
|
344
|
+
sandboxAskedRef.current = true;
|
|
345
|
+
/* A check stopped because TALOS is quitting answers nothing: no label either way. */
|
|
346
|
+
void probe().then(result => { if (!result.aborted)
|
|
347
|
+
setSandboxVerified(result.verified === true); }, () => { });
|
|
348
|
+
};
|
|
349
|
+
/* R3 (lane RD contract): a once-a-second clock for running tool rows (`running 12 s`), ticking only while a tool runs. */
|
|
350
|
+
const [toolNow, setToolNow] = React.useState(() => Date.now());
|
|
351
|
+
/* R1 lane O contract: help scrolls inside maxRows (PgUp/PgDn, Up/Down); the approval scroll stops at the fit's last offset. */
|
|
352
|
+
const [helpOffset, setHelpOffset] = React.useState(0);
|
|
353
|
+
const helpMaxOffsetRef = React.useRef(0);
|
|
354
|
+
const helpPageRef = React.useRef(1);
|
|
355
|
+
const approvalMaxOffsetRef = React.useRef(0);
|
|
356
|
+
const [diff, setDiff] = React.useState('');
|
|
357
|
+
const [pickerQuery, setPickerQuery] = React.useState('');
|
|
358
|
+
const [pickerSelection, setPickerSelection] = React.useState(() => ({ query: '', selected: 0, pageSize: 10 }));
|
|
359
|
+
const [modelRows, setModelRows] = React.useState([]);
|
|
360
|
+
const [providerRows, setProviderRows] = React.useState([]);
|
|
361
|
+
const [sessionRows, setSessionRows] = React.useState([]);
|
|
362
|
+
const [agentRows, setAgentRows] = React.useState([]);
|
|
363
|
+
const [agentTreeOverlay, setAgentTreeOverlay] = React.useState(null);
|
|
364
|
+
/* P13 (owner decisions 2026-09-26): the agent whose transcript takes the main view (agent-view.ts), read-only. */
|
|
365
|
+
const [agentView, setAgentView] = React.useState(null);
|
|
366
|
+
const agentViewRef = React.useRef(null);
|
|
367
|
+
const mainViewportRef = React.useRef(null);
|
|
368
|
+
const [providerStep, setProviderStep] = React.useState({ kind: 'list' });
|
|
369
|
+
const [chosenProvider, setChosenProvider] = React.useState(null);
|
|
370
|
+
const [selectionLoaded, setSelectionLoaded] = React.useState(false);
|
|
371
|
+
const [keyMissing, setKeyMissing] = React.useState(false);
|
|
372
|
+
const [modelList, setModelList] = React.useState({ loading: false, verified: false, notice: '', label: '', engineDown: false });
|
|
373
|
+
/* P17 (owner decisions 2026-09-26): the guided setup — first launch when something is missing, /setup, `talos setup`.
|
|
374
|
+
It chains the provider, key and model windows that exist; `setupTransitionRef` holds the setup open while one window
|
|
375
|
+
hands over to the next (the provider window closes, the model window opens). */
|
|
376
|
+
const [setupActive, setSetupActive] = React.useState(false);
|
|
377
|
+
const setupRef = React.useRef(false);
|
|
378
|
+
const setupTransitionRef = React.useRef(false);
|
|
379
|
+
/* A local engine chosen in the setup that did not answer: its model window says so (R retries, Esc goes back), and the
|
|
380
|
+
engine becomes the chosen provider only when one of its models is chosen. */
|
|
381
|
+
const setupPendingProviderRef = React.useRef(null);
|
|
382
|
+
/* P21: the OpenRouter sign-in under way (its loopback port closes on cancel, timeout or success). */
|
|
383
|
+
const oauthLoginRef = React.useRef(null);
|
|
384
|
+
const [providerSecret, setProviderSecret] = React.useState('');
|
|
385
|
+
const [auxCompletions, setAuxCompletions] = React.useState([]);
|
|
386
|
+
const [fileCompletion, setFileCompletion] = React.useState(null);
|
|
387
|
+
const fileCompletionRef = React.useRef(null);
|
|
388
|
+
fileCompletionRef.current = fileCompletion;
|
|
389
|
+
/* The token last looked up, the token the person closed with Esc (the list stays closed until the token changes), and a
|
|
390
|
+
sequence number so a slow answer for an older token is dropped. */
|
|
391
|
+
/* R5b D6: the project walk behind @, built once and reused for a short while (project-file-index.ts). */
|
|
392
|
+
const projectIndexRef = React.useRef(createProjectFileIndexCache(projectRoot));
|
|
393
|
+
const completionWatchRef = React.useRef({ token: null, dismissed: null, seq: 0 });
|
|
394
|
+
const [currentModelMeta, setCurrentModelMeta] = React.useState(null);
|
|
395
|
+
const [reasoningEffort, setReasoningEffort] = React.useState(initialReasoningEffort);
|
|
396
|
+
const [modelEffortStep, setModelEffortStep] = React.useState(null);
|
|
397
|
+
/* R5a: the effort line was opened by /effort (not as /model's second step): Backspace does not lead to a model list. */
|
|
398
|
+
const [effortOnly, setEffortOnly] = React.useState(false);
|
|
399
|
+
const [busySince, setBusySince] = React.useState(() => Date.now());
|
|
400
|
+
const [motionNow, setMotionNow] = React.useState(() => Date.now());
|
|
401
|
+
const [renderEpoch, setRenderEpoch] = React.useState(0);
|
|
402
|
+
const [queuedActions, setQueuedActions] = React.useState([]);
|
|
403
|
+
const [queueEditor, setQueueEditor] = React.useState(null);
|
|
404
|
+
const [transcriptSearch, setTranscriptSearch] = React.useState(null);
|
|
405
|
+
const [transcriptSelectedId, setTranscriptSelectedId] = React.useState(null);
|
|
406
|
+
const [transcriptRevealId, setTranscriptRevealId] = React.useState(null);
|
|
407
|
+
const [transcriptRaw, setTranscriptRaw] = React.useState(false);
|
|
408
|
+
const [trustCenter, setTrustCenter] = React.useState(null);
|
|
409
|
+
const [mcpCenter, setMcpCenter] = React.useState(null);
|
|
410
|
+
const [hookCenter, setHookCenter] = React.useState(null);
|
|
411
|
+
const [pluginCenter, setPluginCenter] = React.useState(null);
|
|
412
|
+
const [memoryCenter, setMemoryCenter] = React.useState(null);
|
|
413
|
+
const [notesCenter, setNotesCenter] = React.useState(null);
|
|
414
|
+
const [tasksCenter, setTasksCenter] = React.useState(null);
|
|
415
|
+
const [libraryCenter, setLibraryCenter] = React.useState(null);
|
|
416
|
+
const [researchCenter, setResearchCenter] = React.useState(null);
|
|
417
|
+
const [automationCenter, setAutomationCenter] = React.useState(null);
|
|
418
|
+
const [forgeCenter, setForgeCenter] = React.useState(null);
|
|
419
|
+
const [contextStatus, setContextStatus] = React.useState(null);
|
|
420
|
+
const [contextInspector, setContextInspector] = React.useState(null);
|
|
421
|
+
/* R5b D4: the /theme picker — the row chosen, and the theme in use when it opened (Esc puts it back). */
|
|
422
|
+
const [themePicker, setThemePicker] = React.useState(null);
|
|
423
|
+
/* R5b D3 (Q-R5-13): the /undo picker (its rows, the highlighted one, and the question: null, 0 Cancel or 1 act). */
|
|
424
|
+
const [undoPicker, setUndoPicker] = React.useState(null);
|
|
425
|
+
/* R5b: the /history list (its filter and the highlighted row). */
|
|
426
|
+
const [historyPicker, setHistoryPicker] = React.useState(null);
|
|
427
|
+
const checkpointStoreRef = React.useRef(undefined);
|
|
428
|
+
const checkpointStoreFor = () => { if (checkpointStoreRef.current === undefined)
|
|
429
|
+
checkpointStoreRef.current = paths?.checkpointsRoot ? createCheckpointStore({ rootDir: paths.checkpointsRoot, projectRoot }) : null; return checkpointStoreRef.current; };
|
|
430
|
+
const controllerRef = React.useRef(null);
|
|
431
|
+
const modelPickerRef = React.useRef(null);
|
|
432
|
+
const providerPickerRef = React.useRef(null);
|
|
433
|
+
const sessionPickerRef = React.useRef(null);
|
|
434
|
+
/* R4: a resume or fork being confirmed; a second Enter meanwhile attached the session twice and left its cut-off run
|
|
435
|
+
"writing" (measured in s09 once Enter ran the command on the first press: review D-08). */
|
|
436
|
+
const sessionConfirmingRef = React.useRef(false);
|
|
437
|
+
const mcpFacadeRef = React.useRef(null);
|
|
438
|
+
const hookFacadeRef = React.useRef(null);
|
|
439
|
+
const pluginFacadeRef = React.useRef(null);
|
|
440
|
+
const memoryFacadeRef = React.useRef(null);
|
|
441
|
+
const notesFacadeRef = React.useRef(null);
|
|
442
|
+
const tasksFacadeRef = React.useRef(null);
|
|
443
|
+
const libraryFacadeRef = React.useRef(null);
|
|
444
|
+
const researchFacadeRef = React.useRef(null);
|
|
445
|
+
const automationFacadeRef = React.useRef(null);
|
|
446
|
+
const forgeFacadeRef = React.useRef(null);
|
|
447
|
+
/* R4 Q-R4-7 / E7: the exit rule lives in interrupt.ts; it reads, at the moment of the key, whether the composer has a
|
|
448
|
+
draft, a picker or dialog is open, or the `@` list is open (interruptContextRef is refreshed on every render). */
|
|
449
|
+
const interruptContextRef = React.useRef(() => ({ draft: false, dialog: false, completion: false }));
|
|
450
|
+
const interruptRef = React.useRef(null);
|
|
451
|
+
if (!interruptRef.current)
|
|
452
|
+
interruptRef.current = createInterruptController({ context: () => interruptContextRef.current() });
|
|
453
|
+
/* R1 lane X contract (D-12): the first Ctrl+C at idle only arms the exit; the hint stays in the status row for the
|
|
454
|
+
confirmation window. Its single timer is the only commit an idle screen may make (one clear after 2 s, gate E5).
|
|
455
|
+
R4: the hint names the key that armed it (Ctrl+C or Ctrl+D). */
|
|
456
|
+
const [exitHint, setExitHint] = React.useState(false);
|
|
457
|
+
const [exitHintKey, setExitHintKey] = React.useState('ctrl-c');
|
|
458
|
+
const exitHintTimerRef = React.useRef(null);
|
|
459
|
+
const clearExitHintTimer = () => { if (exitHintTimerRef.current !== null) {
|
|
460
|
+
clearTimeout(exitHintTimerRef.current);
|
|
461
|
+
exitHintTimerRef.current = null;
|
|
462
|
+
} };
|
|
463
|
+
const showExitHint = (key = 'ctrl-c') => { clearExitHintTimer(); setExitHintKey(key); setExitHint(true); exitHintTimerRef.current = setTimeout(() => { exitHintTimerRef.current = null; setExitHint(false); }, EXIT_CONFIRM_WINDOW_MS); };
|
|
464
|
+
const hideExitHint = () => { if (exitHintTimerRef.current === null)
|
|
465
|
+
return; clearExitHintTimer(); setExitHint(false); };
|
|
466
|
+
React.useEffect(() => () => clearExitHintTimer(), []);
|
|
467
|
+
/* R1 lane contract (TuiAppProps.onExit): the last session id and transcript, reported once on unmount. The unmount
|
|
468
|
+
covers /exit, Ctrl+C, Ctrl+D and a forced exit, and runs before Ink leaves the alternate screen. */
|
|
469
|
+
const exitSnapshotRef = React.useRef({ sessionId: null, items: [] });
|
|
470
|
+
exitSnapshotRef.current = { sessionId: state.sessionId, items: state.transcript.items };
|
|
471
|
+
const onExitRef = React.useRef(props.onExit);
|
|
472
|
+
onExitRef.current = props.onExit;
|
|
473
|
+
React.useEffect(() => () => { onExitRef.current?.(exitSnapshotRef.current); }, []);
|
|
474
|
+
/* R1 E9/E15: the transcript's last plan, read by the scroll keys; its anchor keeps a scrolled view still. */
|
|
475
|
+
const transcriptPlanRef = React.useRef({ totalRows: 0, maxScroll: 0, scrollRows: 0, itemRows: 0, anchorId: null, anchorBelow: 0, belowOf: () => null });
|
|
476
|
+
const onTranscriptPlan = React.useCallback((plan) => {
|
|
477
|
+
const previous = transcriptPlanRef.current;
|
|
478
|
+
transcriptPlanRef.current = plan;
|
|
479
|
+
if (!previous.anchorId)
|
|
480
|
+
return;
|
|
481
|
+
const below = plan.belowOf(previous.anchorId);
|
|
482
|
+
if (below === null || below === previous.anchorBelow)
|
|
483
|
+
return;
|
|
484
|
+
const delta = below - previous.anchorBelow;
|
|
485
|
+
setViewport((view) => view.followingTail || view.offset === 0 ? view : { ...view, offset: Math.max(0, view.offset + delta) });
|
|
486
|
+
}, []);
|
|
487
|
+
const transcriptPageRows = () => Math.max(1, transcriptPlanRef.current.itemRows - 1);
|
|
488
|
+
const scrollTranscript = (event) => {
|
|
489
|
+
setTranscriptRevealId(null);
|
|
490
|
+
setViewport((view) => {
|
|
491
|
+
const plan = transcriptPlanRef.current, pageSize = transcriptPageRows(), current = { ...view, offset: Math.min(plan.maxScroll, Math.max(0, plan.scrollRows)), pageSize };
|
|
492
|
+
if (event === 'top')
|
|
493
|
+
return plan.maxScroll > 0 ? { ...current, offset: plan.maxScroll, followingTail: false } : reduceViewport(current, 'jump-tail', 0);
|
|
494
|
+
if (event === 'jump-tail')
|
|
495
|
+
return reduceViewport(current, 'jump-tail', 0);
|
|
496
|
+
return reduceViewport(current, event, plan.maxScroll + pageSize);
|
|
497
|
+
});
|
|
498
|
+
};
|
|
499
|
+
const reasoningVisibleRef = React.useRef(appState.reasoningVisible);
|
|
500
|
+
reasoningVisibleRef.current = appState.reasoningVisible;
|
|
501
|
+
const renderCoordinatorRef = React.useRef(null);
|
|
502
|
+
if (!renderCoordinatorRef.current)
|
|
503
|
+
renderCoordinatorRef.current = renderCoordinator ?? createRenderCoordinator();
|
|
504
|
+
const coordinator = renderCoordinatorRef.current;
|
|
505
|
+
const visibleEpochRef = React.useRef(0);
|
|
506
|
+
const eventSequenceRef = React.useRef(0);
|
|
507
|
+
const onboardingShownRef = React.useRef(false);
|
|
508
|
+
React.useEffect(() => {
|
|
509
|
+
const unsubscribe = coordinator.subscribe(batch => setRenderEpoch(batch.epoch));
|
|
510
|
+
return () => { unsubscribe(); if (!renderCoordinator)
|
|
511
|
+
coordinator.dispose(); };
|
|
512
|
+
}, [coordinator, renderCoordinator]);
|
|
513
|
+
React.useLayoutEffect(() => {
|
|
514
|
+
if (renderEpoch <= visibleEpochRef.current)
|
|
515
|
+
return;
|
|
516
|
+
visibleEpochRef.current = renderEpoch;
|
|
517
|
+
coordinator.markVisible(renderEpoch);
|
|
518
|
+
}, [coordinator, renderEpoch]);
|
|
519
|
+
/* ⛔ Fails closed: a catalog that cannot answer is a send that does not happen. */
|
|
520
|
+
const readinessFor = async (model) => {
|
|
521
|
+
if (typeof catalog?.readiness !== 'function')
|
|
522
|
+
return { ready: false, code: 'PROVIDER_NOT_CHOSEN', provider: null, message: t('screen.readiness.unavailable') };
|
|
523
|
+
try {
|
|
524
|
+
return await catalog.readiness(model);
|
|
525
|
+
}
|
|
526
|
+
catch (error) {
|
|
527
|
+
return { ready: false, code: 'PROVIDER_NOT_CHOSEN', provider: null, message: t('screen.readiness.unchecked', { code: errorCode(error) }) };
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
if (!controllerRef.current)
|
|
531
|
+
controllerRef.current = createTuiSessionController({
|
|
532
|
+
runtime, projectRoot, model: baseModel, mode: baseMode, rules: permissionRules ?? { allow: [], ask: [], deny: [] }, paths, ...(autoClassifier ? { autoClassifier } : {}), readiness: readinessFor,
|
|
533
|
+
onEvent: (event) => {
|
|
534
|
+
developmentLog('ui.runtime_event', event, event?.type === 'run.failed' ? 'error' : 'debug', 'tui');
|
|
535
|
+
const eventId = `runtime-${++eventSequenceRef.current}-${String(event.type)}`;
|
|
536
|
+
coordinator.enqueueEvent(eventId, () => {
|
|
537
|
+
if (event.type === 'run.started')
|
|
538
|
+
setBusySince(Date.now());
|
|
539
|
+
if (event.type === 'run.completed' || event.type === 'run.failed' || event.type === 'run.cancelled')
|
|
540
|
+
interruptRef.current.reset();
|
|
541
|
+
if (transcriptEventAddsItem(event) && (event.type !== 'reasoning.started' || reasoningVisibleRef.current))
|
|
542
|
+
setViewport((view) => reduceViewport(view, 'new-content', 0));
|
|
543
|
+
setState((current) => reduceTuiEvent(current, event));
|
|
544
|
+
});
|
|
545
|
+
},
|
|
546
|
+
onApproval: (next) => {
|
|
547
|
+
developmentLog('ui.approval', { requestId: next?.requestId ?? null, action: next?.action ?? null, reason: next?.reason ?? null }, 'info', 'tui');
|
|
548
|
+
/* R3 approval key race: the grace starts when a request is first shown, not on every redraw. */
|
|
549
|
+
pendingRef.current = next;
|
|
550
|
+
if (!next)
|
|
551
|
+
approvalShownRef.current = null;
|
|
552
|
+
else if (approvalShownRef.current?.requestId !== next.requestId) {
|
|
553
|
+
approvalShownRef.current = { requestId: next.requestId, shownAt: clockRef.current(), answered: false };
|
|
554
|
+
approvalUiRef.current = { expanded: false, selected: APPROVAL_DEFAULT_SELECTED };
|
|
555
|
+
}
|
|
556
|
+
return coordinator.immediate('approval', () => {
|
|
557
|
+
if (next)
|
|
558
|
+
setTranscriptSearch(null);
|
|
559
|
+
setPending(next);
|
|
560
|
+
setApprovalExpanded(false);
|
|
561
|
+
setApprovalScroll(0);
|
|
562
|
+
setApprovalSelected(APPROVAL_DEFAULT_SELECTED);
|
|
563
|
+
/* R3 Q-R3-7 (gate E7): the status says `waiting for you` exactly while the dialog is open. */
|
|
564
|
+
setState((current) => current.waitingForPerson === Boolean(next) ? current : { ...current, waitingForPerson: Boolean(next) });
|
|
565
|
+
setAppState((current) => next ? openOverlay(current, { kind: 'approval', requestId: next.requestId }) : (current.overlay?.kind === 'approval' ? closeOverlay(current) : current));
|
|
566
|
+
});
|
|
567
|
+
},
|
|
568
|
+
onQueueChange: (next) => { developmentLog('ui.queue', { count: next.length, kinds: next.map(row => row.kind) }, 'debug', 'tui'); return coordinator.enqueueEvent(`queue-${++eventSequenceRef.current}`, () => setQueuedActions([...next])); },
|
|
569
|
+
onQueueRestored: (next) => coordinator.enqueueEvent(`queue-restored-${++eventSequenceRef.current}`, () => { setQueueEditor({ selected: 0, editing: false, draft: '' }); setState((current) => localMessage(current, tn('screen.queue.restored', next.length))); }),
|
|
570
|
+
onQueuedSlash: (text) => executeSlashRef.current(text),
|
|
571
|
+
onQueuedDispatch: (action) => coordinator.enqueueEvent(`queue-dispatch-${++eventSequenceRef.current}`, () => { setViewport((view) => reduceViewport(view, 'new-content', 0)); setState((current) => userMessage(current, action.kind === 'command' ? `!${action.text}` : action.text)); }),
|
|
572
|
+
onSteerState: (next) => coordinator.enqueueEvent(`steer-${++eventSequenceRef.current}-${next.status}`, () => setState((current) => localMessage(current, next.status === 'requested' ? t('screen.steer.requested')
|
|
573
|
+
: next.status === 'applied' ? t('screen.steer.applied')
|
|
574
|
+
: next.status === 'cancelled' ? t('screen.steer.cancelled')
|
|
575
|
+
: screenErrorText(Object.assign(new Error(next.message), { code: next.code }), act('screen.action.steering'), { rejected: next.status !== 'failed' })))),
|
|
576
|
+
onError: (error) => { developmentLogError('ui.error', error, {}, 'tui'); return coordinator.enqueueEvent(`error-${++eventSequenceRef.current}`, () => setState((current) => localMessage(current, screenErrorText(error)))); },
|
|
577
|
+
});
|
|
578
|
+
const controller = controllerRef.current;
|
|
579
|
+
const executeSlashRef = React.useRef(async () => { });
|
|
580
|
+
React.useEffect(() => { developmentLog('ui.state', { focus: appState.focus.current, overlay: appState.overlay?.kind ?? null, running: state.running, sessionId: state.sessionId, model: state.status.model, permissionMode: state.status.permissionMode, queueCount: queuedActions.length }, 'debug', 'tui'); }, [appState.focus.current, appState.overlay?.kind, state.running, state.sessionId, state.status.model, state.status.permissionMode, queuedActions.length]);
|
|
581
|
+
async function refreshContextStatus() {
|
|
582
|
+
const id = state.sessionId ?? controller.current?.() ?? null;
|
|
583
|
+
if (!id || typeof runtime.contextStatus !== 'function') {
|
|
584
|
+
setContextStatus(null);
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
try {
|
|
588
|
+
const next = await runtime.contextStatus(id);
|
|
589
|
+
setContextStatus((current) => sameValue(current, next) ? current : next);
|
|
590
|
+
return next;
|
|
591
|
+
}
|
|
592
|
+
catch {
|
|
593
|
+
setContextStatus(null);
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
React.useEffect(() => { void refreshContextStatus(); }, [state.sessionId, state.running]);
|
|
598
|
+
function closeAgentView() {
|
|
599
|
+
const handle = agentViewRef.current;
|
|
600
|
+
agentViewRef.current = null;
|
|
601
|
+
handle?.close();
|
|
602
|
+
setAgentView(null);
|
|
603
|
+
const saved = mainViewportRef.current;
|
|
604
|
+
mainViewportRef.current = null;
|
|
605
|
+
setViewport((view) => saved ?? { ...view, offset: 0, unseen: 0, followingTail: true });
|
|
606
|
+
}
|
|
607
|
+
function openAgentView(row) {
|
|
608
|
+
agentViewRef.current?.close();
|
|
609
|
+
if (!mainViewportRef.current)
|
|
610
|
+
mainViewportRef.current = viewport;
|
|
611
|
+
setViewport((view) => ({ ...view, offset: 0, unseen: 0, followingTail: true }));
|
|
612
|
+
agentViewRef.current = createAgentView({ runtime, target: { sessionId: row.id, task: row.task }, secretValues: secretValuesFromEnvironment(process.env), onChange: (next) => setAgentView(next) });
|
|
613
|
+
}
|
|
614
|
+
/* Another session in use (resume, fork, /clear) and the app closing let go of the watched agent. */
|
|
615
|
+
React.useEffect(() => { if (agentViewRef.current)
|
|
616
|
+
closeAgentView(); }, [state.sessionId]);
|
|
617
|
+
React.useEffect(() => () => { agentViewRef.current?.close(); agentViewRef.current = null; }, []);
|
|
618
|
+
async function refreshAgentRows() {
|
|
619
|
+
try {
|
|
620
|
+
const tree = await controller.agentTree();
|
|
621
|
+
const rows = agentRosterRows(tree);
|
|
622
|
+
setAgentRows((current) => sameValue(current, rows) ? current : rows);
|
|
623
|
+
setAgentTreeOverlay((current) => {
|
|
624
|
+
if (!current)
|
|
625
|
+
return current;
|
|
626
|
+
const selectedId = selectedAgentRow(current)?.id ?? tree?.focusId ?? null;
|
|
627
|
+
return createAgentTreeOverlayModel(rows, selectedId);
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
catch { /* Agent roster is read-only chrome; runtime errors remain on the primary session path. */ }
|
|
631
|
+
}
|
|
632
|
+
React.useEffect(() => { void refreshAgentRows(); }, [state.sessionId, state.running, state.tools.length, state.messages.length]);
|
|
633
|
+
React.useEffect(() => {
|
|
634
|
+
let alive = true;
|
|
635
|
+
/* B1 slice 18: a prompt given on the command line waits in the composer when no provider, model or key is ready. */
|
|
636
|
+
void (initialPrompt?.trim() ? readinessFor(controller.model()) : Promise.resolve(null)).then((readiness) => {
|
|
637
|
+
const prompt = readiness && !readiness.ready ? undefined : initialPrompt;
|
|
638
|
+
return controller.initialize({ resume: invocation.resume, fork: invocation.fork, initialPrompt: prompt }).then((id) => {
|
|
639
|
+
if (!alive)
|
|
640
|
+
return;
|
|
641
|
+
if (prompt?.trim())
|
|
642
|
+
setState((current) => ({ ...userMessage(current, prompt.trim(), `user-${Date.now()}`), sessionId: id ?? current.sessionId }));
|
|
643
|
+
if (readiness && !readiness.ready) {
|
|
644
|
+
const editor = replaceEditorText(currentEditor(), String(initialPrompt).trim());
|
|
645
|
+
composerStore.replace(editor);
|
|
646
|
+
setAppState((current) => ({ ...current, editor }));
|
|
647
|
+
setState((current) => localMessage(current, readiness.message));
|
|
648
|
+
}
|
|
649
|
+
setBoot((current) => markBootReady(current, Date.now()));
|
|
650
|
+
void refreshAgentRows();
|
|
651
|
+
});
|
|
652
|
+
}).catch((error) => { if (alive) {
|
|
653
|
+
setBoot((current) => markBootError(current));
|
|
654
|
+
setState((current) => localMessage(current, screenErrorText(error, act('screen.action.startup'))));
|
|
655
|
+
} });
|
|
656
|
+
void refreshSelection(() => alive);
|
|
657
|
+
return () => { alive = false; controller.close(); };
|
|
658
|
+
}, []);
|
|
659
|
+
const updateEditor = (fn) => { const editor = composerStore.update(fn); setAppState((current) => current.editor === editor ? current : { ...current, editor }); return editor; };
|
|
660
|
+
const setEditorText = (text) => updateEditor(editor => replaceEditorText(editor, text));
|
|
661
|
+
/*
|
|
662
|
+
* R4 Q-R4-8 / E8: the `@` list follows the composer while the person types (it opened on Tab only: gate §0.8). It
|
|
663
|
+
* listens to the composer store, so the fast input path keeps its speed: a key that does not change the `@` token at
|
|
664
|
+
* the end of the text costs one comparison and no render. Nothing runs while the screen is idle (R1).
|
|
665
|
+
*/
|
|
666
|
+
const checkCompletion = (force = false) => {
|
|
667
|
+
const watch = completionWatchRef.current;
|
|
668
|
+
const token = lastProjectToken(composerStore.getSnapshot().text);
|
|
669
|
+
if (!force && token === watch.token)
|
|
670
|
+
return;
|
|
671
|
+
watch.token = token;
|
|
672
|
+
const seq = ++watch.seq;
|
|
673
|
+
if (force)
|
|
674
|
+
watch.dismissed = null;
|
|
675
|
+
if (!token || token === watch.dismissed) {
|
|
676
|
+
if (!token)
|
|
677
|
+
watch.dismissed = null;
|
|
678
|
+
setFileCompletion((current) => current === null ? current : null);
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
void completeProjectFiles(projectRoot, token, { index: () => projectIndexRef.current.get() }).then(({ rows, source }) => { if (watch.seq === seq)
|
|
682
|
+
setFileCompletion({ token, rows, selected: 0, source }); }, () => { if (watch.seq === seq)
|
|
683
|
+
setFileCompletion(null); });
|
|
684
|
+
};
|
|
685
|
+
const checkCompletionRef = React.useRef(() => { });
|
|
686
|
+
checkCompletionRef.current = () => checkCompletion(false);
|
|
687
|
+
React.useEffect(() => { const run = () => checkCompletionRef.current(); run(); return composerStore.subscribe(run); }, [composerStore]);
|
|
688
|
+
const addLocal = (text) => setState((current) => localMessage(current, text));
|
|
689
|
+
/* R4 Q-R4-6: Ctrl+L and /redraw repaint the screen and keep the conversation and the draft (Claude Code
|
|
690
|
+
`chat:clearInput` "Force a full screen redraw, preserving input and conversation", keybindings.md, 2.1.280 → 2026-09-22,
|
|
691
|
+
gate B.3). Ink's own write erases the frame it drew and draws it again; a remount alone wrote nothing (Ink skips a
|
|
692
|
+
frame equal to the last one). */
|
|
693
|
+
const repaintScreen = () => { try {
|
|
694
|
+
if (typeof inkStdout?.write === 'function')
|
|
695
|
+
inkStdout.write('');
|
|
696
|
+
}
|
|
697
|
+
catch { /* a repaint that cannot be written leaves the screen as it is */ } };
|
|
698
|
+
/* R5a S5/S8 (Q-R5-7, Q-R5-9): what Enter does while TALOS works, and the reasoning choice, both remembered. */
|
|
699
|
+
const prefs = useUiPreferences(React, { paths, projectRoot, given: { ...(props.busyInput ? { busyInput: props.busyInput } : {}), ...(props.reasoning ? { reasoning: props.reasoning } : {}) }, applyReasoning: (visible) => setAppState((current) => current.reasoningVisible === visible ? current : { ...current, reasoningVisible: visible }), onSaveError: (error) => addLocal(screenErrorText(error, act('screen.action.saveSetting'))) });
|
|
700
|
+
/* R5b D4 (Q-R5-14): a theme kept from /theme holds at once and is saved in the user configuration (every project). */
|
|
701
|
+
/* R5b D3: go back to (or re-apply up to) the chosen checkpoint; another TALOS on this project refuses it first. */
|
|
702
|
+
const runUndo = async (picked) => {
|
|
703
|
+
const store = checkpointStoreFor(), chosen = picked.rows[picked.selected];
|
|
704
|
+
if (!store || !chosen)
|
|
705
|
+
return;
|
|
706
|
+
try {
|
|
707
|
+
if (await hasActiveRuns(paths, { exceptPid: process.pid, projectRoot })) {
|
|
708
|
+
addLocal(t('screen.undo.otherRun'));
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
const { direction } = undoRange(picked.rows, picked.selected);
|
|
712
|
+
const done = (direction === 'rewind' ? await rewindToCheckpoint({ store, id: chosen.id }) : await reapplyToCheckpoint({ store, id: chosen.id })).filter(row => checkpointChanges(row).length > 0);
|
|
713
|
+
addLocal(tn(direction === 'rewind' ? 'screen.undo.done' : 'screen.undo.redone', done.length, { files: checkpointFilesText(rangeFiles(done)) }));
|
|
714
|
+
}
|
|
715
|
+
catch (error) {
|
|
716
|
+
addLocal(screenErrorText(error, act('screen.action.command', { command: 'undo' })));
|
|
717
|
+
}
|
|
718
|
+
};
|
|
719
|
+
/* R5b D3 (gate finding 10: nothing on screen said a checkpoint was taken): a turn that changed files leaves one dim row. The
|
|
720
|
+
runtime finalizes the checkpoint after the run's last event, so an open one is waited for, briefly. */
|
|
721
|
+
const runStartedAtRef = React.useRef(null);
|
|
722
|
+
React.useEffect(() => {
|
|
723
|
+
if (state.running) {
|
|
724
|
+
if (!runStartedAtRef.current)
|
|
725
|
+
runStartedAtRef.current = new Date().toISOString();
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
const since = runStartedAtRef.current, sessionId = state.sessionId;
|
|
729
|
+
runStartedAtRef.current = null;
|
|
730
|
+
const store = since && sessionId ? checkpointStoreFor() : null;
|
|
731
|
+
if (!store || !since || !sessionId)
|
|
732
|
+
return;
|
|
733
|
+
let alive = true;
|
|
734
|
+
void (async () => {
|
|
735
|
+
for (let attempt = 0; attempt < 20 && alive; attempt += 1) {
|
|
736
|
+
const rows = (await sessionCheckpoints(store, sessionId)).filter(row => row.createdAt >= since);
|
|
737
|
+
if (!rows.length)
|
|
738
|
+
return;
|
|
739
|
+
if (rows.some(row => row.state === 'open')) {
|
|
740
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
const files = rangeFiles(rows);
|
|
744
|
+
if (files.length && alive)
|
|
745
|
+
setState((current) => ({ ...current, transcript: appendTranscriptWarning(current.transcript, t('screen.undo.marker', { files: checkpointFilesText(files) })) }));
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
})().catch((error) => developmentLogError('tui.checkpoint.marker.failure', error, {}, 'tui-app'));
|
|
749
|
+
return () => { alive = false; };
|
|
750
|
+
}, [state.running]);
|
|
751
|
+
const keepTheme = (id) => {
|
|
752
|
+
themeStore.set(id);
|
|
753
|
+
void saveUiPreference({ paths, projectRoot, patch: { theme: id } }).then(() => addLocal(t('screen.theme.set', { theme: themeLabel(id) })), () => addLocal(t('screen.theme.setUnsaved', { theme: themeLabel(id) })));
|
|
754
|
+
};
|
|
755
|
+
const runKeyAction = (action) => {
|
|
756
|
+
setAppState((current) => reduceAppAction(current, { type: 'key-action', action }));
|
|
757
|
+
if (action === 'redraw')
|
|
758
|
+
repaintScreen();
|
|
759
|
+
/* R5a S8 (Q-R5-9): Ctrl+T shows or hides every reasoning block, and the choice is remembered in `ui.reasoning`. */
|
|
760
|
+
if (action === 'reasoning-toggle')
|
|
761
|
+
prefs.rememberReasoning(!reasoningVisibleRef.current);
|
|
762
|
+
};
|
|
763
|
+
/* R4 Q-R4-9: a draft goes back into the composer with its paste blocks. */
|
|
764
|
+
const restoreDraft = (draft) => { const editor = editorFromDraft(draft); composerStore.replace(editor); setAppState((current) => current.editor === editor ? current : { ...current, editor }); return editor; };
|
|
765
|
+
const rememberDraft = (draft) => setHistory((rows) => draft.text.trim() ? [draft, ...rows.filter(row => row.text !== draft.text)].slice(0, MAX_HISTORY) : rows);
|
|
766
|
+
/* R5b D5 (Q-R5-15): what was SENT is also kept on disk for the next sessions of this project (a cleared draft is not: the
|
|
767
|
+
person chose to drop it). The kept rows are read once, after the ones typed meanwhile; a prompt sent before the store
|
|
768
|
+
is ready waits for it. Off (ui.history.persist false): nothing is read or written. */
|
|
769
|
+
const historyStoreRef = React.useRef({ store: null, ready: false, waiting: [] });
|
|
770
|
+
const persistDraft = (draft) => {
|
|
771
|
+
const kept = historyStoreRef.current;
|
|
772
|
+
if (!draft.text.trim())
|
|
773
|
+
return;
|
|
774
|
+
if (!kept.ready) {
|
|
775
|
+
kept.waiting.push(draft);
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
if (kept.store)
|
|
779
|
+
void kept.store.append(draft).catch((error) => developmentLogError('tui.history.append.failure', error, {}, 'tui-app'));
|
|
780
|
+
};
|
|
781
|
+
const rememberSent = (draft) => { rememberDraft(draft); persistDraft(draft); };
|
|
782
|
+
React.useEffect(() => {
|
|
783
|
+
let alive = true;
|
|
784
|
+
void (async () => {
|
|
785
|
+
const kept = historyStoreRef.current;
|
|
786
|
+
try {
|
|
787
|
+
if (await promptHistoryEnabled({ paths, projectRoot })) {
|
|
788
|
+
const store = createPromptHistoryStore({ paths, projectRoot, secrets: secretValuesFromEnvironment(process.env) });
|
|
789
|
+
const rows = await store.load();
|
|
790
|
+
kept.store = store;
|
|
791
|
+
if (alive)
|
|
792
|
+
setHistory((current) => { const seen = new Set(current.map(row => row.text)); return [...current, ...rows.filter(row => !seen.has(row.text))].slice(0, MAX_HISTORY); });
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
catch (error) {
|
|
796
|
+
developmentLogError('tui.history.load.failure', error, {}, 'tui-app');
|
|
797
|
+
}
|
|
798
|
+
kept.ready = true;
|
|
799
|
+
const waiting = kept.waiting.splice(0);
|
|
800
|
+
for (const draft of waiting)
|
|
801
|
+
persistDraft(draft);
|
|
802
|
+
})();
|
|
803
|
+
return () => { alive = false; };
|
|
804
|
+
}, []);
|
|
805
|
+
const transcriptItemsForCurrentView = () => appState.reasoningVisible ? state.transcript.items : state.transcript.items.filter((item) => item.kind !== 'reasoning');
|
|
806
|
+
const transcriptItemForSelection = (preferred = transcriptSelectedId) => {
|
|
807
|
+
const items = transcriptItemsForCurrentView();
|
|
808
|
+
return items.find((item) => item.id === preferred) ?? items.at(-1) ?? null;
|
|
809
|
+
};
|
|
810
|
+
const jumpTranscriptItem = (itemId) => {
|
|
811
|
+
const items = transcriptItemsForCurrentView(), index = items.findIndex((item) => item.id === itemId);
|
|
812
|
+
if (index < 0)
|
|
813
|
+
return;
|
|
814
|
+
/* R1: the row planner scrolls the target into view (revealIndex); the view stops following the tail. */
|
|
815
|
+
setTranscriptSelectedId(itemId);
|
|
816
|
+
setTranscriptRevealId(itemId);
|
|
817
|
+
setViewport((view) => index === items.length - 1 ? view : { ...view, followingTail: false });
|
|
818
|
+
};
|
|
819
|
+
const copyTranscriptItem = (preferred = transcriptSelectedId) => {
|
|
820
|
+
const item = transcriptItemForSelection(preferred);
|
|
821
|
+
if (!item) {
|
|
822
|
+
const notice = t('screen.copy.nothing');
|
|
823
|
+
setTranscriptSearch((current) => current ? { ...current, notice } : { ...createTranscriptSearchState(null), raw: transcriptRaw, notice });
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
try {
|
|
827
|
+
const text = transcriptRaw ? transcriptItemRaw(item) : transcriptItemText(item);
|
|
828
|
+
const sequence = osc52CopySequence(text);
|
|
829
|
+
stdout.write(sequence);
|
|
830
|
+
setTranscriptSelectedId(item.id);
|
|
831
|
+
const notice = t('screen.copy.requested');
|
|
832
|
+
setTranscriptSearch((current) => current ? { ...current, selectedId: item.id, raw: transcriptRaw, notice } : { ...createTranscriptSearchState(item.id), raw: transcriptRaw, notice });
|
|
833
|
+
}
|
|
834
|
+
catch (error) {
|
|
835
|
+
const notice = screenErrorLine(error, act('screen.action.copy'));
|
|
836
|
+
setTranscriptSearch((current) => current ? { ...current, notice } : { ...createTranscriptSearchState(item.id), raw: transcriptRaw, notice });
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
const openTranscriptSearch = () => {
|
|
840
|
+
const item = transcriptItemForSelection();
|
|
841
|
+
const selectedId = item?.id ?? null;
|
|
842
|
+
if (selectedId)
|
|
843
|
+
setTranscriptSelectedId(selectedId);
|
|
844
|
+
setTranscriptSearch({ ...createTranscriptSearchState(selectedId), raw: transcriptRaw });
|
|
845
|
+
};
|
|
846
|
+
const showModel = (provider, model) => setState((current) => ({ ...current, status: { ...current.status, model: !provider ? NO_PROVIDER_MODEL : providerOfModel(model) === provider ? model : NO_MODEL } }));
|
|
847
|
+
async function refreshSelection(isAlive = () => true) {
|
|
848
|
+
try {
|
|
849
|
+
const id = typeof catalog?.chosenProvider === 'function' ? await catalog.chosenProvider() : null;
|
|
850
|
+
if (!isAlive())
|
|
851
|
+
return;
|
|
852
|
+
setChosenProvider(id);
|
|
853
|
+
const model = controller.model();
|
|
854
|
+
showModel(id, model);
|
|
855
|
+
if (id && providerOfModel(model) === id) {
|
|
856
|
+
const rows = await catalog.listModels({ provider: id });
|
|
857
|
+
if (!isAlive())
|
|
858
|
+
return;
|
|
859
|
+
setCurrentModelMeta(rows.find(row => row.id === model) ?? null);
|
|
860
|
+
/* R4 Q-R4-13 (lane F's onboardingSteps): a missing key is said on the first screen, before anything is sent. */
|
|
861
|
+
const readiness = await readinessFor(model);
|
|
862
|
+
if (!isAlive())
|
|
863
|
+
return;
|
|
864
|
+
setKeyMissing(!readiness.ready && (readiness.code === 'PROVIDER_KEY_MISSING' || readiness.code === 'PROVIDER_KEY_BENCHED'));
|
|
865
|
+
}
|
|
866
|
+
else
|
|
867
|
+
setCurrentModelMeta(null);
|
|
868
|
+
}
|
|
869
|
+
catch {
|
|
870
|
+
if (isAlive())
|
|
871
|
+
setCurrentModelMeta(null);
|
|
872
|
+
}
|
|
873
|
+
finally {
|
|
874
|
+
if (isAlive())
|
|
875
|
+
setSelectionLoaded(true);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
React.useEffect(() => {
|
|
879
|
+
if (!selectionLoaded || onboardingShownRef.current)
|
|
880
|
+
return;
|
|
881
|
+
const steps = onboardingSteps({ projectTrusted: true, providerConfigured: Boolean(chosenProvider), modelConfigured: Boolean(chosenProvider && providerOfModel(controller.model()) === chosenProvider), keyMissing: keyMissing && chosenProvider ? chosenProvider : keyMissing, ...(props.loadedFiles ? { loadedFiles: props.loadedFiles } : {}) });
|
|
882
|
+
if (!steps.length)
|
|
883
|
+
return;
|
|
884
|
+
onboardingShownRef.current = true;
|
|
885
|
+
const info = steps.filter(step => step.kind === 'info').map(step => step.text);
|
|
886
|
+
/* P17: on the first launch the missing steps are the guided setup, not a list of commands to type. */
|
|
887
|
+
if (bootPlanRef.current?.firstLaunch && !initialPrompt?.trim() && !invocation.setup) {
|
|
888
|
+
void (async () => {
|
|
889
|
+
let row;
|
|
890
|
+
try {
|
|
891
|
+
row = chosenProvider ? (await catalog.listProviders()).find((entry) => entry.id === chosenProvider) : undefined;
|
|
892
|
+
}
|
|
893
|
+
catch {
|
|
894
|
+
row = undefined;
|
|
895
|
+
}
|
|
896
|
+
const plan = setupStart({ firstLaunch: true, providerChosen: Boolean(chosenProvider), providerUsable: Boolean(row?.configured), keyBenched: Boolean(row?.benched), modelChosen: Boolean(chosenProvider && providerOfModel(controller.model()) === chosenProvider) });
|
|
897
|
+
if (info.length)
|
|
898
|
+
setState((current) => localMessage(current, info.join('\n')));
|
|
899
|
+
if (plan.start) {
|
|
900
|
+
await startSetup(plan.from);
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
const todo = steps.filter(step => step.kind !== 'info');
|
|
904
|
+
if (todo.length)
|
|
905
|
+
setState((current) => localMessage(current, [t('screen.onboarding.setup'), ...todo.map(step => '• ' + step.text + (step.command ? ' ' + step.command : ''))].join('\n')));
|
|
906
|
+
})();
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
const todo = steps.filter(step => step.kind !== 'info');
|
|
910
|
+
setState((current) => localMessage(current, [...info, ...(todo.length ? [t('screen.onboarding.setup'), ...todo.map(step => '• ' + step.text + (step.command ? ' ' + step.command : ''))] : [])].join('\n')));
|
|
911
|
+
}, [selectionLoaded, chosenProvider, currentModelMeta, keyMissing]);
|
|
912
|
+
const closeCurrentOverlay = () => { oauthLoginRef.current?.cancel(); oauthLoginRef.current = null; setTrustCenter(null); setMcpCenter(null); setHookCenter(null); setPluginCenter(null); setMemoryCenter(null); setNotesCenter(null); setTasksCenter(null); setLibraryCenter(null); setProviderSecret(''); setProviderStep({ kind: 'list' }); setModelEffortStep(null); setEffortOnly(false); setPickerQuery(''); setPickerSelection({ query: '', selected: 0, pageSize: 10 }); setAppState((current) => closeOverlay(current)); };
|
|
913
|
+
const applyOverlay = (overlay) => setAppState((current) => openOverlay(current, overlay));
|
|
914
|
+
/* B1 slice 18: the models of the chosen provider only; with none chosen, /provider opens first and comes back here. */
|
|
915
|
+
const loadModels = async (provider) => {
|
|
916
|
+
const target = provider ?? chosenProvider;
|
|
917
|
+
if (!target) {
|
|
918
|
+
await loadProviders();
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
applyOverlay({ kind: 'model', provider: target });
|
|
922
|
+
setModelEffortStep(null);
|
|
923
|
+
setPickerQuery('');
|
|
924
|
+
setPickerSelection({ query: '', selected: 0, pageSize: 10 });
|
|
925
|
+
setModelRows([]);
|
|
926
|
+
setModelList({ loading: true, verified: false, notice: t('screen.model.loading'), label: target, engineDown: false });
|
|
927
|
+
modelPickerRef.current?.cancel();
|
|
928
|
+
const model = createModelPickerModel(catalog, { provider: target, current: controller.model() });
|
|
929
|
+
modelPickerRef.current = model;
|
|
930
|
+
try {
|
|
931
|
+
await model.open();
|
|
932
|
+
if (modelPickerRef.current !== model)
|
|
933
|
+
return;
|
|
934
|
+
setModelRows([...model.rows()]);
|
|
935
|
+
setModelList({ loading: false, verified: model.verified(), notice: model.notice(), label: model.label(), engineDown: model.engineDown() });
|
|
936
|
+
}
|
|
937
|
+
catch (error) {
|
|
938
|
+
addLocal(screenErrorText(codeOnly(error), act('screen.action.modelPicker')));
|
|
939
|
+
}
|
|
940
|
+
};
|
|
941
|
+
const chooseProviderAndOpenModels = async (row) => {
|
|
942
|
+
try {
|
|
943
|
+
await catalog.chooseProvider(row.id);
|
|
944
|
+
}
|
|
945
|
+
catch (error) {
|
|
946
|
+
addLocal(screenErrorText(codeOnly(error), act('screen.action.providerChoice')));
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
setChosenProvider(row.id);
|
|
950
|
+
showModel(row.id, controller.model());
|
|
951
|
+
setupTransitionRef.current = true;
|
|
952
|
+
try {
|
|
953
|
+
setProviderStep({ kind: 'list' });
|
|
954
|
+
setProviderSecret('');
|
|
955
|
+
setAppState((current) => closeOverlay(current));
|
|
956
|
+
await loadModels(row.id);
|
|
957
|
+
}
|
|
958
|
+
finally {
|
|
959
|
+
setupTransitionRef.current = false;
|
|
960
|
+
}
|
|
961
|
+
};
|
|
962
|
+
/* P17: the setup starts at the first missing step; it ends when its windows close (a model chosen, or Esc), and says
|
|
963
|
+
whether TALOS is ready — checked, not assumed: a provider, its key and the model's readiness. */
|
|
964
|
+
const startSetup = async (from) => {
|
|
965
|
+
setupRef.current = true;
|
|
966
|
+
setSetupActive(true);
|
|
967
|
+
setupTransitionRef.current = true;
|
|
968
|
+
try {
|
|
969
|
+
if (from === 'model' && chosenProvider) {
|
|
970
|
+
await loadModels(chosenProvider);
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
await loadProviders();
|
|
974
|
+
/* The key step opens at once for the chosen provider (the consent first when its key is in the environment): no
|
|
975
|
+
probe of a provider that has no usable key yet. */
|
|
976
|
+
if (from === 'key' && chosenProvider) {
|
|
977
|
+
const row = (await catalog.listProviders()).find((entry) => entry.id === chosenProvider);
|
|
978
|
+
if (row) {
|
|
979
|
+
const preliminary = providerPickerEnter({ selectedProvider: row.id, configured: row.configured, requiresKey: row.requiresKey, keySource: row.keySource });
|
|
980
|
+
setProviderSecret('');
|
|
981
|
+
setProviderStep(preliminary.kind === 'environment-consent' ? { kind: 'consent', provider: row, choice: 'no' } : keyStepFor(row));
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
catch (error) {
|
|
986
|
+
addLocal(screenErrorText(codeOnly(error), act('screen.action.providerPicker')));
|
|
987
|
+
}
|
|
988
|
+
finally {
|
|
989
|
+
setupTransitionRef.current = false;
|
|
990
|
+
}
|
|
991
|
+
};
|
|
992
|
+
const finishSetup = async () => {
|
|
993
|
+
if (!setupRef.current)
|
|
994
|
+
return;
|
|
995
|
+
setupRef.current = false;
|
|
996
|
+
setSetupActive(false);
|
|
997
|
+
const model = controller.model();
|
|
998
|
+
const pending = setupPendingProviderRef.current;
|
|
999
|
+
setupPendingProviderRef.current = null;
|
|
1000
|
+
if (pending && providerOfModel(model) === pending.id) {
|
|
1001
|
+
try {
|
|
1002
|
+
await catalog.chooseProvider(pending.id);
|
|
1003
|
+
setChosenProvider(pending.id);
|
|
1004
|
+
showModel(pending.id, model);
|
|
1005
|
+
}
|
|
1006
|
+
catch (error) {
|
|
1007
|
+
addLocal(screenErrorText(codeOnly(error), act('screen.action.providerChoice')));
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
let ready = false;
|
|
1011
|
+
try {
|
|
1012
|
+
const provider = typeof catalog?.chosenProvider === 'function' ? await catalog.chosenProvider() : null;
|
|
1013
|
+
ready = Boolean(provider && providerOfModel(model) === provider && (await readinessFor(model)).ready);
|
|
1014
|
+
}
|
|
1015
|
+
catch {
|
|
1016
|
+
ready = false;
|
|
1017
|
+
}
|
|
1018
|
+
addLocal(setupEndLines(ready ? { ready: true, model } : { ready: false }).join('\n'));
|
|
1019
|
+
};
|
|
1020
|
+
React.useEffect(() => {
|
|
1021
|
+
if (!setupRef.current || appState.overlay || setupTransitionRef.current)
|
|
1022
|
+
return;
|
|
1023
|
+
void finishSetup();
|
|
1024
|
+
}, [appState.overlay]);
|
|
1025
|
+
const setupAskedRef = React.useRef(false);
|
|
1026
|
+
React.useEffect(() => {
|
|
1027
|
+
if (!invocation.setup || !selectionLoaded || setupAskedRef.current)
|
|
1028
|
+
return;
|
|
1029
|
+
setupAskedRef.current = true;
|
|
1030
|
+
void startSetup('provider');
|
|
1031
|
+
}, [selectionLoaded]);
|
|
1032
|
+
const loadProviders = async () => {
|
|
1033
|
+
applyOverlay({ kind: 'provider' });
|
|
1034
|
+
setPickerQuery('');
|
|
1035
|
+
setPickerSelection({ query: '', selected: 0, pageSize: 10 });
|
|
1036
|
+
setProviderStep({ kind: 'list' });
|
|
1037
|
+
setProviderSecret('');
|
|
1038
|
+
const model = createProviderPickerModel(catalog);
|
|
1039
|
+
providerPickerRef.current = model;
|
|
1040
|
+
try {
|
|
1041
|
+
await model.open();
|
|
1042
|
+
setProviderRows([...model.rows()]);
|
|
1043
|
+
}
|
|
1044
|
+
catch (error) {
|
|
1045
|
+
addLocal(screenErrorText(error, act('screen.action.providerPicker')));
|
|
1046
|
+
}
|
|
1047
|
+
};
|
|
1048
|
+
const keyStepFor = (row, remediation) => row.id === 'openrouter' && typeof catalog?.startOpenRouterLogin === 'function'
|
|
1049
|
+
? { kind: 'login-choice', provider: row, choice: 'browser', ...(remediation ? { remediation } : {}) }
|
|
1050
|
+
: { kind: 'key', provider: row, testing: false, result: null, ...(remediation ? { remediation } : {}) };
|
|
1051
|
+
const oauthErrorText = (provider, error) => {
|
|
1052
|
+
const code = String(error?.code ?? '');
|
|
1053
|
+
const status = error?.status ?? '?';
|
|
1054
|
+
const known = ['OAUTH_TIMEOUT', 'OAUTH_SCAMBIO_RIFIUTATO', 'OAUTH_RETE', 'OAUTH_RISPOSTA_INATTESA', 'OAUTH_DENIED', 'OAUTH_CODE_MISSING'];
|
|
1055
|
+
return known.includes(code) ? t(`screen.provider.oauthError.${code}`, { provider: provider.label, status }) : t('screen.provider.oauthError.other');
|
|
1056
|
+
};
|
|
1057
|
+
/* P21 (owner decisions 2026-09-26): the browser opens (the address is shown anyway), the loopback waits, a code can be
|
|
1058
|
+
pasted instead; the key that comes back is tested and saved like a pasted one, then the models open. */
|
|
1059
|
+
const completeOAuth = async (provider, key) => {
|
|
1060
|
+
setProviderStep((current) => current.kind === 'oauth' ? { ...current, phase: 'exchanging', message: null } : current);
|
|
1061
|
+
let result;
|
|
1062
|
+
try {
|
|
1063
|
+
result = await catalog.setProviderKey(provider.id, key);
|
|
1064
|
+
}
|
|
1065
|
+
catch (error) {
|
|
1066
|
+
setProviderStep((current) => current.kind === 'oauth' ? { ...current, phase: 'failed', message: screenErrorText(codeOnly(error), act('screen.action.keyTest')) } : current);
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
finally {
|
|
1070
|
+
key = '';
|
|
1071
|
+
}
|
|
1072
|
+
if (!result.saved) {
|
|
1073
|
+
setProviderStep((current) => current.kind === 'oauth' ? { ...current, phase: 'failed', message: result.message } : current);
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
oauthLoginRef.current = null;
|
|
1077
|
+
addLocal(t('screen.provider.oauthSaved', { provider: provider.label }));
|
|
1078
|
+
const refreshed = (await catalog.listProviders()).find((entry) => entry.id === provider.id) ?? { ...provider, configured: true };
|
|
1079
|
+
await chooseProviderAndOpenModels(refreshed);
|
|
1080
|
+
};
|
|
1081
|
+
const startOAuth = async (provider) => {
|
|
1082
|
+
oauthLoginRef.current?.cancel();
|
|
1083
|
+
oauthLoginRef.current = null;
|
|
1084
|
+
setProviderSecret('');
|
|
1085
|
+
let login;
|
|
1086
|
+
try {
|
|
1087
|
+
login = await catalog.startOpenRouterLogin();
|
|
1088
|
+
}
|
|
1089
|
+
catch (error) {
|
|
1090
|
+
setProviderStep({ kind: 'oauth', provider, url: '', mode: 'browser', opened: false, phase: 'failed', message: oauthErrorText(provider, error) });
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
oauthLoginRef.current = login;
|
|
1094
|
+
/* The address also goes to the conversation, unboxed: a person copies it there without the window's borders. */
|
|
1095
|
+
addLocal(t('screen.provider.oauthAddress', { provider: provider.label, url: login.url }));
|
|
1096
|
+
setProviderStep({ kind: 'oauth', provider, url: login.url, mode: login.mode, opened: login.opened, phase: login.mode === 'paste' ? 'paste' : 'waiting', message: null });
|
|
1097
|
+
login.key.then((key) => { if (oauthLoginRef.current === login)
|
|
1098
|
+
void completeOAuth(provider, key); }, (error) => {
|
|
1099
|
+
if (oauthLoginRef.current !== login || error?.code === 'OAUTH_CANCELLED')
|
|
1100
|
+
return;
|
|
1101
|
+
oauthLoginRef.current = null;
|
|
1102
|
+
setProviderStep((current) => current.kind === 'oauth' ? { ...current, phase: 'failed', message: oauthErrorText(provider, error) } : current);
|
|
1103
|
+
});
|
|
1104
|
+
};
|
|
1105
|
+
const routeProviderSelection = async (row, { allowEnvironmentConsent = true } = {}) => {
|
|
1106
|
+
const originalIndex = providerRows.findIndex((provider) => provider.id === row.id);
|
|
1107
|
+
if (originalIndex >= 0)
|
|
1108
|
+
providerPickerRef.current?.select(originalIndex);
|
|
1109
|
+
if (allowEnvironmentConsent) {
|
|
1110
|
+
const preliminary = providerPickerEnter({ selectedProvider: row.id, configured: row.configured, requiresKey: row.requiresKey, keySource: row.keySource });
|
|
1111
|
+
if (preliminary.kind === 'environment-consent') {
|
|
1112
|
+
setProviderStep({ kind: 'consent', provider: row, choice: 'no' });
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
let probe = null;
|
|
1117
|
+
try {
|
|
1118
|
+
probe = await providerPickerRef.current?.probeSelected() ?? await catalog.probeProvider(row.id);
|
|
1119
|
+
}
|
|
1120
|
+
catch (error) {
|
|
1121
|
+
addLocal(screenErrorText(codeOnly(error), act('screen.action.healthCheck', { provider: row.label })));
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
const next = providerPickerEnter({ selectedProvider: row.id, configured: row.configured, requiresKey: row.requiresKey, keySource: row.keySource, probe });
|
|
1125
|
+
if (next.kind === 'provider-setup') {
|
|
1126
|
+
setProviderSecret('');
|
|
1127
|
+
setProviderStep(keyStepFor(row, next.remediation ?? undefined));
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
if (next.kind === 'provider-remediation') {
|
|
1131
|
+
/* P17 (owner decision 8): in the setup, a local engine that is down opens its model window, which says how to start it. */
|
|
1132
|
+
if (setupRef.current && row.local && !row.requiresKey) {
|
|
1133
|
+
setupPendingProviderRef.current = row;
|
|
1134
|
+
setupTransitionRef.current = true;
|
|
1135
|
+
try {
|
|
1136
|
+
setProviderStep({ kind: 'list' });
|
|
1137
|
+
setProviderSecret('');
|
|
1138
|
+
setAppState((current) => closeOverlay(current));
|
|
1139
|
+
await loadModels(row.id);
|
|
1140
|
+
}
|
|
1141
|
+
finally {
|
|
1142
|
+
setupTransitionRef.current = false;
|
|
1143
|
+
}
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
addLocal(next.remediation?.message || probe?.detail || t('screen.provider.notReady', { provider: row.label }));
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
if (next.kind === 'environment-consent') {
|
|
1150
|
+
setProviderStep({ kind: 'consent', provider: row, choice: 'no' });
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
if (next.remediation && next.remediation.code !== 'none')
|
|
1154
|
+
addLocal(next.remediation.message);
|
|
1155
|
+
await chooseProviderAndOpenModels(row);
|
|
1156
|
+
};
|
|
1157
|
+
/* R4 Q-R4-13: the local list is read first and the picker opens with it, so its first frame already holds the rows or
|
|
1158
|
+
the empty-state line (it never shows an empty box that only later turns out to be empty or failed). */
|
|
1159
|
+
const loadSessions = async (action) => {
|
|
1160
|
+
const model = createSessionPickerModel(controller, action, { metadataRoot: sessionMetadataRoot(paths.dataRoot) });
|
|
1161
|
+
sessionPickerRef.current = model;
|
|
1162
|
+
try {
|
|
1163
|
+
await model.open();
|
|
1164
|
+
}
|
|
1165
|
+
catch (error) {
|
|
1166
|
+
addLocal(screenErrorText(error, act('screen.action.sessionPicker')));
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
if (sessionPickerRef.current !== model)
|
|
1170
|
+
return;
|
|
1171
|
+
setSessionRows([...model.rows()]);
|
|
1172
|
+
setPickerQuery('');
|
|
1173
|
+
setPickerSelection({ query: '', selected: 0, pageSize: 10 });
|
|
1174
|
+
applyOverlay({ kind: 'session', action });
|
|
1175
|
+
};
|
|
1176
|
+
const loadTrustCenter = async () => {
|
|
1177
|
+
try {
|
|
1178
|
+
const snapshot = await createTrustAuthority({ projectRoot, trustRoot: paths.trust.projects }).inspect();
|
|
1179
|
+
setTrustCenter(trustCenterModel(snapshot, { permissionMode: controller.mode() }));
|
|
1180
|
+
}
|
|
1181
|
+
catch (error) {
|
|
1182
|
+
addLocal(screenErrorText(error, act('screen.action.trustCenter')));
|
|
1183
|
+
}
|
|
1184
|
+
};
|
|
1185
|
+
const mcpFacade = () => mcpFacadeRef.current ??= (createMcpFacade({ projectRoot, paths }));
|
|
1186
|
+
const loadMcpCenter = async (selectedId) => {
|
|
1187
|
+
try {
|
|
1188
|
+
const rows = await mcpFacade().list();
|
|
1189
|
+
const selected = selectedId ? Math.max(0, rows.findIndex(row => row.id === selectedId)) : mcpCenter?.selected ?? 0;
|
|
1190
|
+
setMcpCenter(createMcpCenterModel(rows, selected));
|
|
1191
|
+
}
|
|
1192
|
+
catch (error) {
|
|
1193
|
+
addLocal(screenErrorText(error, act('screen.action.mcpCenter')));
|
|
1194
|
+
}
|
|
1195
|
+
};
|
|
1196
|
+
const actOnMcpCenter = async (action) => {
|
|
1197
|
+
const row = mcpCenter ? selectedMcpServer(mcpCenter) : null;
|
|
1198
|
+
if (!row)
|
|
1199
|
+
return;
|
|
1200
|
+
try {
|
|
1201
|
+
if (action === 'probe') {
|
|
1202
|
+
const measured = await mcpFacade().probe(row.id);
|
|
1203
|
+
setMcpCenter((current) => current ? replaceMcpCenterRow(current, measured) : current);
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
if (action === 'trust')
|
|
1207
|
+
await mcpFacade().trust(row.id);
|
|
1208
|
+
else
|
|
1209
|
+
await mcpFacade().untrust(row.id);
|
|
1210
|
+
await loadMcpCenter(row.id);
|
|
1211
|
+
}
|
|
1212
|
+
catch (error) {
|
|
1213
|
+
addLocal(screenErrorText(error, act('screen.action.mcp', { action })));
|
|
1214
|
+
}
|
|
1215
|
+
};
|
|
1216
|
+
const hookFacade = () => hookFacadeRef.current ??= (createHookFacade({ projectRoot, paths }));
|
|
1217
|
+
const loadHookCenter = async (selectedId) => {
|
|
1218
|
+
try {
|
|
1219
|
+
const rows = await hookFacade().list();
|
|
1220
|
+
const found = selectedId ? rows.findIndex(row => row.id === selectedId) : -1;
|
|
1221
|
+
const selected = found >= 0 ? found : hookCenter?.selected ?? 0;
|
|
1222
|
+
setHookCenter(createHookCenterModel(rows, selected));
|
|
1223
|
+
}
|
|
1224
|
+
catch (error) {
|
|
1225
|
+
addLocal(screenErrorText(error, act('screen.action.hookCenter')));
|
|
1226
|
+
}
|
|
1227
|
+
};
|
|
1228
|
+
const actOnHookCenter = async (action) => {
|
|
1229
|
+
const row = hookCenter ? selectedHook(hookCenter) : null;
|
|
1230
|
+
if (!row)
|
|
1231
|
+
return;
|
|
1232
|
+
try {
|
|
1233
|
+
if (action === 'dry-run') {
|
|
1234
|
+
if (state.running) {
|
|
1235
|
+
addLocal(t('screen.hooks.dryRunBusy'));
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
const eventType = row.events[0];
|
|
1239
|
+
if (!eventType) {
|
|
1240
|
+
addLocal(t('screen.hooks.noEvent'));
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
const result = await hookFacade().dryRun(row.id, { eventType });
|
|
1244
|
+
setHookCenter((current) => current ? replaceHookCenterRow(current, { ...row, lastDryRun: result }) : current);
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
if (action === 'trust')
|
|
1248
|
+
await hookFacade().trust(row.id);
|
|
1249
|
+
else if (action === 'untrust')
|
|
1250
|
+
await hookFacade().untrust(row.id);
|
|
1251
|
+
else if (row.quarantine.active)
|
|
1252
|
+
await hookFacade().releaseQuarantine(row.id);
|
|
1253
|
+
else
|
|
1254
|
+
await hookFacade().quarantine(row.id, t('screen.hooks.manualQuarantine'));
|
|
1255
|
+
await loadHookCenter(row.id);
|
|
1256
|
+
}
|
|
1257
|
+
catch (error) {
|
|
1258
|
+
addLocal(screenErrorText(error, act('screen.action.hook', { action })));
|
|
1259
|
+
}
|
|
1260
|
+
};
|
|
1261
|
+
const pluginFacade = () => pluginFacadeRef.current ??= (createPluginFacade({ projectRoot, paths }));
|
|
1262
|
+
const loadPluginCenter = async (selectedId) => {
|
|
1263
|
+
try {
|
|
1264
|
+
const rows = await pluginFacade().list();
|
|
1265
|
+
const found = selectedId ? rows.findIndex(row => row.id === selectedId) : -1;
|
|
1266
|
+
const selected = found >= 0 ? found : pluginCenter?.selected ?? 0;
|
|
1267
|
+
setPluginCenter(createPluginCenterModel(rows, selected));
|
|
1268
|
+
}
|
|
1269
|
+
catch (error) {
|
|
1270
|
+
addLocal(screenErrorText(error, act('screen.action.pluginCenter')));
|
|
1271
|
+
}
|
|
1272
|
+
};
|
|
1273
|
+
const actOnPluginCenter = async (action) => {
|
|
1274
|
+
const row = pluginCenter ? selectedPlugin(pluginCenter) : null;
|
|
1275
|
+
if (!row)
|
|
1276
|
+
return;
|
|
1277
|
+
try {
|
|
1278
|
+
if (action === 'trust')
|
|
1279
|
+
await pluginFacade().trust(row.id);
|
|
1280
|
+
else if (action === 'untrust')
|
|
1281
|
+
await pluginFacade().untrust(row.id);
|
|
1282
|
+
else if (row.quarantine.active)
|
|
1283
|
+
await pluginFacade().releaseQuarantine(row.id);
|
|
1284
|
+
else
|
|
1285
|
+
await pluginFacade().quarantine(row.id, t('screen.plugins.manualQuarantine'));
|
|
1286
|
+
await loadPluginCenter(row.id);
|
|
1287
|
+
}
|
|
1288
|
+
catch (error) {
|
|
1289
|
+
addLocal(screenErrorText(error, act('screen.action.plugin', { action })));
|
|
1290
|
+
}
|
|
1291
|
+
};
|
|
1292
|
+
const memoryFacade = () => memoryFacadeRef.current ??= (createMemoryFacade({ projectRoot, paths }));
|
|
1293
|
+
const loadMemoryCenter = async (query, selectedId) => {
|
|
1294
|
+
try {
|
|
1295
|
+
const snapshot = query === undefined ? await memoryFacade().list() : await memoryFacade().search(query);
|
|
1296
|
+
const found = selectedId ? snapshot.rows.findIndex(row => row.id === selectedId) : -1;
|
|
1297
|
+
const selected = found >= 0 ? found : memoryCenter?.selected ?? 0;
|
|
1298
|
+
setMemoryCenter(createMemoryCenterModel(snapshot, selected));
|
|
1299
|
+
}
|
|
1300
|
+
catch (error) {
|
|
1301
|
+
addLocal(screenErrorText(error, act('screen.action.memoryCenter')));
|
|
1302
|
+
}
|
|
1303
|
+
};
|
|
1304
|
+
const notesFacade = () => notesFacadeRef.current ??= (createNotesFacade({ projectRoot, paths }));
|
|
1305
|
+
const loadNotesCenter = async (selectedId) => {
|
|
1306
|
+
try {
|
|
1307
|
+
const snapshot = await notesFacade().list();
|
|
1308
|
+
const found = selectedId ? snapshot.rows.findIndex(row => row.id === selectedId) : -1;
|
|
1309
|
+
const selected = found >= 0 ? found : notesCenter?.selected ?? 0;
|
|
1310
|
+
setNotesCenter(createNotesCenterModel(snapshot, selected));
|
|
1311
|
+
}
|
|
1312
|
+
catch (error) {
|
|
1313
|
+
addLocal(screenErrorText(error, act('screen.action.notesCenter')));
|
|
1314
|
+
}
|
|
1315
|
+
};
|
|
1316
|
+
const tasksFacade = () => tasksFacadeRef.current ??= (createTaskBoardFacade({ projectRoot, paths }));
|
|
1317
|
+
const loadTasksCenter = async (selectedId) => {
|
|
1318
|
+
try {
|
|
1319
|
+
const snapshot = await tasksFacade().list();
|
|
1320
|
+
const found = selectedId ? snapshot.rows.findIndex(row => row.id === selectedId) : -1;
|
|
1321
|
+
const selected = found >= 0 ? found : tasksCenter?.selected ?? 0;
|
|
1322
|
+
setTasksCenter(createTasksCenterModel(snapshot, selected));
|
|
1323
|
+
}
|
|
1324
|
+
catch (error) {
|
|
1325
|
+
addLocal(screenErrorText(error, act('screen.action.taskBoard')));
|
|
1326
|
+
}
|
|
1327
|
+
};
|
|
1328
|
+
const libraryFacade = () => libraryFacadeRef.current ??= (createLibraryFacade({ projectRoot, paths }));
|
|
1329
|
+
const loadLibraryCenter = async (selectedId) => {
|
|
1330
|
+
try {
|
|
1331
|
+
const snapshot = await libraryFacade().list();
|
|
1332
|
+
const found = selectedId ? snapshot.rows.findIndex(row => row.id === selectedId) : -1;
|
|
1333
|
+
const selected = found >= 0 ? found : libraryCenter?.selected ?? 0;
|
|
1334
|
+
setLibraryCenter(createLibraryCenterModel(snapshot, selected));
|
|
1335
|
+
}
|
|
1336
|
+
catch (error) {
|
|
1337
|
+
addLocal(screenErrorText(error, act('screen.action.libraryCenter')));
|
|
1338
|
+
}
|
|
1339
|
+
};
|
|
1340
|
+
const forgeFacade = () => forgeFacadeRef.current ??= createForgeFacade({ forgeRoot: join(paths.dataRoot, 'forge') }, { registry });
|
|
1341
|
+
const loadForgeCenter = async (selectedId) => {
|
|
1342
|
+
try {
|
|
1343
|
+
const snapshot = await forgeFacade().list();
|
|
1344
|
+
const found = selectedId ? snapshot.rows.findIndex(row => row.id === selectedId) : -1;
|
|
1345
|
+
const selected = found >= 0 ? found : forgeCenter?.selected ?? 0;
|
|
1346
|
+
const base = createForgeCenterModel(snapshot, selected);
|
|
1347
|
+
setForgeCenter(base);
|
|
1348
|
+
const row = snapshot.rows[selected];
|
|
1349
|
+
if (row) {
|
|
1350
|
+
const detail = await forgeFacade().read(row.id);
|
|
1351
|
+
setForgeCenter((current) => current ? setForgeCenterDetail(current, detail) : current);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
catch (error) {
|
|
1355
|
+
addLocal(screenErrorText(error, act('screen.action.forgeCenter')));
|
|
1356
|
+
}
|
|
1357
|
+
};
|
|
1358
|
+
const automationFacade = () => automationFacadeRef.current ??= createAutomationFacade({ projectRoot, dataRoot: paths.dataRoot });
|
|
1359
|
+
const loadAutomationCenter = async (selectedId) => {
|
|
1360
|
+
try {
|
|
1361
|
+
const snapshot = await automationFacade().list();
|
|
1362
|
+
const found = selectedId ? snapshot.rows.findIndex(row => row.id === selectedId) : -1;
|
|
1363
|
+
const selected = found >= 0 ? found : automationCenter?.selected ?? 0;
|
|
1364
|
+
const base = createAutomationCenterModel(snapshot, selected);
|
|
1365
|
+
setAutomationCenter(base);
|
|
1366
|
+
const row = snapshot.rows[selected];
|
|
1367
|
+
if (row) {
|
|
1368
|
+
const detail = await automationFacade().read(row.id);
|
|
1369
|
+
setAutomationCenter((current) => current ? setAutomationCenterDetail(current, detail) : current);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
catch (error) {
|
|
1373
|
+
addLocal(screenErrorText(error, act('screen.action.automationCenter')));
|
|
1374
|
+
}
|
|
1375
|
+
};
|
|
1376
|
+
const researchFacade = () => researchFacadeRef.current ??= createResearchFacade({ projectRoot }, { registry });
|
|
1377
|
+
const loadResearchCenter = async (selectedId) => {
|
|
1378
|
+
try {
|
|
1379
|
+
const snapshot = await researchFacade().list();
|
|
1380
|
+
const found = selectedId ? snapshot.rows.findIndex(row => row.id === selectedId) : -1;
|
|
1381
|
+
const selected = found >= 0 ? found : researchCenter?.selected ?? 0;
|
|
1382
|
+
const base = createResearchCenterModel(snapshot, selected);
|
|
1383
|
+
setResearchCenter(base);
|
|
1384
|
+
const row = snapshot.rows[selected];
|
|
1385
|
+
if (row) {
|
|
1386
|
+
const detail = await researchFacade().read(row.id);
|
|
1387
|
+
setResearchCenter((current) => current ? setResearchCenterDetail(current, detail) : current);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
catch (error) {
|
|
1391
|
+
addLocal(screenErrorText(error, act('screen.action.researchCenter')));
|
|
1392
|
+
}
|
|
1393
|
+
};
|
|
1394
|
+
/* Owner 2026-09-25 ("after a restart /effort says the model declares no levels, though I set one with the same model"):
|
|
1395
|
+
the rows TALOS already knows (catalog.listModels, read at startup and by `/model <id>`) may lack a model's levels that
|
|
1396
|
+
the provider's live list (catalog.providerModels, what the /model picker reads) has. Asked only when levels are missing. */
|
|
1397
|
+
const modelMetaWithLevels = async (id, known) => {
|
|
1398
|
+
if (known && reasoningEffortChoices(known).length)
|
|
1399
|
+
return known;
|
|
1400
|
+
const provider = providerOfModel(id);
|
|
1401
|
+
if (!provider || typeof catalog?.providerModels !== 'function')
|
|
1402
|
+
return known;
|
|
1403
|
+
try {
|
|
1404
|
+
return (await catalog.providerModels(provider)).rows.find((candidate) => candidate.id === id) ?? known;
|
|
1405
|
+
}
|
|
1406
|
+
catch {
|
|
1407
|
+
return known;
|
|
1408
|
+
}
|
|
1409
|
+
};
|
|
1410
|
+
const applyConfiguredReasoningDefault = (row) => {
|
|
1411
|
+
const supported = Boolean(reasoningEffort && row?.reasoning === true && row.reasoningEfforts?.includes(reasoningEffort));
|
|
1412
|
+
if (typeof runtime.setDefaultReasoningEffort === 'function')
|
|
1413
|
+
runtime.setDefaultReasoningEffort(supported ? reasoningEffort : null);
|
|
1414
|
+
if (reasoningEffort && !supported) {
|
|
1415
|
+
setReasoningEffort(null);
|
|
1416
|
+
addLocal(t('screen.effort.notAffirmed', { effort: reasoningEffort, model: row?.id ?? controller.model() }));
|
|
1417
|
+
}
|
|
1418
|
+
};
|
|
1419
|
+
/* The effort chosen on the line of nodes, from /model's second step or from /effort (R5a, owner 2026-09-24). */
|
|
1420
|
+
const applyEffortChoice = async (row, choice) => {
|
|
1421
|
+
const sameLiveModel = Boolean(state.sessionId && state.status.model === row.id);
|
|
1422
|
+
try {
|
|
1423
|
+
await commitModelSelection({ id: row.id, catalog, setControllerModel: (id) => controller.setModel(id) });
|
|
1424
|
+
if (choice === 'default') {
|
|
1425
|
+
await configUnset({ paths, projectRoot, scope: 'project-user', path: 'reasoningEffort' });
|
|
1426
|
+
if (typeof runtime.setDefaultReasoningEffort === 'function')
|
|
1427
|
+
runtime.setDefaultReasoningEffort(null);
|
|
1428
|
+
const retainedLiveEffort = sameLiveModel ? reasoningEffort : null;
|
|
1429
|
+
setReasoningEffort(retainedLiveEffort);
|
|
1430
|
+
setCurrentModelMeta(row);
|
|
1431
|
+
setState((current) => localMessage({ ...current, status: { ...current.status, model: row.id } }, retainedLiveEffort
|
|
1432
|
+
? t('screen.model.changedEffortClearedKept', { model: row.id, effort: retainedLiveEffort })
|
|
1433
|
+
: t('screen.model.changedEffortCleared', { model: row.id })));
|
|
1434
|
+
}
|
|
1435
|
+
else {
|
|
1436
|
+
await configSet({ paths, projectRoot, scope: 'project-user', path: 'reasoningEffort', value: choice });
|
|
1437
|
+
if (typeof runtime.setDefaultReasoningEffort === 'function')
|
|
1438
|
+
runtime.setDefaultReasoningEffort(choice);
|
|
1439
|
+
if (sameLiveModel && typeof runtime.setReasoningEffort === 'function')
|
|
1440
|
+
await runtime.setReasoningEffort(state.sessionId, choice);
|
|
1441
|
+
setReasoningEffort(choice);
|
|
1442
|
+
setCurrentModelMeta(row);
|
|
1443
|
+
setState((current) => localMessage({ ...current, status: { ...current.status, model: row.id } }, t(sameLiveModel ? 'screen.model.changedEffortSession' : 'screen.model.changedEffort', { model: row.id, effort: choice })));
|
|
1444
|
+
}
|
|
1445
|
+
closeCurrentOverlay();
|
|
1446
|
+
}
|
|
1447
|
+
catch (error) {
|
|
1448
|
+
addLocal(screenErrorText(error, act('screen.action.effortChange')));
|
|
1449
|
+
}
|
|
1450
|
+
};
|
|
1451
|
+
/* R5b D2 (owner decision Q-R5-12, R5 gate §5.1): /clear, and every resume or fork, start from an empty view; the session
|
|
1452
|
+
attached then redraws its own history once (a fork drew the earlier conversation twice: the old view, then the replay). */
|
|
1453
|
+
const freshView = () => { setState((current) => ({ ...current, messages: [], streamingAssistant: null, reasoning: null, tools: [], warnings: [], transcript: createTranscriptModel(), compactions: 0 })); setDiff(''); setTranscriptRevealId(null); setViewport((view) => ({ ...view, offset: 0, unseen: 0, followingTail: true })); };
|
|
1454
|
+
const executeSlash = async (text) => {
|
|
1455
|
+
const parsed = parseSlashCommand(text);
|
|
1456
|
+
setAuxCompletions([]);
|
|
1457
|
+
if (!SLASH_COMMANDS.some(command => command.name === parsed.name)) {
|
|
1458
|
+
addLocal(parsed.suggestions.length ? t('screen.slash.unknownSuggest', { name: parsed.name, suggestions: parsed.suggestions.map(value => `/${value}`).join(', ') }) : t('screen.slash.unknown', { name: parsed.name }));
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
/* P17 (owner decision 4): /setup runs the guided setup again, from the provider. */
|
|
1462
|
+
if (parsed.name === 'setup') {
|
|
1463
|
+
await startSetup('provider');
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
const intent = slashCommandIntent(text);
|
|
1467
|
+
if (intent?.kind === 'open-overlay') {
|
|
1468
|
+
if (intent.overlay.kind === 'model') {
|
|
1469
|
+
await loadModels(intent.overlay.provider);
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
if (intent.overlay.kind === 'provider') {
|
|
1473
|
+
await loadProviders();
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
if (intent.overlay.kind === 'session') {
|
|
1477
|
+
await loadSessions(intent.overlay.action);
|
|
1478
|
+
return;
|
|
1479
|
+
}
|
|
1480
|
+
applyOverlay(intent.overlay);
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1483
|
+
if (intent?.kind === 'key-action') {
|
|
1484
|
+
runKeyAction(intent.action);
|
|
1485
|
+
return;
|
|
1486
|
+
}
|
|
1487
|
+
if (intent?.kind === 'select-model') {
|
|
1488
|
+
if (!MODEL_ID.test(intent.id)) {
|
|
1489
|
+
addLocal(t('screen.usage.model'));
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
const providerId = providerOfModel(intent.id);
|
|
1493
|
+
try {
|
|
1494
|
+
const row = (await catalog.listProviders()).find((entry) => entry.id === providerId);
|
|
1495
|
+
if (!row) {
|
|
1496
|
+
addLocal(t('screen.model.unknownProvider', { provider: String(providerId) }));
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
if (!row.configured) {
|
|
1500
|
+
if (row.requiresKey) {
|
|
1501
|
+
const refusal = unusableKeyRefusal(row.id, row.benched);
|
|
1502
|
+
addLocal(refusal.message);
|
|
1503
|
+
}
|
|
1504
|
+
else
|
|
1505
|
+
addLocal(t('screen.model.configureFirst', { provider: row.label }));
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1508
|
+
const probe = await catalog.probeProvider(row.id);
|
|
1509
|
+
if (probe.readinessCode) {
|
|
1510
|
+
addLocal(probe.remediation?.message || probe.detail || t('screen.provider.notReady', { provider: row.label }));
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
if (probe.remediation && probe.remediation.code !== 'none')
|
|
1514
|
+
addLocal(probe.remediation.message);
|
|
1515
|
+
if (chosenProvider !== row.id) {
|
|
1516
|
+
await catalog.chooseProvider(row.id);
|
|
1517
|
+
setChosenProvider(row.id);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
catch (error) {
|
|
1521
|
+
addLocal(screenErrorText(codeOnly(error), act('screen.action.modelChange')));
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1524
|
+
try {
|
|
1525
|
+
await selectExplicitModel(intent.id, catalog, (id) => controller.setModel(id));
|
|
1526
|
+
setState((current) => localMessage({ ...current, status: { ...current.status, model: intent.id } }, t('screen.model.changed', { model: intent.id })));
|
|
1527
|
+
const rows = await catalog.listModels({ provider: providerId ?? undefined });
|
|
1528
|
+
const meta = await modelMetaWithLevels(intent.id, rows.find(row => row.id === intent.id) ?? null);
|
|
1529
|
+
setCurrentModelMeta(meta);
|
|
1530
|
+
applyConfiguredReasoningDefault(meta);
|
|
1531
|
+
}
|
|
1532
|
+
catch (error) {
|
|
1533
|
+
addLocal(screenErrorText(error, act('screen.action.modelChange')));
|
|
1534
|
+
}
|
|
1535
|
+
return;
|
|
1536
|
+
}
|
|
1537
|
+
if (intent?.kind === 'session-id') {
|
|
1538
|
+
try {
|
|
1539
|
+
freshView();
|
|
1540
|
+
const id = intent.action === 'resume' ? await controller.resumeSession(intent.id) : await controller.forkSession(intent.id);
|
|
1541
|
+
addLocal(intent.action === 'resume' ? t('screen.session.attached', { id }) : t('screen.session.forked', { from: intent.id, id }));
|
|
1542
|
+
}
|
|
1543
|
+
catch (error) {
|
|
1544
|
+
addLocal(screenErrorText(error, act(intent.action === 'resume' ? 'screen.action.resume' : 'screen.action.fork')));
|
|
1545
|
+
}
|
|
1546
|
+
return;
|
|
1547
|
+
}
|
|
1548
|
+
if (parsed.name === 'forge') {
|
|
1549
|
+
const operation = parsed.args[0] ?? 'list';
|
|
1550
|
+
if (operation === 'list' || operation === 'inspect') {
|
|
1551
|
+
const id = operation === 'inspect' ? parsed.args[1] : undefined;
|
|
1552
|
+
if (operation === 'inspect' && !id) {
|
|
1553
|
+
addLocal(t('screen.usage.forgeInspect'));
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1556
|
+
await loadForgeCenter(id);
|
|
1557
|
+
return;
|
|
1558
|
+
}
|
|
1559
|
+
if (operation === 'enable' || operation === 'disable') {
|
|
1560
|
+
const id = parsed.args[1], ownerSessionId = state.sessionId;
|
|
1561
|
+
if (!id || !ownerSessionId) {
|
|
1562
|
+
addLocal(t('screen.forge.needsSessionAndTool'));
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1565
|
+
try {
|
|
1566
|
+
await forgeFacade().setEnabled(ownerSessionId, id, operation === 'enable');
|
|
1567
|
+
await loadForgeCenter(id);
|
|
1568
|
+
}
|
|
1569
|
+
catch (error) {
|
|
1570
|
+
addLocal(screenErrorText(error, act('screen.action.commandOperation', { command: 'forge', operation })));
|
|
1571
|
+
}
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
if (operation === 'rollback') {
|
|
1575
|
+
const id = parsed.args[1], revision = Number(parsed.args[2]), ownerSessionId = state.sessionId;
|
|
1576
|
+
if (!id || !Number.isSafeInteger(revision) || revision < 1 || !ownerSessionId) {
|
|
1577
|
+
addLocal(t('screen.usage.forgeRollback'));
|
|
1578
|
+
return;
|
|
1579
|
+
}
|
|
1580
|
+
try {
|
|
1581
|
+
await forgeFacade().rollback(ownerSessionId, id, revision);
|
|
1582
|
+
await loadForgeCenter(id);
|
|
1583
|
+
}
|
|
1584
|
+
catch (error) {
|
|
1585
|
+
addLocal(screenErrorText(error, act('screen.action.commandOperation', { command: 'forge', operation })));
|
|
1586
|
+
}
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
if (!['validate', 'scan', 'simulate', 'install', 'versions'].includes(operation)) {
|
|
1590
|
+
addLocal(t('screen.usage.forge'));
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
if (parsed.name === 'automations') {
|
|
1595
|
+
const operation = parsed.args[0] ?? 'list';
|
|
1596
|
+
if (operation === 'list' || operation === 'read' || operation === 'dry-run' || operation === 'history') {
|
|
1597
|
+
const id = operation === 'list' ? undefined : parsed.args[1];
|
|
1598
|
+
if (operation !== 'list' && !id) {
|
|
1599
|
+
addLocal(t('screen.usage.automationsOperation', { operation }));
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
await loadAutomationCenter(id);
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
if (operation === 'health') {
|
|
1606
|
+
try {
|
|
1607
|
+
const health = await automationFacade().health();
|
|
1608
|
+
addLocal(t('screen.automation.runner', { state: String(health.state) }));
|
|
1609
|
+
}
|
|
1610
|
+
catch (error) {
|
|
1611
|
+
addLocal(screenErrorText(error, act('screen.action.commandOperation', { command: 'automations', operation })));
|
|
1612
|
+
}
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1615
|
+
if (!['add', 'enable', 'disable', 'delete', 'run', 'serve', 'schedule', 'install-runner', 'uninstall-runner'].includes(operation)) {
|
|
1616
|
+
addLocal(t('screen.usage.automations'));
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
if (parsed.name === 'research') {
|
|
1621
|
+
const operation = parsed.args[0] ?? 'list';
|
|
1622
|
+
if (operation === 'list' || operation === 'read') {
|
|
1623
|
+
const id = operation === 'read' ? parsed.args[1] : undefined;
|
|
1624
|
+
if (operation === 'list' && parsed.args.length > 1) {
|
|
1625
|
+
addLocal(t('screen.usage.research'));
|
|
1626
|
+
return;
|
|
1627
|
+
}
|
|
1628
|
+
if (operation === 'read' && (!id || parsed.args.length !== 2)) {
|
|
1629
|
+
addLocal(t('screen.usage.researchRead'));
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
await loadResearchCenter(id);
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
if (operation === 'pause' || operation === 'resume' || operation === 'cancel') {
|
|
1636
|
+
const id = parsed.args[1];
|
|
1637
|
+
if (!id || parsed.args.length !== 2) {
|
|
1638
|
+
addLocal(t('screen.usage.researchOperation', { operation }));
|
|
1639
|
+
return;
|
|
1640
|
+
}
|
|
1641
|
+
const ownerSessionId = state.sessionId ?? id;
|
|
1642
|
+
try {
|
|
1643
|
+
if (operation === 'pause')
|
|
1644
|
+
await researchFacade().pause(ownerSessionId, id);
|
|
1645
|
+
else if (operation === 'resume')
|
|
1646
|
+
await researchFacade().resume(ownerSessionId, id);
|
|
1647
|
+
else
|
|
1648
|
+
await researchFacade().cancel(ownerSessionId, id);
|
|
1649
|
+
await loadResearchCenter(id);
|
|
1650
|
+
}
|
|
1651
|
+
catch (error) {
|
|
1652
|
+
addLocal(screenErrorText(error, act('screen.action.commandOperation', { command: 'research', operation })));
|
|
1653
|
+
}
|
|
1654
|
+
return;
|
|
1655
|
+
}
|
|
1656
|
+
if (operation === 'export') {
|
|
1657
|
+
const id = parsed.args[1], format = (parsed.args[2] ?? 'md'), out = parsed.args[3];
|
|
1658
|
+
if (!id || parsed.args.length > 4) {
|
|
1659
|
+
addLocal(t('screen.usage.researchExport'));
|
|
1660
|
+
return;
|
|
1661
|
+
}
|
|
1662
|
+
try {
|
|
1663
|
+
const result = await researchFacade().exportFile(id, format, out);
|
|
1664
|
+
addLocal(t('screen.research.exported', { path: result.path }));
|
|
1665
|
+
}
|
|
1666
|
+
catch (error) {
|
|
1667
|
+
addLocal(screenErrorText(error, act('screen.action.commandOperation', { command: 'research', operation })));
|
|
1668
|
+
}
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
if (!['start', 'rename', 'delete'].includes(operation)) {
|
|
1672
|
+
addLocal(t('screen.usage.research'));
|
|
1673
|
+
return;
|
|
1674
|
+
}
|
|
1675
|
+
/* start/rename/delete keep the existing child-CLI behavior; live controls above never cross the process boundary. */
|
|
1676
|
+
}
|
|
1677
|
+
if (parsed.name === 'library') {
|
|
1678
|
+
const operation = parsed.args[0] ?? 'list';
|
|
1679
|
+
if (operation === 'list') {
|
|
1680
|
+
if (parsed.args.length > 1) {
|
|
1681
|
+
addLocal(t('screen.usage.library'));
|
|
1682
|
+
return;
|
|
1683
|
+
}
|
|
1684
|
+
await loadLibraryCenter();
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1687
|
+
if (!['search', 'read', 'preview', 'duplicates', 'context-ref', 'import', 'export', 'rename', 'delete'].includes(operation)) {
|
|
1688
|
+
addLocal(t('screen.usage.library'));
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1691
|
+
/* non-list operations continue through the child CLI; runLibraryCommand uses this same facade. */
|
|
1692
|
+
}
|
|
1693
|
+
if (parsed.name === 'tasks') {
|
|
1694
|
+
const operation = parsed.args[0] ?? 'list';
|
|
1695
|
+
if (operation === 'list') {
|
|
1696
|
+
if (parsed.args.length > 1) {
|
|
1697
|
+
addLocal(t('screen.usage.tasks'));
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
await loadTasksCenter();
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
if (['depend', 'undepend', 'evidence', 'unevidence', 'assign', 'unassign', 'schedule', 'unschedule'].includes(operation)) {
|
|
1704
|
+
const taskId = parsed.args[1], targetId = parsed.args[2];
|
|
1705
|
+
if (parsed.args.length !== 3 || !taskId || !targetId) {
|
|
1706
|
+
addLocal(t('screen.usage.tasksOperation', { operation }));
|
|
1707
|
+
return;
|
|
1708
|
+
}
|
|
1709
|
+
try {
|
|
1710
|
+
if (operation === 'depend')
|
|
1711
|
+
await tasksFacade().depend(taskId, targetId);
|
|
1712
|
+
else if (operation === 'undepend')
|
|
1713
|
+
await tasksFacade().undepend(taskId, targetId);
|
|
1714
|
+
else if (operation === 'evidence')
|
|
1715
|
+
await tasksFacade().addEvidence(taskId, targetId);
|
|
1716
|
+
else if (operation === 'unevidence')
|
|
1717
|
+
await tasksFacade().removeEvidence(taskId, targetId);
|
|
1718
|
+
else if (operation === 'assign')
|
|
1719
|
+
await tasksFacade().assignAgent(taskId, targetId);
|
|
1720
|
+
else if (operation === 'unassign')
|
|
1721
|
+
await tasksFacade().unassignAgent(taskId, targetId);
|
|
1722
|
+
else if (operation === 'schedule')
|
|
1723
|
+
await tasksFacade().attachSchedule(taskId, targetId);
|
|
1724
|
+
else
|
|
1725
|
+
await tasksFacade().detachSchedule(taskId, targetId);
|
|
1726
|
+
await loadTasksCenter(taskId);
|
|
1727
|
+
}
|
|
1728
|
+
catch (error) {
|
|
1729
|
+
addLocal(screenErrorText(error, act('screen.action.commandOperation', { command: 'tasks', operation })));
|
|
1730
|
+
}
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
if (!['add', 'update', 'complete', 'delete'].includes(operation)) {
|
|
1734
|
+
addLocal(t('screen.usage.tasks'));
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1737
|
+
/* add/update/complete/delete deliberately continue through the child CLI; runTasksCommand uses this same facade. */
|
|
1738
|
+
}
|
|
1739
|
+
if (parsed.name === 'notes') {
|
|
1740
|
+
const operation = parsed.args[0] ?? 'list';
|
|
1741
|
+
if (operation === 'list') {
|
|
1742
|
+
if (parsed.args.length > 1) {
|
|
1743
|
+
addLocal(t('screen.usage.notes'));
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
await loadNotesCenter();
|
|
1747
|
+
return;
|
|
1748
|
+
}
|
|
1749
|
+
if (operation === 'link' || operation === 'unlink') {
|
|
1750
|
+
const noteId = parsed.args[1], targetKind = parsed.args[2], targetId = parsed.args[3];
|
|
1751
|
+
if (parsed.args.length !== 4 || !noteId || !targetKind || !targetId || !['session', 'task', 'research'].includes(targetKind)) {
|
|
1752
|
+
addLocal(t('screen.usage.notesOperation', { operation }));
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1755
|
+
try {
|
|
1756
|
+
if (operation === 'link')
|
|
1757
|
+
await notesFacade().link(noteId, targetKind, targetId);
|
|
1758
|
+
else
|
|
1759
|
+
await notesFacade().unlink(noteId, targetKind, targetId);
|
|
1760
|
+
await loadNotesCenter(noteId);
|
|
1761
|
+
}
|
|
1762
|
+
catch (error) {
|
|
1763
|
+
addLocal(screenErrorText(error, act('screen.action.commandOperation', { command: 'notes', operation })));
|
|
1764
|
+
}
|
|
1765
|
+
return;
|
|
1766
|
+
}
|
|
1767
|
+
if (!['add', 'update', 'delete'].includes(operation)) {
|
|
1768
|
+
addLocal(t('screen.usage.notes'));
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1771
|
+
/* add/update/delete deliberately continue through the existing child CLI path; runNotesCommand uses this same facade. */
|
|
1772
|
+
}
|
|
1773
|
+
if (parsed.name === 'memory') {
|
|
1774
|
+
const operation = parsed.args[0] ?? 'list';
|
|
1775
|
+
if (operation === 'list') {
|
|
1776
|
+
if (parsed.args.length > 1) {
|
|
1777
|
+
addLocal(t('screen.usage.memory'));
|
|
1778
|
+
return;
|
|
1779
|
+
}
|
|
1780
|
+
await loadMemoryCenter();
|
|
1781
|
+
return;
|
|
1782
|
+
}
|
|
1783
|
+
if (operation === 'search') {
|
|
1784
|
+
const query = parsed.args.slice(1).join(' ').trim();
|
|
1785
|
+
if (!query) {
|
|
1786
|
+
addLocal(t('screen.usage.memorySearch'));
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
await loadMemoryCenter(query);
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
/* add/update/delete deliberately continue through the existing child CLI path; that command now uses this same facade. */
|
|
1793
|
+
}
|
|
1794
|
+
if (parsed.name === 'mcp') {
|
|
1795
|
+
const operation = parsed.args[0] ?? 'list';
|
|
1796
|
+
if (operation === 'list') {
|
|
1797
|
+
if (parsed.args.length > 1) {
|
|
1798
|
+
addLocal(t('screen.usage.mcp'));
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1801
|
+
await loadMcpCenter();
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1804
|
+
if (!['test', 'trust', 'untrust'].includes(operation) || parsed.args.length !== 2) {
|
|
1805
|
+
addLocal(t('screen.usage.mcp'));
|
|
1806
|
+
return;
|
|
1807
|
+
}
|
|
1808
|
+
const serverId = parsed.args[1];
|
|
1809
|
+
try {
|
|
1810
|
+
if (operation === 'test') {
|
|
1811
|
+
const measured = await mcpFacade().probe(serverId);
|
|
1812
|
+
const rows = await mcpFacade().list();
|
|
1813
|
+
const selected = Math.max(0, rows.findIndex(row => row.id === serverId));
|
|
1814
|
+
setMcpCenter(replaceMcpCenterRow(createMcpCenterModel(rows, selected), measured));
|
|
1815
|
+
}
|
|
1816
|
+
else {
|
|
1817
|
+
if (operation === 'trust')
|
|
1818
|
+
await mcpFacade().trust(serverId);
|
|
1819
|
+
else
|
|
1820
|
+
await mcpFacade().untrust(serverId);
|
|
1821
|
+
await loadMcpCenter(serverId);
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
catch (error) {
|
|
1825
|
+
addLocal(screenErrorText(error, act('screen.action.commandOperation', { command: 'mcp', operation })));
|
|
1826
|
+
}
|
|
1827
|
+
return;
|
|
1828
|
+
}
|
|
1829
|
+
if (parsed.name === 'hooks') {
|
|
1830
|
+
const operation = parsed.args[0] ?? 'list';
|
|
1831
|
+
if (operation === 'list') {
|
|
1832
|
+
if (parsed.args.length > 1) {
|
|
1833
|
+
addLocal(t('screen.usage.hooks'));
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1836
|
+
await loadHookCenter();
|
|
1837
|
+
return;
|
|
1838
|
+
}
|
|
1839
|
+
const hookId = parsed.args[1];
|
|
1840
|
+
if (!hookId) {
|
|
1841
|
+
addLocal(t('screen.usage.hooks'));
|
|
1842
|
+
return;
|
|
1843
|
+
}
|
|
1844
|
+
try {
|
|
1845
|
+
if (operation === 'history') {
|
|
1846
|
+
if (parsed.args.length > 3) {
|
|
1847
|
+
addLocal(t('screen.usage.hooksHistory'));
|
|
1848
|
+
return;
|
|
1849
|
+
}
|
|
1850
|
+
const rawLimit = parsed.args[2];
|
|
1851
|
+
let limit = 8;
|
|
1852
|
+
if (rawLimit !== undefined) {
|
|
1853
|
+
limit = Number(rawLimit);
|
|
1854
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 50) {
|
|
1855
|
+
addLocal(t('screen.hooks.historyLimit'));
|
|
1856
|
+
return;
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
const [history, rows] = await Promise.all([hookFacade().history(hookId, { limit }), hookFacade().list()]);
|
|
1860
|
+
const selected = Math.max(0, rows.findIndex(row => row.id === hookId));
|
|
1861
|
+
const base = createHookCenterModel(rows, selected);
|
|
1862
|
+
const row = selectedHook(base);
|
|
1863
|
+
setHookCenter(row ? replaceHookCenterRow(base, { ...row, lastExecutions: history }) : base);
|
|
1864
|
+
return;
|
|
1865
|
+
}
|
|
1866
|
+
if (operation === 'dry-run') {
|
|
1867
|
+
const eventType = parsed.args[2];
|
|
1868
|
+
if (!eventType) {
|
|
1869
|
+
addLocal(t('screen.usage.hooksDryRun'));
|
|
1870
|
+
return;
|
|
1871
|
+
}
|
|
1872
|
+
const action = parsed.args.length > 3 ? parsed.args.slice(3).join(' ') : null;
|
|
1873
|
+
const result = await hookFacade().dryRun(hookId, { eventType, action });
|
|
1874
|
+
const rows = await hookFacade().list();
|
|
1875
|
+
const selected = Math.max(0, rows.findIndex(row => row.id === hookId));
|
|
1876
|
+
const base = createHookCenterModel(rows, selected);
|
|
1877
|
+
const row = selectedHook(base);
|
|
1878
|
+
setHookCenter(row ? replaceHookCenterRow(base, { ...row, lastDryRun: result }) : base);
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1881
|
+
if (operation === 'trust') {
|
|
1882
|
+
if (parsed.args.length !== 2) {
|
|
1883
|
+
addLocal(t('screen.usage.hooksTrust'));
|
|
1884
|
+
return;
|
|
1885
|
+
}
|
|
1886
|
+
await hookFacade().trust(hookId);
|
|
1887
|
+
}
|
|
1888
|
+
else if (operation === 'untrust') {
|
|
1889
|
+
if (parsed.args.length !== 2) {
|
|
1890
|
+
addLocal(t('screen.usage.hooksUntrust'));
|
|
1891
|
+
return;
|
|
1892
|
+
}
|
|
1893
|
+
await hookFacade().untrust(hookId);
|
|
1894
|
+
}
|
|
1895
|
+
else if (operation === 'quarantine') {
|
|
1896
|
+
await hookFacade().quarantine(hookId, parsed.args.length > 2 ? parsed.args.slice(2).join(' ') : null);
|
|
1897
|
+
}
|
|
1898
|
+
else if (operation === 'release' || operation === 'release-quarantine') {
|
|
1899
|
+
if (parsed.args.length !== 2) {
|
|
1900
|
+
addLocal(t('screen.usage.hooksRelease'));
|
|
1901
|
+
return;
|
|
1902
|
+
}
|
|
1903
|
+
await hookFacade().releaseQuarantine(hookId);
|
|
1904
|
+
}
|
|
1905
|
+
else {
|
|
1906
|
+
addLocal(t('screen.usage.hooks'));
|
|
1907
|
+
return;
|
|
1908
|
+
}
|
|
1909
|
+
await loadHookCenter(hookId);
|
|
1910
|
+
}
|
|
1911
|
+
catch (error) {
|
|
1912
|
+
addLocal(screenErrorText(error, act('screen.action.commandOperation', { command: 'hooks', operation })));
|
|
1913
|
+
}
|
|
1914
|
+
return;
|
|
1915
|
+
}
|
|
1916
|
+
if (parsed.name === 'plugins') {
|
|
1917
|
+
const operation = parsed.args[0] ?? 'list';
|
|
1918
|
+
if (operation === 'list') {
|
|
1919
|
+
if (parsed.args.length > 1) {
|
|
1920
|
+
addLocal(t('screen.usage.plugins'));
|
|
1921
|
+
return;
|
|
1922
|
+
}
|
|
1923
|
+
await loadPluginCenter();
|
|
1924
|
+
return;
|
|
1925
|
+
}
|
|
1926
|
+
if (['trust', 'untrust', 'quarantine', 'release', 'release-quarantine'].includes(operation)) {
|
|
1927
|
+
const pluginId = parsed.args[1];
|
|
1928
|
+
if (!pluginId) {
|
|
1929
|
+
addLocal(t('screen.usage.pluginsOperation', { operation }));
|
|
1930
|
+
return;
|
|
1931
|
+
}
|
|
1932
|
+
try {
|
|
1933
|
+
if (operation === 'trust') {
|
|
1934
|
+
if (parsed.args.length !== 2) {
|
|
1935
|
+
addLocal(t('screen.usage.pluginsTrust'));
|
|
1936
|
+
return;
|
|
1937
|
+
}
|
|
1938
|
+
await pluginFacade().trust(pluginId);
|
|
1939
|
+
}
|
|
1940
|
+
else if (operation === 'untrust') {
|
|
1941
|
+
if (parsed.args.length !== 2) {
|
|
1942
|
+
addLocal(t('screen.usage.pluginsUntrust'));
|
|
1943
|
+
return;
|
|
1944
|
+
}
|
|
1945
|
+
await pluginFacade().untrust(pluginId);
|
|
1946
|
+
}
|
|
1947
|
+
else if (operation === 'quarantine')
|
|
1948
|
+
await pluginFacade().quarantine(pluginId, parsed.args.length > 2 ? parsed.args.slice(2).join(' ') : null);
|
|
1949
|
+
else {
|
|
1950
|
+
if (parsed.args.length !== 2) {
|
|
1951
|
+
addLocal(t('screen.usage.pluginsRelease'));
|
|
1952
|
+
return;
|
|
1953
|
+
}
|
|
1954
|
+
await pluginFacade().releaseQuarantine(pluginId);
|
|
1955
|
+
}
|
|
1956
|
+
await loadPluginCenter(pluginId);
|
|
1957
|
+
}
|
|
1958
|
+
catch (error) {
|
|
1959
|
+
addLocal(screenErrorText(error, act('screen.action.commandOperation', { command: 'plugins', operation })));
|
|
1960
|
+
}
|
|
1961
|
+
return;
|
|
1962
|
+
}
|
|
1963
|
+
if (operation !== 'install' && operation !== 'remove') {
|
|
1964
|
+
addLocal(t('screen.usage.plugins'));
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1967
|
+
/* install/remove deliberately continue into the existing child CLI installer path below. */
|
|
1968
|
+
}
|
|
1969
|
+
if (parsed.name === 'exit') {
|
|
1970
|
+
inkApp.exit();
|
|
1971
|
+
return;
|
|
1972
|
+
}
|
|
1973
|
+
if (parsed.name === 'clear') {
|
|
1974
|
+
freshView();
|
|
1975
|
+
return;
|
|
1976
|
+
}
|
|
1977
|
+
/* R5b D7 (Q-R5-17): the R4 block, then the sandbox, the rules by scope, tasks and notes, and the usage. */
|
|
1978
|
+
if (parsed.name === 'status') {
|
|
1979
|
+
const work = async () => { try {
|
|
1980
|
+
const [tasks, notes] = await Promise.all([tasksFacade().list(), notesFacade().list()]);
|
|
1981
|
+
if (tasks.state !== 'ready' || notes.state !== 'ready')
|
|
1982
|
+
return null;
|
|
1983
|
+
return { open: tasks.rows.filter(row => row.status !== 'done').length, doing: tasks.rows.filter(row => row.status === 'doing').length, notes: notes.rows.length };
|
|
1984
|
+
}
|
|
1985
|
+
catch {
|
|
1986
|
+
return null;
|
|
1987
|
+
} };
|
|
1988
|
+
const [rules, counts] = await Promise.all([permissionRuleCounts({ paths, projectRoot }), work()]);
|
|
1989
|
+
const facts = usageFacts({ model: state.status.model, usage: state.usage, contextStatus, compactions: state.compactions });
|
|
1990
|
+
addLocal(sessionStatusText({ session: state.sessionId, running: state.running, model: state.status.model, mode: state.status.permissionMode, tools: state.tools.length, facts: { sandbox: sandboxVerified, rules, work: counts, usage: usageLine(facts.snapshot, state.status.model) } }));
|
|
1991
|
+
return;
|
|
1992
|
+
}
|
|
1993
|
+
if (parsed.name === 'usage') {
|
|
1994
|
+
addLocal(usageViewText({ model: state.status.model, facts: usageFacts({ model: state.status.model, usage: state.usage, contextStatus, compactions: state.compactions }) }));
|
|
1995
|
+
return;
|
|
1996
|
+
}
|
|
1997
|
+
/* R5a (owner 2026-09-24): /effort opens the current model's levels as a line of nodes; /effort <level> sets one. */
|
|
1998
|
+
if (parsed.name === 'effort') {
|
|
1999
|
+
let row = await modelMetaWithLevels(state.status.model, currentModelMeta);
|
|
2000
|
+
if (row !== currentModelMeta)
|
|
2001
|
+
setCurrentModelMeta(row);
|
|
2002
|
+
const choices = row ? reasoningEffortChoices(row) : [];
|
|
2003
|
+
if (!row || !choices.length) {
|
|
2004
|
+
addLocal(t('screen.effort.none', { model: state.status.model }));
|
|
2005
|
+
return;
|
|
2006
|
+
}
|
|
2007
|
+
const wanted = parsed.args[0]?.toLowerCase();
|
|
2008
|
+
if (wanted) {
|
|
2009
|
+
const choice = choices.find(level => level === wanted);
|
|
2010
|
+
if (!choice) {
|
|
2011
|
+
addLocal(t('screen.effort.invalid', { level: wanted, model: row.id, choices: choices.join(', ') }));
|
|
2012
|
+
return;
|
|
2013
|
+
}
|
|
2014
|
+
await applyEffortChoice(row, choice);
|
|
2015
|
+
return;
|
|
2016
|
+
}
|
|
2017
|
+
applyOverlay({ kind: 'model', provider: row.provider });
|
|
2018
|
+
setModelEffortStep(row);
|
|
2019
|
+
setEffortOnly(true);
|
|
2020
|
+
setPickerQuery('');
|
|
2021
|
+
setPickerSelection({ query: '', selected: Math.max(0, choices.indexOf(reasoningEffort ?? 'default')), pageSize: 10 });
|
|
2022
|
+
return;
|
|
2023
|
+
}
|
|
2024
|
+
/* R5b D1 (Q-R5-12): the conversation to a file, Markdown by default, the person's messages kept. */
|
|
2025
|
+
/* R5b D2 (Q-R5-12): a name for this conversation, kept in the CLI's session metadata (the /resume picker shows it). */
|
|
2026
|
+
if (parsed.name === 'rename') {
|
|
2027
|
+
const name = parsed.args.join(' ').trim();
|
|
2028
|
+
if (!state.sessionId) {
|
|
2029
|
+
addLocal(t('screen.rename.noSession'));
|
|
2030
|
+
return;
|
|
2031
|
+
}
|
|
2032
|
+
if (!name) {
|
|
2033
|
+
addLocal(t('screen.usage.rename'));
|
|
2034
|
+
return;
|
|
2035
|
+
}
|
|
2036
|
+
try {
|
|
2037
|
+
const store = createSessionMetadataStore({ rootDir: sessionMetadataRoot(paths.dataRoot) });
|
|
2038
|
+
if (name === '--clear') {
|
|
2039
|
+
await store.setName(state.sessionId, null);
|
|
2040
|
+
addLocal(t('screen.rename.cleared'));
|
|
2041
|
+
}
|
|
2042
|
+
else {
|
|
2043
|
+
const saved = await store.setName(state.sessionId, name);
|
|
2044
|
+
addLocal(t('screen.rename.done', { name: saved.name ?? name }));
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
catch (error) {
|
|
2048
|
+
addLocal(error?.code === 'SESSION_METADATA_INVALID' ? t('screen.usage.rename') : screenErrorText(error, act('screen.action.command', { command: 'rename' })));
|
|
2049
|
+
}
|
|
2050
|
+
return;
|
|
2051
|
+
}
|
|
2052
|
+
/* R5b D4 (Q-R5-14): /theme opens the picker (the arrows preview); /theme <name> sets one at once. */
|
|
2053
|
+
/* R5b (owner 2026-09-25): the messages sent in this project, newest first; choosing one fills the composer. */
|
|
2054
|
+
if (parsed.name === 'history') {
|
|
2055
|
+
setHistoryPicker({ query: '', selected: 0 });
|
|
2056
|
+
return;
|
|
2057
|
+
}
|
|
2058
|
+
/* R5b (Q-R5-19): the person's last message, sent again through the same path as Enter (queue, steer, readiness). */
|
|
2059
|
+
if (parsed.name === 'retry') {
|
|
2060
|
+
const last = [...state.transcript.items].reverse().find((item) => item.kind === 'message' && item.role === 'user');
|
|
2061
|
+
if (!last?.text.trim()) {
|
|
2062
|
+
addLocal(t('screen.retry.none'));
|
|
2063
|
+
return;
|
|
2064
|
+
}
|
|
2065
|
+
const editor = createEditorState(last.text);
|
|
2066
|
+
composerStore.replace(editor);
|
|
2067
|
+
setAppState((current) => ({ ...current, editor }));
|
|
2068
|
+
await submit();
|
|
2069
|
+
return;
|
|
2070
|
+
}
|
|
2071
|
+
/* R5b (Q-R5-19): an answer to the clipboard through the terminal (OSC 52, as Alt+C); the notice says what that means. */
|
|
2072
|
+
if (parsed.name === 'copy') {
|
|
2073
|
+
const nth = parsed.args[0] === undefined ? 1 : Number(parsed.args[0]);
|
|
2074
|
+
if (parsed.args.length > 1 || !Number.isSafeInteger(nth) || nth < 1) {
|
|
2075
|
+
addLocal(t('screen.usage.copy'));
|
|
2076
|
+
return;
|
|
2077
|
+
}
|
|
2078
|
+
const answers = state.transcript.items.filter((item) => item.kind === 'message' && item.role === 'assistant' && item.text.trim());
|
|
2079
|
+
if (!answers.length) {
|
|
2080
|
+
addLocal(t('screen.copy.noAnswer'));
|
|
2081
|
+
return;
|
|
2082
|
+
}
|
|
2083
|
+
const answer = answers[answers.length - nth];
|
|
2084
|
+
if (!answer) {
|
|
2085
|
+
addLocal(tn('screen.copy.noSuchAnswer', answers.length));
|
|
2086
|
+
return;
|
|
2087
|
+
}
|
|
2088
|
+
try {
|
|
2089
|
+
stdout.write(osc52CopySequence(answer.text));
|
|
2090
|
+
addLocal(t('screen.copy.answer', { n: nth, chars: answer.text.length.toLocaleString('en-US') }));
|
|
2091
|
+
}
|
|
2092
|
+
catch (error) {
|
|
2093
|
+
addLocal(screenErrorText(error, act('screen.action.copy')));
|
|
2094
|
+
}
|
|
2095
|
+
return;
|
|
2096
|
+
}
|
|
2097
|
+
/* R5b D3 (Q-R5-13): this conversation's checkpoints with their files; never while a turn runs. */
|
|
2098
|
+
if (parsed.name === 'undo') {
|
|
2099
|
+
if (state.running) {
|
|
2100
|
+
addLocal(t('screen.undo.busy'));
|
|
2101
|
+
return;
|
|
2102
|
+
}
|
|
2103
|
+
const store = checkpointStoreFor();
|
|
2104
|
+
if (!store) {
|
|
2105
|
+
addLocal(t('screen.undo.unavailable'));
|
|
2106
|
+
return;
|
|
2107
|
+
}
|
|
2108
|
+
/* A resumed conversation is known to the controller before its first event reaches the state. */
|
|
2109
|
+
const undoSession = state.sessionId ?? controller.current() ?? null;
|
|
2110
|
+
if (!undoSession) {
|
|
2111
|
+
addLocal(t('screen.undo.none'));
|
|
2112
|
+
return;
|
|
2113
|
+
}
|
|
2114
|
+
try {
|
|
2115
|
+
const rows = (await sessionCheckpoints(store, undoSession)).filter(row => row.state !== 'open' && checkpointChanges(row).length > 0);
|
|
2116
|
+
if (!rows.length) {
|
|
2117
|
+
addLocal(t('screen.undo.none'));
|
|
2118
|
+
return;
|
|
2119
|
+
}
|
|
2120
|
+
setUndoPicker({ rows, selected: 0, confirm: null });
|
|
2121
|
+
}
|
|
2122
|
+
catch (error) {
|
|
2123
|
+
addLocal(screenErrorText(error, act('screen.action.command', { command: 'undo' })));
|
|
2124
|
+
}
|
|
2125
|
+
return;
|
|
2126
|
+
}
|
|
2127
|
+
if (parsed.name === 'theme') {
|
|
2128
|
+
const wanted = parsed.args.join(' ').trim();
|
|
2129
|
+
if (!wanted) {
|
|
2130
|
+
setThemePicker({ selected: Math.max(0, THEME_CHOICES.indexOf(themeStore.getSnapshot().id)), original: themeStore.getSnapshot().id });
|
|
2131
|
+
return;
|
|
2132
|
+
}
|
|
2133
|
+
const id = parseThemeChoice(wanted);
|
|
2134
|
+
if (!id) {
|
|
2135
|
+
addLocal(t('screen.theme.invalid', { name: wanted, choices: THEME_CHOICES.join(', ') }));
|
|
2136
|
+
return;
|
|
2137
|
+
}
|
|
2138
|
+
keepTheme(id);
|
|
2139
|
+
return;
|
|
2140
|
+
}
|
|
2141
|
+
if (parsed.name === 'export') {
|
|
2142
|
+
try {
|
|
2143
|
+
const done = await exportSession({ runtime, sessionId: state.sessionId, projectRoot, args: parsed.args, secrets: secretValuesFromEnvironment(process.env) });
|
|
2144
|
+
addLocal(t('screen.export.saved', { format: done.format, path: done.path }));
|
|
2145
|
+
}
|
|
2146
|
+
catch (error) {
|
|
2147
|
+
addLocal(error?.code === 'EXPORT_USAGE' ? t('screen.usage.export') : error?.code === 'SESSION_NOT_FOUND' ? t('screen.export.nothing') : screenErrorText(error, act('screen.action.command', { command: 'export' })));
|
|
2148
|
+
}
|
|
2149
|
+
return;
|
|
2150
|
+
}
|
|
2151
|
+
if (parsed.name === 'busy') {
|
|
2152
|
+
const out = busyCommand(parsed.args, prefs.currentBusyInput());
|
|
2153
|
+
if (out.mode)
|
|
2154
|
+
prefs.setBusyInput(out.mode);
|
|
2155
|
+
addLocal(out.text);
|
|
2156
|
+
return;
|
|
2157
|
+
}
|
|
2158
|
+
if (parsed.name === 'queue') {
|
|
2159
|
+
const operation = parsed.args[0] ?? 'show';
|
|
2160
|
+
if (operation === 'clear') {
|
|
2161
|
+
try {
|
|
2162
|
+
const dropped = await controller.clearQueue();
|
|
2163
|
+
setQueueEditor(null);
|
|
2164
|
+
addLocal(dropped.length ? tn('screen.queue.cleared', dropped.length) : t('screen.queue.empty'));
|
|
2165
|
+
}
|
|
2166
|
+
catch (error) {
|
|
2167
|
+
addLocal(screenErrorText(error, act('screen.action.queueClear')));
|
|
2168
|
+
}
|
|
2169
|
+
return;
|
|
2170
|
+
}
|
|
2171
|
+
if (operation === 'run') {
|
|
2172
|
+
if (state.running) {
|
|
2173
|
+
addLocal(t('screen.queue.busy'));
|
|
2174
|
+
return;
|
|
2175
|
+
}
|
|
2176
|
+
try {
|
|
2177
|
+
if (!controller.queue().length) {
|
|
2178
|
+
addLocal(t('screen.queue.empty'));
|
|
2179
|
+
return;
|
|
2180
|
+
}
|
|
2181
|
+
await controller.dispatchQueue();
|
|
2182
|
+
addLocal(t('screen.queue.resumed'));
|
|
2183
|
+
}
|
|
2184
|
+
catch (error) {
|
|
2185
|
+
addLocal(screenErrorText(error, act('screen.action.queueDispatch')));
|
|
2186
|
+
}
|
|
2187
|
+
return;
|
|
2188
|
+
}
|
|
2189
|
+
if (operation !== 'show') {
|
|
2190
|
+
addLocal(t('screen.usage.queue'));
|
|
2191
|
+
return;
|
|
2192
|
+
}
|
|
2193
|
+
if (!controller.queue().length) {
|
|
2194
|
+
addLocal(t('screen.queue.empty'));
|
|
2195
|
+
return;
|
|
2196
|
+
}
|
|
2197
|
+
setQueueEditor({ selected: 0, editing: false, draft: '' });
|
|
2198
|
+
return;
|
|
2199
|
+
}
|
|
2200
|
+
if (parsed.name === 'context') {
|
|
2201
|
+
if (!state.sessionId) {
|
|
2202
|
+
addLocal(t('screen.context.noSession'));
|
|
2203
|
+
return;
|
|
2204
|
+
}
|
|
2205
|
+
if (typeof runtime.contextStatus !== 'function') {
|
|
2206
|
+
addLocal(t('screen.context.unavailable'));
|
|
2207
|
+
return;
|
|
2208
|
+
}
|
|
2209
|
+
try {
|
|
2210
|
+
const next = await runtime.contextStatus(state.sessionId);
|
|
2211
|
+
setContextStatus(next);
|
|
2212
|
+
setContextInspector(next);
|
|
2213
|
+
}
|
|
2214
|
+
catch (error) {
|
|
2215
|
+
addLocal(screenErrorText(error, act('screen.action.contextInspection')));
|
|
2216
|
+
}
|
|
2217
|
+
return;
|
|
2218
|
+
}
|
|
2219
|
+
if (parsed.name === 'permissions') {
|
|
2220
|
+
try {
|
|
2221
|
+
const dry = permissionDryRunSlash(parsed.args, permissionRules ?? { allow: [], ask: [], deny: [] }, projectRoot);
|
|
2222
|
+
if (dry !== null) {
|
|
2223
|
+
addLocal(dry);
|
|
2224
|
+
return;
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
catch (error) {
|
|
2228
|
+
addLocal(screenErrorText(error, act('screen.action.permissionDryRun')));
|
|
2229
|
+
return;
|
|
2230
|
+
}
|
|
2231
|
+
await loadTrustCenter();
|
|
2232
|
+
return;
|
|
2233
|
+
}
|
|
2234
|
+
if (parsed.name === 'plan') {
|
|
2235
|
+
const next = togglePlanMode(controller.mode(), baseMode);
|
|
2236
|
+
controller.setMode(next);
|
|
2237
|
+
setState((current) => localMessage({ ...current, status: { ...current.status, permissionMode: next } }, t(next === 'plan' ? 'screen.plan.enabled' : 'screen.plan.disabled')));
|
|
2238
|
+
return;
|
|
2239
|
+
}
|
|
2240
|
+
if (parsed.name === 'diff') {
|
|
2241
|
+
try {
|
|
2242
|
+
const result = await execFileAsync('git', ['diff', '--no-ext-diff', '--unified=3'], { cwd: projectRoot, windowsHide: true, maxBuffer: 512 * 1024 });
|
|
2243
|
+
setDiff(String(result.stdout || t('screen.diff.noChanges')));
|
|
2244
|
+
}
|
|
2245
|
+
catch (error) {
|
|
2246
|
+
setDiff(String(error?.stdout || error?.stderr || error?.message || t('screen.diff.failed')));
|
|
2247
|
+
}
|
|
2248
|
+
return;
|
|
2249
|
+
}
|
|
2250
|
+
try {
|
|
2251
|
+
const local = await runControllerSlashAction(parsed.name, controller);
|
|
2252
|
+
if (local !== null) {
|
|
2253
|
+
addLocal(local);
|
|
2254
|
+
return;
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
catch (error) {
|
|
2258
|
+
addLocal(screenErrorText(error, act('screen.action.command', { command: parsed.name })));
|
|
2259
|
+
return;
|
|
2260
|
+
}
|
|
2261
|
+
const cliArgs = slashCommandToCliArgs(parsed.name, parsed.args);
|
|
2262
|
+
/* ⛔ B1 slice 18: the child runs with TALOS_ENVIRONMENT_KEYS=consent, so a command started from this screen never uses an
|
|
2263
|
+
environment key the person has not approved here, whatever this process's own environment says. */
|
|
2264
|
+
if (cliArgs) {
|
|
2265
|
+
try {
|
|
2266
|
+
const entry = process.argv[1];
|
|
2267
|
+
if (!entry)
|
|
2268
|
+
throw new Error('CLI_ENTRY_UNAVAILABLE');
|
|
2269
|
+
const result = await execFileAsync(process.execPath, [entry, '--project', projectRoot, '--no-color', ...cliArgs], { windowsHide: true, maxBuffer: 512 * 1024, env: screenChildEnvironment(process.env) });
|
|
2270
|
+
addLocal(String(result.stdout || result.stderr || t('screen.word.ok')).trim());
|
|
2271
|
+
}
|
|
2272
|
+
catch (error) {
|
|
2273
|
+
/* The child CLI already printed its failure in the error anatomy (errors.ts): it is shown under one headline; a
|
|
2274
|
+
child that could not start at all is described like any other error. */
|
|
2275
|
+
const printed = String(error?.stderr ?? '').trim();
|
|
2276
|
+
addLocal(printed ? `${t('screen.slash.childFailed', { name: parsed.name })}\n${printed}` : screenErrorText(error, act('screen.action.command', { command: parsed.name })));
|
|
2277
|
+
}
|
|
2278
|
+
return;
|
|
2279
|
+
}
|
|
2280
|
+
addLocal(t('screen.slash.notAvailable', { name: parsed.name }));
|
|
2281
|
+
};
|
|
2282
|
+
executeSlashRef.current = executeSlash;
|
|
2283
|
+
/* R5a Q-R5-8: a command that changes the session waits for the turn, in the same queue as the prompts. */
|
|
2284
|
+
const deferSlashCommand = async (text) => { const name = parseSlashCommand(text).name; try {
|
|
2285
|
+
await controller.deferSlash(text, { running: state.running });
|
|
2286
|
+
addLocal(t('screen.busyInput.queuedSlash', { name }));
|
|
2287
|
+
}
|
|
2288
|
+
catch (error) {
|
|
2289
|
+
addLocal(screenErrorText(error, act('screen.action.command', { command: name })));
|
|
2290
|
+
} };
|
|
2291
|
+
/* Alt+S, and Enter in the `steer` busy mode (Q-R5-7): the draft steers the running turn; it leaves the composer only once
|
|
2292
|
+
the runtime took it. */
|
|
2293
|
+
const steerDraft = () => {
|
|
2294
|
+
const draft = draftOf(currentEditor()), text = draft.text;
|
|
2295
|
+
return controller.steer(text, { running: state.running }).then((outcome) => {
|
|
2296
|
+
if (outcome.status !== 'requested')
|
|
2297
|
+
return;
|
|
2298
|
+
const clearedEditor = createEditorState('');
|
|
2299
|
+
composerStore.replace(clearedEditor);
|
|
2300
|
+
setAppState((current) => ({ ...current, editor: clearedEditor }));
|
|
2301
|
+
rememberSent(draft);
|
|
2302
|
+
historyIndexRef.current = -1;
|
|
2303
|
+
reverseIndexRef.current = 0;
|
|
2304
|
+
setAuxCompletions([]);
|
|
2305
|
+
}).catch((error) => addLocal(screenErrorText(error, act('screen.action.steering'), { rejected: true })));
|
|
2306
|
+
};
|
|
2307
|
+
const submit = async () => {
|
|
2308
|
+
const draft = draftOf(currentEditor());
|
|
2309
|
+
const text = currentEditor().text;
|
|
2310
|
+
if (!text.trim())
|
|
2311
|
+
return;
|
|
2312
|
+
developmentLog('ui.submit.begin', { text: developmentTextEvidence(text), model: controller.model(), running: state.running, mode: controller.mode() }, 'info', 'tui');
|
|
2313
|
+
/* R5a S5 (Q-R5-7, Q-R5-8; E8): what Enter does now (busy-input.ts); the first time while TALOS works, a one-time tip. */
|
|
2314
|
+
const busyAction = busyEnterAction({ text, running: state.running, mode: prefs.currentBusyInput() });
|
|
2315
|
+
if (tipsOnEnter(busyAction) && prefs.firstBusyEnter())
|
|
2316
|
+
addLocal(busyTip(prefs.currentBusyInput()));
|
|
2317
|
+
if (busyAction === 'steer') {
|
|
2318
|
+
await steerDraft();
|
|
2319
|
+
return;
|
|
2320
|
+
}
|
|
2321
|
+
const clearedEditor = createEditorState('');
|
|
2322
|
+
composerStore.replace(clearedEditor);
|
|
2323
|
+
setAppState((current) => ({ ...current, editor: clearedEditor, focus: current.focus.current === 'composer' ? current.focus : createFocusState('composer'), overlay: null }));
|
|
2324
|
+
setVim((current) => current.enabled ? { ...current, mode: 'insert' } : current);
|
|
2325
|
+
rememberSent(draft);
|
|
2326
|
+
historyIndexRef.current = -1;
|
|
2327
|
+
reverseIndexRef.current = 0;
|
|
2328
|
+
setTranscriptRevealId(null);
|
|
2329
|
+
setViewport((view) => ({ ...view, offset: 0, unseen: 0, followingTail: true }));
|
|
2330
|
+
if (text.trim().startsWith('/')) {
|
|
2331
|
+
if (busyAction === 'slash-after-turn')
|
|
2332
|
+
await deferSlashCommand(text.trim());
|
|
2333
|
+
else
|
|
2334
|
+
await executeSlash(text.trim());
|
|
2335
|
+
return;
|
|
2336
|
+
}
|
|
2337
|
+
if (!text.trim().startsWith('!')) {
|
|
2338
|
+
const readiness = await readinessFor(controller.model());
|
|
2339
|
+
developmentLog('ui.readiness', { model: controller.model(), readiness }, readiness.ready ? 'debug' : 'warning', 'tui');
|
|
2340
|
+
if (!readiness.ready) {
|
|
2341
|
+
restoreDraft(draft);
|
|
2342
|
+
addLocal(readiness.message);
|
|
2343
|
+
return;
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
const willQueue = state.running || controller.queue().length > 0;
|
|
2347
|
+
/* R3 Q-R3-7 (gate E7): a prompt that goes straight to the model says `starting` until its run starts (measured gate §0.3:
|
|
2348
|
+
without it the screen said `idle` and the roster `unknown` after the provider had the request). A `!` command has no run. */
|
|
2349
|
+
const sendsToModel = !willQueue && !text.trim().startsWith('!');
|
|
2350
|
+
if (!willQueue)
|
|
2351
|
+
setState((current) => { const next = userMessage(current, text.trim()); return sendsToModel ? { ...next, sentAt: Date.now(), lastTurn: null } : next; });
|
|
2352
|
+
try {
|
|
2353
|
+
const outcome = await controller.send(text, { model: controller.model(), running: state.running });
|
|
2354
|
+
developmentLog('ui.submit.accepted', { outcome }, 'info', 'tui');
|
|
2355
|
+
if (busyAction === 'interrupt-and-send' && outcome.queued)
|
|
2356
|
+
await controller.interrupt();
|
|
2357
|
+
}
|
|
2358
|
+
catch (error) {
|
|
2359
|
+
if (sendsToModel)
|
|
2360
|
+
setState((current) => current.running ? current : { ...current, sentAt: null });
|
|
2361
|
+
developmentLogError('ui.submit.failure', error, { model: controller.model() }, 'tui');
|
|
2362
|
+
addLocal(error instanceof TuiNotReadyError ? error.message : screenErrorText(error, act('screen.action.run')));
|
|
2363
|
+
}
|
|
2364
|
+
};
|
|
2365
|
+
const historyMove = (delta) => { if (!history.length)
|
|
2366
|
+
return; const next = Math.max(-1, Math.min(history.length - 1, historyIndexRef.current + delta)); historyIndexRef.current = next; reverseIndexRef.current = 0; restoreDraft(next >= 0 ? (history[next] ?? '') : ''); };
|
|
2367
|
+
const reverseSearch = () => { const found = reverseHistoryMatch(history, currentEditor().text, reverseIndexRef.current); if (!found) {
|
|
2368
|
+
reverseIndexRef.current = 0;
|
|
2369
|
+
return;
|
|
2370
|
+
} reverseIndexRef.current = found.index + 1; historyIndexRef.current = found.index; restoreDraft(history[found.index] ?? found.value); };
|
|
2371
|
+
const cycleMode = () => { const next = cyclePermissionMode(controller.mode()); controller.setMode(next); setState((current) => ({ ...current, status: { ...current.status, permissionMode: next } })); };
|
|
2372
|
+
const cancelActiveRun = () => { void controller.cancel().then((dropped) => { if (dropped.length)
|
|
2373
|
+
addLocal(tn('screen.queue.cancelledRun', dropped.length)); }).catch((error) => addLocal(screenErrorText(error, act('screen.action.cancel')))); };
|
|
2374
|
+
const filteredModels = modelItems(modelRows, pickerQuery);
|
|
2375
|
+
const effortRows = modelEffortStep ? reasoningEffortChoices(modelEffortStep) : [];
|
|
2376
|
+
const filteredProviders = providerPickerItems(providerRows, pickerQuery, { reachable: id => providerPickerRef.current?.reachable(id) ?? null });
|
|
2377
|
+
const filteredSessions = filterSessions(sessionRows, pickerQuery);
|
|
2378
|
+
const activePickerRows = appState.focus.current === 'model-picker' ? (modelEffortStep ? effortRows : filteredModels) : appState.focus.current === 'provider-picker' ? filteredProviders : appState.focus.current === 'session-picker' ? filteredSessions : [];
|
|
2379
|
+
const movePicker = (action) => {
|
|
2380
|
+
const movement = pickerMove(action);
|
|
2381
|
+
if (!movement)
|
|
2382
|
+
return;
|
|
2383
|
+
if (appState.focus.current === 'command-menu') {
|
|
2384
|
+
/* R4 Q-R4-5: after a typo nothing is highlighted; the first arrow key lands on the top row. */
|
|
2385
|
+
const text = currentEditor().text;
|
|
2386
|
+
if (commandMenuHighlight(text, commandMenuSelectionStore.getSnapshot()) < 0)
|
|
2387
|
+
commandMenuSelectionStore.highlight(0);
|
|
2388
|
+
else
|
|
2389
|
+
commandMenuSelectionStore.move(commandMenuItems(text).length, movement);
|
|
2390
|
+
return;
|
|
2391
|
+
}
|
|
2392
|
+
setPickerSelection((current) => moveSelection({ ...current, query: pickerQuery }, activePickerRows.length, movement));
|
|
2393
|
+
};
|
|
2394
|
+
const pickerBackspace = () => {
|
|
2395
|
+
if (appState.focus.current === 'model-picker' && modelEffortStep) {
|
|
2396
|
+
if (effortOnly)
|
|
2397
|
+
return;
|
|
2398
|
+
setModelEffortStep(null);
|
|
2399
|
+
setPickerSelection({ query: '', selected: 0, pageSize: 10 });
|
|
2400
|
+
return;
|
|
2401
|
+
}
|
|
2402
|
+
if (appState.focus.current === 'command-menu') {
|
|
2403
|
+
updateEditor(editor => { const next = deleteBackward(editor); if (next.text.length === 0)
|
|
2404
|
+
setTimeout(() => setAppState((current) => ({ ...current, focus: closeFocus(current.focus) })), 0); return next; });
|
|
2405
|
+
commandMenuSelectionStore.reset();
|
|
2406
|
+
return;
|
|
2407
|
+
}
|
|
2408
|
+
if (appState.focus.current === 'provider-picker' && (providerStep.kind === 'key' || providerStep.kind === 'oauth'))
|
|
2409
|
+
setProviderSecret((value) => value.slice(0, -1));
|
|
2410
|
+
else if (appState.focus.current === 'provider-picker' && providerStep.kind === 'consent') { /* nothing to erase */ }
|
|
2411
|
+
else
|
|
2412
|
+
setPickerQuery((value) => value.slice(0, -1));
|
|
2413
|
+
setPickerSelection((current) => ({ ...current, selected: 0 }));
|
|
2414
|
+
};
|
|
2415
|
+
/*
|
|
2416
|
+
* R4 Q-R4-5 / gate E6: in the command menu Enter RUNS the highlighted command through the one dispatcher — the composer
|
|
2417
|
+
* gets `/<name>` and `submit` runs it exactly as if the person had typed the name and pressed Enter (history, slash
|
|
2418
|
+
* branch, "Did you mean"). With nothing highlighted (a typo, or arguments typed) Enter sends the text as typed. Tab
|
|
2419
|
+
* (`complete`) keeps completing `/<name> ` for arguments (the highlighted row, else the top row).
|
|
2420
|
+
*/
|
|
2421
|
+
const confirmPicker = async (mode = 'run') => {
|
|
2422
|
+
if (appState.focus.current === 'command-menu') {
|
|
2423
|
+
const selected = commandMenuSelectionStore.getSnapshot().selected;
|
|
2424
|
+
const rows = commandMenuItems(currentEditor().text);
|
|
2425
|
+
const highlighted = commandMenuHighlight(currentEditor().text, commandMenuSelectionStore.getSnapshot());
|
|
2426
|
+
const row = highlighted >= 0 ? rows[highlighted] : mode === 'complete' ? rows[selected] : undefined;
|
|
2427
|
+
if (mode === 'complete' && !row)
|
|
2428
|
+
return;
|
|
2429
|
+
setAppState((current) => ({ ...current, focus: closeFocus(current.focus) }));
|
|
2430
|
+
commandMenuSelectionStore.reset();
|
|
2431
|
+
setPickerSelection({ query: '', selected: 0, pageSize: 10 });
|
|
2432
|
+
if (mode === 'complete') {
|
|
2433
|
+
setEditorText(completeCommandSelection(row.command));
|
|
2434
|
+
return;
|
|
2435
|
+
}
|
|
2436
|
+
if (row)
|
|
2437
|
+
setEditorText(`/${row.command.name}`);
|
|
2438
|
+
await submit();
|
|
2439
|
+
return;
|
|
2440
|
+
}
|
|
2441
|
+
const selected = pickerSelection.selected;
|
|
2442
|
+
if (appState.focus.current === 'model-picker') {
|
|
2443
|
+
if (modelEffortStep) {
|
|
2444
|
+
const choice = effortRows[selected];
|
|
2445
|
+
if (!choice)
|
|
2446
|
+
return;
|
|
2447
|
+
await applyEffortChoice(modelEffortStep, choice);
|
|
2448
|
+
return;
|
|
2449
|
+
}
|
|
2450
|
+
const row = filteredModels[selected];
|
|
2451
|
+
if (!row)
|
|
2452
|
+
return;
|
|
2453
|
+
const choices = reasoningEffortChoices(row);
|
|
2454
|
+
if (choices.length) {
|
|
2455
|
+
setModelEffortStep(row);
|
|
2456
|
+
setPickerQuery('');
|
|
2457
|
+
setPickerSelection({ query: '', selected: 0, pageSize: 10 });
|
|
2458
|
+
return;
|
|
2459
|
+
}
|
|
2460
|
+
try {
|
|
2461
|
+
await commitModelSelection({ id: row.id, catalog, setControllerModel: (id) => controller.setModel(id) });
|
|
2462
|
+
if (typeof runtime.setDefaultReasoningEffort === 'function')
|
|
2463
|
+
runtime.setDefaultReasoningEffort(null);
|
|
2464
|
+
setReasoningEffort(null);
|
|
2465
|
+
setCurrentModelMeta(row);
|
|
2466
|
+
setState((current) => localMessage({ ...current, status: { ...current.status, model: row.id } }, t('screen.model.changedNoEffort', { model: row.id })));
|
|
2467
|
+
closeCurrentOverlay();
|
|
2468
|
+
}
|
|
2469
|
+
catch (error) {
|
|
2470
|
+
addLocal(screenErrorText(error, act('screen.action.modelChange')));
|
|
2471
|
+
}
|
|
2472
|
+
return;
|
|
2473
|
+
}
|
|
2474
|
+
if (appState.focus.current === 'provider-picker') {
|
|
2475
|
+
if (providerStep.kind === 'login-choice') {
|
|
2476
|
+
const step = providerStep;
|
|
2477
|
+
if (step.choice === 'browser') {
|
|
2478
|
+
await startOAuth(step.provider);
|
|
2479
|
+
return;
|
|
2480
|
+
}
|
|
2481
|
+
setProviderSecret('');
|
|
2482
|
+
setProviderStep({ kind: 'key', provider: step.provider, testing: false, result: null, ...(step.remediation ? { remediation: step.remediation } : {}) });
|
|
2483
|
+
return;
|
|
2484
|
+
}
|
|
2485
|
+
if (providerStep.kind === 'oauth') {
|
|
2486
|
+
const step = providerStep;
|
|
2487
|
+
if (step.phase === 'failed') {
|
|
2488
|
+
await startOAuth(step.provider);
|
|
2489
|
+
return;
|
|
2490
|
+
}
|
|
2491
|
+
if (step.phase !== 'paste' || !oauthLoginRef.current)
|
|
2492
|
+
return;
|
|
2493
|
+
const pasted = providerSecret;
|
|
2494
|
+
setProviderSecret('');
|
|
2495
|
+
if (!pasted.trim())
|
|
2496
|
+
return;
|
|
2497
|
+
try {
|
|
2498
|
+
await oauthLoginRef.current.submit(pasted);
|
|
2499
|
+
}
|
|
2500
|
+
catch (error) {
|
|
2501
|
+
if (error?.code === 'OAUTH_CODE_MISSING')
|
|
2502
|
+
setProviderStep({ ...step, message: oauthErrorText(step.provider, error) });
|
|
2503
|
+
}
|
|
2504
|
+
return;
|
|
2505
|
+
}
|
|
2506
|
+
if (providerStep.kind === 'consent') {
|
|
2507
|
+
const step = providerStep;
|
|
2508
|
+
try {
|
|
2509
|
+
await catalog.answerEnvironmentKey(step.provider.id, step.choice);
|
|
2510
|
+
}
|
|
2511
|
+
catch (error) {
|
|
2512
|
+
addLocal(screenErrorText(codeOnly(error), act('screen.action.answer')));
|
|
2513
|
+
return;
|
|
2514
|
+
}
|
|
2515
|
+
const refreshed = (await catalog.listProviders()).find((entry) => entry.id === step.provider.id) ?? { ...step.provider, keySource: step.choice === 'yes' ? 'environment' : 'environment-declined' };
|
|
2516
|
+
if (step.choice === 'yes')
|
|
2517
|
+
addLocal(t('screen.provider.envKeyUsed', { variable: step.provider.environmentVariable ?? t('screen.provider.envKeyFallback'), provider: step.provider.label }));
|
|
2518
|
+
await routeProviderSelection(step.choice === 'yes' ? refreshed : { ...refreshed, keySource: 'environment-declined' }, { allowEnvironmentConsent: false });
|
|
2519
|
+
return;
|
|
2520
|
+
}
|
|
2521
|
+
if (providerStep.kind === 'key') {
|
|
2522
|
+
const step = providerStep;
|
|
2523
|
+
if (step.testing)
|
|
2524
|
+
return;
|
|
2525
|
+
let secret = providerSecret;
|
|
2526
|
+
setProviderSecret('');
|
|
2527
|
+
if (!secret.trim())
|
|
2528
|
+
return;
|
|
2529
|
+
setProviderStep({ ...step, testing: true, result: null });
|
|
2530
|
+
let result;
|
|
2531
|
+
try {
|
|
2532
|
+
result = await catalog.setProviderKey(step.provider.id, secret);
|
|
2533
|
+
}
|
|
2534
|
+
catch (error) {
|
|
2535
|
+
setProviderStep({ ...step, testing: false, result: null });
|
|
2536
|
+
addLocal(screenErrorText(codeOnly(error), act('screen.action.keyTest'), { note: t('screen.provider.keyNotSaved') }));
|
|
2537
|
+
return;
|
|
2538
|
+
}
|
|
2539
|
+
finally {
|
|
2540
|
+
secret = '';
|
|
2541
|
+
}
|
|
2542
|
+
if (!result.saved) {
|
|
2543
|
+
setProviderStep({ ...step, testing: false, result });
|
|
2544
|
+
return;
|
|
2545
|
+
}
|
|
2546
|
+
addLocal(t('screen.provider.keySaved', { message: result.message, saved: keySavedText() })); /* R2 lane K (E13): the memory store must not claim the system keyring */
|
|
2547
|
+
const refreshed = (await catalog.listProviders()).find((entry) => entry.id === step.provider.id) ?? { ...step.provider, configured: true };
|
|
2548
|
+
await chooseProviderAndOpenModels(refreshed);
|
|
2549
|
+
return;
|
|
2550
|
+
}
|
|
2551
|
+
const row = filteredProviders[selected];
|
|
2552
|
+
if (!row)
|
|
2553
|
+
return;
|
|
2554
|
+
await routeProviderSelection(row);
|
|
2555
|
+
return;
|
|
2556
|
+
}
|
|
2557
|
+
if (appState.focus.current === 'session-picker') {
|
|
2558
|
+
const model = sessionPickerRef.current;
|
|
2559
|
+
if (!model || sessionConfirmingRef.current)
|
|
2560
|
+
return;
|
|
2561
|
+
model.setQuery(pickerQuery);
|
|
2562
|
+
if (!model.rows()[selected])
|
|
2563
|
+
return;
|
|
2564
|
+
model.select(selected);
|
|
2565
|
+
const from = model.rows()[selected];
|
|
2566
|
+
sessionConfirmingRef.current = true;
|
|
2567
|
+
try {
|
|
2568
|
+
freshView();
|
|
2569
|
+
const id = await model.confirm();
|
|
2570
|
+
closeCurrentOverlay();
|
|
2571
|
+
if (id && model.action() === 'fork')
|
|
2572
|
+
addLocal(t('screen.session.forked', { from: from.title || from.id, id }));
|
|
2573
|
+
}
|
|
2574
|
+
catch (error) {
|
|
2575
|
+
addLocal(screenErrorText(error, act('screen.action.sessionAction')));
|
|
2576
|
+
}
|
|
2577
|
+
finally {
|
|
2578
|
+
sessionConfirmingRef.current = false;
|
|
2579
|
+
}
|
|
2580
|
+
return;
|
|
2581
|
+
}
|
|
2582
|
+
};
|
|
2583
|
+
/* R4 Q-R4-7: a picker, dialog or center is open (the approval dialog answers its own keys, R3 E11). */
|
|
2584
|
+
const dialogOpen = () => Boolean((appState.overlay && appState.overlay.kind !== 'approval') || appState.focus.current === 'command-menu' || queueEditor || transcriptSearch || contextInspector || themePicker || undoPicker || historyPicker || agentTreeOverlay || trustCenter || mcpCenter || hookCenter || pluginCenter || memoryCenter || notesCenter || tasksCenter || libraryCenter || researchCenter || automationCenter || forgeCenter);
|
|
2585
|
+
interruptContextRef.current = () => ({ draft: composerStore.getSnapshot().text.length > 0, dialog: dialogOpen(), completion: Boolean(fileCompletionRef.current) });
|
|
2586
|
+
const closeAnyDialog = () => {
|
|
2587
|
+
if (appState.focus.current === 'provider-picker')
|
|
2588
|
+
providerPickerRef.current?.cancel();
|
|
2589
|
+
if (appState.focus.current === 'model-picker')
|
|
2590
|
+
modelPickerRef.current?.cancel();
|
|
2591
|
+
if (appState.focus.current === 'session-picker')
|
|
2592
|
+
sessionPickerRef.current?.cancel();
|
|
2593
|
+
if (appState.focus.current === 'command-menu')
|
|
2594
|
+
commandMenuSelectionStore.reset();
|
|
2595
|
+
setQueueEditor(null);
|
|
2596
|
+
setTranscriptSearch(null);
|
|
2597
|
+
setContextInspector(null);
|
|
2598
|
+
setAgentTreeOverlay(null);
|
|
2599
|
+
setResearchCenter(null);
|
|
2600
|
+
setAutomationCenter(null);
|
|
2601
|
+
setForgeCenter(null);
|
|
2602
|
+
closeCurrentOverlay();
|
|
2603
|
+
};
|
|
2604
|
+
/* R4 Q-R4-7 / E7: a cleared draft goes into history first, so ↑ brings it back (with its paste blocks, Q-R4-9). */
|
|
2605
|
+
const clearDraftIntoHistory = () => { rememberDraft(draftOf(currentEditor())); historyIndexRef.current = -1; reverseIndexRef.current = 0; const cleared = createEditorState(''); composerStore.replace(cleared); setAppState((current) => ({ ...current, editor: cleared })); setAuxCompletions([]); };
|
|
2606
|
+
/* R4 Q-R4-8: Esc closes the `@` list until the token changes; ↑/↓ move its highlight; Tab or Enter insert the
|
|
2607
|
+
highlighted path (a folder keeps the list open one level down, a file ends with a space and closes it). Enter on a
|
|
2608
|
+
file already typed in full is not taken: it sends the prompt. */
|
|
2609
|
+
const dismissCompletion = () => { const watch = completionWatchRef.current; watch.dismissed = fileCompletionRef.current?.token ?? watch.token; watch.seq++; setFileCompletion(null); };
|
|
2610
|
+
const moveCompletion = (delta) => setFileCompletion((current) => current && current.rows.length ? { ...current, selected: Math.max(0, Math.min(current.rows.length - 1, current.selected + delta)) } : current);
|
|
2611
|
+
const acceptCompletion = (how) => {
|
|
2612
|
+
const current = fileCompletionRef.current, row = current?.rows[current.selected];
|
|
2613
|
+
if (!current || !row)
|
|
2614
|
+
return false;
|
|
2615
|
+
if (how === 'enter' && row.value === current.token)
|
|
2616
|
+
return false;
|
|
2617
|
+
const value = row.directory ? row.value : `${row.value} `;
|
|
2618
|
+
updateEditor(editor => lastProjectToken(editor.text) === current.token ? replaceLastProjectToken(editor, current.token, value) : editor);
|
|
2619
|
+
return true;
|
|
2620
|
+
};
|
|
2621
|
+
/* R4 E7: interrupt.ts decides; app.ts only carries the decision out. */
|
|
2622
|
+
const applyInterrupt = (decision, key) => {
|
|
2623
|
+
if (decision === 'cancel') {
|
|
2624
|
+
cancelActiveRun();
|
|
2625
|
+
return;
|
|
2626
|
+
}
|
|
2627
|
+
if (decision === 'arm-exit') {
|
|
2628
|
+
showExitHint(key);
|
|
2629
|
+
return;
|
|
2630
|
+
}
|
|
2631
|
+
if (decision === 'exit' || decision === 'force-exit') {
|
|
2632
|
+
inkApp.exit();
|
|
2633
|
+
return;
|
|
2634
|
+
}
|
|
2635
|
+
if (decision === 'close-dialog') {
|
|
2636
|
+
hideExitHint();
|
|
2637
|
+
closeAnyDialog();
|
|
2638
|
+
return;
|
|
2639
|
+
}
|
|
2640
|
+
if (decision === 'clear-draft') {
|
|
2641
|
+
hideExitHint();
|
|
2642
|
+
clearDraftIntoHistory();
|
|
2643
|
+
return;
|
|
2644
|
+
}
|
|
2645
|
+
if (decision === 'delete-forward') {
|
|
2646
|
+
updateEditor(deleteForward);
|
|
2647
|
+
return;
|
|
2648
|
+
}
|
|
2649
|
+
if (decision === 'close-completion') {
|
|
2650
|
+
dismissCompletion();
|
|
2651
|
+
return;
|
|
2652
|
+
}
|
|
2653
|
+
if (decision === 'stop-run') {
|
|
2654
|
+
void controller.interrupt().catch((error) => addLocal(screenErrorText(error, act('screen.action.interrupt'))));
|
|
2655
|
+
return;
|
|
2656
|
+
}
|
|
2657
|
+
setAuxCompletions([]);
|
|
2658
|
+
setTranscriptRevealId(null);
|
|
2659
|
+
setViewport((view) => ({ ...view, offset: 0, unseen: 0, followingTail: true }));
|
|
2660
|
+
};
|
|
2661
|
+
const handleSemanticAction = (action, key) => {
|
|
2662
|
+
/* R3 E11: the approval dialog's keys are handled by handleApprovalKey, from the synchronous refs; a render that still
|
|
2663
|
+
shows the dialog after it closed must not answer anything. */
|
|
2664
|
+
if (appState.focus.current === 'approval' && (action.startsWith('approval-') || action === 'interrupt'))
|
|
2665
|
+
return;
|
|
2666
|
+
if (fileCompletionRef.current?.rows.length && appState.focus.current === 'composer') {
|
|
2667
|
+
if ((action === 'history-prev' || action === 'history-next') && !key?.ctrl) {
|
|
2668
|
+
moveCompletion(action === 'history-prev' ? -1 : 1);
|
|
2669
|
+
return;
|
|
2670
|
+
}
|
|
2671
|
+
if (action === 'palette' && acceptCompletion('tab'))
|
|
2672
|
+
return;
|
|
2673
|
+
if (action === 'submit' && acceptCompletion('enter'))
|
|
2674
|
+
return;
|
|
2675
|
+
}
|
|
2676
|
+
if (action === 'overlay-close') {
|
|
2677
|
+
closeCurrentOverlay();
|
|
2678
|
+
return;
|
|
2679
|
+
}
|
|
2680
|
+
if (action === 'picker-cancel' && setupRef.current && appState.focus.current === 'model-picker' && modelList.engineDown && !modelEffortStep) {
|
|
2681
|
+
modelPickerRef.current?.cancel();
|
|
2682
|
+
setupTransitionRef.current = true;
|
|
2683
|
+
setupPendingProviderRef.current = null;
|
|
2684
|
+
void loadProviders().finally(() => { setupTransitionRef.current = false; });
|
|
2685
|
+
return;
|
|
2686
|
+
}
|
|
2687
|
+
if (action === 'picker-cancel') {
|
|
2688
|
+
if (appState.focus.current === 'provider-picker')
|
|
2689
|
+
providerPickerRef.current?.cancel();
|
|
2690
|
+
if (appState.focus.current === 'model-picker')
|
|
2691
|
+
modelPickerRef.current?.cancel();
|
|
2692
|
+
if (appState.focus.current === 'session-picker')
|
|
2693
|
+
sessionPickerRef.current?.cancel();
|
|
2694
|
+
closeCurrentOverlay();
|
|
2695
|
+
return;
|
|
2696
|
+
}
|
|
2697
|
+
if (appState.focus.current === 'provider-picker' && providerStep.kind === 'login-choice' && (action === 'picker-up' || action === 'picker-home')) {
|
|
2698
|
+
setProviderStep({ ...providerStep, choice: 'browser' });
|
|
2699
|
+
return;
|
|
2700
|
+
}
|
|
2701
|
+
if (appState.focus.current === 'provider-picker' && providerStep.kind === 'login-choice' && (action === 'picker-down' || action === 'picker-end')) {
|
|
2702
|
+
setProviderStep({ ...providerStep, choice: 'paste-key' });
|
|
2703
|
+
return;
|
|
2704
|
+
}
|
|
2705
|
+
if (appState.focus.current === 'provider-picker' && providerStep.kind === 'consent' && (action === 'picker-up' || action === 'picker-home')) {
|
|
2706
|
+
setProviderStep({ ...providerStep, choice: 'no' });
|
|
2707
|
+
return;
|
|
2708
|
+
}
|
|
2709
|
+
if (appState.focus.current === 'provider-picker' && providerStep.kind === 'consent' && (action === 'picker-down' || action === 'picker-end')) {
|
|
2710
|
+
setProviderStep({ ...providerStep, choice: 'yes' });
|
|
2711
|
+
return;
|
|
2712
|
+
}
|
|
2713
|
+
if (appState.focus.current === 'provider-picker' && providerStep.kind !== 'list' && action.startsWith('picker-') && action !== 'picker-confirm' && action !== 'picker-backspace')
|
|
2714
|
+
return;
|
|
2715
|
+
if (action.startsWith('picker-') && action !== 'picker-confirm' && action !== 'picker-backspace') {
|
|
2716
|
+
movePicker(action);
|
|
2717
|
+
return;
|
|
2718
|
+
}
|
|
2719
|
+
if (action === 'picker-backspace') {
|
|
2720
|
+
pickerBackspace();
|
|
2721
|
+
return;
|
|
2722
|
+
}
|
|
2723
|
+
if (action === 'picker-confirm') {
|
|
2724
|
+
void confirmPicker();
|
|
2725
|
+
return;
|
|
2726
|
+
}
|
|
2727
|
+
if (action === 'command-complete' && appState.focus.current === 'command-menu') {
|
|
2728
|
+
void confirmPicker('complete');
|
|
2729
|
+
return;
|
|
2730
|
+
}
|
|
2731
|
+
if (action === 'interrupt') {
|
|
2732
|
+
if (key?.escape) {
|
|
2733
|
+
const decision = interruptRef.current.esc(state.running);
|
|
2734
|
+
if (decision === 'stop-run') {
|
|
2735
|
+
void controller.interrupt().catch((error) => addLocal(screenErrorText(error, act('screen.action.interrupt'))));
|
|
2736
|
+
return;
|
|
2737
|
+
}
|
|
2738
|
+
applyInterrupt(decision, 'ctrl-c');
|
|
2739
|
+
return;
|
|
2740
|
+
}
|
|
2741
|
+
const result = interruptRef.current.ctrlC(state.running);
|
|
2742
|
+
if (result === 'cancel')
|
|
2743
|
+
cancelActiveRun();
|
|
2744
|
+
else
|
|
2745
|
+
applyInterrupt(result, 'ctrl-c');
|
|
2746
|
+
return;
|
|
2747
|
+
}
|
|
2748
|
+
/* R4 Q-R4-7: Ctrl+D where the composer does not own it (a picker), and a key a person bound to clear-draft. */
|
|
2749
|
+
if (action === 'exit') {
|
|
2750
|
+
applyInterrupt(interruptRef.current.ctrlD(state.running), 'ctrl-d');
|
|
2751
|
+
return;
|
|
2752
|
+
}
|
|
2753
|
+
if (action === 'clear-draft') {
|
|
2754
|
+
if (currentEditor().text.length)
|
|
2755
|
+
applyInterrupt('clear-draft', 'ctrl-c');
|
|
2756
|
+
return;
|
|
2757
|
+
}
|
|
2758
|
+
if (action === 'vim-toggle') {
|
|
2759
|
+
setVim((current) => toggleVim(current));
|
|
2760
|
+
return;
|
|
2761
|
+
}
|
|
2762
|
+
if (action === 'agent-tree') {
|
|
2763
|
+
void controller.agentTree().then((tree) => {
|
|
2764
|
+
const rows = agentRosterRows(tree);
|
|
2765
|
+
setAgentRows(rows);
|
|
2766
|
+
if (!rows.length) {
|
|
2767
|
+
addLocal(t('screen.agents.none'));
|
|
2768
|
+
return;
|
|
2769
|
+
}
|
|
2770
|
+
setAgentTreeOverlay(createAgentTreeOverlayModel(rows, tree?.focusId ?? controller.current()));
|
|
2771
|
+
}).catch((error) => addLocal(screenErrorText(error, act('screen.action.agentTree'))));
|
|
2772
|
+
return;
|
|
2773
|
+
}
|
|
2774
|
+
if (action === 'steer') {
|
|
2775
|
+
void steerDraft();
|
|
2776
|
+
return;
|
|
2777
|
+
}
|
|
2778
|
+
if (action === 'command-menu') {
|
|
2779
|
+
const editor = composerStore.update(current => editInsert(current, '/'));
|
|
2780
|
+
commandMenuSelectionStore.reset();
|
|
2781
|
+
setAppState((current) => ({ ...current, editor, focus: openFocus(current.focus, 'command-menu') }));
|
|
2782
|
+
setPickerSelection({ query: '', selected: 0, pageSize: 10 });
|
|
2783
|
+
return;
|
|
2784
|
+
}
|
|
2785
|
+
if (action === 'model-picker') {
|
|
2786
|
+
void loadModels();
|
|
2787
|
+
return;
|
|
2788
|
+
}
|
|
2789
|
+
if (action === 'provider-picker') {
|
|
2790
|
+
void loadProviders();
|
|
2791
|
+
return;
|
|
2792
|
+
}
|
|
2793
|
+
if (action === 'help') {
|
|
2794
|
+
setHelpOffset(0);
|
|
2795
|
+
applyOverlay({ kind: 'help' });
|
|
2796
|
+
return;
|
|
2797
|
+
}
|
|
2798
|
+
if (action === 'reasoning-toggle' || action === 'tool-details' || action === 'redraw') {
|
|
2799
|
+
runKeyAction(action);
|
|
2800
|
+
return;
|
|
2801
|
+
}
|
|
2802
|
+
if (action === 'transcript-search') {
|
|
2803
|
+
openTranscriptSearch();
|
|
2804
|
+
return;
|
|
2805
|
+
}
|
|
2806
|
+
if (action === 'transcript-raw') {
|
|
2807
|
+
setTranscriptRaw((value) => !value);
|
|
2808
|
+
setTranscriptSearch((current) => current ? { ...current, raw: !transcriptRaw, notice: null } : current);
|
|
2809
|
+
return;
|
|
2810
|
+
}
|
|
2811
|
+
if (action === 'transcript-copy') {
|
|
2812
|
+
copyTranscriptItem();
|
|
2813
|
+
return;
|
|
2814
|
+
}
|
|
2815
|
+
if (action === 'permission-cycle') {
|
|
2816
|
+
cycleMode();
|
|
2817
|
+
return;
|
|
2818
|
+
}
|
|
2819
|
+
if (action === 'page-up') {
|
|
2820
|
+
scrollTranscript('page-up');
|
|
2821
|
+
return;
|
|
2822
|
+
}
|
|
2823
|
+
if (action === 'page-down') {
|
|
2824
|
+
scrollTranscript('page-down');
|
|
2825
|
+
return;
|
|
2826
|
+
}
|
|
2827
|
+
if (action === 'history-search') {
|
|
2828
|
+
reverseSearch();
|
|
2829
|
+
return;
|
|
2830
|
+
}
|
|
2831
|
+
if (action === 'history-prev' && key?.ctrl) {
|
|
2832
|
+
historyMove(1);
|
|
2833
|
+
return;
|
|
2834
|
+
}
|
|
2835
|
+
if (action === 'history-next' && key?.ctrl) {
|
|
2836
|
+
historyMove(-1);
|
|
2837
|
+
return;
|
|
2838
|
+
}
|
|
2839
|
+
if (action === 'external-editor') {
|
|
2840
|
+
void editExternally({ text: currentEditor().text, editor: process.env.VISUAL ?? process.env.EDITOR, tmpRoot: paths.cacheRoot }).then(setEditorText).catch(error => addLocal(screenErrorText(error, act('screen.action.externalEditor'))));
|
|
2841
|
+
return;
|
|
2842
|
+
}
|
|
2843
|
+
if (action === 'palette') {
|
|
2844
|
+
const token = lastProjectToken(currentEditor().text);
|
|
2845
|
+
if (token) {
|
|
2846
|
+
checkCompletion(true);
|
|
2847
|
+
return;
|
|
2848
|
+
}
|
|
2849
|
+
const rows = commandMenuItems(currentEditor().text);
|
|
2850
|
+
setAuxCompletions(rows.slice(0, 8).map(row => `/${row.command.name}`));
|
|
2851
|
+
if (rows.length === 1)
|
|
2852
|
+
setEditorText(completeCommandSelection(rows[0].command));
|
|
2853
|
+
return;
|
|
2854
|
+
}
|
|
2855
|
+
if (action === 'newline') {
|
|
2856
|
+
updateEditor(editor => editInsert(editor, '\n'));
|
|
2857
|
+
return;
|
|
2858
|
+
}
|
|
2859
|
+
if (action === 'submit') {
|
|
2860
|
+
void submit();
|
|
2861
|
+
return;
|
|
2862
|
+
}
|
|
2863
|
+
if (action === 'undo') {
|
|
2864
|
+
updateEditor(undoEditor);
|
|
2865
|
+
return;
|
|
2866
|
+
}
|
|
2867
|
+
if (action === 'redo') {
|
|
2868
|
+
updateEditor(redoEditor);
|
|
2869
|
+
return;
|
|
2870
|
+
}
|
|
2871
|
+
if (action === 'home') {
|
|
2872
|
+
updateEditor(editor => moveCursor(editor, 'home'));
|
|
2873
|
+
return;
|
|
2874
|
+
}
|
|
2875
|
+
if (action === 'end') {
|
|
2876
|
+
updateEditor(editor => moveCursor(editor, 'end'));
|
|
2877
|
+
return;
|
|
2878
|
+
}
|
|
2879
|
+
if (action === 'left') {
|
|
2880
|
+
updateEditor(editor => moveCursor(editor, 'left'));
|
|
2881
|
+
return;
|
|
2882
|
+
}
|
|
2883
|
+
if (action === 'right') {
|
|
2884
|
+
updateEditor(editor => moveCursor(editor, 'right'));
|
|
2885
|
+
return;
|
|
2886
|
+
}
|
|
2887
|
+
if (action === 'word-left') {
|
|
2888
|
+
updateEditor(editor => moveWord(editor, -1));
|
|
2889
|
+
return;
|
|
2890
|
+
}
|
|
2891
|
+
if (action === 'word-right') {
|
|
2892
|
+
updateEditor(editor => moveWord(editor, 1));
|
|
2893
|
+
return;
|
|
2894
|
+
}
|
|
2895
|
+
if (action === 'select-home') {
|
|
2896
|
+
updateEditor(editor => moveCursor(editor, 'home', true));
|
|
2897
|
+
return;
|
|
2898
|
+
}
|
|
2899
|
+
if (action === 'select-end') {
|
|
2900
|
+
updateEditor(editor => moveCursor(editor, 'end', true));
|
|
2901
|
+
return;
|
|
2902
|
+
}
|
|
2903
|
+
if (action === 'select-left') {
|
|
2904
|
+
updateEditor(editor => moveCursor(editor, 'left', true));
|
|
2905
|
+
return;
|
|
2906
|
+
}
|
|
2907
|
+
if (action === 'select-right') {
|
|
2908
|
+
updateEditor(editor => moveCursor(editor, 'right', true));
|
|
2909
|
+
return;
|
|
2910
|
+
}
|
|
2911
|
+
if (action === 'select-up') {
|
|
2912
|
+
updateEditor(editor => moveVertical(editor, -1, true));
|
|
2913
|
+
return;
|
|
2914
|
+
}
|
|
2915
|
+
if (action === 'select-down') {
|
|
2916
|
+
updateEditor(editor => moveVertical(editor, 1, true));
|
|
2917
|
+
return;
|
|
2918
|
+
}
|
|
2919
|
+
if (action === 'select-word-left') {
|
|
2920
|
+
updateEditor(editor => moveWord(editor, -1, true));
|
|
2921
|
+
return;
|
|
2922
|
+
}
|
|
2923
|
+
if (action === 'select-word-right') {
|
|
2924
|
+
updateEditor(editor => moveWord(editor, 1, true));
|
|
2925
|
+
return;
|
|
2926
|
+
}
|
|
2927
|
+
if (action === 'copy-selection') {
|
|
2928
|
+
updateEditor(copySelection);
|
|
2929
|
+
return;
|
|
2930
|
+
}
|
|
2931
|
+
if (action === 'kill-start') {
|
|
2932
|
+
updateEditor(killToStart);
|
|
2933
|
+
return;
|
|
2934
|
+
}
|
|
2935
|
+
if (action === 'kill-end') {
|
|
2936
|
+
updateEditor(killToEnd);
|
|
2937
|
+
return;
|
|
2938
|
+
}
|
|
2939
|
+
if (action === 'kill-word') {
|
|
2940
|
+
updateEditor(killWordBackward);
|
|
2941
|
+
return;
|
|
2942
|
+
}
|
|
2943
|
+
if (action === 'yank') {
|
|
2944
|
+
updateEditor(yank);
|
|
2945
|
+
return;
|
|
2946
|
+
}
|
|
2947
|
+
if (action === 'delete-forward') {
|
|
2948
|
+
if (key?.ctrl) {
|
|
2949
|
+
applyInterrupt(interruptRef.current.ctrlD(state.running), 'ctrl-d');
|
|
2950
|
+
return;
|
|
2951
|
+
}
|
|
2952
|
+
updateEditor(deleteForward);
|
|
2953
|
+
return;
|
|
2954
|
+
}
|
|
2955
|
+
if (action === 'backspace') {
|
|
2956
|
+
updateEditor(deleteBackward);
|
|
2957
|
+
return;
|
|
2958
|
+
}
|
|
2959
|
+
if (action === 'history-prev') {
|
|
2960
|
+
const editor = currentEditor();
|
|
2961
|
+
const hadSelection = editor.selectionAnchor !== undefined;
|
|
2962
|
+
const moved = moveVertical(editor, -1);
|
|
2963
|
+
if (moved.cursor !== editor.cursor || hadSelection)
|
|
2964
|
+
updateEditor(() => moved);
|
|
2965
|
+
else
|
|
2966
|
+
historyMove(1);
|
|
2967
|
+
return;
|
|
2968
|
+
}
|
|
2969
|
+
if (action === 'history-next') {
|
|
2970
|
+
const editor = currentEditor();
|
|
2971
|
+
const hadSelection = editor.selectionAnchor !== undefined;
|
|
2972
|
+
const moved = moveVertical(editor, 1);
|
|
2973
|
+
if (moved.cursor !== editor.cursor || hadSelection)
|
|
2974
|
+
updateEditor(() => moved);
|
|
2975
|
+
else
|
|
2976
|
+
historyMove(-1);
|
|
2977
|
+
return;
|
|
2978
|
+
}
|
|
2979
|
+
};
|
|
2980
|
+
/* R4 Q-R4-8: an open `@` list counts as completions: the composer's fast paths step aside so its keys reach the list. */
|
|
2981
|
+
const auxCount = auxCompletions.length + (fileCompletion ? 1 : 0);
|
|
2982
|
+
const pasteOwner = composerOwnsPaste({
|
|
2983
|
+
bootPhase: boot.phase,
|
|
2984
|
+
focus: appState.focus.current,
|
|
2985
|
+
modalOwner: Boolean(appState.overlay || queueEditor || transcriptSearch || contextInspector || themePicker || undoPicker || historyPicker || agentTreeOverlay || agentView || trustCenter || mcpCenter || hookCenter || pluginCenter || memoryCenter || notesCenter || tasksCenter || libraryCenter || researchCenter || automationCenter || forgeCenter || pending),
|
|
2986
|
+
vimMode: vim.enabled ? vim.mode : 'disabled',
|
|
2987
|
+
auxCount,
|
|
2988
|
+
});
|
|
2989
|
+
Ink.usePaste((pastedText) => {
|
|
2990
|
+
/* R3-KEYS: no C0 or DEL character reaches the editor or the model, except a line break. */
|
|
2991
|
+
composerStore.update(editor => applyComposerFastPasteInput(editor, sanitizeInputText(pastedText)));
|
|
2992
|
+
}, { isActive: pasteOwner });
|
|
2993
|
+
/* R3 gate E11: one key in the approval dialog (Q-R3-5/6/14). Esc from the summary and Ctrl+C deny and stop the turn;
|
|
2994
|
+
Enter answers the highlight (Deny by default: deny and continue); a digit answers at once; [5] only in the full review. */
|
|
2995
|
+
const answerApproval = (choice, stop) => {
|
|
2996
|
+
const current = pendingRef.current, shown = approvalShownRef.current;
|
|
2997
|
+
if (!current || !shown || shown.answered || shown.requestId !== current.requestId)
|
|
2998
|
+
return;
|
|
2999
|
+
shown.answered = true;
|
|
3000
|
+
void controller.resolveApproval(current.requestId, choice).then(() => { if (stop)
|
|
3001
|
+
return controller.interrupt(); }).catch((error) => addLocal(screenErrorText(error, act('screen.action.approval'))));
|
|
3002
|
+
};
|
|
3003
|
+
const handleApprovalKey = (ch, key) => {
|
|
3004
|
+
const ui = approvalUiRef.current;
|
|
3005
|
+
if (ui.expanded && (key.pageUp || key.pageup || key.pageDown || key.pagedown || key.upArrow || key.downArrow)) {
|
|
3006
|
+
const delta = (key.pageDown || key.pagedown) ? 4 : (key.pageUp || key.pageup) ? -4 : key.downArrow ? 1 : -1;
|
|
3007
|
+
setApprovalScroll((value) => Math.max(0, Math.min(approvalMaxOffsetRef.current, value + delta)));
|
|
3008
|
+
return;
|
|
3009
|
+
}
|
|
3010
|
+
const action = resolveKeybinding(ch, key, ['approval', 'global'], effectiveKeymap);
|
|
3011
|
+
const outcome = approvalKeyOutcome(action, { expanded: ui.expanded, selected: ui.selected, escape: Boolean(key.escape) });
|
|
3012
|
+
if (outcome.kind === 'answer') {
|
|
3013
|
+
answerApproval(outcome.choice, outcome.stop);
|
|
3014
|
+
return;
|
|
3015
|
+
}
|
|
3016
|
+
if (outcome.kind === 'select') {
|
|
3017
|
+
approvalUiRef.current = { ...ui, selected: outcome.selected };
|
|
3018
|
+
setApprovalSelected(outcome.selected);
|
|
3019
|
+
return;
|
|
3020
|
+
}
|
|
3021
|
+
if (outcome.kind === 'toggle-review' || outcome.kind === 'back') {
|
|
3022
|
+
const expanded = outcome.kind === 'toggle-review' ? !ui.expanded : false;
|
|
3023
|
+
approvalUiRef.current = { ...ui, expanded };
|
|
3024
|
+
setApprovalExpanded(expanded);
|
|
3025
|
+
setApprovalScroll(0);
|
|
3026
|
+
return;
|
|
3027
|
+
}
|
|
3028
|
+
if (action === 'tool-details' || action === 'reasoning-toggle' || action === 'redraw')
|
|
3029
|
+
runKeyAction(action);
|
|
3030
|
+
};
|
|
3031
|
+
Ink.useInput((rawCh, rawKey) => {
|
|
3032
|
+
/* R3-KEYS: a mixed chunk is its leading key, then text with no control character. */
|
|
3033
|
+
for (const part of splitInputChunk(rawCh, rawKey))
|
|
3034
|
+
handleInput(part.ch, part.key);
|
|
3035
|
+
});
|
|
3036
|
+
const handleInput = (ch, key) => {
|
|
3037
|
+
const approvalGate = approvalInputGate(approvalShownRef.current, clockRef.current());
|
|
3038
|
+
if (approvalGate === 'dialog') {
|
|
3039
|
+
coordinator.immediate('approval', () => handleApprovalKey(ch, key));
|
|
3040
|
+
return;
|
|
3041
|
+
}
|
|
3042
|
+
/* In the grace the key belongs to what is under the dialog, and only the composer takes it. */
|
|
3043
|
+
const graceComposer = approvalGate === 'grace';
|
|
3044
|
+
const focusNow = graceComposer && appState.focus.current === 'approval' ? (appState.focus.stack.at(-1) ?? 'composer') : appState.focus.current;
|
|
3045
|
+
if (graceComposer && focusNow !== 'composer')
|
|
3046
|
+
return;
|
|
3047
|
+
const modalOwner = Boolean((appState.overlay && !(graceComposer && appState.overlay.kind === 'approval')) || queueEditor || transcriptSearch || contextInspector || themePicker || undoPicker || historyPicker || agentTreeOverlay || agentView || trustCenter || mcpCenter || hookCenter || pluginCenter || memoryCenter || notesCenter || tasksCenter || libraryCenter || researchCenter || automationCenter || forgeCenter || (pending && !graceComposer));
|
|
3048
|
+
/* R1 E15 / Q-R1-7 (no mouse capture): Ctrl+Home and Ctrl+End move the transcript to its first and newest rows, as in
|
|
3049
|
+
Claude Code fullscreen and Qwen Code. The keymap resolver drops Ctrl on Home/End, so they are read here first. */
|
|
3050
|
+
if (key?.ctrl && (key.home || key.end) && !modalOwner && boot.phase === 'ready') {
|
|
3051
|
+
scrollTranscript(key.home ? 'top' : 'jump-tail');
|
|
3052
|
+
return;
|
|
3053
|
+
}
|
|
3054
|
+
/* P13: the watched agent's transcript scrolls the same way (the view owns the composer, not the transcript). */
|
|
3055
|
+
if (agentView && !agentTreeOverlay && !appState.overlay && key?.ctrl && (key.home || key.end) && boot.phase === 'ready') {
|
|
3056
|
+
scrollTranscript(key.home ? 'top' : 'jump-tail');
|
|
3057
|
+
return;
|
|
3058
|
+
}
|
|
3059
|
+
/* Lane O: the help overlay scrolls inside its budget; the keymap has no help-context scroll keys, so they are read here. */
|
|
3060
|
+
if (appState.overlay?.kind === 'help' && (key.pageUp || key.pageDown || key.upArrow || key.downArrow)) {
|
|
3061
|
+
const step = key.pageUp || key.pageDown ? helpPageRef.current : 1, delta = key.pageUp || key.upArrow ? -step : step;
|
|
3062
|
+
setHelpOffset((value) => Math.max(0, Math.min(helpMaxOffsetRef.current, value + delta)));
|
|
3063
|
+
return;
|
|
3064
|
+
}
|
|
3065
|
+
const fastDecision = decideShellInput({
|
|
3066
|
+
ch, key, focus: focusNow, composerText: currentEditor().text, commandMenuOpen: focusNow === 'command-menu',
|
|
3067
|
+
trustCenterOpen: Boolean(trustCenter), approvalExpanded, boot, keymap: effectiveKeymap,
|
|
3068
|
+
});
|
|
3069
|
+
const fastText = composerFastTextInput({ bootPhase: boot.phase, focus: focusNow, modalOwner, vimMode: vim.enabled ? vim.mode : 'disabled', auxCount, key, routed: fastDecision.routed });
|
|
3070
|
+
if (fastText !== null) {
|
|
3071
|
+
composerStore.update(editor => editInsert(editor, fastText));
|
|
3072
|
+
return;
|
|
3073
|
+
}
|
|
3074
|
+
const fastPaste = composerFastPasteInput({ bootPhase: boot.phase, focus: focusNow, modalOwner, vimMode: vim.enabled ? vim.mode : 'disabled', auxCount, key, routed: fastDecision.routed });
|
|
3075
|
+
if (fastPaste !== null) {
|
|
3076
|
+
composerStore.update(editor => applyComposerFastPasteInput(editor, fastPaste));
|
|
3077
|
+
return;
|
|
3078
|
+
}
|
|
3079
|
+
const fastEditorAction = composerFastEditorAction({ bootPhase: boot.phase, focus: focusNow, modalOwner, vimMode: vim.enabled ? vim.mode : 'disabled', auxCount, routed: fastDecision.routed, editor: currentEditor() });
|
|
3080
|
+
if (fastEditorAction !== null) {
|
|
3081
|
+
composerStore.update(editor => applyComposerFastEditorAction(editor, fastEditorAction));
|
|
3082
|
+
return;
|
|
3083
|
+
}
|
|
3084
|
+
const historyPlan = planComposerHistoryAction({
|
|
3085
|
+
bootPhase: boot.phase, focus: focusNow, modalOwner, vimMode: vim.enabled ? vim.mode : 'disabled', auxCount,
|
|
3086
|
+
routed: fastDecision.routed, key, editor: currentEditor(), history, historyIndex: historyIndexRef.current, reverseIndex: reverseIndexRef.current,
|
|
3087
|
+
});
|
|
3088
|
+
if (historyPlan !== null) {
|
|
3089
|
+
historyIndexRef.current = historyPlan.historyIndex;
|
|
3090
|
+
reverseIndexRef.current = historyPlan.reverseIndex;
|
|
3091
|
+
composerStore.replace(historyPlan.editor);
|
|
3092
|
+
return;
|
|
3093
|
+
}
|
|
3094
|
+
const commandQuery = commandMenuFastTextInput({ bootPhase: boot.phase, focus: focusNow, modalOwner, key, routed: fastDecision.routed });
|
|
3095
|
+
if (commandQuery !== null) {
|
|
3096
|
+
commandMenuSelectionStore.reset();
|
|
3097
|
+
composerStore.update(editor => editInsert(editor, commandQuery));
|
|
3098
|
+
return;
|
|
3099
|
+
}
|
|
3100
|
+
coordinator.immediate('input', () => {
|
|
3101
|
+
if (forgeCenter) {
|
|
3102
|
+
if (key.ctrl && (ch === 'c' || ch === 'C')) {
|
|
3103
|
+
setForgeCenter(null);
|
|
3104
|
+
return;
|
|
3105
|
+
}
|
|
3106
|
+
if (key.escape || (ch === 'q' && !key.ctrl && !key.meta)) {
|
|
3107
|
+
setForgeCenter(null);
|
|
3108
|
+
return;
|
|
3109
|
+
}
|
|
3110
|
+
const loadSelected = (next) => { setForgeCenter(next); const row = selectedForgeTool(next); if (row)
|
|
3111
|
+
void forgeFacade().read(row.id).then(detail => setForgeCenter((current) => current ? setForgeCenterDetail(current, detail) : current)).catch((error) => addLocal(screenErrorText(error, act('screen.action.forgeDetail')))); };
|
|
3112
|
+
if (key.upArrow) {
|
|
3113
|
+
loadSelected(moveForgeCenterSelection(forgeCenter, -1));
|
|
3114
|
+
return;
|
|
3115
|
+
}
|
|
3116
|
+
if (key.downArrow) {
|
|
3117
|
+
loadSelected(moveForgeCenterSelection(forgeCenter, 1));
|
|
3118
|
+
return;
|
|
3119
|
+
}
|
|
3120
|
+
const row = selectedForgeTool(forgeCenter);
|
|
3121
|
+
if (ch === 'r') {
|
|
3122
|
+
void loadForgeCenter(row?.id);
|
|
3123
|
+
return;
|
|
3124
|
+
}
|
|
3125
|
+
if (!row)
|
|
3126
|
+
return;
|
|
3127
|
+
const ownerSessionId = state.sessionId;
|
|
3128
|
+
if (ch === 'e') {
|
|
3129
|
+
if (!ownerSessionId) {
|
|
3130
|
+
addLocal(t('screen.forge.needsSession'));
|
|
3131
|
+
return;
|
|
3132
|
+
}
|
|
3133
|
+
void forgeFacade().setEnabled(ownerSessionId, row.id, !row.enabled).then(() => loadForgeCenter(row.id)).catch((error) => addLocal(screenErrorText(error, act('screen.action.forgeToggle'))));
|
|
3134
|
+
return;
|
|
3135
|
+
}
|
|
3136
|
+
if (ch === 'b') {
|
|
3137
|
+
if (!ownerSessionId) {
|
|
3138
|
+
addLocal(t('screen.forge.rollbackNeedsSession'));
|
|
3139
|
+
return;
|
|
3140
|
+
}
|
|
3141
|
+
const versions = Array.isArray(forgeCenter.detail?.versions) ? forgeCenter.detail.versions : [];
|
|
3142
|
+
const current = row.ownerRevision ?? Number.MAX_SAFE_INTEGER;
|
|
3143
|
+
const candidates = versions.map((v) => Number(v.revision ?? v.ownerRevision)).filter((v) => Number.isSafeInteger(v) && v < current).sort((a, b) => b - a);
|
|
3144
|
+
const target = candidates[0];
|
|
3145
|
+
if (!target) {
|
|
3146
|
+
addLocal(t('screen.forge.noEarlierRevision'));
|
|
3147
|
+
return;
|
|
3148
|
+
}
|
|
3149
|
+
void forgeFacade().rollback(ownerSessionId, row.id, target).then(() => loadForgeCenter(row.id)).catch((error) => addLocal(screenErrorText(error, act('screen.action.forgeRollback'))));
|
|
3150
|
+
return;
|
|
3151
|
+
}
|
|
3152
|
+
return;
|
|
3153
|
+
}
|
|
3154
|
+
if (automationCenter) {
|
|
3155
|
+
if (key.ctrl && (ch === 'c' || ch === 'C')) {
|
|
3156
|
+
setAutomationCenter(null);
|
|
3157
|
+
return;
|
|
3158
|
+
}
|
|
3159
|
+
if (key.escape || (ch === 'q' && !key.ctrl && !key.meta)) {
|
|
3160
|
+
setAutomationCenter(null);
|
|
3161
|
+
return;
|
|
3162
|
+
}
|
|
3163
|
+
const loadSelected = (next) => { setAutomationCenter(next); const row = selectedAutomation(next); if (row)
|
|
3164
|
+
void automationFacade().read(row.id).then(detail => setAutomationCenter((current) => current ? setAutomationCenterDetail(current, detail) : current)).catch((error) => addLocal(screenErrorText(error, act('screen.action.automationDetail')))); };
|
|
3165
|
+
if (key.upArrow) {
|
|
3166
|
+
loadSelected(moveAutomationCenterSelection(automationCenter, -1));
|
|
3167
|
+
return;
|
|
3168
|
+
}
|
|
3169
|
+
if (key.downArrow) {
|
|
3170
|
+
loadSelected(moveAutomationCenterSelection(automationCenter, 1));
|
|
3171
|
+
return;
|
|
3172
|
+
}
|
|
3173
|
+
const row = selectedAutomation(automationCenter);
|
|
3174
|
+
if (ch === 'u') {
|
|
3175
|
+
void loadAutomationCenter(row?.id);
|
|
3176
|
+
return;
|
|
3177
|
+
}
|
|
3178
|
+
if (ch === 'd' && row) {
|
|
3179
|
+
void automationFacade().read(row.id).then(detail => setAutomationCenter((current) => current ? setAutomationCenterDetail(current, detail) : current)).catch((error) => addLocal(screenErrorText(error, act('screen.action.automationDryRun'))));
|
|
3180
|
+
return;
|
|
3181
|
+
}
|
|
3182
|
+
return;
|
|
3183
|
+
}
|
|
3184
|
+
if (researchCenter) {
|
|
3185
|
+
if (key.ctrl && (ch === 'c' || ch === 'C')) {
|
|
3186
|
+
setResearchCenter(null);
|
|
3187
|
+
return;
|
|
3188
|
+
}
|
|
3189
|
+
if (key.escape || (ch === 'q' && !key.ctrl && !key.meta)) {
|
|
3190
|
+
setResearchCenter(null);
|
|
3191
|
+
return;
|
|
3192
|
+
}
|
|
3193
|
+
if (key.upArrow) {
|
|
3194
|
+
const next = moveResearchCenterSelection(researchCenter, -1);
|
|
3195
|
+
setResearchCenter(next);
|
|
3196
|
+
const row = selectedResearch(next);
|
|
3197
|
+
if (row)
|
|
3198
|
+
void researchFacade().read(row.id).then(detail => setResearchCenter((current) => current ? setResearchCenterDetail(current, detail) : current)).catch((error) => addLocal(screenErrorText(error, act('screen.action.researchDetail'))));
|
|
3199
|
+
return;
|
|
3200
|
+
}
|
|
3201
|
+
if (key.downArrow) {
|
|
3202
|
+
const next = moveResearchCenterSelection(researchCenter, 1);
|
|
3203
|
+
setResearchCenter(next);
|
|
3204
|
+
const row = selectedResearch(next);
|
|
3205
|
+
if (row)
|
|
3206
|
+
void researchFacade().read(row.id).then(detail => setResearchCenter((current) => current ? setResearchCenterDetail(current, detail) : current)).catch((error) => addLocal(screenErrorText(error, act('screen.action.researchDetail'))));
|
|
3207
|
+
return;
|
|
3208
|
+
}
|
|
3209
|
+
const row = selectedResearch(researchCenter);
|
|
3210
|
+
if (ch === 'u') {
|
|
3211
|
+
void loadResearchCenter(row?.id);
|
|
3212
|
+
return;
|
|
3213
|
+
}
|
|
3214
|
+
if (!row)
|
|
3215
|
+
return;
|
|
3216
|
+
const ownerSessionId = state.sessionId ?? row.id;
|
|
3217
|
+
if (ch === 'p') {
|
|
3218
|
+
void researchFacade().pause(ownerSessionId, row.id).then(() => loadResearchCenter(row.id)).catch((error) => addLocal(screenErrorText(error, act('screen.action.researchPause'))));
|
|
3219
|
+
return;
|
|
3220
|
+
}
|
|
3221
|
+
if (ch === 'r') {
|
|
3222
|
+
void researchFacade().resume(ownerSessionId, row.id).then(() => loadResearchCenter(row.id)).catch((error) => addLocal(screenErrorText(error, act('screen.action.researchResume'))));
|
|
3223
|
+
return;
|
|
3224
|
+
}
|
|
3225
|
+
if (ch === 'c') {
|
|
3226
|
+
void researchFacade().cancel(ownerSessionId, row.id).then(() => loadResearchCenter(row.id)).catch((error) => addLocal(screenErrorText(error, act('screen.action.researchCancel'))));
|
|
3227
|
+
return;
|
|
3228
|
+
}
|
|
3229
|
+
if (ch === 'e') {
|
|
3230
|
+
void researchFacade().exportFile(row.id, 'md').then(result => addLocal(t('screen.research.exported', { path: result.path }))).catch((error) => addLocal(screenErrorText(error, act('screen.action.researchExport'))));
|
|
3231
|
+
return;
|
|
3232
|
+
}
|
|
3233
|
+
return;
|
|
3234
|
+
}
|
|
3235
|
+
if (libraryCenter) {
|
|
3236
|
+
if (key.ctrl && (ch === 'c' || ch === 'C')) {
|
|
3237
|
+
setLibraryCenter(null);
|
|
3238
|
+
return;
|
|
3239
|
+
}
|
|
3240
|
+
if (key.escape || (ch === 'q' && !key.ctrl && !key.meta)) {
|
|
3241
|
+
setLibraryCenter(null);
|
|
3242
|
+
return;
|
|
3243
|
+
}
|
|
3244
|
+
if (key.upArrow) {
|
|
3245
|
+
setLibraryCenter((current) => current ? moveLibraryCenterSelection(current, -1) : current);
|
|
3246
|
+
return;
|
|
3247
|
+
}
|
|
3248
|
+
if (key.downArrow) {
|
|
3249
|
+
setLibraryCenter((current) => current ? moveLibraryCenterSelection(current, 1) : current);
|
|
3250
|
+
return;
|
|
3251
|
+
}
|
|
3252
|
+
if (ch === 'r') {
|
|
3253
|
+
const row = selectedLibraryEntry(libraryCenter);
|
|
3254
|
+
void loadLibraryCenter(row?.id);
|
|
3255
|
+
return;
|
|
3256
|
+
}
|
|
3257
|
+
if (ch === 'p') {
|
|
3258
|
+
const row = selectedLibraryEntry(libraryCenter);
|
|
3259
|
+
if (!row)
|
|
3260
|
+
return;
|
|
3261
|
+
void libraryFacade().preview(row.id).then(preview => setLibraryCenter((current) => current ? setLibraryCenterPreview(current, preview) : current)).catch((error) => addLocal(screenErrorText(error, act('screen.action.libraryPreview'))));
|
|
3262
|
+
return;
|
|
3263
|
+
}
|
|
3264
|
+
if (ch === 'u') {
|
|
3265
|
+
const row = selectedLibraryEntry(libraryCenter);
|
|
3266
|
+
if (!row)
|
|
3267
|
+
return;
|
|
3268
|
+
void libraryFacade().duplicates(row.id).then(scan => setLibraryCenter((current) => current ? setLibraryCenterDuplicateScan(current, scan) : current)).catch((error) => addLocal(screenErrorText(error, act('screen.action.libraryDuplicates'))));
|
|
3269
|
+
return;
|
|
3270
|
+
}
|
|
3271
|
+
if (ch === 'a') {
|
|
3272
|
+
const row = selectedLibraryEntry(libraryCenter);
|
|
3273
|
+
if (!row)
|
|
3274
|
+
return;
|
|
3275
|
+
void libraryFacade().prepareContextReference(row.id).then(reference => { setEditorText(attachLibraryContextToken(currentEditor().text, reference.token)); setLibraryCenter(null); }).catch((error) => addLocal(screenErrorText(error, act('screen.action.libraryAttach'))));
|
|
3276
|
+
return;
|
|
3277
|
+
}
|
|
3278
|
+
if (ch === 'd') {
|
|
3279
|
+
const row = selectedLibraryEntry(libraryCenter), token = row?.context.token;
|
|
3280
|
+
if (!token)
|
|
3281
|
+
return;
|
|
3282
|
+
setEditorText(detachLibraryContextToken(currentEditor().text, token));
|
|
3283
|
+
setLibraryCenter(null);
|
|
3284
|
+
return;
|
|
3285
|
+
}
|
|
3286
|
+
return;
|
|
3287
|
+
}
|
|
3288
|
+
if (tasksCenter) {
|
|
3289
|
+
if (key.ctrl && (ch === 'c' || ch === 'C')) {
|
|
3290
|
+
setTasksCenter(null);
|
|
3291
|
+
return;
|
|
3292
|
+
}
|
|
3293
|
+
if (key.escape || (ch === 'q' && !key.ctrl && !key.meta)) {
|
|
3294
|
+
setTasksCenter(null);
|
|
3295
|
+
return;
|
|
3296
|
+
}
|
|
3297
|
+
if (key.upArrow) {
|
|
3298
|
+
setTasksCenter((current) => current ? moveTasksCenterSelection(current, -1) : current);
|
|
3299
|
+
return;
|
|
3300
|
+
}
|
|
3301
|
+
if (key.downArrow) {
|
|
3302
|
+
setTasksCenter((current) => current ? moveTasksCenterSelection(current, 1) : current);
|
|
3303
|
+
return;
|
|
3304
|
+
}
|
|
3305
|
+
if (ch === 'r') {
|
|
3306
|
+
const row = selectedTask(tasksCenter);
|
|
3307
|
+
void loadTasksCenter(row?.id);
|
|
3308
|
+
return;
|
|
3309
|
+
}
|
|
3310
|
+
return;
|
|
3311
|
+
}
|
|
3312
|
+
if (notesCenter) {
|
|
3313
|
+
if (key.ctrl && (ch === 'c' || ch === 'C')) {
|
|
3314
|
+
if (state.running)
|
|
3315
|
+
cancelActiveRun();
|
|
3316
|
+
else
|
|
3317
|
+
setNotesCenter(null);
|
|
3318
|
+
return;
|
|
3319
|
+
}
|
|
3320
|
+
if (key.escape || (ch === 'q' && !key.ctrl && !key.meta)) {
|
|
3321
|
+
setNotesCenter(null);
|
|
3322
|
+
return;
|
|
3323
|
+
}
|
|
3324
|
+
if (key.upArrow) {
|
|
3325
|
+
setNotesCenter((current) => current ? moveNotesCenterSelection(current, -1) : current);
|
|
3326
|
+
return;
|
|
3327
|
+
}
|
|
3328
|
+
if (key.downArrow) {
|
|
3329
|
+
setNotesCenter((current) => current ? moveNotesCenterSelection(current, 1) : current);
|
|
3330
|
+
return;
|
|
3331
|
+
}
|
|
3332
|
+
if (ch === 'r') {
|
|
3333
|
+
const row = selectedNote(notesCenter);
|
|
3334
|
+
void loadNotesCenter(row?.id);
|
|
3335
|
+
return;
|
|
3336
|
+
}
|
|
3337
|
+
if (ch === 'c') {
|
|
3338
|
+
const row = selectedNote(notesCenter);
|
|
3339
|
+
if (!row)
|
|
3340
|
+
return;
|
|
3341
|
+
void notesFacade().composerReference(row.id).then((reference) => { const existing = currentEditor().text; setEditorText(existing.trim() ? existing + '\n\n' + reference : reference); setNotesCenter(null); }).catch((error) => addLocal(screenErrorText(error, act('screen.action.noteAttach'))));
|
|
3342
|
+
return;
|
|
3343
|
+
}
|
|
3344
|
+
return;
|
|
3345
|
+
}
|
|
3346
|
+
if (memoryCenter) {
|
|
3347
|
+
if (key.ctrl && (ch === 'c' || ch === 'C')) {
|
|
3348
|
+
if (state.running)
|
|
3349
|
+
cancelActiveRun();
|
|
3350
|
+
else
|
|
3351
|
+
setMemoryCenter(null);
|
|
3352
|
+
return;
|
|
3353
|
+
}
|
|
3354
|
+
if (key.escape || (ch === 'q' && !key.ctrl && !key.meta)) {
|
|
3355
|
+
setMemoryCenter(null);
|
|
3356
|
+
return;
|
|
3357
|
+
}
|
|
3358
|
+
if (key.upArrow) {
|
|
3359
|
+
setMemoryCenter((current) => current ? moveMemoryCenterSelection(current, -1) : current);
|
|
3360
|
+
return;
|
|
3361
|
+
}
|
|
3362
|
+
if (key.downArrow) {
|
|
3363
|
+
setMemoryCenter((current) => current ? moveMemoryCenterSelection(current, 1) : current);
|
|
3364
|
+
return;
|
|
3365
|
+
}
|
|
3366
|
+
if (ch === 'r') {
|
|
3367
|
+
const row = selectedMemory(memoryCenter);
|
|
3368
|
+
void loadMemoryCenter(undefined, row?.id);
|
|
3369
|
+
return;
|
|
3370
|
+
}
|
|
3371
|
+
return;
|
|
3372
|
+
}
|
|
3373
|
+
if (pluginCenter) {
|
|
3374
|
+
if (key.ctrl && (ch === 'c' || ch === 'C')) {
|
|
3375
|
+
if (state.running)
|
|
3376
|
+
cancelActiveRun();
|
|
3377
|
+
else
|
|
3378
|
+
setPluginCenter(null);
|
|
3379
|
+
return;
|
|
3380
|
+
}
|
|
3381
|
+
if (key.escape) {
|
|
3382
|
+
setPluginCenter(null);
|
|
3383
|
+
return;
|
|
3384
|
+
}
|
|
3385
|
+
if (key.upArrow) {
|
|
3386
|
+
setPluginCenter((current) => current ? movePluginCenterSelection(current, -1) : current);
|
|
3387
|
+
return;
|
|
3388
|
+
}
|
|
3389
|
+
if (key.downArrow) {
|
|
3390
|
+
setPluginCenter((current) => current ? movePluginCenterSelection(current, 1) : current);
|
|
3391
|
+
return;
|
|
3392
|
+
}
|
|
3393
|
+
if (ch === 't') {
|
|
3394
|
+
void actOnPluginCenter('trust');
|
|
3395
|
+
return;
|
|
3396
|
+
}
|
|
3397
|
+
if (ch === 'u') {
|
|
3398
|
+
void actOnPluginCenter('untrust');
|
|
3399
|
+
return;
|
|
3400
|
+
}
|
|
3401
|
+
if (ch === 'q') {
|
|
3402
|
+
void actOnPluginCenter('toggle-quarantine');
|
|
3403
|
+
return;
|
|
3404
|
+
}
|
|
3405
|
+
if (ch === 'r') {
|
|
3406
|
+
const row = selectedPlugin(pluginCenter);
|
|
3407
|
+
void loadPluginCenter(row?.id);
|
|
3408
|
+
return;
|
|
3409
|
+
}
|
|
3410
|
+
return;
|
|
3411
|
+
}
|
|
3412
|
+
if (hookCenter) {
|
|
3413
|
+
if (key.ctrl && (ch === 'c' || ch === 'C')) {
|
|
3414
|
+
if (state.running)
|
|
3415
|
+
cancelActiveRun();
|
|
3416
|
+
else
|
|
3417
|
+
setHookCenter(null);
|
|
3418
|
+
return;
|
|
3419
|
+
}
|
|
3420
|
+
if (key.escape) {
|
|
3421
|
+
setHookCenter(null);
|
|
3422
|
+
return;
|
|
3423
|
+
}
|
|
3424
|
+
if (key.upArrow) {
|
|
3425
|
+
setHookCenter((current) => current ? moveHookCenterSelection(current, -1) : current);
|
|
3426
|
+
return;
|
|
3427
|
+
}
|
|
3428
|
+
if (key.downArrow) {
|
|
3429
|
+
setHookCenter((current) => current ? moveHookCenterSelection(current, 1) : current);
|
|
3430
|
+
return;
|
|
3431
|
+
}
|
|
3432
|
+
if (ch === 'd') {
|
|
3433
|
+
void actOnHookCenter('dry-run');
|
|
3434
|
+
return;
|
|
3435
|
+
}
|
|
3436
|
+
if (ch === 't') {
|
|
3437
|
+
void actOnHookCenter('trust');
|
|
3438
|
+
return;
|
|
3439
|
+
}
|
|
3440
|
+
if (ch === 'u') {
|
|
3441
|
+
void actOnHookCenter('untrust');
|
|
3442
|
+
return;
|
|
3443
|
+
}
|
|
3444
|
+
if (ch === 'q') {
|
|
3445
|
+
void actOnHookCenter('toggle-quarantine');
|
|
3446
|
+
return;
|
|
3447
|
+
}
|
|
3448
|
+
if (ch === 'r') {
|
|
3449
|
+
const row = selectedHook(hookCenter);
|
|
3450
|
+
void loadHookCenter(row?.id);
|
|
3451
|
+
return;
|
|
3452
|
+
}
|
|
3453
|
+
return;
|
|
3454
|
+
}
|
|
3455
|
+
if (mcpCenter) {
|
|
3456
|
+
if (key.ctrl && (ch === 'c' || ch === 'C')) {
|
|
3457
|
+
if (state.running)
|
|
3458
|
+
cancelActiveRun();
|
|
3459
|
+
else
|
|
3460
|
+
setMcpCenter(null);
|
|
3461
|
+
return;
|
|
3462
|
+
}
|
|
3463
|
+
if (key.escape || (ch === 'q' && !key.ctrl && !key.meta)) {
|
|
3464
|
+
setMcpCenter(null);
|
|
3465
|
+
return;
|
|
3466
|
+
}
|
|
3467
|
+
if (key.upArrow) {
|
|
3468
|
+
setMcpCenter((current) => current ? moveMcpCenterSelection(current, -1) : current);
|
|
3469
|
+
return;
|
|
3470
|
+
}
|
|
3471
|
+
if (key.downArrow) {
|
|
3472
|
+
setMcpCenter((current) => current ? moveMcpCenterSelection(current, 1) : current);
|
|
3473
|
+
return;
|
|
3474
|
+
}
|
|
3475
|
+
if (key.return || ch === 'p') {
|
|
3476
|
+
void actOnMcpCenter('probe');
|
|
3477
|
+
return;
|
|
3478
|
+
}
|
|
3479
|
+
if (ch === 't') {
|
|
3480
|
+
void actOnMcpCenter('trust');
|
|
3481
|
+
return;
|
|
3482
|
+
}
|
|
3483
|
+
if (ch === 'u') {
|
|
3484
|
+
void actOnMcpCenter('untrust');
|
|
3485
|
+
return;
|
|
3486
|
+
}
|
|
3487
|
+
if (ch === 'r') {
|
|
3488
|
+
const row = selectedMcpServer(mcpCenter);
|
|
3489
|
+
void loadMcpCenter(row?.id);
|
|
3490
|
+
return;
|
|
3491
|
+
}
|
|
3492
|
+
return;
|
|
3493
|
+
}
|
|
3494
|
+
if (historyPicker) {
|
|
3495
|
+
const shown = filterHistory(history, historyPicker.query);
|
|
3496
|
+
if (key.escape || (key.ctrl && (ch === 'c' || ch === 'C'))) {
|
|
3497
|
+
setHistoryPicker(null);
|
|
3498
|
+
return;
|
|
3499
|
+
}
|
|
3500
|
+
if (key.return) {
|
|
3501
|
+
const chosen = shown[Math.min(historyPicker.selected, shown.length - 1)];
|
|
3502
|
+
setHistoryPicker(null);
|
|
3503
|
+
if (chosen)
|
|
3504
|
+
restoreDraft(chosen);
|
|
3505
|
+
return;
|
|
3506
|
+
}
|
|
3507
|
+
if (key.upArrow) {
|
|
3508
|
+
setHistoryPicker({ ...historyPicker, selected: Math.max(0, historyPicker.selected - 1) });
|
|
3509
|
+
return;
|
|
3510
|
+
}
|
|
3511
|
+
if (key.downArrow) {
|
|
3512
|
+
setHistoryPicker({ ...historyPicker, selected: Math.min(Math.max(0, shown.length - 1), historyPicker.selected + 1) });
|
|
3513
|
+
return;
|
|
3514
|
+
}
|
|
3515
|
+
if (key.backspace || key.delete) {
|
|
3516
|
+
setHistoryPicker({ query: historyPicker.query.slice(0, previousGraphemeBoundary(historyPicker.query, historyPicker.query.length)), selected: 0 });
|
|
3517
|
+
return;
|
|
3518
|
+
}
|
|
3519
|
+
if (ch && !key.ctrl && !key.meta && !/[\x00-\x1f\x7f]/u.test(ch)) {
|
|
3520
|
+
setHistoryPicker({ query: historyPicker.query + ch, selected: 0 });
|
|
3521
|
+
return;
|
|
3522
|
+
}
|
|
3523
|
+
return;
|
|
3524
|
+
}
|
|
3525
|
+
if (undoPicker) {
|
|
3526
|
+
if (key.ctrl && (ch === 'c' || ch === 'C')) {
|
|
3527
|
+
setUndoPicker(null);
|
|
3528
|
+
return;
|
|
3529
|
+
}
|
|
3530
|
+
if (undoPicker.confirm === null) {
|
|
3531
|
+
if (key.escape) {
|
|
3532
|
+
setUndoPicker(null);
|
|
3533
|
+
return;
|
|
3534
|
+
}
|
|
3535
|
+
if (key.upArrow) {
|
|
3536
|
+
setUndoPicker({ ...undoPicker, selected: Math.max(0, undoPicker.selected - 1) });
|
|
3537
|
+
return;
|
|
3538
|
+
}
|
|
3539
|
+
if (key.downArrow) {
|
|
3540
|
+
setUndoPicker({ ...undoPicker, selected: Math.min(undoPicker.rows.length - 1, undoPicker.selected + 1) });
|
|
3541
|
+
return;
|
|
3542
|
+
}
|
|
3543
|
+
if (key.return) {
|
|
3544
|
+
setUndoPicker({ ...undoPicker, confirm: 0 });
|
|
3545
|
+
return;
|
|
3546
|
+
}
|
|
3547
|
+
return;
|
|
3548
|
+
}
|
|
3549
|
+
if (key.escape) {
|
|
3550
|
+
setUndoPicker({ ...undoPicker, confirm: null });
|
|
3551
|
+
return;
|
|
3552
|
+
}
|
|
3553
|
+
if (key.upArrow || key.leftArrow) {
|
|
3554
|
+
setUndoPicker({ ...undoPicker, confirm: 0 });
|
|
3555
|
+
return;
|
|
3556
|
+
}
|
|
3557
|
+
if (key.downArrow || key.rightArrow) {
|
|
3558
|
+
setUndoPicker({ ...undoPicker, confirm: 1 });
|
|
3559
|
+
return;
|
|
3560
|
+
}
|
|
3561
|
+
if (key.return) {
|
|
3562
|
+
const picked = undoPicker;
|
|
3563
|
+
setUndoPicker(null);
|
|
3564
|
+
if (picked.confirm === 1)
|
|
3565
|
+
void runUndo(picked);
|
|
3566
|
+
return;
|
|
3567
|
+
}
|
|
3568
|
+
return;
|
|
3569
|
+
}
|
|
3570
|
+
if (themePicker) {
|
|
3571
|
+
const move = (selected) => { const next = Math.max(0, Math.min(THEME_CHOICES.length - 1, selected)); setThemePicker({ ...themePicker, selected: next }); themeStore.set(THEME_CHOICES[next]); };
|
|
3572
|
+
if (key.escape || (key.ctrl && (ch === 'c' || ch === 'C'))) {
|
|
3573
|
+
themeStore.set(themePicker.original);
|
|
3574
|
+
setThemePicker(null);
|
|
3575
|
+
return;
|
|
3576
|
+
}
|
|
3577
|
+
if (key.return) {
|
|
3578
|
+
setThemePicker(null);
|
|
3579
|
+
keepTheme(THEME_CHOICES[themePicker.selected]);
|
|
3580
|
+
return;
|
|
3581
|
+
}
|
|
3582
|
+
if (key.upArrow) {
|
|
3583
|
+
move(themePicker.selected - 1);
|
|
3584
|
+
return;
|
|
3585
|
+
}
|
|
3586
|
+
if (key.downArrow) {
|
|
3587
|
+
move(themePicker.selected + 1);
|
|
3588
|
+
return;
|
|
3589
|
+
}
|
|
3590
|
+
if (key.home) {
|
|
3591
|
+
move(0);
|
|
3592
|
+
return;
|
|
3593
|
+
}
|
|
3594
|
+
if (key.end) {
|
|
3595
|
+
move(THEME_CHOICES.length - 1);
|
|
3596
|
+
return;
|
|
3597
|
+
}
|
|
3598
|
+
return;
|
|
3599
|
+
}
|
|
3600
|
+
if (contextInspector) {
|
|
3601
|
+
if (key.ctrl && (ch === 'c' || ch === 'C')) {
|
|
3602
|
+
applyInterrupt(interruptRef.current.ctrlC(state.running), 'ctrl-c');
|
|
3603
|
+
return;
|
|
3604
|
+
}
|
|
3605
|
+
if (key.escape || key.return || (ch === 'q' && !key.ctrl && !key.meta)) {
|
|
3606
|
+
setContextInspector(null);
|
|
3607
|
+
return;
|
|
3608
|
+
}
|
|
3609
|
+
return;
|
|
3610
|
+
}
|
|
3611
|
+
if (agentTreeOverlay) {
|
|
3612
|
+
if (key.ctrl && (ch === 'c' || ch === 'C')) {
|
|
3613
|
+
if (state.running)
|
|
3614
|
+
cancelActiveRun();
|
|
3615
|
+
else
|
|
3616
|
+
setAgentTreeOverlay(null);
|
|
3617
|
+
return;
|
|
3618
|
+
}
|
|
3619
|
+
if (key.escape || (ch === 'q' && !key.ctrl && !key.meta)) {
|
|
3620
|
+
setAgentTreeOverlay(null);
|
|
3621
|
+
return;
|
|
3622
|
+
}
|
|
3623
|
+
/* P13: Enter watches the selected agent live; on the session in use it goes back to it. */
|
|
3624
|
+
if (key.return) {
|
|
3625
|
+
const row = selectedAgentRow(agentTreeOverlay);
|
|
3626
|
+
setAgentTreeOverlay(null);
|
|
3627
|
+
if (!row || row.id === agentView?.sessionId)
|
|
3628
|
+
return;
|
|
3629
|
+
if (row.id === controller.current())
|
|
3630
|
+
closeAgentView();
|
|
3631
|
+
else
|
|
3632
|
+
openAgentView(row);
|
|
3633
|
+
return;
|
|
3634
|
+
}
|
|
3635
|
+
if (key.upArrow) {
|
|
3636
|
+
setAgentTreeOverlay((current) => current ? moveAgentTreeSelection(current, -1) : current);
|
|
3637
|
+
return;
|
|
3638
|
+
}
|
|
3639
|
+
if (key.downArrow) {
|
|
3640
|
+
setAgentTreeOverlay((current) => current ? moveAgentTreeSelection(current, 1) : current);
|
|
3641
|
+
return;
|
|
3642
|
+
}
|
|
3643
|
+
if (key.pageUp || key.pageup) {
|
|
3644
|
+
setAgentTreeOverlay((current) => current ? moveAgentTreeSelection(current, -5) : current);
|
|
3645
|
+
return;
|
|
3646
|
+
}
|
|
3647
|
+
if (key.pageDown || key.pagedown) {
|
|
3648
|
+
setAgentTreeOverlay((current) => current ? moveAgentTreeSelection(current, 5) : current);
|
|
3649
|
+
return;
|
|
3650
|
+
}
|
|
3651
|
+
if (key.home) {
|
|
3652
|
+
setAgentTreeOverlay((current) => current ? { ...current, selected: 0 } : current);
|
|
3653
|
+
return;
|
|
3654
|
+
}
|
|
3655
|
+
if (key.end) {
|
|
3656
|
+
setAgentTreeOverlay((current) => current ? { ...current, selected: Math.max(0, current.rows.length - 1) } : current);
|
|
3657
|
+
return;
|
|
3658
|
+
}
|
|
3659
|
+
return;
|
|
3660
|
+
}
|
|
3661
|
+
if (queueEditor) {
|
|
3662
|
+
const entries = controller.queueEntries(), selected = queueEditorSelected(entries, queueEditor.selected);
|
|
3663
|
+
if (queueEditor.editing) {
|
|
3664
|
+
if (key.escape) {
|
|
3665
|
+
setQueueEditor({ ...queueEditor, editing: false, draft: '' });
|
|
3666
|
+
return;
|
|
3667
|
+
}
|
|
3668
|
+
if (key.return) {
|
|
3669
|
+
if (selected)
|
|
3670
|
+
void controller.editQueue(selected.id, queueEditor.draft).then(() => setQueueEditor((current) => current ? { ...current, editing: false, draft: '' } : current)).catch((error) => addLocal(screenErrorText(error, act('screen.action.queueEdit'))));
|
|
3671
|
+
return;
|
|
3672
|
+
}
|
|
3673
|
+
if (key.backspace) {
|
|
3674
|
+
setQueueEditor({ ...queueEditor, draft: queueEditor.draft.slice(0, previousGraphemeBoundary(queueEditor.draft, queueEditor.draft.length)) });
|
|
3675
|
+
return;
|
|
3676
|
+
}
|
|
3677
|
+
if (ch && !key.ctrl && !key.meta) {
|
|
3678
|
+
setQueueEditor({ ...queueEditor, draft: queueEditor.draft + ch });
|
|
3679
|
+
return;
|
|
3680
|
+
}
|
|
3681
|
+
}
|
|
3682
|
+
else {
|
|
3683
|
+
if (key.escape || (ch === 'q' && !key.ctrl && !key.meta)) {
|
|
3684
|
+
setQueueEditor(null);
|
|
3685
|
+
return;
|
|
3686
|
+
}
|
|
3687
|
+
if (key.shift && key.upArrow && selected) {
|
|
3688
|
+
void controller.moveQueue(selected.id, -1).then(() => setQueueEditor((current) => current ? { ...current, selected: Math.max(0, current.selected - 1) } : current)).catch((error) => addLocal(screenErrorText(error, act('screen.action.queueMove'))));
|
|
3689
|
+
return;
|
|
3690
|
+
}
|
|
3691
|
+
if (key.shift && key.downArrow && selected) {
|
|
3692
|
+
void controller.moveQueue(selected.id, 1).then(() => setQueueEditor((current) => current ? { ...current, selected: Math.min(Math.max(0, controller.queueEntries().length - 1), current.selected + 1) } : current)).catch((error) => addLocal(screenErrorText(error, act('screen.action.queueMove'))));
|
|
3693
|
+
return;
|
|
3694
|
+
}
|
|
3695
|
+
if (key.upArrow) {
|
|
3696
|
+
setQueueEditor({ ...queueEditor, selected: Math.max(0, queueEditor.selected - 1) });
|
|
3697
|
+
return;
|
|
3698
|
+
}
|
|
3699
|
+
if (key.downArrow) {
|
|
3700
|
+
setQueueEditor({ ...queueEditor, selected: Math.min(Math.max(0, entries.length - 1), queueEditor.selected + 1) });
|
|
3701
|
+
return;
|
|
3702
|
+
}
|
|
3703
|
+
if (ch === 'e' && selected) {
|
|
3704
|
+
setQueueEditor({ ...queueEditor, editing: true, draft: selected.text });
|
|
3705
|
+
return;
|
|
3706
|
+
}
|
|
3707
|
+
if (ch === 'd' && selected) {
|
|
3708
|
+
void controller.deleteQueue(selected.id).then(() => setQueueEditor((current) => { const count = controller.queueEntries().length; if (!count)
|
|
3709
|
+
return null; return current ? { ...current, selected: Math.min(current.selected, count - 1) } : current; })).catch((error) => addLocal(screenErrorText(error, act('screen.action.queueDelete'))));
|
|
3710
|
+
return;
|
|
3711
|
+
}
|
|
3712
|
+
if (ch === 'c') {
|
|
3713
|
+
void controller.clearQueue().then(() => setQueueEditor(null)).catch((error) => addLocal(screenErrorText(error, act('screen.action.queueClear'))));
|
|
3714
|
+
return;
|
|
3715
|
+
}
|
|
3716
|
+
if (ch === 'r') {
|
|
3717
|
+
if (state.running) {
|
|
3718
|
+
addLocal(t('screen.queue.resumeBusy'));
|
|
3719
|
+
return;
|
|
3720
|
+
}
|
|
3721
|
+
void controller.dispatchQueue().then(() => { if (!controller.queue().length)
|
|
3722
|
+
setQueueEditor(null); }).catch((error) => addLocal(screenErrorText(error, act('screen.action.queueResume'))));
|
|
3723
|
+
return;
|
|
3724
|
+
}
|
|
3725
|
+
}
|
|
3726
|
+
}
|
|
3727
|
+
if (transcriptSearch) {
|
|
3728
|
+
const items = transcriptItemsForCurrentView();
|
|
3729
|
+
const matches = transcriptSearchMatches(items, transcriptSearch.query, transcriptSearch.mode);
|
|
3730
|
+
const selected = selectedTranscriptMatch(matches, transcriptSearch.selectedId ?? transcriptSelectedId);
|
|
3731
|
+
const applySelection = (itemId) => { setTranscriptSelectedId(itemId); setTranscriptSearch((current) => current ? { ...current, selectedId: itemId, notice: null } : current); if (itemId)
|
|
3732
|
+
jumpTranscriptItem(itemId); };
|
|
3733
|
+
if (key.escape) {
|
|
3734
|
+
setTranscriptSearch(null);
|
|
3735
|
+
return;
|
|
3736
|
+
}
|
|
3737
|
+
/* R4 Q-R4-7: Ctrl+C closes the search like any dialog, before any exit is armed. */
|
|
3738
|
+
if (key.ctrl && (ch === 'c' || ch === 'C')) {
|
|
3739
|
+
applyInterrupt(interruptRef.current.ctrlC(state.running), 'ctrl-c');
|
|
3740
|
+
return;
|
|
3741
|
+
}
|
|
3742
|
+
if (key.tab) {
|
|
3743
|
+
const mode = transcriptSearch.mode === 'literal' ? 'fuzzy' : 'literal';
|
|
3744
|
+
const nextMatches = transcriptSearchMatches(items, transcriptSearch.query, mode);
|
|
3745
|
+
const itemId = selectedTranscriptMatch(nextMatches, transcriptSearch.selectedId ?? transcriptSelectedId)?.itemId ?? null;
|
|
3746
|
+
setTranscriptSelectedId(itemId);
|
|
3747
|
+
setTranscriptSearch({ ...transcriptSearch, mode, selectedId: itemId, notice: null });
|
|
3748
|
+
if (itemId)
|
|
3749
|
+
jumpTranscriptItem(itemId);
|
|
3750
|
+
return;
|
|
3751
|
+
}
|
|
3752
|
+
if (key.meta && ch.toLowerCase() === 'r') {
|
|
3753
|
+
const raw = !transcriptRaw;
|
|
3754
|
+
setTranscriptRaw(raw);
|
|
3755
|
+
setTranscriptSearch({ ...transcriptSearch, raw, notice: null });
|
|
3756
|
+
return;
|
|
3757
|
+
}
|
|
3758
|
+
if (key.meta && ch.toLowerCase() === 'c') {
|
|
3759
|
+
copyTranscriptItem(selected?.itemId ?? null);
|
|
3760
|
+
return;
|
|
3761
|
+
}
|
|
3762
|
+
if (key.upArrow || key.downArrow) {
|
|
3763
|
+
const itemId = moveTranscriptSearchSelection(matches, selected?.itemId ?? null, key.downArrow ? 1 : -1);
|
|
3764
|
+
applySelection(itemId);
|
|
3765
|
+
return;
|
|
3766
|
+
}
|
|
3767
|
+
if (key.return) {
|
|
3768
|
+
if (selected)
|
|
3769
|
+
jumpTranscriptItem(selected.itemId);
|
|
3770
|
+
setTranscriptSearch(null);
|
|
3771
|
+
return;
|
|
3772
|
+
}
|
|
3773
|
+
if (key.backspace) {
|
|
3774
|
+
const query = transcriptSearch.query.slice(0, previousGraphemeBoundary(transcriptSearch.query, transcriptSearch.query.length));
|
|
3775
|
+
const nextMatches = transcriptSearchMatches(items, query, transcriptSearch.mode), itemId = selectedTranscriptMatch(nextMatches, transcriptSearch.selectedId)?.itemId ?? null;
|
|
3776
|
+
setTranscriptSelectedId(itemId);
|
|
3777
|
+
setTranscriptSearch({ ...transcriptSearch, query, selectedId: itemId, notice: null });
|
|
3778
|
+
if (itemId)
|
|
3779
|
+
jumpTranscriptItem(itemId);
|
|
3780
|
+
return;
|
|
3781
|
+
}
|
|
3782
|
+
if (ch && !key.ctrl && !key.meta) {
|
|
3783
|
+
const query = transcriptSearch.query + ch, nextMatches = transcriptSearchMatches(items, query, transcriptSearch.mode), itemId = selectedTranscriptMatch(nextMatches, transcriptSearch.selectedId)?.itemId ?? null;
|
|
3784
|
+
setTranscriptSelectedId(itemId);
|
|
3785
|
+
setTranscriptSearch({ ...transcriptSearch, query, selectedId: itemId, notice: null });
|
|
3786
|
+
if (itemId)
|
|
3787
|
+
jumpTranscriptItem(itemId);
|
|
3788
|
+
return;
|
|
3789
|
+
}
|
|
3790
|
+
return;
|
|
3791
|
+
}
|
|
3792
|
+
/* P13 (owner decisions 2 and 7): watching an agent is read-only. Esc goes back to the main session and does NOT
|
|
3793
|
+
interrupt the turn (Claude Code fixed exactly that); Ctrl+C still stops the whole turn; the agent tree, scrolling,
|
|
3794
|
+
redraw, reasoning and tool details still work; typing does nothing. */
|
|
3795
|
+
if (agentView) {
|
|
3796
|
+
if (key.escape) {
|
|
3797
|
+
closeAgentView();
|
|
3798
|
+
return;
|
|
3799
|
+
}
|
|
3800
|
+
const viewAction = resolveKeybinding(ch, key, ['global', 'transcript', 'composer'], effectiveKeymap);
|
|
3801
|
+
if (viewAction && AGENT_VIEW_ACTIONS.has(viewAction)) {
|
|
3802
|
+
handleSemanticAction(viewAction, key);
|
|
3803
|
+
return;
|
|
3804
|
+
}
|
|
3805
|
+
return;
|
|
3806
|
+
}
|
|
3807
|
+
const vimOwnsInput = boot.phase === 'ready' && !trustCenter && !mcpCenter && !hookCenter && !pluginCenter && !memoryCenter && !notesCenter && !tasksCenter && !libraryCenter && !researchCenter && !automationCenter && !forgeCenter && focusNow === 'composer' && vim.enabled && (vim.mode === 'normal' || Boolean(key.escape));
|
|
3808
|
+
if (vimOwnsInput) {
|
|
3809
|
+
const vimResult = handleVimInput({ state: vim, editor: currentEditor(), ch, key });
|
|
3810
|
+
if (vimResult.handled) {
|
|
3811
|
+
setVim(vimResult.state);
|
|
3812
|
+
if (vimResult.editor !== currentEditor())
|
|
3813
|
+
updateEditor(() => vimResult.editor);
|
|
3814
|
+
setAuxCompletions([]);
|
|
3815
|
+
return;
|
|
3816
|
+
}
|
|
3817
|
+
}
|
|
3818
|
+
const decision = decideShellInput({
|
|
3819
|
+
ch, key, focus: focusNow, composerText: currentEditor().text, commandMenuOpen: focusNow === 'command-menu',
|
|
3820
|
+
trustCenterOpen: Boolean(trustCenter), approvalExpanded, boot, keymap: effectiveKeymap,
|
|
3821
|
+
});
|
|
3822
|
+
if (decision.closeTrustCenter) {
|
|
3823
|
+
setTrustCenter(null);
|
|
3824
|
+
return;
|
|
3825
|
+
}
|
|
3826
|
+
if (decision.approvalScrollDelta !== null) {
|
|
3827
|
+
const delta = decision.approvalScrollDelta;
|
|
3828
|
+
setApprovalScroll((value) => Math.max(0, Math.min(approvalMaxOffsetRef.current, value + delta)));
|
|
3829
|
+
return;
|
|
3830
|
+
}
|
|
3831
|
+
if (decision.skipBoot)
|
|
3832
|
+
setBoot((current) => skipBoot(current));
|
|
3833
|
+
const routed = decision.routed;
|
|
3834
|
+
if (!routed)
|
|
3835
|
+
return;
|
|
3836
|
+
if (routed.kind === 'action') {
|
|
3837
|
+
handleSemanticAction(routed.action, key);
|
|
3838
|
+
return;
|
|
3839
|
+
}
|
|
3840
|
+
if (routed.kind === 'text') {
|
|
3841
|
+
if (focusNow === 'provider-picker' && providerStep.kind === 'key') {
|
|
3842
|
+
if (!providerStep.testing) {
|
|
3843
|
+
if (providerStep.result)
|
|
3844
|
+
setProviderStep({ ...providerStep, result: null });
|
|
3845
|
+
setProviderSecret((value) => value + routed.text);
|
|
3846
|
+
}
|
|
3847
|
+
return;
|
|
3848
|
+
}
|
|
3849
|
+
if (focusNow === 'provider-picker' && providerStep.kind === 'login-choice')
|
|
3850
|
+
return;
|
|
3851
|
+
if (focusNow === 'provider-picker' && providerStep.kind === 'oauth') {
|
|
3852
|
+
if (providerStep.phase === 'waiting') {
|
|
3853
|
+
if (routed.text.trim().toLowerCase() === 'c')
|
|
3854
|
+
setProviderStep({ ...providerStep, phase: 'paste', message: null });
|
|
3855
|
+
return;
|
|
3856
|
+
}
|
|
3857
|
+
if (providerStep.phase === 'paste') {
|
|
3858
|
+
setProviderSecret((value) => value + routed.text);
|
|
3859
|
+
return;
|
|
3860
|
+
}
|
|
3861
|
+
return;
|
|
3862
|
+
}
|
|
3863
|
+
if (focusNow === 'provider-picker' && providerStep.kind === 'consent') {
|
|
3864
|
+
const letter = routed.text.trim().toLowerCase();
|
|
3865
|
+
if (letter === 'y' || letter === 'n')
|
|
3866
|
+
setProviderStep({ ...providerStep, choice: letter === 'y' ? 'yes' : 'no' });
|
|
3867
|
+
return;
|
|
3868
|
+
}
|
|
3869
|
+
if (focusNow === 'model-picker' && modelEffortStep)
|
|
3870
|
+
return;
|
|
3871
|
+
if (focusNow === 'model-picker' && setupRef.current && modelList.engineDown && !pickerQuery && (routed.text === 'r' || routed.text === 'R') && appState.overlay?.kind === 'model') {
|
|
3872
|
+
void loadModels(appState.overlay.provider);
|
|
3873
|
+
return;
|
|
3874
|
+
}
|
|
3875
|
+
if (focusNow === 'model-picker' || focusNow === 'provider-picker' || focusNow === 'session-picker') {
|
|
3876
|
+
setPickerQuery((value) => value + routed.text);
|
|
3877
|
+
setPickerSelection((current) => ({ ...current, selected: 0 }));
|
|
3878
|
+
return;
|
|
3879
|
+
}
|
|
3880
|
+
if (focusNow === 'command-menu') {
|
|
3881
|
+
updateEditor(editor => editInsert(editor, routed.text));
|
|
3882
|
+
commandMenuSelectionStore.reset();
|
|
3883
|
+
setPickerSelection((current) => ({ ...current, selected: 0 }));
|
|
3884
|
+
return;
|
|
3885
|
+
}
|
|
3886
|
+
setAuxCompletions([]);
|
|
3887
|
+
updateEditor(editor => routed.text.length > 1 || routed.text.includes('\n') ? insertPaste(editor, routed.text) : editInsert(editor, routed.text));
|
|
3888
|
+
}
|
|
3889
|
+
});
|
|
3890
|
+
};
|
|
3891
|
+
const terminalRows = terminalSize?.rows ?? Number(stdout?.rows ?? process.stdout.rows ?? 30), terminalColumns = terminalSize?.columns ?? Number(stdout?.columns ?? process.stdout.columns ?? 100);
|
|
3892
|
+
/* R5a S1 (gate finding 1): the turn's usage AND the context status go to lane R's snapshot; neither hides the other. */
|
|
3893
|
+
const facts = React.useMemo(() => usageFacts({ model: state.status.model, usage: state.usage, contextStatus, compactions: state.compactions }), [state.status.model, state.usage, contextStatus, state.compactions]);
|
|
3894
|
+
/* R5a (owner 2026-09-24): a window below 64k keeps the simple mode; said once per model. */
|
|
3895
|
+
const smallWindowSaidRef = React.useRef(null);
|
|
3896
|
+
React.useEffect(() => { const text = smallWindowNotice(facts.profile, state.status.model); if (text && smallWindowSaidRef.current !== state.status.model) {
|
|
3897
|
+
smallWindowSaidRef.current = state.status.model;
|
|
3898
|
+
addLocal(text);
|
|
3899
|
+
} }, [facts.profile, state.status.model]);
|
|
3900
|
+
/* R3 Q-R3-13 (R0 decisions 1 and 4): `Full access` and `No sandbox` lead the status row and no width drops them. */
|
|
3901
|
+
const layout = deriveShellLayout({
|
|
3902
|
+
state, appState, viewport, terminalRows, terminalColumns, projectRoot, queuedActions, busySince, bullet: theme.glyphs.bullet, branch: theme.glyphs.branch,
|
|
3903
|
+
snapshot: facts.snapshot, safety: { fullAccess: state.status.permissionMode === 'bypassPermissions', noSandbox: sandboxVerified === false }, busyInput: prefs.busyInput, exitHint,
|
|
3904
|
+
});
|
|
3905
|
+
const { columns, activity, shortcutLine, rosterLine, queueLine, footerLine, statusBar: statusRow } = layout;
|
|
3906
|
+
/* R3 Q-R3-9: no `Run · …` rows; R5a S8: a collapsed reasoning block is drawn once it has ended (thinking-row.ts). */
|
|
3907
|
+
/* P13: the watched agent's transcript takes the main view; the main session's is kept as it was. */
|
|
3908
|
+
const shownTranscriptItems = (agentView ? agentView.transcript.items : state.transcript.items);
|
|
3909
|
+
/* In an agent's transcript its prompt is the task the main session gave it, not the person's words, and the answers are
|
|
3910
|
+
the agent's: "You" and "TALOS" would say otherwise. */
|
|
3911
|
+
const watchingAgent = Boolean(agentView);
|
|
3912
|
+
const visibleTranscriptItems = React.useMemo(() => visibleItemsOf(shownTranscriptItems, appState.reasoningVisible), [shownTranscriptItems, appState.reasoningVisible]);
|
|
3913
|
+
const renderTranscriptItem = React.useCallback((item) => {
|
|
3914
|
+
const selected = item.id === transcriptSelectedId, prefix = selected ? '› ' : '';
|
|
3915
|
+
if (transcriptRaw)
|
|
3916
|
+
return h(Ink.Text, { key: item.id, inverse: selected, dimColor: !selected }, prefix + transcriptItemRaw(item));
|
|
3917
|
+
if (item.kind === 'message')
|
|
3918
|
+
return h(Ink.Box, { key: item.id, flexDirection: 'column' }, h(Ink.Text, { bold: item.role === 'user', color: transcriptColor, inverse: selected }, prefix + t(item.role === 'user' ? (watchingAgent ? 'screen.agentView.task' : 'screen.transcript.you') : (watchingAgent ? 'screen.agentView.agent' : 'screen.transcript.assistant'))), h(MarkdownView, { text: item.text, width: columns }));
|
|
3919
|
+
if (item.kind === 'reasoning')
|
|
3920
|
+
return appState.reasoningVisible ? h(Ink.Box, { key: item.id, borderStyle: 'single', flexDirection: 'column' }, h(Ink.Text, { dimColor: true, inverse: selected }, prefix + t('screen.transcript.reasoning')), h(Ink.Text, { dimColor: true }, item.text || ' ')) : h(Ink.Text, { key: item.id, dimColor: true, inverse: selected }, prefix + thoughtLine(item));
|
|
3921
|
+
/* The selected row carries a two-cell prefix: its header is drawn two cells narrower so it never wraps (lane RD). */
|
|
3922
|
+
if (item.kind === 'tool') {
|
|
3923
|
+
const rendered = toolRowWithClock(item, selected ? Math.max(1, columns - 2) : columns, appState.expandedTools, toolNow);
|
|
3924
|
+
return h(Ink.Box, { key: item.id, flexDirection: 'column' }, h(Ink.Text, { color: toolColor, inverse: selected }, prefix + rendered.title + ' · ' + rendered.statusLabel), ...rendered.detailLines.map((line, index) => h(Ink.Text, { key: index, dimColor: true }, line)));
|
|
3925
|
+
}
|
|
3926
|
+
if (item.kind === 'warning')
|
|
3927
|
+
return h(Ink.Text, { key: item.id, dimColor: true, inverse: selected }, prefix + item.text);
|
|
3928
|
+
return null;
|
|
3929
|
+
}, [transcriptSelectedId, transcriptRaw, columns, appState.expandedTools, appState.reasoningVisible, toolNow, watchingAgent]);
|
|
3930
|
+
const diffView = diff ? renderDiffModel(parseUnifiedDiff(diff), columns) : null;
|
|
3931
|
+
const vimHint = vimModeHint(vim);
|
|
3932
|
+
React.useEffect(() => {
|
|
3933
|
+
if (!capabilities.motion || boot.phase !== 'ready' || !isAnimatedActivity(activity.kind))
|
|
3934
|
+
return;
|
|
3935
|
+
return coordinator.startMotion(() => setMotionNow(Date.now()));
|
|
3936
|
+
}, [coordinator, capabilities.motion, boot.phase, activity.kind]);
|
|
3937
|
+
/* Lane RD contract: running rows show their elapsed seconds; a clock, not an animation, so it ticks with motion off.
|
|
3938
|
+
R5a S4: the same clock counts a known retry wait down, and stops when the wait is over (nothing ticks for nothing). */
|
|
3939
|
+
React.useEffect(() => {
|
|
3940
|
+
const until = activity.kind === 'tool' ? Infinity : activity.kind === 'retrying' ? retryCountdownEnd(state.retry) : null;
|
|
3941
|
+
if (boot.phase !== 'ready' || until === null)
|
|
3942
|
+
return;
|
|
3943
|
+
setToolNow(Date.now());
|
|
3944
|
+
const timer = setInterval(() => { const now = Date.now(); coordinator.enqueueEvent(`tool-clock-${++eventSequenceRef.current}`, () => setToolNow(now)); if (now >= until)
|
|
3945
|
+
clearInterval(timer); }, 1_000);
|
|
3946
|
+
return () => clearInterval(timer);
|
|
3947
|
+
}, [coordinator, boot.phase, activity.kind, state.retry]);
|
|
3948
|
+
/*
|
|
3949
|
+
* R3 Q-R3-13 (R0 decision 4, owner 2026-09-24): the check starts at STARTUP, in the background, after the first frame,
|
|
3950
|
+
* so neither the first frame nor a key waits for it; the label appears when the verdict arrives (a cached verdict:
|
|
3951
|
+
* at once). Quitting aborts a check still running (main.ts, abortIsolationChecks): it ends what it started and
|
|
3952
|
+
* answers nothing. Asked once.
|
|
3953
|
+
*/
|
|
3954
|
+
React.useEffect(() => { const timer = setTimeout(() => askSandboxRef.current(), 0); return () => clearTimeout(timer); }, []);
|
|
3955
|
+
const statusStartedAt = activity.startedAt ?? busySince;
|
|
3956
|
+
const statusElapsed = typeof activity.frozenMs === 'number' ? activity.frozenMs : Math.max(0, motionNow - statusStartedAt);
|
|
3957
|
+
const statusFrame = frameIndex(statusElapsed, MOTION_FRAME_MS, theme.glyphs.spinner.length);
|
|
3958
|
+
const statusText = exitHint ? exitHintText(exitHintKey) : busyIndicatorText({ kind: activity.kind, elapsedMs: statusElapsed, frame: statusFrame, unicode: capabilities.unicode, frames: theme.glyphs.spinner, ...(activity.tool ? { tool: activity.tool } : {}), ...(activity.agents ? { agents: activity.agents } : {}), retry: state.retry, nowMs: Math.max(toolNow, state.retry?.at ?? 0) });
|
|
3959
|
+
/* R3 gate E7: the main roster row says what the CLI knows while a turn is open (the kernel list says `unknown` or `running`),
|
|
3960
|
+
and after the turn done / interrupted / failed like the status row (owner decision, R3 follow-up: one vocabulary). */
|
|
3961
|
+
const turnOpen = state.running || typeof state.sentAt === 'number';
|
|
3962
|
+
const mainRowStatus = turnOpen ? activityWord(activity.kind, activity.tool, activity.agents) : state.lastTurn ? activityWord(state.lastTurn.outcome) : undefined;
|
|
3963
|
+
/*
|
|
3964
|
+
* R1 gate E1: one height budget per frame. The composer's row count comes from its store as a primitive, so the
|
|
3965
|
+
* fast input path still re-renders only the composer on a key and the whole screen only when the composer gains or
|
|
3966
|
+
* loses a row. `frameRowsFor` is the same switch run.ts uses for the root frame (Q-R1-6).
|
|
3967
|
+
*/
|
|
3968
|
+
const composerSize = React.useSyncExternalStore(composerStore.subscribe, () => { const measured = composerMeasure(composerStore.getSnapshot(), columns, vimHint); return `${measured.lines}:${measured.hints}`; }, () => '1:0');
|
|
3969
|
+
const [composerLines, composerHints] = composerSize.split(':').map(Number);
|
|
3970
|
+
const reasoningLine = (currentModelMeta?.reasoning === true || reasoningEffort !== null) ? t('screen.reasoning.line', { effort: reasoningEffort ?? t('screen.effort.modelDefault'), usage: reasoningUsageLabel(state.usage) }) : null;
|
|
3971
|
+
const rosterLines = agentRows.length
|
|
3972
|
+
? agentRows.map((row) => h(Ink.Text, { key: `agent-roster-${row.id}`, color: row.hasCollision ? approvalColor : footerColor, bold: row.hasCollision, wrap: 'truncate-end' }, agentRosterLine({ row, width: columns, nowMs: motionNow, hideModel: row.role === 'main' && row.id === state.sessionId, ...(mainRowStatus && row.role === 'main' && row.id === state.sessionId ? { status: mainRowStatus } : {}) })))
|
|
3973
|
+
: [h(Ink.Text, { key: 'agent-roster-main', color: footerColor, wrap: 'truncate-end' }, rosterLine)];
|
|
3974
|
+
const budgetInput = { rows: terminalRows, columns: terminalColumns, frameRows: frameRowsFor(terminalRows), composerLines, composerHints, header: 1, shortcut: 1, roster: rosterLines.length, extras: (queueLine ? 1 : 0) + (reasoningLine ? 1 : 0), footer: 1, status: layout.showActivity ? 1 : 0 };
|
|
3975
|
+
/* The most rows an overlay, menu or diff may take (contract with lane O: `maxRows`), known before any is built. */
|
|
3976
|
+
const maxRows = layoutBudget(budgetInput).overlayMaxRows;
|
|
3977
|
+
const pickerRowsLimit = Math.max(1, Math.min(12, maxRows - 5));
|
|
3978
|
+
/* The command menu's height follows its query, which the fast input path writes to the stores without a root render:
|
|
3979
|
+
the root reads the fitted height as a primitive, so it re-renders only when the menu gains or loses a row. */
|
|
3980
|
+
const menuOpen = appState.focus.current === 'command-menu';
|
|
3981
|
+
const menuRows = React.useSyncExternalStore(composerStore.subscribe, () => { if (!menuOpen)
|
|
3982
|
+
return 0; const text = composerStore.getSnapshot().text; const fitted = fitOverlay(commandMenuOverlay({ text, selected: commandMenuHighlight(text, commandMenuSelectionStore.getSnapshot()) }), { maxRows, columns }); return fitted.rows.length + (fitted.border ? 2 : 0); }, () => 0);
|
|
3983
|
+
if (terminalColumns < MIN_COLUMNS || terminalRows < MIN_ROWS)
|
|
3984
|
+
return h(Regions.TooSmall, { key: 'too-small', columns: terminalColumns });
|
|
3985
|
+
if (boot.phase !== 'ready' && boot.phase !== 'error')
|
|
3986
|
+
return h(Regions.Region, { key: `boot-${appState.redrawNonce}`, rows: frameRowsFor(terminalRows), paddingTop: 1 }, h(BootSequence, { state: boot, mode: bootPlanRef.current.mode, hint: t('firstrun.boot.skipHint'), logo: shouldRenderBootLogo(capabilities), motion: capabilities.motion, unicode: capabilities.unicode, width: columns, accentColor: bootColor, mutedColor: footerColor, onComplete: () => setBoot((current) => completeBoot(current, Date.now())) }), h(Ink.Text, { dimColor: true, color: footerColor, wrap: 'truncate-end' }, projectRoot));
|
|
3987
|
+
const renderPickerRows = (rowsToRender, selected, label) => { const view = pickerWindow(rowsToRender, selected, pickerRowsLimit); return view.rows.map((row, index) => { const absolute = view.start + index; return h(Ink.Text, { key: `${absolute}-${label(row)}`, inverse: absolute === selected, color: pickerColor }, `${absolute === selected ? '›' : ' '} ${label(row)}`); }); };
|
|
3988
|
+
/*
|
|
3989
|
+
* Q-R1-3 / E3: every overlay gets `maxRows` (contract with lane O) and is fitted by overlays/scroll-window.ts, which
|
|
3990
|
+
* wraps each line itself and returns one Ink row per row: the rows it returns are the rows it draws, so the budget
|
|
3991
|
+
* receives the overlay's exact height and no overlay can push the composer off the screen.
|
|
3992
|
+
*/
|
|
3993
|
+
const fittedPanel = (fitted) => ({ element: renderFittedOverlay(h, Ink, fitted, { accentColor: pickerColor }), rows: fitted.rows.length + (fitted.border ? 2 : 0) });
|
|
3994
|
+
const overlayOf = (content) => fittedPanel(fitOverlay(content, { maxRows, columns }));
|
|
3995
|
+
/* P17: in the guided setup, each window says its step; a local engine that is down offers R and Esc (decision 8). */
|
|
3996
|
+
const setupWrap = (content, step) => {
|
|
3997
|
+
if (!setupActive)
|
|
3998
|
+
return content;
|
|
3999
|
+
/* On a local engine that is down, Esc goes back to the providers: the step line does not also say it skips. */
|
|
4000
|
+
if (step === 'model' && modelList.engineDown) {
|
|
4001
|
+
const wrapped = setupOverlay(content, step, { escSkips: false });
|
|
4002
|
+
return { ...wrapped, foot: [...(wrapped.foot ?? []), { text: t('firstrun.setup.retryHint'), tone: 'dim' }] };
|
|
4003
|
+
}
|
|
4004
|
+
/* The key and consent windows say what Esc does in their own line ("Esc cancels"): one wording per window. */
|
|
4005
|
+
return setupOverlay(content, step, { escSkips: step !== 'key' });
|
|
4006
|
+
};
|
|
4007
|
+
let overlayNode = null;
|
|
4008
|
+
if (historyPicker)
|
|
4009
|
+
overlayNode = overlayOf(historyPickerOverlay({ rows: history, picker: historyPicker, width: columns }));
|
|
4010
|
+
else if (undoPicker)
|
|
4011
|
+
overlayNode = overlayOf(undoPickerOverlay(undoPicker));
|
|
4012
|
+
else if (themePicker)
|
|
4013
|
+
overlayNode = overlayOf(themePickerOverlay({ selected: themePicker.selected, current: themePicker.original, colors: live.capabilities.color }));
|
|
4014
|
+
else if (contextInspector)
|
|
4015
|
+
overlayNode = overlayOf(contextInspectorOverlay(contextInspector));
|
|
4016
|
+
else if (agentTreeOverlay)
|
|
4017
|
+
overlayNode = overlayOf(agentTreeOverlayContent(agentTreeOverlay, { width: Math.max(1, columns - 2), nowMs: motionNow, currentSessionId: controller.current(), running: state.running, viewingId: agentView?.sessionId ?? null }));
|
|
4018
|
+
else if (trustCenter)
|
|
4019
|
+
overlayNode = overlayOf(trustCenterOverlay(trustCenter));
|
|
4020
|
+
else if (mcpCenter)
|
|
4021
|
+
overlayNode = overlayOf(mcpCenterOverlay(mcpCenter));
|
|
4022
|
+
else if (hookCenter)
|
|
4023
|
+
overlayNode = overlayOf(hookCenterOverlay(hookCenter));
|
|
4024
|
+
else if (pluginCenter)
|
|
4025
|
+
overlayNode = overlayOf(pluginCenterOverlay(pluginCenter));
|
|
4026
|
+
else if (memoryCenter)
|
|
4027
|
+
overlayNode = overlayOf(memoryCenterOverlay(memoryCenter));
|
|
4028
|
+
else if (notesCenter)
|
|
4029
|
+
overlayNode = overlayOf(notesCenterOverlay(notesCenter));
|
|
4030
|
+
else if (tasksCenter)
|
|
4031
|
+
overlayNode = overlayOf(tasksCenterOverlay(tasksCenter));
|
|
4032
|
+
else if (forgeCenter)
|
|
4033
|
+
overlayNode = overlayOf(forgeCenterOverlay(forgeCenter));
|
|
4034
|
+
else if (automationCenter)
|
|
4035
|
+
overlayNode = overlayOf(automationCenterOverlay(automationCenter));
|
|
4036
|
+
else if (researchCenter)
|
|
4037
|
+
overlayNode = overlayOf(researchCenterOverlay(researchCenter));
|
|
4038
|
+
else if (libraryCenter)
|
|
4039
|
+
overlayNode = overlayOf(libraryCenterOverlay(libraryCenter));
|
|
4040
|
+
else if (queueEditor)
|
|
4041
|
+
overlayNode = overlayOf(queueEditorOverlay({ entries: controller.queueEntries(), selected: queueEditor.selected, paused: controller.queuePaused(), editing: queueEditor.editing, draft: queueEditor.draft }));
|
|
4042
|
+
else if (appState.overlay?.kind === 'help') {
|
|
4043
|
+
const fitted = fitOverlay(helpDialogOverlay(effectiveKeymap, { offset: helpOffset }), { maxRows, columns });
|
|
4044
|
+
helpMaxOffsetRef.current = fitted.sections[0]?.maxOffset ?? 0;
|
|
4045
|
+
helpPageRef.current = Math.max(1, maxRows - 4);
|
|
4046
|
+
overlayNode = fittedPanel(fitted);
|
|
4047
|
+
}
|
|
4048
|
+
else if (appState.overlay?.kind === 'approval' && pending) {
|
|
4049
|
+
const model = approvalDialogModel(pending, { expanded: approvalExpanded, selected: approvalSelected, unsandboxed: sandboxVerified === false });
|
|
4050
|
+
const fit = approvalDialogFit(model, { columns, maxRows, scrollOffset: approvalScroll });
|
|
4051
|
+
approvalMaxOffsetRef.current = fit.maxOffset;
|
|
4052
|
+
overlayNode = { element: renderApprovalDialog(h, Ink, model, { accentColor: approvalColor, columns, maxRows, scrollOffset: Math.min(approvalScroll, fit.maxOffset) }), rows: fit.rows + (fit.border ? 2 : 0) };
|
|
4053
|
+
}
|
|
4054
|
+
else if (transcriptSearch)
|
|
4055
|
+
overlayNode = overlayOf(transcriptSearchOverlay({ items: visibleTranscriptItems, state: { ...transcriptSearch, selectedId: transcriptSelectedId, raw: transcriptRaw }, raw: transcriptRaw }));
|
|
4056
|
+
else if (appState.overlay?.kind === 'model')
|
|
4057
|
+
overlayNode = modelEffortStep
|
|
4058
|
+
? overlayOf(effortLineOverlay({ name: modelEffortStep.name, choices: effortRows, selected: pickerSelection.selected, current: state.status.model === modelEffortStep.id ? (reasoningEffort ?? 'default') : null, width: Math.max(20, columns - 6), unicode: capabilities.unicode }))
|
|
4059
|
+
: overlayOf(setupWrap(modelPickerOverlay({ label: modelList.label, provider: appState.overlay.provider, loading: modelList.loading, verified: modelList.verified, notice: modelList.notice ?? '', query: pickerQuery, rows: filteredModels, selected: pickerSelection.selected }), 'model'));
|
|
4060
|
+
else if (appState.overlay?.kind === 'provider' && providerStep.kind === 'login-choice')
|
|
4061
|
+
overlayNode = overlayOf(setupWrap(providerLoginChoiceOverlay({ label: providerStep.provider.label, choice: providerStep.choice, notice: providerStep.remediation?.message ?? (providerStep.provider.benched ? t('screen.provider.benchedRetry', { benched: benchedKeyText(providerStep.provider.label, providerStep.provider.benched) }) : null) }), 'key'));
|
|
4062
|
+
else if (appState.overlay?.kind === 'provider' && providerStep.kind === 'oauth')
|
|
4063
|
+
overlayNode = overlayOf(setupWrap(providerOAuthOverlay({ label: providerStep.provider.label, url: providerStep.url, mode: providerStep.mode, opened: providerStep.opened, phase: providerStep.phase, masked: maskedSecret(providerSecret), message: providerStep.message }), 'key'));
|
|
4064
|
+
else if (appState.overlay?.kind === 'provider' && providerStep.kind === 'consent')
|
|
4065
|
+
overlayNode = overlayOf(setupWrap(providerConsentOverlay({ provider: providerStep.provider, choice: providerStep.choice }), 'key'));
|
|
4066
|
+
else if (appState.overlay?.kind === 'provider' && providerStep.kind === 'key') {
|
|
4067
|
+
const label = providerStep.provider.label;
|
|
4068
|
+
/* B1 slice 18: a one-token key test is stated on this step, before Enter sends it; no further question is asked. */
|
|
4069
|
+
const keyTestNotice = typeof catalog?.keyTestNotice === 'function' ? catalog.keyTestNotice(providerStep.provider.id) : null;
|
|
4070
|
+
/* B1 slice 25: a provider whose saved key is benched says so here, with the cause and until when, before another key is typed. */
|
|
4071
|
+
const remediation = providerStep.remediation?.message ?? null;
|
|
4072
|
+
const benched = !remediation && providerStep.provider.benched ? t('screen.provider.benchedRetry', { benched: benchedKeyText(label, providerStep.provider.benched) }) : null;
|
|
4073
|
+
overlayNode = overlayOf(setupWrap(providerKeyOverlay({ label, remediation, benched, masked: maskedSecret(providerSecret), keyTestNotice, testing: providerStep.testing, result: providerStep.result?.message ?? null }), 'key'));
|
|
4074
|
+
}
|
|
4075
|
+
else if (appState.overlay?.kind === 'provider')
|
|
4076
|
+
overlayNode = overlayOf(setupWrap(providerPickerOverlay({ query: pickerQuery, rows: filteredProviders, selected: pickerSelection.selected, chosen: chosenProvider, reachable: id => providerPickerRef.current?.reachable(id) ?? null }), 'provider'));
|
|
4077
|
+
else if (appState.overlay?.kind === 'session')
|
|
4078
|
+
overlayNode = overlayOf(sessionPickerOverlay({ action: appState.overlay.action, query: pickerQuery, rows: filteredSessions, selected: pickerSelection.selected }));
|
|
4079
|
+
const diffPanel = diffView ? overlayOf({ head: [{ text: t('screen.diff.title', { mode: diffView.mode }), tone: 'title' }], sections: [{ lines: diffView.lines.map(line => ({ text: line.text, tone: line.kind === 'file' || line.kind === 'hunk' || line.kind === 'add' || line.kind === 'remove' ? 'strong' : 'dim' })), offset: 0 }] }) : null;
|
|
4080
|
+
const auxLines = auxCompletions.length && !appState.overlay && !queueEditor && !transcriptSearch && !contextInspector && !themePicker && !undoPicker && !historyPicker && !mcpCenter && !hookCenter && !pluginCenter && !memoryCenter && !notesCenter && !tasksCenter && !libraryCenter && !researchCenter && !automationCenter && !forgeCenter ? auxCompletions.map((value) => h(Ink.Text, { key: value, dimColor: true, wrap: 'truncate-end' }, ` ${value}`)) : [];
|
|
4081
|
+
/* R4 Q-R4-8 / E13: the live `@` list in the completions region, fitted to its own cap and to the R1 budget. */
|
|
4082
|
+
const completionPanel = fileCompletion && appState.focus.current === 'composer' && !appState.overlay && !queueEditor && !transcriptSearch && !contextInspector && !themePicker && !undoPicker && !historyPicker && !agentTreeOverlay && !trustCenter && !mcpCenter && !hookCenter && !pluginCenter && !memoryCenter && !notesCenter && !tasksCenter && !libraryCenter && !researchCenter && !automationCenter && !forgeCenter
|
|
4083
|
+
? fittedPanel(fitOverlay(fileCompletionOverlay(fileCompletion), { maxRows: Math.min(maxRows, FILE_COMPLETION_MAX_ROWS), columns }))
|
|
4084
|
+
: null;
|
|
4085
|
+
const auxNodes = completionPanel ? [completionPanel.element] : auxLines;
|
|
4086
|
+
const b = layoutBudget({ ...budgetInput, overlay: overlayNode?.rows ?? null, menu: menuRows || null, diff: diffPanel?.rows ?? null, aux: completionPanel?.rows ?? (auxLines.length || null) });
|
|
4087
|
+
/* R5a S9 (review D-14): the header names the folder once; `interactive` said nothing, `degraded` still does. */
|
|
4088
|
+
const agentViewTask = agentView ? (agentView.task ?? t('screen.agentView.unnamed')) : '';
|
|
4089
|
+
const headerStatus = agentView ? t(agentView.done ? 'screen.agentView.done' : 'screen.agentView.watching', { task: agentViewTask }) : boot.phase === 'error' ? t('screen.header.degraded') : '';
|
|
4090
|
+
const header = headerLine({ workspace: truncateMiddle(projectRoot, Math.max(1, columns - displayWidth(headerLine({ workspace: '', status: headerStatus })) - 3)), status: headerStatus });
|
|
4091
|
+
/* E1/E2: the root is exactly the frame; every region below is a box of the height the budget gave it, in this order,
|
|
4092
|
+
so the composer, the status row and the chrome sit at the bottom of the screen (D-03) and nothing overwrites
|
|
4093
|
+
anything (D-02). */
|
|
4094
|
+
return h(Ink.Box, { key: `main-${appState.redrawNonce}-${live.id}`, flexDirection: 'column', height: b.frameRows, flexShrink: 0, overflowY: 'hidden' }, h(Regions.Header, { key: 'header', rows: b.header, text: header, color: bootColor }), h(TranscriptView, { key: 'transcript', items: visibleTranscriptItems, scrollRows: viewport.offset, rowBudget: b.transcript, width: columns, raw: transcriptRaw, expandedTools: appState.expandedTools, reasoningVisible: appState.reasoningVisible, now: toolNow, revealId: transcriptRevealId, measurementStore: transcriptMeasurementStore, renderItem: renderTranscriptItem, onPlan: onTranscriptPlan, dimColor: footerColor }), h(Regions.Panels, { key: 'panels', budget: b, diff: diffPanel?.element ?? null, overlay: overlayNode?.element ?? null, aux: auxNodes, menu: menuOpen ? h(CommandMenuView, { composerStore, selectionStore: commandMenuSelectionStore, maxRows: b.menu, columns }) : null }), b.status ? h(Ink.Text, { key: 'status', color: footerColor, dimColor: exitHint, wrap: 'truncate-end' }, statusText) : null, b.composer ? (agentView
|
|
4095
|
+
? h(Ink.Box, { key: 'composer', borderStyle: 'single', flexDirection: 'column', flexShrink: 0, height: 2 + b.composerContent + b.composerHints, overflowY: 'hidden' }, h(Ink.Text, { dimColor: true, wrap: 'truncate-end' }, t('screen.agentView.readOnly')))
|
|
4096
|
+
: h(ComposerView, { key: 'composer', store: composerStore, vimHint, contentRows: b.composerContent, hintRows: b.composerHints, columns })) : null, h(Regions.StatusBar, { key: 'status-bar', budget: b, shortcutLine: agentView ? truncateDisplay(t('screen.agentView.shortcuts'), Math.max(1, columns)) : shortcutLine, rosterLines, queueLine, reasoningLine, footerLine, statusSegments: statusRow.segments, toneColors: { warn: theme.colors.warning, high: theme.colors.danger, critical: theme.colors.danger }, color: footerColor }));
|
|
4097
|
+
};
|
|
4098
|
+
}
|