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
|
@@ -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; }
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { KEYBINDINGS } from "./keybindings.js";
|
|
2
|
+
const SPECIAL = new Map([
|
|
3
|
+
['pageup', 'PageUp'], ['pagedown', 'PageDown'], ['home', 'Home'], ['end', 'End'],
|
|
4
|
+
['left', 'Left'], ['right', 'Right'], ['up', 'Up'], ['down', 'Down'],
|
|
5
|
+
['enter', 'Enter'], ['tab', 'Tab'], ['esc', 'Esc'], ['escape', 'Esc'],
|
|
6
|
+
['backspace', 'Backspace'], ['delete', 'Delete'],
|
|
7
|
+
]);
|
|
8
|
+
const SAFETY = [
|
|
9
|
+
{ bindingId: 'interrupt', chord: 'Ctrl+C' },
|
|
10
|
+
{ bindingId: 'exit', chord: 'Ctrl+D' },
|
|
11
|
+
{ bindingId: 'approval-cancel', chord: 'Esc' },
|
|
12
|
+
];
|
|
13
|
+
export function canonicalKeyChord(value) {
|
|
14
|
+
if (typeof value !== 'string')
|
|
15
|
+
return null;
|
|
16
|
+
const raw = value.trim();
|
|
17
|
+
if (!raw || /[\x00-\x1f\x7f]/u.test(raw))
|
|
18
|
+
return null;
|
|
19
|
+
if ([...raw].length === 1 && raw !== '+')
|
|
20
|
+
return raw;
|
|
21
|
+
const special = SPECIAL.get(raw.toLowerCase());
|
|
22
|
+
if (special)
|
|
23
|
+
return special;
|
|
24
|
+
const shifted = /^shift\+(tab|enter)$/iu.exec(raw);
|
|
25
|
+
if (shifted)
|
|
26
|
+
return shifted[1].toLowerCase() === 'tab' ? 'Shift+Tab' : 'Shift+Enter';
|
|
27
|
+
const modified = /^(ctrl|alt)\+([^+\s])$/iu.exec(raw);
|
|
28
|
+
if (modified) {
|
|
29
|
+
const modifier = modified[1].toLowerCase() === 'ctrl' ? 'Ctrl' : 'Alt';
|
|
30
|
+
return modifier + '+' + modified[2].toUpperCase();
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
function cloneDefaults() { return KEYBINDINGS.map(row => ({ ...row, keys: [...row.keys] })); }
|
|
35
|
+
export function resolveKeymap(overrides) {
|
|
36
|
+
const bindings = cloneDefaults();
|
|
37
|
+
const diagnostics = [];
|
|
38
|
+
if (overrides) {
|
|
39
|
+
for (const [bindingId, rawKeys] of Object.entries(overrides)) {
|
|
40
|
+
const targets = bindings.filter(row => row.id === bindingId);
|
|
41
|
+
if (targets.length === 0) {
|
|
42
|
+
diagnostics.push({ code: 'KEYMAP_UNKNOWN_BINDING', bindingId, message: 'Unknown key binding: ' + bindingId });
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const keys = [];
|
|
46
|
+
const seen = new Set();
|
|
47
|
+
for (const raw of rawKeys) {
|
|
48
|
+
const chord = canonicalKeyChord(raw);
|
|
49
|
+
if (!chord) {
|
|
50
|
+
diagnostics.push({ code: 'KEYMAP_CHORD_INVALID', bindingId, chord: String(raw), message: 'Invalid key chord for ' + bindingId + ': ' + String(raw) });
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (seen.has(chord)) {
|
|
54
|
+
diagnostics.push({ code: 'KEYMAP_DUPLICATE_CHORD', bindingId, chord, message: 'Duplicate key chord for ' + bindingId + ': ' + chord });
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
seen.add(chord);
|
|
58
|
+
keys.push(chord);
|
|
59
|
+
}
|
|
60
|
+
for (const binding of targets)
|
|
61
|
+
binding.keys = [...keys];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const byContext = new Map();
|
|
65
|
+
for (const binding of bindings) {
|
|
66
|
+
for (const chord of binding.keys) {
|
|
67
|
+
const key = binding.context + '\u0000' + chord;
|
|
68
|
+
const previous = byContext.get(key);
|
|
69
|
+
if (previous && previous.bindingId !== binding.id) {
|
|
70
|
+
diagnostics.push({ code: 'KEYMAP_CONFLICT', bindingId: binding.id, chord, conflictWith: previous.bindingId, message: 'Key chord ' + chord + ' conflicts in ' + binding.context + ': ' + previous.bindingId + ' vs ' + binding.id });
|
|
71
|
+
}
|
|
72
|
+
else
|
|
73
|
+
byContext.set(key, { bindingId: binding.id, chord });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
for (const reserved of SAFETY) {
|
|
77
|
+
const binding = bindings.find(row => row.id === reserved.bindingId);
|
|
78
|
+
if (!binding?.keys.includes(reserved.chord)) {
|
|
79
|
+
diagnostics.push({ code: 'KEYMAP_RESERVED_CHORD', bindingId: reserved.bindingId, chord: reserved.chord, message: 'Safety key ' + reserved.chord + ' must remain bound to ' + reserved.bindingId });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { bindings, diagnostics };
|
|
83
|
+
}
|
|
84
|
+
export function assertValidKeymap(overrides) {
|
|
85
|
+
const resolved = resolveKeymap(overrides);
|
|
86
|
+
if (resolved.diagnostics.length) {
|
|
87
|
+
const first = resolved.diagnostics[0];
|
|
88
|
+
throw Object.assign(new Error(first.message), { code: 'KEYMAP_INVALID', details: { diagnostics: resolved.diagnostics } }); /* R2 (gate E6): code on error.code only */
|
|
89
|
+
}
|
|
90
|
+
return resolved.bindings;
|
|
91
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* R4 CONTRACT (owner: lane F; stub written by the coordinator so both lanes compile). Owner decision Q-R4-4: the full boot
|
|
3
|
+
* plays only on the first launch and after an update (a new version or build commit); every other launch gets the short
|
|
4
|
+
* boot (the final frame, at most 600 ms). The launch state lives under the data root, never the config root (gate E5), so
|
|
5
|
+
* a test profile starts empty and its first launch is a full boot. Lane N only CALLS bootPlan (app.ts) and passes `mode`
|
|
6
|
+
* to BootSequence; lane F owns the body (reading and recording the launch) and may not change the signature.
|
|
7
|
+
*
|
|
8
|
+
* R4 lane F (the body):
|
|
9
|
+
* - The record is `<dataRoot>/launch-state.json`, schema `talos.cli.launch-state.v1`: the version and build seen by the last
|
|
10
|
+
* launch, when TALOS first ran and when it last ran. Nothing else is kept (no path, no project, no person's data).
|
|
11
|
+
* - It fails TOWARD the full boot: a record that is missing, unreadable or of another shape is a first launch, and a record
|
|
12
|
+
* that cannot be written (read-only profile, a file where the folder should be) never throws — that launch just plays the
|
|
13
|
+
* full boot again, which is the harmless side.
|
|
14
|
+
* - A build that cannot be read (`build` null or absent) is not an update: development builds report no build, and a
|
|
15
|
+
* missing value must not replay the full boot at every start. A different non-empty build is an update.
|
|
16
|
+
* - Written through a temporary file and a rename, so a crash or a second TALOS starting at the same moment leaves either
|
|
17
|
+
* the old record or the new one, never half of one (the pattern of `security/project-trust.ts` atomicWriteText).
|
|
18
|
+
* - Synchronous on purpose: lane N calls it once while the first frame is prepared, and the file is a few hundred bytes.
|
|
19
|
+
*/
|
|
20
|
+
import { mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
21
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
const SCHEMA = 'talos.cli.launch-state.v1';
|
|
24
|
+
export function launchStatePath(dataRoot) { return join(dataRoot, 'launch-state.json'); }
|
|
25
|
+
function readRecord(file) {
|
|
26
|
+
try {
|
|
27
|
+
const value = JSON.parse(readFileSync(file, 'utf8'));
|
|
28
|
+
if (!value || value.schema !== SCHEMA || typeof value.version !== 'string' || (value.build !== null && typeof value.build !== 'string')
|
|
29
|
+
|| typeof value.firstLaunchAt !== 'string' || typeof value.lastLaunchAt !== 'string')
|
|
30
|
+
return null;
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function writeRecord(dataRoot, record) {
|
|
38
|
+
const file = launchStatePath(dataRoot);
|
|
39
|
+
const temporary = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
40
|
+
try {
|
|
41
|
+
mkdirSync(dataRoot, { recursive: true });
|
|
42
|
+
writeFileSync(temporary, `${JSON.stringify(record, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
43
|
+
renameSync(temporary, file);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
try {
|
|
47
|
+
rmSync(temporary, { force: true });
|
|
48
|
+
}
|
|
49
|
+
catch { /* the temporary file was never created */ }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** P17: whether this profile has never launched the interface, read without recording a launch (the trust question asks). */
|
|
53
|
+
export function isFirstLaunch(dataRoot) {
|
|
54
|
+
try {
|
|
55
|
+
return readRecord(launchStatePath(dataRoot)) === null;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export function bootPlan(input) {
|
|
62
|
+
const previous = readRecord(launchStatePath(input.dataRoot));
|
|
63
|
+
const build = typeof input.build === 'string' && input.build !== '' ? input.build : null;
|
|
64
|
+
const firstLaunch = previous === null;
|
|
65
|
+
const updated = !firstLaunch && (previous.version !== input.version || (build !== null && previous.build !== null && previous.build !== build));
|
|
66
|
+
const at = new Date(input.now ?? Date.now()).toISOString();
|
|
67
|
+
writeRecord(input.dataRoot, { schema: SCHEMA, version: input.version, build: build ?? previous?.build ?? null, firstLaunchAt: previous?.firstLaunchAt ?? at, lastLaunchAt: at });
|
|
68
|
+
return { mode: firstLaunch || updated ? 'full' : 'short', firstLaunch };
|
|
69
|
+
}
|
|
70
|
+
/*
|
|
71
|
+
* R4 Q-R4-4 (i)(a): the build commit, so that "after an update" also means a new commit of a development checkout (whose
|
|
72
|
+
* version is always 1.0.0). It is the commit git records for the checkout the running CLI comes from, read the way git
|
|
73
|
+
* stores it — HEAD, a branch ref or packed-refs, and for a linked worktree its `.git` file and `commondir` — without spawning
|
|
74
|
+
* git. An installed copy has no checkout: null, and the version alone decides (a release has its own version). Read once
|
|
75
|
+
* per process; a checkout that cannot be read is null, never an error (the boot falls back to the version).
|
|
76
|
+
*/
|
|
77
|
+
const SHA = /^[0-9a-f]{40}$/u;
|
|
78
|
+
function readText(file) { try {
|
|
79
|
+
return readFileSync(file, 'utf8');
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return null;
|
|
83
|
+
} }
|
|
84
|
+
function gitDirFrom(start) {
|
|
85
|
+
let cursor = resolve(start);
|
|
86
|
+
for (let depth = 0; depth < 12; depth++) {
|
|
87
|
+
const dotGit = join(cursor, '.git');
|
|
88
|
+
try {
|
|
89
|
+
const info = statSync(dotGit);
|
|
90
|
+
if (info.isDirectory())
|
|
91
|
+
return dotGit;
|
|
92
|
+
if (info.isFile()) {
|
|
93
|
+
const match = /^gitdir:\s*(.+?)\s*$/mu.exec(readText(dotGit) ?? '');
|
|
94
|
+
if (!match)
|
|
95
|
+
return null;
|
|
96
|
+
return isAbsolute(match[1]) ? match[1] : resolve(cursor, match[1]);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch { /* no .git here: go up */ }
|
|
100
|
+
const parent = dirname(cursor);
|
|
101
|
+
if (parent === cursor)
|
|
102
|
+
return null;
|
|
103
|
+
cursor = parent;
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
function refCommit(gitDir, commonDir, ref) {
|
|
108
|
+
if (!/^refs\/[\w./-]+$/u.test(ref) || ref.includes('..'))
|
|
109
|
+
return null;
|
|
110
|
+
for (const dir of [gitDir, commonDir]) {
|
|
111
|
+
const loose = readText(join(dir, ...ref.split('/')))?.trim();
|
|
112
|
+
if (loose && SHA.test(loose))
|
|
113
|
+
return loose;
|
|
114
|
+
}
|
|
115
|
+
for (const line of (readText(join(commonDir, 'packed-refs')) ?? '').split(/\r?\n/u)) {
|
|
116
|
+
const [sha, name] = line.trim().split(' ');
|
|
117
|
+
if (name === ref && sha && SHA.test(sha))
|
|
118
|
+
return sha;
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
export function buildIdFrom(start) {
|
|
123
|
+
const gitDir = gitDirFrom(start);
|
|
124
|
+
if (!gitDir)
|
|
125
|
+
return null;
|
|
126
|
+
const common = readText(join(gitDir, 'commondir'))?.trim();
|
|
127
|
+
const commonDir = common ? resolve(gitDir, common) : gitDir;
|
|
128
|
+
const head = readText(join(gitDir, 'HEAD'))?.trim() ?? '';
|
|
129
|
+
if (SHA.test(head))
|
|
130
|
+
return head;
|
|
131
|
+
const match = /^ref:\s*(\S+)$/u.exec(head);
|
|
132
|
+
return match ? refCommit(gitDir, commonDir, match[1]) : null;
|
|
133
|
+
}
|
|
134
|
+
let cachedBuildId;
|
|
135
|
+
/** The running CLI's build commit (or that of the checkout holding `start`); null for an installed copy. */
|
|
136
|
+
export function currentBuildId(start) {
|
|
137
|
+
if (start !== undefined)
|
|
138
|
+
return buildIdFrom(start);
|
|
139
|
+
if (cachedBuildId === undefined) {
|
|
140
|
+
try {
|
|
141
|
+
cachedBuildId = buildIdFrom(dirname(fileURLToPath(import.meta.url)));
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
cachedBuildId = null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return cachedBuildId;
|
|
148
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* R3 CONTRACT (owner: lane R+D; stub written by the coordinator so both lanes compile). The ONE line diff (Q-R3-11: jsdiff
|
|
3
|
+
* 9.0.0, pinned) feeding the write/edit row summary, its Ctrl+O diff and the approval preview (gate E10). It returns the
|
|
4
|
+
* existing DiffModel so components/diff.ts renders it. Lane S+P only CALLS lineDiff; lane R+D owns the body and may not
|
|
5
|
+
* change the signature.
|
|
6
|
+
*
|
|
7
|
+
* R3 lane RD body. jsdiff's `structuredPatch` (Myers, the library Gemini 8.0.3, Pi 8.0.4, Qwen ^7 and OpenCode diff with,
|
|
8
|
+
* gate §3.2) with 3 context lines, the same as `/diff` (`git diff --unified=3`). The counts are the `+`/`-` lines of the
|
|
9
|
+
* hunks, so a moved line and an edit of one of two identical lines are seen — the multiset count it replaces saw neither.
|
|
10
|
+
* Lines are compared exactly as written (a CRLF → LF change is a change); a trailing `\r` never reaches the screen.
|
|
11
|
+
* A change too large to compare in time (jsdiff `timeout`, and a line cap that keeps a huge file off the event loop)
|
|
12
|
+
* returns the whole replacement flagged `approximate` with one plain row saying so: never a fake minimal diff.
|
|
13
|
+
*/
|
|
14
|
+
import { structuredPatch } from 'diff';
|
|
15
|
+
import { t } from "../i18n/index.js";
|
|
16
|
+
/* Beyond these the diff is not attempted (Myers is O((N+M)·D)); the row then states the sizes, not a count it cannot know. */
|
|
17
|
+
const MAX_LINES = 50_000;
|
|
18
|
+
const TIMEOUT_MS = 250;
|
|
19
|
+
const CONTEXT = 3;
|
|
20
|
+
function linesOf(text) { if (text === '')
|
|
21
|
+
return 0; const n = text.split('\n').length; return text.endsWith('\n') ? n - 1 : n; }
|
|
22
|
+
function row(kind, text, oldLine = null, newLine = null) { return { kind, text: text.endsWith('\r') ? text.slice(0, -1) : text, oldLine, newLine }; }
|
|
23
|
+
export function lineDiff(before, after, { path = 'file' } = {}) {
|
|
24
|
+
const old = before ?? '', next = String(after ?? '');
|
|
25
|
+
if (old === next)
|
|
26
|
+
return { added: 0, removed: 0, model: { files: [], plain: [], empty: true } };
|
|
27
|
+
const oldCount = linesOf(old), newCount = linesOf(next);
|
|
28
|
+
const patch = oldCount + newCount > MAX_LINES ? undefined : structuredPatch(path, path, old, next, undefined, undefined, { context: CONTEXT, timeout: TIMEOUT_MS });
|
|
29
|
+
if (!patch) {
|
|
30
|
+
const note = row('plain', t('tool.diff.tooLarge', { before: oldCount, after: newCount }));
|
|
31
|
+
return { added: newCount, removed: oldCount, approximate: true, model: { files: [], plain: [note], empty: false } };
|
|
32
|
+
}
|
|
33
|
+
const rows = [];
|
|
34
|
+
let added = 0, removed = 0;
|
|
35
|
+
for (const hunk of patch.hunks) {
|
|
36
|
+
/* The header the way `git diff` and jsdiff's own formatPatch print it: an empty side starts one line earlier. */
|
|
37
|
+
const oldStart = hunk.oldLines === 0 ? hunk.oldStart - 1 : hunk.oldStart, newStart = hunk.newLines === 0 ? hunk.newStart - 1 : hunk.newStart;
|
|
38
|
+
rows.push(row('hunk', `@@ -${oldStart},${hunk.oldLines} +${newStart},${hunk.newLines} @@`));
|
|
39
|
+
let oldLine = hunk.oldStart, newLine = hunk.newStart;
|
|
40
|
+
for (const text of hunk.lines) {
|
|
41
|
+
const mark = text[0], body = text.slice(1);
|
|
42
|
+
if (mark === '+') {
|
|
43
|
+
rows.push(row('add', body, null, newLine++));
|
|
44
|
+
added++;
|
|
45
|
+
}
|
|
46
|
+
else if (mark === '-') {
|
|
47
|
+
rows.push(row('remove', body, oldLine++, null));
|
|
48
|
+
removed++;
|
|
49
|
+
}
|
|
50
|
+
else if (mark === '\\')
|
|
51
|
+
rows.push(row('no-newline', text));
|
|
52
|
+
else
|
|
53
|
+
rows.push(row('context', body, oldLine++, newLine++));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { added, removed, model: { files: [{ oldPath: path, newPath: path, rows }], plain: [], empty: rows.length === 0 } };
|
|
57
|
+
}
|