talos-code 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +111 -3
- package/THIRD_PARTY_NOTICES.md +40 -0
- package/dist/archive/zip.js +73 -0
- package/dist/args.js +97 -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 +185 -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 +43 -0
- package/dist/i18n/en/common.js +9 -0
- package/dist/i18n/en/credentials.js +82 -0
- package/dist/i18n/en/errors.js +246 -0
- package/dist/i18n/en/firstrun.js +89 -0
- package/dist/i18n/en/providers.js +37 -0
- package/dist/i18n/en/screen.js +736 -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 +330 -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/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-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 +262 -0
- package/dist/runtime/talos-composition.js +1006 -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/app.js +3761 -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 +559 -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 +32 -0
- package/dist/tui/components/terminal-shell.js +205 -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/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 +139 -0
- package/dist/tui/line-diff.js +57 -0
- package/dist/tui/live-activity.js +45 -0
- package/dist/tui/metrics.js +123 -0
- package/dist/tui/onboarding.js +36 -0
- package/dist/tui/overlays/agent-tree.js +39 -0
- package/dist/tui/overlays/approval-dialog.js +640 -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 +72 -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 +145 -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 +287 -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 +778 -0
- package/dist/tui/session-export.js +54 -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 +80 -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 +25 -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 +63 -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
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { sanitizeTranscriptText } from "./transcript-model.js";
|
|
2
|
+
import { toolRowView } from "./components/tool-row.js";
|
|
3
|
+
/* The resume line is pasted into a shell, so the id must be one shell word: the pattern `talos session import`
|
|
4
|
+
already enforces for archive ids (session-cli.ts), no leading dash, no `..`. */
|
|
5
|
+
const SESSION_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
|
|
6
|
+
const TOOL_ARGS_MAX = 72;
|
|
7
|
+
/* R3 gate E1: a tool row is the ONE row formatter's line, fitted to this width (the target is cut in the middle first). */
|
|
8
|
+
export const EXIT_TOOL_WIDTH = 120;
|
|
9
|
+
const hasText = (item) => item.kind === 'message' && item.text.trim().length > 0;
|
|
10
|
+
/** The id worth printing as `talos --resume <id>`: valid, and only for a session that holds a message (Qwen gating,
|
|
11
|
+
* startInteractiveUI.tsx:421-449 @2026-09-15, gate C10). */
|
|
12
|
+
export function resumableSessionId(snapshot) {
|
|
13
|
+
const id = snapshot?.sessionId;
|
|
14
|
+
if (!id || !SESSION_ID.test(id) || id.includes('..'))
|
|
15
|
+
return null;
|
|
16
|
+
return snapshot.items.some(hasText) ? id : null;
|
|
17
|
+
}
|
|
18
|
+
const oneLine = (value, max) => { const text = sanitizeTranscriptText(value).replace(/\s+/gu, ' ').trim(); return text.length > max ? text.slice(0, max - 1) + '…' : text; };
|
|
19
|
+
export function formatExitOutput(snapshot, { mode = 'transcript', color = false } = {}) {
|
|
20
|
+
if (!snapshot || !snapshot.items.some(hasText))
|
|
21
|
+
return '';
|
|
22
|
+
const bold = (text) => color ? `\u001b[1m${text}\u001b[22m` : text;
|
|
23
|
+
const dim = (text) => color ? `\u001b[2m${text}\u001b[22m` : text;
|
|
24
|
+
const id = resumableSessionId(snapshot);
|
|
25
|
+
const resume = id ? `${dim('To resume this session:')} talos --resume ${id}\n` : '';
|
|
26
|
+
if (mode === 'resume')
|
|
27
|
+
return resume;
|
|
28
|
+
/* Blocks are separated by one blank line; consecutive one-line rows (tools, run outcomes) stay together. Reasoning
|
|
29
|
+
and local notices (help, /status, warnings) are screen chrome, not the conversation, so they are left out. */
|
|
30
|
+
const blocks = [];
|
|
31
|
+
let rows = null;
|
|
32
|
+
const row = (text) => { if (!rows) {
|
|
33
|
+
rows = [];
|
|
34
|
+
blocks.push(rows);
|
|
35
|
+
} rows.push(dim(text)); };
|
|
36
|
+
for (const item of snapshot.items) {
|
|
37
|
+
if (item.kind === 'message') {
|
|
38
|
+
if (!hasText(item))
|
|
39
|
+
continue;
|
|
40
|
+
rows = null;
|
|
41
|
+
blocks.push([bold(item.role === 'user' ? 'You' : 'TALOS'), ...sanitizeTranscriptText(item.text).replace(/\s+$/u, '').split('\n')]);
|
|
42
|
+
}
|
|
43
|
+
else if (item.kind === 'tool') {
|
|
44
|
+
/* R3 gate E1 (R2 Q-R2-3 kept): the same header row as the screen — English label, primary argument, facts; never the
|
|
45
|
+
kernel name, raw JSON or the enum. The detail lines stay on the screen. */
|
|
46
|
+
row(toolRowView(item, EXIT_TOOL_WIDTH, false).line);
|
|
47
|
+
}
|
|
48
|
+
else if (item.kind === 'warning' && item.outcome) {
|
|
49
|
+
/* R3 Q-R3-8/9: a turn that did not end normally leaves its one line (its first line: the headline). */
|
|
50
|
+
row(oneLine(item.text.split(/\r?\n/u)[0] ?? '', EXIT_TOOL_WIDTH));
|
|
51
|
+
}
|
|
52
|
+
else if (item.kind === 'run' && (item.status === 'failed' || item.status === 'cancelled')) {
|
|
53
|
+
const message = oneLine(item.message.split(/\r?\n/u)[0] ?? '', TOOL_ARGS_MAX);
|
|
54
|
+
row(`Run ${item.status}${message ? ` · ${message}` : ''}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return blocks.map(block => block.join('\n') + '\n').join('\n') + (resume ? `\n${resume}` : '');
|
|
58
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
export function parseEditorCommand(value) { const out = []; let token = '', quote = ''; for (let i = 0; i < value.length; i++) {
|
|
5
|
+
const ch = value[i];
|
|
6
|
+
if (quote) {
|
|
7
|
+
if (ch === quote) {
|
|
8
|
+
quote = '';
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
if (ch === '\\' && quote === '"' && i + 1 < value.length) {
|
|
12
|
+
token += value[++i];
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
token += ch;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (ch === '"' || ch === "'") {
|
|
19
|
+
quote = ch;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (/\s/u.test(ch)) {
|
|
23
|
+
if (token) {
|
|
24
|
+
out.push(token);
|
|
25
|
+
token = '';
|
|
26
|
+
}
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (ch === '\\' && i + 1 < value.length) {
|
|
30
|
+
token += value[++i];
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
token += ch;
|
|
34
|
+
} if (quote)
|
|
35
|
+
throw Object.assign(new Error('EXTERNAL_EDITOR_PARSE_FAILED'), { code: 'EXTERNAL_EDITOR_PARSE_FAILED' }); if (token)
|
|
36
|
+
out.push(token); if (out.length === 0)
|
|
37
|
+
throw Object.assign(new Error('EXTERNAL_EDITOR_NOT_CONFIGURED'), { code: 'EXTERNAL_EDITOR_NOT_CONFIGURED' }); return out; }
|
|
38
|
+
export async function editExternally({ text, editor, tmpRoot, spawn = spawnSync }) {
|
|
39
|
+
if (!editor?.trim())
|
|
40
|
+
throw Object.assign(new Error('EXTERNAL_EDITOR_NOT_CONFIGURED'), { code: 'EXTERNAL_EDITOR_NOT_CONFIGURED' });
|
|
41
|
+
const [command, ...args] = parseEditorCommand(editor);
|
|
42
|
+
const file = join(tmpRoot, `talos-editor-${process.pid}-${Date.now()}.txt`);
|
|
43
|
+
await writeFile(file, text, { encoding: 'utf8', mode: 0o600 });
|
|
44
|
+
try {
|
|
45
|
+
const result = spawn(command, [...args, file], { stdio: 'inherit', shell: false, windowsHide: false });
|
|
46
|
+
if (result.status !== 0)
|
|
47
|
+
throw Object.assign(new Error('EXTERNAL_EDITOR_FAILED'), { code: 'EXTERNAL_EDITOR_FAILED' });
|
|
48
|
+
return await readFile(file, 'utf8');
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
await rm(file, { force: true });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { readdir, stat } from 'node:fs/promises';
|
|
2
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
3
|
+
import { t } from "../i18n/index.js";
|
|
4
|
+
import { isPathInsideWorkspace, resolvePathInsideWorkspace, resolveWorkspaceIdentity } from "../security/workspace-identity.js";
|
|
5
|
+
import { pickerSection } from "./overlays/scroll-window.js";
|
|
6
|
+
import { buildProjectFileIndex, rankProjectFiles } from "./project-file-index.js";
|
|
7
|
+
/** The most rows the list takes, border and title included (E13: inside the R1 budget, which may give it fewer). */
|
|
8
|
+
export const FILE_COMPLETION_MAX_ROWS = 10;
|
|
9
|
+
const UNSAFE_NAME = /[\x00-\x1f\x7f\p{Zl}\p{Zp}\p{Bidi_Control}]/u;
|
|
10
|
+
/** The typed path after `@`: quotes removed, `\` read as `/`, repeated slashes collapsed; `folder` keeps its final `/`. */
|
|
11
|
+
export function projectPathQuery(token) {
|
|
12
|
+
const raw = (token.startsWith('@') ? token.slice(1) : token).replace(/^['"]|['"]$/gu, '').replace(/\\/gu, '/').replace(/\/{2,}/gu, '/');
|
|
13
|
+
const slash = raw.lastIndexOf('/');
|
|
14
|
+
return { path: raw, folder: slash >= 0 ? raw.slice(0, slash + 1) : '', prefix: raw.slice(slash + 1) };
|
|
15
|
+
}
|
|
16
|
+
function displayValue(path, directory) { const value = path + (directory ? '/' : ''); return /\s/u.test(value) ? `@"${value}"` : `@${value}`; }
|
|
17
|
+
export async function completeProjectPath(projectRoot, token) {
|
|
18
|
+
const workspace = await resolveWorkspaceIdentity({ projectRoot });
|
|
19
|
+
const { path: raw, folder, prefix } = projectPathQuery(token);
|
|
20
|
+
if (isAbsolute(raw) || /^[A-Za-z]:/u.test(raw))
|
|
21
|
+
return [];
|
|
22
|
+
const logicalParent = resolve(workspace.requestedRoot, folder || '.');
|
|
23
|
+
if (!isPathInsideWorkspace(workspace, logicalParent))
|
|
24
|
+
return [];
|
|
25
|
+
const parent = await resolvePathInsideWorkspace(workspace, logicalParent);
|
|
26
|
+
if (!parent)
|
|
27
|
+
return [];
|
|
28
|
+
let entries;
|
|
29
|
+
try {
|
|
30
|
+
entries = await readdir(parent, { withFileTypes: true });
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
const rows = [];
|
|
36
|
+
for (const entry of entries) {
|
|
37
|
+
if (!entry.name.startsWith(prefix) || UNSAFE_NAME.test(entry.name))
|
|
38
|
+
continue;
|
|
39
|
+
const canonical = await resolvePathInsideWorkspace(workspace, resolve(parent, entry.name));
|
|
40
|
+
if (!canonical)
|
|
41
|
+
continue;
|
|
42
|
+
let info;
|
|
43
|
+
try {
|
|
44
|
+
info = await stat(canonical);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (!info.isDirectory() && !info.isFile())
|
|
50
|
+
continue;
|
|
51
|
+
rows.push({ name: entry.name, directory: info.isDirectory() });
|
|
52
|
+
}
|
|
53
|
+
return rows.sort((a, b) => Number(a.directory) - Number(b.directory) || a.name.localeCompare(b.name)).map(entry => ({ value: displayValue(folder + entry.name, entry.directory), directory: entry.directory }));
|
|
54
|
+
}
|
|
55
|
+
/*
|
|
56
|
+
* R5b D6 (Q-R5-16; plan Part III items 2 and 5): `@` with part of a name searches the whole project (project-file-index.ts);
|
|
57
|
+
* `@` alone and `@folder/` still browse one folder (R4). The list says where its rows come from and how many it found, and
|
|
58
|
+
* shows at most PROJECT_COMPLETION_SHOWN of them, saying so when there are more.
|
|
59
|
+
*/
|
|
60
|
+
export const PROJECT_COMPLETION_SHOWN = 50;
|
|
61
|
+
export async function completeProjectFiles(projectRoot, token, { index } = {}) {
|
|
62
|
+
const query = projectPathQuery(token);
|
|
63
|
+
if (!query.path || query.path.endsWith('/'))
|
|
64
|
+
return { rows: await completeProjectPath(projectRoot, token), source: { kind: 'folder', folder: query.folder } };
|
|
65
|
+
if (isAbsolute(query.path) || /^[A-Za-z]:/u.test(query.path) || query.path.split('/').includes('..'))
|
|
66
|
+
return { rows: [], source: { kind: 'project', found: 0, shown: 0, capped: false, max: 0 } };
|
|
67
|
+
const built = await (index ? index() : buildProjectFileIndex(projectRoot));
|
|
68
|
+
const ranked = rankProjectFiles(built, query.path);
|
|
69
|
+
const shown = ranked.slice(0, PROJECT_COMPLETION_SHOWN);
|
|
70
|
+
return { rows: shown.map(entry => ({ value: displayValue(entry.path, entry.directory), directory: entry.directory })), source: { kind: 'project', found: ranked.length, shown: shown.length, capped: built.capped, max: built.max } };
|
|
71
|
+
}
|
|
72
|
+
function sourceLine(source) {
|
|
73
|
+
if (source.kind === 'folder')
|
|
74
|
+
return { text: t('screen.completion.sourceFolder', { folder: source.folder || t('screen.completion.thisFolder') }), tone: 'dim' };
|
|
75
|
+
const base = source.shown < source.found ? t('screen.completion.sourceProjectMore', { shown: source.shown, found: source.found }) : t('screen.completion.sourceProject', { found: source.found });
|
|
76
|
+
return { text: base + (source.capped ? t('screen.completion.sourceCapped', { max: source.max.toLocaleString('en-US') }) : ''), tone: 'dim' };
|
|
77
|
+
}
|
|
78
|
+
/* The list as app.ts draws it (single border, like the command menu), fitted by overlays/scroll-window.ts to at most
|
|
79
|
+
FILE_COMPLETION_MAX_ROWS and the R1 budget, the highlight kept in view; with nothing to offer, one dim line says so
|
|
80
|
+
(Q-R4-13: `No files match src/x`, gate §5). */
|
|
81
|
+
export function fileCompletionOverlay({ token, rows, selected, source }) {
|
|
82
|
+
const head = [{ text: t('screen.completion.title'), tone: 'strong' }, ...(source ? [sourceLine(source)] : [])];
|
|
83
|
+
if (!rows.length) {
|
|
84
|
+
const query = projectPathQuery(token);
|
|
85
|
+
const text = query.prefix ? t('screen.completion.none', { path: query.path }) : t('screen.completion.emptyFolder', { folder: query.folder || t('screen.completion.thisFolder') });
|
|
86
|
+
return { head, sections: [{ lines: [{ text, tone: 'dim' }] }], borderStyle: 'single' };
|
|
87
|
+
}
|
|
88
|
+
return { head, sections: [pickerSection(rows, selected, row => row.value)], borderStyle: 'single' };
|
|
89
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export function focusForOverlay(overlay) { return overlay === 'model' ? 'model-picker' : overlay === 'provider' ? 'provider-picker' : overlay === 'session' ? 'session-picker' : overlay; }
|
|
2
|
+
export function createFocusState(initial) { return { current: initial, stack: [] }; }
|
|
3
|
+
export function openFocus(state, next) { return next === state.current ? state : { current: next, stack: [...state.stack, state.current] }; }
|
|
4
|
+
export function closeFocus(state) { const previous = state.stack.at(-1); if (!previous)
|
|
5
|
+
return state; return { current: previous, stack: state.stack.slice(0, -1) }; }
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const JS_LANGS = new Set(['js', 'jsx', 'javascript', 'ts', 'tsx', 'typescript']);
|
|
2
|
+
const SH_LANGS = new Set(['sh', 'bash', 'shell', 'powershell', 'ps1']);
|
|
3
|
+
const JS_KEYWORDS = new Set(['const', 'let', 'var', 'function', 'return', 'if', 'else', 'for', 'while', 'class', 'new', 'import', 'from', 'export', 'async', 'await', 'try', 'catch', 'throw', 'interface', 'type', 'extends', 'implements', 'public', 'private', 'protected', 'readonly']);
|
|
4
|
+
function tokenize(line, re, classify) { const out = []; let pos = 0; for (const m of line.matchAll(re)) {
|
|
5
|
+
const i = m.index ?? 0;
|
|
6
|
+
if (i > pos)
|
|
7
|
+
out.push({ text: line.slice(pos, i), kind: 'plain' });
|
|
8
|
+
out.push({ text: m[0], kind: classify(m[0], i) });
|
|
9
|
+
pos = i + m[0].length;
|
|
10
|
+
} if (pos < line.length)
|
|
11
|
+
out.push({ text: line.slice(pos), kind: 'plain' }); return out.length ? out : [{ text: line, kind: 'plain' }]; }
|
|
12
|
+
function json(line) { return tokenize(line, /"(?:\\.|[^"\\])*"(?=\s*:)|"(?:\\.|[^"\\])*"|-?\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b|\b(?:true|false|null)\b/gu, (t) => { if (/^"/u.test(t))
|
|
13
|
+
return /"\s*$/u.test(t) && new RegExp(`${t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*:`).test(line) ? 'key' : 'string'; if (/^(?:true|false|null)$/u.test(t))
|
|
14
|
+
return 'literal'; return 'number'; }); }
|
|
15
|
+
function js(line) { const comment = line.search(/\/\/|\/\*/u); const head = comment >= 0 ? line.slice(0, comment) : line; const body = tokenize(head, /'(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*"|`(?:\\.|[^`\\])*`|\b[A-Za-z_$][A-Za-z0-9_$]*\b|\b\d+(?:\.\d+)?\b|=>|===|!==|==|!=|<=|>=|&&|\|\||[+\-*/%=<>!?]/gu, (t) => { if (/^['"`]/u.test(t))
|
|
16
|
+
return 'string'; if (/^\d/u.test(t))
|
|
17
|
+
return 'number'; if (JS_KEYWORDS.has(t))
|
|
18
|
+
return 'keyword'; if (/^(?:true|false|null|undefined)$/u.test(t))
|
|
19
|
+
return 'literal'; if (/^[+\-*/%=<>!?&|]/u.test(t))
|
|
20
|
+
return 'operator'; return 'plain'; }); if (comment >= 0)
|
|
21
|
+
body.push({ text: line.slice(comment), kind: 'comment' }); return body; }
|
|
22
|
+
function sh(line) { const comment = line.indexOf('#'); const head = comment >= 0 ? line.slice(0, comment) : line; const body = tokenize(head, /'(?:[^']*)'|"(?:\\.|[^"\\])*"|\$[A-Za-z_][A-Za-z0-9_]*|\b(?:if|then|else|fi|for|in|do|done|case|esac|function|param|foreach|throw|exit)\b|\b\d+\b/gu, (t) => { if (/^['"]/u.test(t))
|
|
23
|
+
return 'string'; if (/^\$/u.test(t))
|
|
24
|
+
return 'literal'; if (/^\d/u.test(t))
|
|
25
|
+
return 'number'; return 'keyword'; }); if (comment >= 0)
|
|
26
|
+
body.push({ text: line.slice(comment), kind: 'comment' }); return body; }
|
|
27
|
+
export function highlightCodeLine(line, language = '') { const lang = language.toLowerCase().trim(); if (lang === 'json' || lang === 'jsonc')
|
|
28
|
+
return json(line); if (JS_LANGS.has(lang))
|
|
29
|
+
return js(line); if (SH_LANGS.has(lang))
|
|
30
|
+
return sh(line); return [{ text: line, kind: 'plain' }]; }
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { resolveKeybinding } from "./keybindings.js";
|
|
2
|
+
const TEXT_INPUT_FOCUS = new Set(['composer', 'command-menu', 'model-picker', 'provider-picker', 'session-picker']);
|
|
3
|
+
export function acceptsTextInput(focus) { return TEXT_INPUT_FOCUS.has(focus); }
|
|
4
|
+
export function routeInput(input) {
|
|
5
|
+
const { ch, key, composerText } = input;
|
|
6
|
+
const focus = input.commandMenuOpen && input.focus === 'composer' ? 'command-menu' : input.focus;
|
|
7
|
+
if (focus === 'composer' && ch === '?' && composerText.length > 0)
|
|
8
|
+
return { kind: 'text', text: '?' };
|
|
9
|
+
if (focus === 'composer' && ch === '/' && composerText.trimStart().length === 0)
|
|
10
|
+
return { kind: 'action', action: 'command-menu' };
|
|
11
|
+
const contexts = (focus === 'composer' ? [focus, 'history', 'transcript', 'global'] : [focus, 'global']);
|
|
12
|
+
const action = resolveKeybinding(ch, key, contexts, input.keymap);
|
|
13
|
+
if (action)
|
|
14
|
+
return { kind: 'action', action };
|
|
15
|
+
if (acceptsTextInput(focus) && ch && !key.ctrl && !key.meta)
|
|
16
|
+
return { kind: 'text', text: ch };
|
|
17
|
+
return { kind: 'ignore' };
|
|
18
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { t } from "../i18n/index.js";
|
|
2
|
+
/*
|
|
3
|
+
* Ctrl+C policy (review D-12, gate E10). The keymap documents exit as `Ctrl+D / Ctrl+C Ctrl+C`, so at idle the first
|
|
4
|
+
* press only arms the exit and the caller shows EXIT_HINT (Claude Code prints "Press Ctrl-C again to exit",
|
|
5
|
+
* CHANGELOG 2.1.248, npm 2026-08-27); a second press inside the window exits. During a run the first press cancels
|
|
6
|
+
* and a quick second one forces the exit, as before. The idle window is 2 s, not Pi's 500 ms (interactive-mode.ts:
|
|
7
|
+
* 4117-4125 @a328aa8 2026-09-23): the hint is on screen, and the s10 acceptance cadence presses again after 1.5 s.
|
|
8
|
+
*
|
|
9
|
+
* R4 (owner decision Q-R4-7, gate E7): ONE exit rule for Ctrl+C, Ctrl+D and Esc, decided here; app.ts only calls it and
|
|
10
|
+
* carries out the answer. The composer and dialog state is read through the `context` callback the app injects, at the
|
|
11
|
+
* moment of the key:
|
|
12
|
+
* - an open picker or dialog is closed first, before any exit is armed (Claude Code 2.1.280/2.1.281, gate D.2/D.5);
|
|
13
|
+
* - with a draft, Ctrl+C and Esc Esc (within ESC_CLEAR_WINDOW_MS) clear it — the app puts it in history first, so ↑ brings
|
|
14
|
+
* it back (Claude Code "Interactive mode" :21/:37, 2.1.275 → 2026-09-17, gate B.2; Hermes, Pi, Goose clear too);
|
|
15
|
+
* - the two-press exit (2 s window and the status-row hint, R1) happens only on an empty composer;
|
|
16
|
+
* - Ctrl+D with a draft deletes at the cursor; on an empty composer it is Ctrl+C (Claude Code :23, Codex `mod.rs:217-224`);
|
|
17
|
+
* one armed state, so either key confirms it, and the hint names the key that armed it.
|
|
18
|
+
* During a run Ctrl+C still cancels first (R1) and Esc stops the run (the queue-preserving interrupt).
|
|
19
|
+
*/
|
|
20
|
+
export const EXIT_CONFIRM_WINDOW_MS = 2000;
|
|
21
|
+
export const ESC_CLEAR_WINDOW_MS = 500;
|
|
22
|
+
export function exitHintText(key = 'ctrl-c') { return t('screen.exit.hint', { key: key === 'ctrl-d' ? 'Ctrl+D' : 'Ctrl+C' }); }
|
|
23
|
+
export const EXIT_HINT = exitHintText('ctrl-c');
|
|
24
|
+
const NO_CONTEXT = { draft: false, dialog: false, completion: false };
|
|
25
|
+
export function createInterruptController({ now = Date.now, windowMs = 1000, exitWindowMs = EXIT_CONFIRM_WINDOW_MS, escWindowMs = ESC_CLEAR_WINDOW_MS, context = () => NO_CONTEXT } = {}) {
|
|
26
|
+
let lastCancelAt = null, armedAt = null, armedKey = null, lastEscAt = null;
|
|
27
|
+
const armed = (at) => armedAt !== null && at - armedAt <= exitWindowMs;
|
|
28
|
+
const disarm = () => { armedAt = null; armedKey = null; };
|
|
29
|
+
const read = () => { try {
|
|
30
|
+
return { ...NO_CONTEXT, ...context() };
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return NO_CONTEXT;
|
|
34
|
+
} };
|
|
35
|
+
/* The R1 two-press rule: idle arms then exits; during a run cancels then forces the exit. */
|
|
36
|
+
const exitPress = (running, key) => {
|
|
37
|
+
const at = now();
|
|
38
|
+
if (!running) {
|
|
39
|
+
lastCancelAt = null;
|
|
40
|
+
if (armed(at)) {
|
|
41
|
+
disarm();
|
|
42
|
+
return 'exit';
|
|
43
|
+
}
|
|
44
|
+
armedAt = at;
|
|
45
|
+
armedKey = key;
|
|
46
|
+
return 'arm-exit';
|
|
47
|
+
}
|
|
48
|
+
disarm();
|
|
49
|
+
if (lastCancelAt !== null && at - lastCancelAt <= windowMs) {
|
|
50
|
+
lastCancelAt = null;
|
|
51
|
+
return 'force-exit';
|
|
52
|
+
}
|
|
53
|
+
lastCancelAt = at;
|
|
54
|
+
return 'cancel';
|
|
55
|
+
};
|
|
56
|
+
const exitKey = (running, key) => {
|
|
57
|
+
lastEscAt = null;
|
|
58
|
+
const ctx = read();
|
|
59
|
+
if (ctx.dialog) {
|
|
60
|
+
disarm();
|
|
61
|
+
lastCancelAt = null;
|
|
62
|
+
return 'close-dialog';
|
|
63
|
+
}
|
|
64
|
+
if (key === 'ctrl-d' && ctx.draft)
|
|
65
|
+
return 'delete-forward';
|
|
66
|
+
if (!running && ctx.draft) {
|
|
67
|
+
disarm();
|
|
68
|
+
lastCancelAt = null;
|
|
69
|
+
return 'clear-draft';
|
|
70
|
+
}
|
|
71
|
+
return exitPress(running, key);
|
|
72
|
+
};
|
|
73
|
+
return {
|
|
74
|
+
/** Ctrl+C: close a dialog · cancel a run · clear a draft · arm or confirm the exit. */
|
|
75
|
+
ctrlC(running) { return exitKey(running, 'ctrl-c'); },
|
|
76
|
+
/** Ctrl+D: close a dialog · delete at the cursor when there is a draft · otherwise exactly Ctrl+C. */
|
|
77
|
+
ctrlD(running) { return exitKey(running, 'ctrl-d'); },
|
|
78
|
+
/** Esc: close a dialog or the `@` list · stop a run · the second of two quick presses clears a draft. */
|
|
79
|
+
esc(running) {
|
|
80
|
+
const ctx = read(), at = now();
|
|
81
|
+
if (ctx.dialog || ctx.completion || running) {
|
|
82
|
+
lastEscAt = null;
|
|
83
|
+
return ctx.dialog ? 'close-dialog' : ctx.completion ? 'close-completion' : 'stop-run';
|
|
84
|
+
}
|
|
85
|
+
if (ctx.draft && lastEscAt !== null && at - lastEscAt <= escWindowMs) {
|
|
86
|
+
lastEscAt = null;
|
|
87
|
+
disarm();
|
|
88
|
+
return 'clear-draft';
|
|
89
|
+
}
|
|
90
|
+
lastEscAt = at;
|
|
91
|
+
return 'esc';
|
|
92
|
+
},
|
|
93
|
+
/** True while a second idle Ctrl+C/Ctrl+D would exit: the caller keeps the hint visible for exactly this long. */
|
|
94
|
+
exitArmed() { return armed(now()); },
|
|
95
|
+
/** The key that armed the exit (the hint names it), or null. */
|
|
96
|
+
armedBy() { return armed(now()) ? armedKey : null; },
|
|
97
|
+
reset() { lastCancelAt = null; },
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { t } from "../i18n/index.js";
|
|
2
|
+
const PICKER_CONTEXTS = ['model-picker', 'provider-picker', 'session-picker'];
|
|
3
|
+
const PICKER_KEYS = [
|
|
4
|
+
['picker-up', ['Up']], ['picker-down', ['Down']], ['picker-page-up', ['PageUp']], ['picker-page-down', ['PageDown']], ['picker-home', ['Home']], ['picker-end', ['End']], ['picker-confirm', ['Enter']], ['picker-cancel', ['Esc']], ['picker-backspace', ['Backspace']]
|
|
5
|
+
];
|
|
6
|
+
/* R2 (gate E1, Q-R2-10): /help shows every description, so each one is a dictionary word. The picker wording is kept
|
|
7
|
+
byte-identical: docs/talos-cli/command-reference.md is generated from it and pinned by a test. */
|
|
8
|
+
/* R5a (owner 2026-09-24, /effort's line of nodes): ← → move like ↑ ↓ in the pickers; not in the command menu, where they
|
|
9
|
+
move the caret of the typed text. */
|
|
10
|
+
const PICKER_SIDE_KEYS = { 'picker-up': ['Left'], 'picker-down': ['Right'] };
|
|
11
|
+
const pickerBindings = PICKER_CONTEXTS.flatMap(context => PICKER_KEYS.map(([id, keys]) => ({ id, keys: [...keys, ...(PICKER_SIDE_KEYS[id] ?? [])], context, description: t(`screen.key.${id}`) })));
|
|
12
|
+
const commandBindings = PICKER_KEYS.map(([id, keys]) => ({ id, keys: [...keys], context: 'command-menu', description: t(`screen.key.${id}`) }));
|
|
13
|
+
commandBindings.push({ id: 'command-complete', keys: ['Tab'], context: 'command-menu', description: t('screen.key.command-complete') });
|
|
14
|
+
export const KEYBINDINGS = Object.freeze([
|
|
15
|
+
{ id: 'submit', keys: ['Enter'], context: 'composer', description: t('screen.key.submit') },
|
|
16
|
+
{ id: 'newline', keys: ['Shift+Enter', 'Ctrl+J'], context: 'composer', description: t('screen.key.newline') },
|
|
17
|
+
{ id: 'interrupt', keys: ['Ctrl+C', 'Esc'], context: 'global', description: t('screen.key.interrupt') },
|
|
18
|
+
{ id: 'exit', keys: ['Ctrl+D', 'Ctrl+C Ctrl+C'], context: 'global', description: t('screen.key.exit') },
|
|
19
|
+
/* R4 Q-R4-7 (gate E16): the draft keys are listed where /help and the command reference read them. `Esc Esc` is two
|
|
20
|
+
presses (interrupt.ts decides it); a person may bind `clear-draft` to one key of their own. */
|
|
21
|
+
{ id: 'clear-draft', keys: ['Esc Esc'], context: 'composer', description: t('screen.key.clear-draft') },
|
|
22
|
+
{ id: 'undo', keys: ['Ctrl+Z', 'Alt+Z'], context: 'composer', description: t('screen.key.undo') },
|
|
23
|
+
{ id: 'redo', keys: ['Ctrl+Shift+Z', 'Alt+Shift+Z', 'Alt+/'], context: 'composer', description: t('screen.key.redo') },
|
|
24
|
+
{ id: 'vim-toggle', keys: ['Alt+V'], context: 'composer', description: t('screen.key.vim-toggle') },
|
|
25
|
+
{ id: 'steer', keys: ['Alt+S'], context: 'composer', description: t('screen.key.steer') },
|
|
26
|
+
{ id: 'agent-tree', keys: ['Alt+A'], context: 'composer', description: t('screen.key.agent-tree') },
|
|
27
|
+
{ id: 'left', keys: ['Left', 'Ctrl+B'], context: 'composer', description: t('screen.key.left') },
|
|
28
|
+
{ id: 'right', keys: ['Right', 'Ctrl+F'], context: 'composer', description: t('screen.key.right') },
|
|
29
|
+
{ id: 'home', keys: ['Home', 'Ctrl+A'], context: 'composer', description: t('screen.key.home') },
|
|
30
|
+
{ id: 'end', keys: ['End', 'Ctrl+E'], context: 'composer', description: t('screen.key.end') },
|
|
31
|
+
{ id: 'word-left', keys: ['Alt+B'], context: 'composer', description: t('screen.key.word-left') },
|
|
32
|
+
{ id: 'word-right', keys: ['Alt+F'], context: 'composer', description: t('screen.key.word-right') },
|
|
33
|
+
{ id: 'select-left', keys: ['Shift+Left'], context: 'composer', description: t('screen.key.select-left') },
|
|
34
|
+
{ id: 'select-right', keys: ['Shift+Right'], context: 'composer', description: t('screen.key.select-right') },
|
|
35
|
+
{ id: 'select-home', keys: ['Shift+Home'], context: 'composer', description: t('screen.key.select-home') },
|
|
36
|
+
{ id: 'select-end', keys: ['Shift+End'], context: 'composer', description: t('screen.key.select-end') },
|
|
37
|
+
{ id: 'select-up', keys: ['Shift+Up'], context: 'composer', description: t('screen.key.select-up') },
|
|
38
|
+
{ id: 'select-down', keys: ['Shift+Down'], context: 'composer', description: t('screen.key.select-down') },
|
|
39
|
+
{ id: 'select-word-left', keys: ['Alt+Shift+B'], context: 'composer', description: t('screen.key.select-word-left') },
|
|
40
|
+
{ id: 'select-word-right', keys: ['Alt+Shift+F'], context: 'composer', description: t('screen.key.select-word-right') },
|
|
41
|
+
{ id: 'copy-selection', keys: ['Alt+W'], context: 'composer', description: t('screen.key.copy-selection') },
|
|
42
|
+
{ id: 'backspace', keys: ['Backspace'], context: 'composer', description: t('screen.key.backspace') },
|
|
43
|
+
{ id: 'delete-forward', keys: ['Delete', 'Ctrl+D'], context: 'composer', description: t('screen.key.delete-forward') },
|
|
44
|
+
{ id: 'kill-start', keys: ['Ctrl+U'], context: 'composer', description: t('screen.key.kill-start') },
|
|
45
|
+
{ id: 'kill-end', keys: ['Ctrl+K'], context: 'composer', description: t('screen.key.kill-end') },
|
|
46
|
+
{ id: 'kill-word', keys: ['Ctrl+W'], context: 'composer', description: t('screen.key.kill-word') },
|
|
47
|
+
{ id: 'yank', keys: ['Ctrl+Y'], context: 'composer', description: t('screen.key.yank') },
|
|
48
|
+
{ id: 'history-prev', keys: ['Up', 'Ctrl+P'], context: 'history', description: t('screen.key.history-prev') },
|
|
49
|
+
{ id: 'history-next', keys: ['Down', 'Ctrl+N'], context: 'history', description: t('screen.key.history-next') },
|
|
50
|
+
{ id: 'history-search', keys: ['Ctrl+R'], context: 'history', description: t('screen.key.history-search') },
|
|
51
|
+
{ id: 'palette', keys: ['Tab'], context: 'composer', description: t('screen.key.palette') },
|
|
52
|
+
{ id: 'transcript-search', keys: ['Alt+T'], context: 'transcript', description: t('screen.key.transcript-search') },
|
|
53
|
+
{ id: 'transcript-raw', keys: ['Alt+R'], context: 'transcript', description: t('screen.key.transcript-raw') },
|
|
54
|
+
{ id: 'transcript-copy', keys: ['Alt+C'], context: 'transcript', description: t('screen.key.transcript-copy') },
|
|
55
|
+
{ id: 'page-up', keys: ['PageUp'], context: 'transcript', description: t('screen.key.page-up') },
|
|
56
|
+
{ id: 'page-down', keys: ['PageDown'], context: 'transcript', description: t('screen.key.page-down') },
|
|
57
|
+
{ id: 'transcript-top', keys: ['Ctrl+Home'], context: 'transcript', description: t('screen.key.transcript-top') },
|
|
58
|
+
{ id: 'transcript-bottom', keys: ['Ctrl+End'], context: 'transcript', description: t('screen.key.transcript-bottom') },
|
|
59
|
+
/* R4 Q-R4-6: Ctrl+L redraws, keeping the conversation and the draft (Claude Code 2.1.280, 2026-09-22, gate D.4; Codex,
|
|
60
|
+
Gemini, Hermes); the model picker moves to Alt+P (Claude Code `Meta+P`, keybindings.md :105) and the provider picker
|
|
61
|
+
keeps /provider only. */
|
|
62
|
+
{ id: 'redraw', keys: ['Ctrl+L'], context: 'global', description: t('screen.key.redraw') },
|
|
63
|
+
{ id: 'permission-cycle', keys: ['Shift+Tab', 'Alt+M'], context: 'global', description: t('screen.key.permission-cycle') },
|
|
64
|
+
{ id: 'model-picker', keys: ['Alt+P'], context: 'composer', description: t('screen.key.model-picker') },
|
|
65
|
+
{ id: 'provider-picker', keys: [], context: 'composer', description: t('screen.key.provider-picker') },
|
|
66
|
+
{ id: 'reasoning-toggle', keys: ['Ctrl+T'], context: 'global', description: t('screen.key.reasoning-toggle') },
|
|
67
|
+
{ id: 'external-editor', keys: ['Ctrl+G'], context: 'composer', description: t('screen.key.external-editor') },
|
|
68
|
+
{ id: 'tool-details', keys: ['Ctrl+O'], context: 'global', description: t('screen.key.tool-details') },
|
|
69
|
+
{ id: 'approval-once', keys: ['1'], context: 'approval', description: t('screen.key.approval-once') },
|
|
70
|
+
{ id: 'approval-session', keys: ['2'], context: 'approval', description: t('screen.key.approval-session') },
|
|
71
|
+
{ id: 'approval-always', keys: ['3'], context: 'approval', description: t('screen.key.approval-always') },
|
|
72
|
+
{ id: 'approval-deny-once', keys: ['4'], context: 'approval', description: t('screen.key.approval-deny-once') },
|
|
73
|
+
{ id: 'approval-deny-always', keys: ['5'], context: 'approval', description: t('screen.key.approval-deny-always') },
|
|
74
|
+
{ id: 'approval-cancel', keys: ['Esc', 'Ctrl+C'], context: 'approval', description: t('screen.key.approval-cancel') },
|
|
75
|
+
{ id: 'approval-view', keys: ['v'], context: 'approval', description: t('screen.key.approval-view') },
|
|
76
|
+
/* R3 gate E11, Q-R3-6: the four choices are a list with Deny highlighted; ↑/↓ move the highlight, Enter confirms it. */
|
|
77
|
+
{ id: 'approval-up', keys: ['Up'], context: 'approval', description: t('screen.key.approval-up') },
|
|
78
|
+
{ id: 'approval-down', keys: ['Down'], context: 'approval', description: t('screen.key.approval-down') },
|
|
79
|
+
{ id: 'approval-confirm', keys: ['Enter'], context: 'approval', description: t('screen.key.approval-confirm') },
|
|
80
|
+
{ id: 'overlay-close', keys: ['Esc', 'Enter', '?'], context: 'help', description: t('screen.key.overlay-close') },
|
|
81
|
+
{ id: 'help', keys: ['?'], context: 'composer', description: t('screen.key.help') },
|
|
82
|
+
...pickerBindings,
|
|
83
|
+
...commandBindings,
|
|
84
|
+
]);
|
|
85
|
+
export function keybindingHelp(bindings = KEYBINDINGS) { return bindings.filter(x => x.keys.length > 0).map(x => x.keys.join(' / ') + ' — ' + x.description).join('\n'); }
|
|
86
|
+
export function cyclePermissionMode(mode) { return mode === 'default' ? 'acceptEdits' : mode === 'acceptEdits' ? 'plan' : 'default'; }
|
|
87
|
+
export function inkKeyChord(ch, key) {
|
|
88
|
+
if (key.shift && key.tab)
|
|
89
|
+
return 'Shift+Tab';
|
|
90
|
+
if (key.shift && key.return)
|
|
91
|
+
return 'Shift+Enter';
|
|
92
|
+
if (key.shift && (key.pageUp || key.pageup))
|
|
93
|
+
return 'Shift+PageUp';
|
|
94
|
+
if (key.shift && (key.pageDown || key.pagedown))
|
|
95
|
+
return 'Shift+PageDown';
|
|
96
|
+
if (key.shift && key.home)
|
|
97
|
+
return 'Shift+Home';
|
|
98
|
+
if (key.shift && key.end)
|
|
99
|
+
return 'Shift+End';
|
|
100
|
+
if (key.shift && key.leftArrow)
|
|
101
|
+
return 'Shift+Left';
|
|
102
|
+
if (key.shift && key.rightArrow)
|
|
103
|
+
return 'Shift+Right';
|
|
104
|
+
if (key.shift && key.upArrow)
|
|
105
|
+
return 'Shift+Up';
|
|
106
|
+
if (key.shift && key.downArrow)
|
|
107
|
+
return 'Shift+Down';
|
|
108
|
+
if (key.ctrl && key.home)
|
|
109
|
+
return 'Ctrl+Home';
|
|
110
|
+
if (key.ctrl && key.end)
|
|
111
|
+
return 'Ctrl+End'; /* R1 E15: listed in /help; app.ts scrolls on them */
|
|
112
|
+
if (key.ctrl && key.shift && ch)
|
|
113
|
+
return 'Ctrl+Shift+' + ch.toUpperCase();
|
|
114
|
+
if (key.meta && key.shift && ch)
|
|
115
|
+
return 'Alt+Shift+' + ch.toUpperCase();
|
|
116
|
+
if (key.ctrl && ch)
|
|
117
|
+
return 'Ctrl+' + ch.toUpperCase();
|
|
118
|
+
if (key.meta && ch)
|
|
119
|
+
return 'Alt+' + ch.toUpperCase();
|
|
120
|
+
if (key.pageUp || key.pageup)
|
|
121
|
+
return 'PageUp';
|
|
122
|
+
if (key.pageDown || key.pagedown)
|
|
123
|
+
return 'PageDown';
|
|
124
|
+
if (key.home)
|
|
125
|
+
return 'Home';
|
|
126
|
+
if (key.end)
|
|
127
|
+
return 'End';
|
|
128
|
+
if (key.leftArrow)
|
|
129
|
+
return 'Left';
|
|
130
|
+
if (key.rightArrow)
|
|
131
|
+
return 'Right';
|
|
132
|
+
if (key.upArrow)
|
|
133
|
+
return 'Up';
|
|
134
|
+
if (key.downArrow)
|
|
135
|
+
return 'Down';
|
|
136
|
+
if (key.return)
|
|
137
|
+
return 'Enter';
|
|
138
|
+
if (key.tab)
|
|
139
|
+
return 'Tab';
|
|
140
|
+
if (key.escape)
|
|
141
|
+
return 'Esc';
|
|
142
|
+
if (key.backspace)
|
|
143
|
+
return 'Backspace';
|
|
144
|
+
if (key.delete)
|
|
145
|
+
return 'Delete';
|
|
146
|
+
return ch.length === 1 ? ch : null;
|
|
147
|
+
}
|
|
148
|
+
/*
|
|
149
|
+
* R3-KEYS (coordinator, measured in the R3 acceptance run): Ctrl+O typed in the same stdin chunk as text
|
|
150
|
+
* ("\u000fKEYS glued…") reached Ink as ONE input string, so U+000F was inserted into the composer and sent to the model.
|
|
151
|
+
* No C0 or DEL character may reach the editor or the model except a line break: in a mixed chunk the LEADING control
|
|
152
|
+
* character acts as its key (Ctrl+<letter>, Enter, Backspace; LF is Ctrl+J, the newline key), and the rest is text in
|
|
153
|
+
* which CR and CRLF become LF and every other C0/DEL character is dropped. The same text rule applies to a bracketed paste
|
|
154
|
+
* (app.ts). A chunk with no control character, and a key Ink already parsed, pass unchanged.
|
|
155
|
+
* Owner decision (R3 follow-up): TAB is kept. A tab reaches the model unchanged (Gemini CLI's stripUnsafeCharacters keeps
|
|
156
|
+
* 0x09 so a pasted Makefile stays valid); a chunk that STARTS with a tab is text, only a lone tab is the Tab key (Ink parses
|
|
157
|
+
* that one). The composer draws a tab as spaces for display only (components/composer.ts).
|
|
158
|
+
*/
|
|
159
|
+
const CONTROL = /[\x00-\x08\x0b-\x1f\x7f]/u;
|
|
160
|
+
export function sanitizeInputText(text) {
|
|
161
|
+
return text.replace(/\r\n?/gu, '\n').replace(/[\x00-\x08\x0b-\x1f\x7f]/gu, '');
|
|
162
|
+
}
|
|
163
|
+
function leadingKey(code) {
|
|
164
|
+
if (code === 0x0d)
|
|
165
|
+
return { ch: '\r', key: { return: true } };
|
|
166
|
+
if (code === 0x09)
|
|
167
|
+
return null; /* a tab is text (owner decision, R3 follow-up), never Ctrl+I */
|
|
168
|
+
if (code === 0x7f || code === 0x08)
|
|
169
|
+
return { ch: '', key: { backspace: true } };
|
|
170
|
+
if (code === 0x0a)
|
|
171
|
+
return { ch: 'j', key: { ctrl: true } };
|
|
172
|
+
if (code >= 0x01 && code <= 0x1a)
|
|
173
|
+
return { ch: String.fromCharCode(code + 0x60), key: { ctrl: true } };
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
export function splitInputChunk(ch, key) {
|
|
177
|
+
if (typeof ch !== 'string' || ch.length <= 1 || key.ctrl || key.meta || !CONTROL.test(ch.replace(/\n/gu, '')))
|
|
178
|
+
return [{ ch, key }];
|
|
179
|
+
const parts = [];
|
|
180
|
+
const lead = leadingKey(ch.charCodeAt(0));
|
|
181
|
+
const rest = lead ? ch.slice(1) : ch;
|
|
182
|
+
if (lead)
|
|
183
|
+
parts.push(lead);
|
|
184
|
+
const text = sanitizeInputText(rest);
|
|
185
|
+
if (text)
|
|
186
|
+
parts.push({ ch: text, key: {} });
|
|
187
|
+
return parts;
|
|
188
|
+
}
|
|
189
|
+
export function resolveKeybinding(ch, key, contexts, bindings = KEYBINDINGS) { const chord = inkKeyChord(ch, key); if (!chord)
|
|
190
|
+
return null; for (const context of contexts) {
|
|
191
|
+
const binding = bindings.find(row => row.context === context && row.keys.includes(chord));
|
|
192
|
+
if (binding)
|
|
193
|
+
return binding.id;
|
|
194
|
+
} return null; }
|