harness-mix 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 +202 -0
- package/NOTICE +14 -0
- package/README.md +184 -0
- package/Start-Codex.cmd +5 -0
- package/config/codex-desktop-compatibility.json +23 -0
- package/docs/chatgpt-web-sidebar.md +27 -0
- package/docs/harness-management.md +41 -0
- package/docs/images/harness-mix-home.png +0 -0
- package/docs/images/harness-mix-session.png +0 -0
- package/docs/multi-agent-collaboration.md +89 -0
- package/docs/native-codex.md +72 -0
- package/licenses/codex-host-MIT.txt +21 -0
- package/output/native-build/desktop-controller.mjs +1460 -0
- package/output/native-build/harness-mix-appx.exe +0 -0
- package/output/native-build/harness-mix-shim.exe +0 -0
- package/output/native-build/renderer-extension.js +28884 -0
- package/package.json +142 -0
- package/scripts/acp-capabilities-probe.cjs +100 -0
- package/scripts/adapters-test.cjs +17 -0
- package/scripts/antigravity-adapter-test.cjs +174 -0
- package/scripts/architecture-test.cjs +62 -0
- package/scripts/build-native.cjs +46 -0
- package/scripts/catalog-test.cjs +25 -0
- package/scripts/cdp-eval.cjs +40 -0
- package/scripts/cdp-watch.cjs +36 -0
- package/scripts/check.cjs +23 -0
- package/scripts/codex-adapter-test.cjs +83 -0
- package/scripts/collaboration-recovery-test.cjs +91 -0
- package/scripts/collaboration-test.cjs +102 -0
- package/scripts/collaboration-ui-smoke.cjs +52 -0
- package/scripts/concurrency-layers-test.cjs +179 -0
- package/scripts/contracts-test.cjs +105 -0
- package/scripts/core-concurrency-test.cjs +103 -0
- package/scripts/core-replay-test.cjs +179 -0
- package/scripts/core-runtime-test.cjs +104 -0
- package/scripts/core-services-test.cjs +50 -0
- package/scripts/determinism-test.cjs +36 -0
- package/scripts/docs-command-test.cjs +49 -0
- package/scripts/dsh-adapter-test.cjs +87 -0
- package/scripts/dsh-native.cjs +9 -0
- package/scripts/dsh-native.cmd +2 -0
- package/scripts/e2e-codex.cjs +82 -0
- package/scripts/e2e-collaboration.cjs +58 -0
- package/scripts/e2e-core-files.cjs +58 -0
- package/scripts/e2e-core-fork.cjs +49 -0
- package/scripts/e2e-core-resume.cjs +49 -0
- package/scripts/e2e-delegate.cjs +38 -0
- package/scripts/e2e-dsh.cjs +23 -0
- package/scripts/e2e-native-commands.cjs +54 -0
- package/scripts/e2e-native-host.cjs +144 -0
- package/scripts/e2e-openclaw-image.cjs +59 -0
- package/scripts/e2e-openclaw.cjs +21 -0
- package/scripts/e2e-pi.cjs +23 -0
- package/scripts/e2e-shadow.cjs +52 -0
- package/scripts/fixture-capture.cjs +103 -0
- package/scripts/git-test.cjs +33 -0
- package/scripts/launch-codex.cjs +5 -0
- package/scripts/native-compatibility-test.cjs +42 -0
- package/scripts/native-config-test.cjs +25 -0
- package/scripts/native-file-change-test.cjs +94 -0
- package/scripts/native-host.cjs +8 -0
- package/scripts/native-images-test.cjs +56 -0
- package/scripts/native-protocol-test.cjs +233 -0
- package/scripts/native-rollback-test.cjs +46 -0
- package/scripts/native-sidebar-test.cjs +88 -0
- package/scripts/native-ui-smoke.cjs +94 -0
- package/scripts/native-updater-test.cjs +100 -0
- package/scripts/native-vendor-adapters-test.cjs +40 -0
- package/scripts/openclaw-acp-probe.cjs +39 -0
- package/scripts/openclaw-adapter-test.cjs +180 -0
- package/scripts/openclaw-models-probe.cjs +13 -0
- package/scripts/projector-test.cjs +144 -0
- package/scripts/sequence-test.cjs +57 -0
- package/scripts/shadow-test.cjs +154 -0
- package/scripts/support/legacy-projection.cjs +95 -0
- package/scripts/support/legacy-transcript.cjs +48 -0
- package/scripts/support/parity-observer.cjs +27 -0
- package/scripts/support/shadow-comparator.cjs +62 -0
- package/scripts/support/shadow.cjs +114 -0
- package/scripts/switch-harness-test.cjs +145 -0
- package/scripts/tool-concurrency-regression-test.cjs +118 -0
- package/scripts/transcript-test.cjs +24 -0
- package/scripts/turn-manager-test.cjs +91 -0
- package/scripts/workspace-test.cjs +98 -0
- package/src/assets/brand-harness-mix.png +0 -0
- package/src/assets/icons/activity-compact.svg +1 -0
- package/src/assets/icons/activity-edit.svg +1 -0
- package/src/assets/icons/activity-terminal.svg +1 -0
- package/src/assets/icons/antigravity-color.svg +1 -0
- package/src/assets/icons/claude-color.svg +1 -0
- package/src/assets/icons/codex-color.svg +1 -0
- package/src/assets/icons/codex-harness.svg +1 -0
- package/src/assets/icons/commands.svg +1 -0
- package/src/assets/icons/deepseek-color.svg +1 -0
- package/src/assets/icons/grok-color.svg +1 -0
- package/src/assets/icons/hermes-color.svg +1 -0
- package/src/assets/icons/home-chat.svg +1 -0
- package/src/assets/icons/home-folder.svg +1 -0
- package/src/assets/icons/model-astra.svg +1 -0
- package/src/assets/icons/model-claude.svg +1 -0
- package/src/assets/icons/model-deepseek.svg +1 -0
- package/src/assets/icons/model-gemini.svg +1 -0
- package/src/assets/icons/model-hunyuan.svg +1 -0
- package/src/assets/icons/model-kimi.svg +1 -0
- package/src/assets/icons/model-minimax.svg +1 -0
- package/src/assets/icons/model-openai.svg +1 -0
- package/src/assets/icons/model-qwen-color.svg +1 -0
- package/src/assets/icons/model-xiaomimimo.svg +1 -0
- package/src/assets/icons/model-zai.svg +1 -0
- package/src/assets/icons/omp-color.svg +1 -0
- package/src/assets/icons/openclaw-color.svg +1 -0
- package/src/assets/icons/opencode-color.svg +1 -0
- package/src/assets/icons/pi.svg +1 -0
- package/src/assets/icons/pinumber1_80899.svg +48 -0
- package/src/assets/icons/qoder-color.svg +1 -0
- package/src/assets/icons/thread-fork.svg +1 -0
- package/src/assets/icons/trae-color.svg +1 -0
- package/src/assets/icons/workbuddy-color.svg +1 -0
- package/src/assets/icons/zcode-color.svg +1 -0
- package/src/main/adapters/acp.js +171 -0
- package/src/main/adapters/antigravity.js +895 -0
- package/src/main/adapters/claude.js +399 -0
- package/src/main/adapters/codex-app-server.js +119 -0
- package/src/main/adapters/codex.js +547 -0
- package/src/main/adapters/dsh-web-host.js +207 -0
- package/src/main/adapters/dsh.js +415 -0
- package/src/main/adapters/grok.js +161 -0
- package/src/main/adapters/hermes.js +24 -0
- package/src/main/adapters/index.js +30 -0
- package/src/main/adapters/omp.js +12 -0
- package/src/main/adapters/openclaw-gateway.js +215 -0
- package/src/main/adapters/openclaw.js +337 -0
- package/src/main/adapters/opencode-server.js +72 -0
- package/src/main/adapters/opencode.js +166 -0
- package/src/main/adapters/pi-family.js +315 -0
- package/src/main/adapters/pi-usage.js +24 -0
- package/src/main/adapters/pi.js +10 -0
- package/src/main/adapters/qoder.js +35 -0
- package/src/main/adapters/trae.js +19 -0
- package/src/main/adapters/workbuddy.js +67 -0
- package/src/main/adapters/zcode.js +38 -0
- package/src/main/harness-adapter/contract-test.js +53 -0
- package/src/main/harness-adapter/contract.js +34 -0
- package/src/main/harness-adapter/event-normalizer.js +233 -0
- package/src/main/harness-adapter/fixture-recorder.js +56 -0
- package/src/main/harness-adapter/index.js +6 -0
- package/src/main/harness-adapter/manifest.js +44 -0
- package/src/main/host/collaboration-mcp.cjs +35 -0
- package/src/main/host/collaboration-pi.mjs +17 -0
- package/src/main/host/collaboration-tools.js +16 -0
- package/src/main/host/collaboration-worktree.js +82 -0
- package/src/main/host/collaboration.js +238 -0
- package/src/main/host/core-session.js +81 -0
- package/src/main/host/handoff.js +46 -0
- package/src/main/host/history-import.js +55 -0
- package/src/main/host/jsonl.js +115 -0
- package/src/main/host/native-history.js +74 -0
- package/src/main/host/runtime.js +927 -0
- package/src/main/host/session-history.js +63 -0
- package/src/main/host/store.js +42 -0
- package/src/main/native/compatibility.js +55 -0
- package/src/main/native/config.js +49 -0
- package/src/main/native/host.js +108 -0
- package/src/main/native/icons.js +101 -0
- package/src/main/native/input.js +43 -0
- package/src/main/native/launcher.js +100 -0
- package/src/main/native/protocol.js +552 -0
- package/src/main/native/rs/Cargo.lock +11 -0
- package/src/main/native/rs/Cargo.toml +9 -0
- package/src/main/native/rs/crates/appx/Cargo.toml +13 -0
- package/src/main/native/rs/crates/appx/src/main.rs +309 -0
- package/src/main/native/rs/crates/shim/Cargo.toml +10 -0
- package/src/main/native/rs/crates/shim/src/main.rs +207 -0
- package/src/main/native/thread-list.js +38 -0
- package/src/main/native/updater.js +102 -0
- package/src/main/native/usage.js +29 -0
- package/src/main/protocol-core/capability-manager.js +16 -0
- package/src/main/protocol-core/event-router.js +32 -0
- package/src/main/protocol-core/file-change-projector.js +45 -0
- package/src/main/protocol-core/index.js +8 -0
- package/src/main/protocol-core/interaction-router.js +30 -0
- package/src/main/protocol-core/projector.js +238 -0
- package/src/main/protocol-core/protocol-core.js +132 -0
- package/src/main/protocol-core/sequence-validator.js +44 -0
- package/src/main/protocol-core/thread-manager.js +50 -0
- package/src/main/protocol-core/turn-manager.js +63 -0
- package/src/main/shared-contracts/capability.js +35 -0
- package/src/main/shared-contracts/event.js +55 -0
- package/src/main/shared-contracts/ids.js +12 -0
- package/src/main/shared-contracts/index.js +11 -0
- package/src/main/shared-contracts/interaction.js +37 -0
- package/src/main/shared-contracts/item.js +50 -0
- package/src/main/shared-contracts/native-ref.js +24 -0
- package/src/main/shared-contracts/thread.js +36 -0
- package/src/main/shared-contracts/turn.js +54 -0
- package/src/main/workspace/core-review.js +53 -0
- package/src/main/workspace/diff.js +27 -0
- package/src/main/workspace/file-changes.js +21 -0
- package/src/main/workspace/files.js +65 -0
- package/src/main/workspace/git.js +73 -0
- package/src/main/workspace/ipc.js +50 -0
- package/src/main/workspace/review-controller.js +102 -0
- package/src/main/workspace/review.js +83 -0
- package/src/main/workspace/terminal.js +36 -0
- package/src/native-ui/LICENSE +21 -0
- package/src/native-ui/README.md +12 -0
- package/src/native-ui/desktop-control/dist/cdp-client.d.ts +53 -0
- package/src/native-ui/desktop-control/dist/cdp-client.d.ts.map +1 -0
- package/src/native-ui/desktop-control/dist/cdp-client.js +258 -0
- package/src/native-ui/desktop-control/dist/cdp-client.js.map +1 -0
- package/src/native-ui/desktop-control/dist/contract-audit.d.ts +73 -0
- package/src/native-ui/desktop-control/dist/contract-audit.d.ts.map +1 -0
- package/src/native-ui/desktop-control/dist/contract-audit.js +152 -0
- package/src/native-ui/desktop-control/dist/contract-audit.js.map +1 -0
- package/src/native-ui/desktop-control/dist/controller-attachment-server.d.ts +10 -0
- package/src/native-ui/desktop-control/dist/controller-attachment-server.d.ts.map +1 -0
- package/src/native-ui/desktop-control/dist/controller-attachment-server.js +77 -0
- package/src/native-ui/desktop-control/dist/controller-attachment-server.js.map +1 -0
- package/src/native-ui/desktop-control/dist/index.d.ts +21 -0
- package/src/native-ui/desktop-control/dist/index.d.ts.map +1 -0
- package/src/native-ui/desktop-control/dist/index.js +14 -0
- package/src/native-ui/desktop-control/dist/index.js.map +1 -0
- package/src/native-ui/desktop-control/dist/main-process-title-policy.d.ts +24 -0
- package/src/native-ui/desktop-control/dist/main-process-title-policy.d.ts.map +1 -0
- package/src/native-ui/desktop-control/dist/main-process-title-policy.js +246 -0
- package/src/native-ui/desktop-control/dist/main-process-title-policy.js.map +1 -0
- package/src/native-ui/desktop-control/dist/production-controller.d.ts +32 -0
- package/src/native-ui/desktop-control/dist/production-controller.d.ts.map +1 -0
- package/src/native-ui/desktop-control/dist/production-controller.js +291 -0
- package/src/native-ui/desktop-control/dist/production-controller.js.map +1 -0
- package/src/native-ui/desktop-control/dist/release-main.d.ts +2 -0
- package/src/native-ui/desktop-control/dist/release-main.d.ts.map +1 -0
- package/src/native-ui/desktop-control/dist/release-main.js +17 -0
- package/src/native-ui/desktop-control/dist/release-main.js.map +1 -0
- package/src/native-ui/desktop-control/dist/renderer-cdp-control-session.d.ts +49 -0
- package/src/native-ui/desktop-control/dist/renderer-cdp-control-session.d.ts.map +1 -0
- package/src/native-ui/desktop-control/dist/renderer-cdp-control-session.js +214 -0
- package/src/native-ui/desktop-control/dist/renderer-cdp-control-session.js.map +1 -0
- package/src/native-ui/desktop-control/dist/renderer-control-session.d.ts +79 -0
- package/src/native-ui/desktop-control/dist/renderer-control-session.d.ts.map +1 -0
- package/src/native-ui/desktop-control/dist/renderer-control-session.js +366 -0
- package/src/native-ui/desktop-control/dist/renderer-control-session.js.map +1 -0
- package/src/native-ui/desktop-control/dist/renderer-dom.d.ts +24 -0
- package/src/native-ui/desktop-control/dist/renderer-dom.d.ts.map +1 -0
- package/src/native-ui/desktop-control/dist/renderer-dom.js +110 -0
- package/src/native-ui/desktop-control/dist/renderer-dom.js.map +1 -0
- package/src/native-ui/desktop-control/dist/renderer-draft-prewarm-policy.d.ts +19 -0
- package/src/native-ui/desktop-control/dist/renderer-draft-prewarm-policy.d.ts.map +1 -0
- package/src/native-ui/desktop-control/dist/renderer-draft-prewarm-policy.js +162 -0
- package/src/native-ui/desktop-control/dist/renderer-draft-prewarm-policy.js.map +1 -0
- package/src/native-ui/desktop-control/dist/renderer-draft-prewarm-runtime.d.ts +38 -0
- package/src/native-ui/desktop-control/dist/renderer-draft-prewarm-runtime.d.ts.map +1 -0
- package/src/native-ui/desktop-control/dist/renderer-draft-prewarm-runtime.js +633 -0
- package/src/native-ui/desktop-control/dist/renderer-draft-prewarm-runtime.js.map +1 -0
- package/src/native-ui/desktop-control/dist/tsconfig.tsbuildinfo +1 -0
- package/src/native-ui/desktop-control/package.json +22 -0
- package/src/native-ui/desktop-control/scripts/build-release.mjs +94 -0
- package/src/native-ui/desktop-control/src/cdp-client.ts +347 -0
- package/src/native-ui/desktop-control/src/contract-audit.ts +291 -0
- package/src/native-ui/desktop-control/src/controller-attachment-server.ts +93 -0
- package/src/native-ui/desktop-control/src/index.ts +91 -0
- package/src/native-ui/desktop-control/src/main-process-title-policy.ts +334 -0
- package/src/native-ui/desktop-control/src/production-controller.ts +344 -0
- package/src/native-ui/desktop-control/src/release-main.ts +18 -0
- package/src/native-ui/desktop-control/src/renderer-cdp-control-session.ts +358 -0
- package/src/native-ui/desktop-control/src/renderer-control-session.ts +595 -0
- package/src/native-ui/desktop-control/src/renderer-dom.ts +144 -0
- package/src/native-ui/desktop-control/src/renderer-draft-prewarm-policy.ts +209 -0
- package/src/native-ui/desktop-control/src/renderer-draft-prewarm-runtime.ts +746 -0
- package/src/native-ui/desktop-control/test/cdp-client.test.ts +157 -0
- package/src/native-ui/desktop-control/test/contract-audit.test.ts +56 -0
- package/src/native-ui/desktop-control/test/controller-attachment-server.test.ts +98 -0
- package/src/native-ui/desktop-control/test/index.test.ts +11 -0
- package/src/native-ui/desktop-control/test/main-process-title-policy.test.ts +166 -0
- package/src/native-ui/desktop-control/test/production-controller.test.ts +389 -0
- package/src/native-ui/desktop-control/test/renderer-cdp-control-session.test.ts +205 -0
- package/src/native-ui/desktop-control/test/renderer-control-session.test.ts +451 -0
- package/src/native-ui/desktop-control/test/renderer-dom.test.ts +62 -0
- package/src/native-ui/desktop-control/test/renderer-draft-prewarm-policy.test.ts +998 -0
- package/src/native-ui/desktop-control/tsconfig.json +12 -0
- package/src/native-ui/renderer-extension/dist/types/agent-group-preference.d.ts +39 -0
- package/src/native-ui/renderer-extension/dist/types/agent-group-preference.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/agent-selection-state.d.ts +83 -0
- package/src/native-ui/renderer-extension/dist/types/agent-selection-state.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/audit-entry.d.ts +2 -0
- package/src/native-ui/renderer-extension/dist/types/audit-entry.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/codex-locale-adapter.d.ts +23 -0
- package/src/native-ui/renderer-extension/dist/types/codex-locale-adapter.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/collaboration-icon.d.ts +2 -0
- package/src/native-ui/renderer-extension/dist/types/collaboration-icon.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/contract-audit.d.ts +37 -0
- package/src/native-ui/renderer-extension/dist/types/contract-audit.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/harness-mix-icons.d.ts +3 -0
- package/src/native-ui/renderer-extension/dist/types/harness-mix-icons.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/harness-mix-settings.d.ts +3 -0
- package/src/native-ui/renderer-extension/dist/types/harness-mix-settings.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/index.d.ts +49 -0
- package/src/native-ui/renderer-extension/dist/types/index.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/install-renderer-binding.d.ts +4 -0
- package/src/native-ui/renderer-extension/dist/types/install-renderer-binding.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/probe-entry.d.ts +2 -0
- package/src/native-ui/renderer-extension/dist/types/probe-entry.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/production-entry.d.ts +9 -0
- package/src/native-ui/renderer-extension/dist/types/production-entry.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-agent-icon.d.ts +4 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-agent-icon.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-agent-picker.d.ts +62 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-agent-picker.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-binding-probe.d.ts +70 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-binding-probe.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-codex-account-options.d.ts +34 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-codex-account-options.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-codex-account-state.d.ts +19 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-codex-account-state.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-composer-dom.d.ts +82 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-composer-dom.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-credits-control.d.ts +21 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-credits-control.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-external-steering.d.ts +7 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-external-steering.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-fork-control.d.ts +34 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-fork-control.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-harness-command-claim.d.ts +4 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-harness-command-claim.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-harness-command-control.d.ts +15 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-harness-command-control.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-harness-localization.d.ts +25 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-harness-localization.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-harness-mentions.d.ts +23 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-harness-mentions.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts +94 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-model-picker-positioning.d.ts +22 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-model-picker-positioning.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-model-picker.d.ts +54 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-model-picker.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-new-thread-preference.d.ts +18 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-new-thread-preference.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-permission-mode-picker.d.ts +42 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-permission-mode-picker.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-permission-mode-preference.d.ts +9 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-permission-mode-preference.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-request-sender.d.ts +9 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-request-sender.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-session-import-client.d.ts +12 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-session-import-client.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-settings-lifecycle.d.ts +18 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-settings-lifecycle.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-sidebar-agent-icons.d.ts +45 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-sidebar-agent-icons.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-transcript-dom.d.ts +23 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-transcript-dom.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-trigger-chip-style.d.ts +3 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-trigger-chip-style.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-usage-control.d.ts +72 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-usage-control.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/settings/accounts-list.d.ts +24 -0
- package/src/native-ui/renderer-extension/dist/types/settings/accounts-list.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/settings/accounts-page.d.ts +18 -0
- package/src/native-ui/renderer-extension/dist/types/settings/accounts-page.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/settings/accounts-usage.d.ts +27 -0
- package/src/native-ui/renderer-extension/dist/types/settings/accounts-usage.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/settings/connections-page.d.ts +42 -0
- package/src/native-ui/renderer-extension/dist/types/settings/connections-page.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/settings/core.d.ts +38 -0
- package/src/native-ui/renderer-extension/dist/types/settings/core.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/settings/harness-accounts.d.ts +14 -0
- package/src/native-ui/renderer-extension/dist/types/settings/harness-accounts.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/settings/icons.d.ts +6 -0
- package/src/native-ui/renderer-extension/dist/types/settings/icons.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/settings/localization.d.ts +231 -0
- package/src/native-ui/renderer-extension/dist/types/settings/localization.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts +22 -0
- package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/settings/release-notes.d.ts +4 -0
- package/src/native-ui/renderer-extension/dist/types/settings/release-notes.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/settings/session-import-list-controls.d.ts +12 -0
- package/src/native-ui/renderer-extension/dist/types/settings/session-import-list-controls.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/settings/session-import-page.d.ts +8 -0
- package/src/native-ui/renderer-extension/dist/types/settings/session-import-page.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/settings/shell.d.ts +24 -0
- package/src/native-ui/renderer-extension/dist/types/settings/shell.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/settings/trigger.d.ts +45 -0
- package/src/native-ui/renderer-extension/dist/types/settings/trigger.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/settings/update-request.d.ts +6 -0
- package/src/native-ui/renderer-extension/dist/types/settings/update-request.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/tsconfig.tsbuildinfo +1 -0
- package/src/native-ui/renderer-extension/dist/types/versioned-renderer-adapter.d.ts +143 -0
- package/src/native-ui/renderer-extension/dist/types/versioned-renderer-adapter.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/package.json +22 -0
- package/src/native-ui/renderer-extension/src/agent-group-preference.ts +174 -0
- package/src/native-ui/renderer-extension/src/agent-selection-state.ts +610 -0
- package/src/native-ui/renderer-extension/src/assets/README.md +43 -0
- package/src/native-ui/renderer-extension/src/assets/antigravity-agent.svg +1 -0
- package/src/native-ui/renderer-extension/src/assets/claude-agent.svg +3 -0
- package/src/native-ui/renderer-extension/src/assets/codex-agent.png +0 -0
- package/src/native-ui/renderer-extension/src/assets/codex-logo-bright.png +0 -0
- package/src/native-ui/renderer-extension/src/assets/codex-logo-transparent.png +0 -0
- package/src/native-ui/renderer-extension/src/assets/codex-logo.png +0 -0
- package/src/native-ui/renderer-extension/src/assets/codexhost-icon.png +0 -0
- package/src/native-ui/renderer-extension/src/assets/codexhost-logo-transparent.png +0 -0
- package/src/native-ui/renderer-extension/src/assets/codexhost-logo.png +0 -0
- package/src/native-ui/renderer-extension/src/assets/codexhost-readme.svg +8 -0
- package/src/native-ui/renderer-extension/src/assets/grok-agent.png +0 -0
- package/src/native-ui/renderer-extension/src/assets/kiro-agent.svg +11 -0
- package/src/native-ui/renderer-extension/src/assets/omp-agent.svg +14 -0
- package/src/native-ui/renderer-extension/src/assets/opencode-agent.png +0 -0
- package/src/native-ui/renderer-extension/src/assets/pi-agent.svg +4 -0
- package/src/native-ui/renderer-extension/src/assets.d.ts +26 -0
- package/src/native-ui/renderer-extension/src/audit-entry.ts +5 -0
- package/src/native-ui/renderer-extension/src/codex-locale-adapter.ts +321 -0
- package/src/native-ui/renderer-extension/src/collaboration-icon.ts +13 -0
- package/src/native-ui/renderer-extension/src/contract-audit.ts +102 -0
- package/src/native-ui/renderer-extension/src/harness-mix-icons.ts +24 -0
- package/src/native-ui/renderer-extension/src/harness-mix-settings.ts +80 -0
- package/src/native-ui/renderer-extension/src/index.ts +266 -0
- package/src/native-ui/renderer-extension/src/install-renderer-binding.ts +30 -0
- package/src/native-ui/renderer-extension/src/probe-entry.ts +4 -0
- package/src/native-ui/renderer-extension/src/production-entry.ts +23 -0
- package/src/native-ui/renderer-extension/src/renderer-agent-icon.ts +145 -0
- package/src/native-ui/renderer-extension/src/renderer-agent-picker.ts +813 -0
- package/src/native-ui/renderer-extension/src/renderer-binding-probe.ts +2936 -0
- package/src/native-ui/renderer-extension/src/renderer-codex-account-options.ts +281 -0
- package/src/native-ui/renderer-extension/src/renderer-codex-account-state.ts +53 -0
- package/src/native-ui/renderer-extension/src/renderer-composer-dom.ts +782 -0
- package/src/native-ui/renderer-extension/src/renderer-credits-control.ts +500 -0
- package/src/native-ui/renderer-extension/src/renderer-external-steering.ts +339 -0
- package/src/native-ui/renderer-extension/src/renderer-fork-control.ts +273 -0
- package/src/native-ui/renderer-extension/src/renderer-harness-command-claim.ts +64 -0
- package/src/native-ui/renderer-extension/src/renderer-harness-command-control.ts +417 -0
- package/src/native-ui/renderer-extension/src/renderer-harness-localization.ts +144 -0
- package/src/native-ui/renderer-extension/src/renderer-harness-mentions.ts +213 -0
- package/src/native-ui/renderer-extension/src/renderer-model-client.ts +567 -0
- package/src/native-ui/renderer-extension/src/renderer-model-picker-positioning.ts +65 -0
- package/src/native-ui/renderer-extension/src/renderer-model-picker.ts +640 -0
- package/src/native-ui/renderer-extension/src/renderer-new-thread-preference.ts +175 -0
- package/src/native-ui/renderer-extension/src/renderer-permission-mode-picker.ts +413 -0
- package/src/native-ui/renderer-extension/src/renderer-permission-mode-preference.ts +49 -0
- package/src/native-ui/renderer-extension/src/renderer-request-sender.ts +43 -0
- package/src/native-ui/renderer-extension/src/renderer-session-import-client.ts +68 -0
- package/src/native-ui/renderer-extension/src/renderer-settings-lifecycle.ts +239 -0
- package/src/native-ui/renderer-extension/src/renderer-sidebar-agent-icons.ts +456 -0
- package/src/native-ui/renderer-extension/src/renderer-transcript-dom.ts +47 -0
- package/src/native-ui/renderer-extension/src/renderer-trigger-chip-style.ts +52 -0
- package/src/native-ui/renderer-extension/src/renderer-usage-control.ts +602 -0
- package/src/native-ui/renderer-extension/src/settings/accounts-list.ts +236 -0
- package/src/native-ui/renderer-extension/src/settings/accounts-page.ts +585 -0
- package/src/native-ui/renderer-extension/src/settings/accounts-quota-interactive.prototype.html +1418 -0
- package/src/native-ui/renderer-extension/src/settings/accounts-usage.ts +221 -0
- package/src/native-ui/renderer-extension/src/settings/accounts.css +607 -0
- package/src/native-ui/renderer-extension/src/settings/connections-page.ts +1354 -0
- package/src/native-ui/renderer-extension/src/settings/core.ts +158 -0
- package/src/native-ui/renderer-extension/src/settings/harness-accounts.ts +134 -0
- package/src/native-ui/renderer-extension/src/settings/icons.ts +141 -0
- package/src/native-ui/renderer-extension/src/settings/localization.ts +752 -0
- package/src/native-ui/renderer-extension/src/settings/pages.ts +615 -0
- package/src/native-ui/renderer-extension/src/settings/release-notes.ts +222 -0
- package/src/native-ui/renderer-extension/src/settings/session-import-list-controls.ts +128 -0
- package/src/native-ui/renderer-extension/src/settings/session-import-page.ts +461 -0
- package/src/native-ui/renderer-extension/src/settings/shell.css +2149 -0
- package/src/native-ui/renderer-extension/src/settings/shell.ts +329 -0
- package/src/native-ui/renderer-extension/src/settings/trigger.ts +323 -0
- package/src/native-ui/renderer-extension/src/settings/update-notes.preview.html +491 -0
- package/src/native-ui/renderer-extension/src/settings/update-request.ts +49 -0
- package/src/native-ui/renderer-extension/src/settings/windows-update.preview.html +2135 -0
- package/src/native-ui/renderer-extension/src/versioned-renderer-adapter.ts +1278 -0
- package/src/native-ui/renderer-extension/test/agent-selection-state.test.ts +611 -0
- package/src/native-ui/renderer-extension/test/codex-locale-adapter.test.ts +152 -0
- package/src/native-ui/renderer-extension/test/contract-audit.test.ts +71 -0
- package/src/native-ui/renderer-extension/test/index.test.ts +9 -0
- package/src/native-ui/renderer-extension/test/renderer-agent-icon.test.ts +98 -0
- package/src/native-ui/renderer-extension/test/renderer-agent-picker.test.ts +226 -0
- package/src/native-ui/renderer-extension/test/renderer-binding-probe-host-catalog.test.ts +728 -0
- package/src/native-ui/renderer-extension/test/renderer-binding-probe.test.ts +1290 -0
- package/src/native-ui/renderer-extension/test/renderer-codex-account-state.test.ts +65 -0
- package/src/native-ui/renderer-extension/test/renderer-composer-permissions.test.ts +75 -0
- package/src/native-ui/renderer-extension/test/renderer-credits-control.test.ts +70 -0
- package/src/native-ui/renderer-extension/test/renderer-external-steering-rpc.test.ts +184 -0
- package/src/native-ui/renderer-extension/test/renderer-external-steering.test.ts +279 -0
- package/src/native-ui/renderer-extension/test/renderer-fork-control.test.ts +363 -0
- package/src/native-ui/renderer-extension/test/renderer-harness-command-claim.test.ts +167 -0
- package/src/native-ui/renderer-extension/test/renderer-harness-command-control.test.ts +54 -0
- package/src/native-ui/renderer-extension/test/renderer-model-client.test.ts +708 -0
- package/src/native-ui/renderer-extension/test/renderer-model-picker.test.ts +302 -0
- package/src/native-ui/renderer-extension/test/renderer-native-thread-ownership.test.ts +84 -0
- package/src/native-ui/renderer-extension/test/renderer-new-thread-preference.test.ts +81 -0
- package/src/native-ui/renderer-extension/test/renderer-permission-mode-picker.test.ts +269 -0
- package/src/native-ui/renderer-extension/test/renderer-permission-mode-preference.test.ts +48 -0
- package/src/native-ui/renderer-extension/test/renderer-settings-lifecycle.test.ts +156 -0
- package/src/native-ui/renderer-extension/test/renderer-sidebar-agent-icons.test.ts +575 -0
- package/src/native-ui/renderer-extension/test/renderer-unsupported-methods.test.ts +167 -0
- package/src/native-ui/renderer-extension/test/renderer-usage-control.test.ts +96 -0
- package/src/native-ui/renderer-extension/test/settings/accounts-usage.test.ts +208 -0
- package/src/native-ui/renderer-extension/test/settings/core.test.ts +145 -0
- package/src/native-ui/renderer-extension/test/settings/harness-settings.test.ts +88 -0
- package/src/native-ui/renderer-extension/test/settings/localization.test.ts +80 -0
- package/src/native-ui/renderer-extension/test/settings/pages.test.ts +2000 -0
- package/src/native-ui/renderer-extension/test/settings/release-notes.test.ts +163 -0
- package/src/native-ui/renderer-extension/test/settings/shell.test.ts +74 -0
- package/src/native-ui/renderer-extension/test/settings/trigger.test.ts +270 -0
- package/src/native-ui/renderer-extension/test/settings/update-request.test.ts +48 -0
- package/src/native-ui/renderer-extension/test/versioned-renderer-adapter.test.ts +682 -0
- package/src/native-ui/renderer-extension/tsconfig.json +15 -0
- package/src/native-ui/shared-contracts/dist/codex-accounts.d.ts +246 -0
- package/src/native-ui/shared-contracts/dist/codex-accounts.d.ts.map +1 -0
- package/src/native-ui/shared-contracts/dist/codex-accounts.js +99 -0
- package/src/native-ui/shared-contracts/dist/codex-accounts.js.map +1 -0
- package/src/native-ui/shared-contracts/dist/deepseek-modern-sessions.d.ts +37 -0
- package/src/native-ui/shared-contracts/dist/deepseek-modern-sessions.d.ts.map +1 -0
- package/src/native-ui/shared-contracts/dist/deepseek-modern-sessions.js +36 -0
- package/src/native-ui/shared-contracts/dist/deepseek-modern-sessions.js.map +1 -0
- package/src/native-ui/shared-contracts/dist/errors.d.ts +17 -0
- package/src/native-ui/shared-contracts/dist/errors.d.ts.map +1 -0
- package/src/native-ui/shared-contracts/dist/errors.js +14 -0
- package/src/native-ui/shared-contracts/dist/errors.js.map +1 -0
- package/src/native-ui/shared-contracts/dist/external-thread-fork.d.ts +11 -0
- package/src/native-ui/shared-contracts/dist/external-thread-fork.d.ts.map +1 -0
- package/src/native-ui/shared-contracts/dist/external-thread-fork.js +14 -0
- package/src/native-ui/shared-contracts/dist/external-thread-fork.js.map +1 -0
- package/src/native-ui/shared-contracts/dist/harness-accounts.d.ts +64 -0
- package/src/native-ui/shared-contracts/dist/harness-accounts.d.ts.map +1 -0
- package/src/native-ui/shared-contracts/dist/harness-accounts.js +24 -0
- package/src/native-ui/shared-contracts/dist/harness-accounts.js.map +1 -0
- package/src/native-ui/shared-contracts/dist/harness-commands.d.ts +46 -0
- package/src/native-ui/shared-contracts/dist/harness-commands.d.ts.map +1 -0
- package/src/native-ui/shared-contracts/dist/harness-commands.js +61 -0
- package/src/native-ui/shared-contracts/dist/harness-commands.js.map +1 -0
- package/src/native-ui/shared-contracts/dist/harness-models.d.ts +278 -0
- package/src/native-ui/shared-contracts/dist/harness-models.d.ts.map +1 -0
- package/src/native-ui/shared-contracts/dist/harness-models.js +317 -0
- package/src/native-ui/shared-contracts/dist/harness-models.js.map +1 -0
- package/src/native-ui/shared-contracts/dist/harness-permission-modes.d.ts +30 -0
- package/src/native-ui/shared-contracts/dist/harness-permission-modes.d.ts.map +1 -0
- package/src/native-ui/shared-contracts/dist/harness-permission-modes.js +57 -0
- package/src/native-ui/shared-contracts/dist/harness-permission-modes.js.map +1 -0
- package/src/native-ui/shared-contracts/dist/harness-plugins.d.ts +56 -0
- package/src/native-ui/shared-contracts/dist/harness-plugins.d.ts.map +1 -0
- package/src/native-ui/shared-contracts/dist/harness-plugins.js +75 -0
- package/src/native-ui/shared-contracts/dist/harness-plugins.js.map +1 -0
- package/src/native-ui/shared-contracts/dist/harness-route.d.ts +16 -0
- package/src/native-ui/shared-contracts/dist/harness-route.d.ts.map +1 -0
- package/src/native-ui/shared-contracts/dist/harness-route.js +42 -0
- package/src/native-ui/shared-contracts/dist/harness-route.js.map +1 -0
- package/src/native-ui/shared-contracts/dist/harness-session-import.d.ts +54 -0
- package/src/native-ui/shared-contracts/dist/harness-session-import.d.ts.map +1 -0
- package/src/native-ui/shared-contracts/dist/harness-session-import.js +72 -0
- package/src/native-ui/shared-contracts/dist/harness-session-import.js.map +1 -0
- package/src/native-ui/shared-contracts/dist/ids.d.ts +12 -0
- package/src/native-ui/shared-contracts/dist/ids.d.ts.map +1 -0
- package/src/native-ui/shared-contracts/dist/ids.js +10 -0
- package/src/native-ui/shared-contracts/dist/ids.js.map +1 -0
- package/src/native-ui/shared-contracts/dist/index.d.ts +47 -0
- package/src/native-ui/shared-contracts/dist/index.d.ts.map +1 -0
- package/src/native-ui/shared-contracts/dist/index.js +29 -0
- package/src/native-ui/shared-contracts/dist/index.js.map +1 -0
- package/src/native-ui/shared-contracts/dist/json-rpc.d.ts +84 -0
- package/src/native-ui/shared-contracts/dist/json-rpc.d.ts.map +1 -0
- package/src/native-ui/shared-contracts/dist/json-rpc.js +65 -0
- package/src/native-ui/shared-contracts/dist/json-rpc.js.map +1 -0
- package/src/native-ui/shared-contracts/dist/json-value.d.ts +13 -0
- package/src/native-ui/shared-contracts/dist/json-value.d.ts.map +1 -0
- package/src/native-ui/shared-contracts/dist/json-value.js +60 -0
- package/src/native-ui/shared-contracts/dist/json-value.js.map +1 -0
- package/src/native-ui/shared-contracts/dist/native-refs.d.ts +43 -0
- package/src/native-ui/shared-contracts/dist/native-refs.d.ts.map +1 -0
- package/src/native-ui/shared-contracts/dist/native-refs.js +35 -0
- package/src/native-ui/shared-contracts/dist/native-refs.js.map +1 -0
- package/src/native-ui/shared-contracts/dist/reasoning-transcript.d.ts +7 -0
- package/src/native-ui/shared-contracts/dist/reasoning-transcript.d.ts.map +1 -0
- package/src/native-ui/shared-contracts/dist/reasoning-transcript.js +7 -0
- package/src/native-ui/shared-contracts/dist/reasoning-transcript.js.map +1 -0
- package/src/native-ui/shared-contracts/dist/thread-delegate.d.ts +22 -0
- package/src/native-ui/shared-contracts/dist/thread-delegate.d.ts.map +1 -0
- package/src/native-ui/shared-contracts/dist/thread-delegate.js +30 -0
- package/src/native-ui/shared-contracts/dist/thread-delegate.js.map +1 -0
- package/src/native-ui/shared-contracts/dist/thread-harness-switch.d.ts +16 -0
- package/src/native-ui/shared-contracts/dist/thread-harness-switch.d.ts.map +1 -0
- package/src/native-ui/shared-contracts/dist/thread-harness-switch.js +19 -0
- package/src/native-ui/shared-contracts/dist/thread-harness-switch.js.map +1 -0
- package/src/native-ui/shared-contracts/dist/thread-usage.d.ts +106 -0
- package/src/native-ui/shared-contracts/dist/thread-usage.d.ts.map +1 -0
- package/src/native-ui/shared-contracts/dist/thread-usage.js +103 -0
- package/src/native-ui/shared-contracts/dist/thread-usage.js.map +1 -0
- package/src/native-ui/shared-contracts/dist/tsconfig.tsbuildinfo +1 -0
- package/src/native-ui/shared-contracts/dist/updates.d.ts +128 -0
- package/src/native-ui/shared-contracts/dist/updates.d.ts.map +1 -0
- package/src/native-ui/shared-contracts/dist/updates.js +58 -0
- package/src/native-ui/shared-contracts/dist/updates.js.map +1 -0
- package/src/native-ui/shared-contracts/dist/version.d.ts +2 -0
- package/src/native-ui/shared-contracts/dist/version.d.ts.map +1 -0
- package/src/native-ui/shared-contracts/dist/version.js +2 -0
- package/src/native-ui/shared-contracts/dist/version.js.map +1 -0
- package/src/native-ui/shared-contracts/package.json +25 -0
- package/src/native-ui/shared-contracts/src/codex-accounts.ts +140 -0
- package/src/native-ui/shared-contracts/src/deepseek-modern-sessions.ts +67 -0
- package/src/native-ui/shared-contracts/src/errors.ts +25 -0
- package/src/native-ui/shared-contracts/src/external-thread-fork.ts +20 -0
- package/src/native-ui/shared-contracts/src/harness-accounts.ts +29 -0
- package/src/native-ui/shared-contracts/src/harness-commands.ts +81 -0
- package/src/native-ui/shared-contracts/src/harness-models.ts +401 -0
- package/src/native-ui/shared-contracts/src/harness-permission-modes.ts +73 -0
- package/src/native-ui/shared-contracts/src/harness-plugins.ts +96 -0
- package/src/native-ui/shared-contracts/src/harness-route.ts +47 -0
- package/src/native-ui/shared-contracts/src/harness-session-import.ts +90 -0
- package/src/native-ui/shared-contracts/src/ids.ts +20 -0
- package/src/native-ui/shared-contracts/src/index.ts +320 -0
- package/src/native-ui/shared-contracts/src/json-rpc.ts +80 -0
- package/src/native-ui/shared-contracts/src/json-value.ts +78 -0
- package/src/native-ui/shared-contracts/src/native-refs.ts +58 -0
- package/src/native-ui/shared-contracts/src/reasoning-transcript.ts +6 -0
- package/src/native-ui/shared-contracts/src/thread-delegate.ts +40 -0
- package/src/native-ui/shared-contracts/src/thread-harness-switch.ts +25 -0
- package/src/native-ui/shared-contracts/src/thread-usage.ts +126 -0
- package/src/native-ui/shared-contracts/src/updates.ts +78 -0
- package/src/native-ui/shared-contracts/src/version.ts +1 -0
- package/src/native-ui/shared-contracts/test/browser-bundle.test.ts +36 -0
- package/src/native-ui/shared-contracts/test/deepseek-modern-sessions.test.ts +120 -0
- package/src/native-ui/shared-contracts/test/errors.test.ts +72 -0
- package/src/native-ui/shared-contracts/test/harness-commands.test.ts +64 -0
- package/src/native-ui/shared-contracts/test/harness-models.test.ts +390 -0
- package/src/native-ui/shared-contracts/test/harness-permission-modes.test.ts +134 -0
- package/src/native-ui/shared-contracts/test/harness-plugins.test.ts +113 -0
- package/src/native-ui/shared-contracts/test/harness-session-import.test.ts +117 -0
- package/src/native-ui/shared-contracts/test/ids.test.ts +69 -0
- package/src/native-ui/shared-contracts/test/index.test.ts +80 -0
- package/src/native-ui/shared-contracts/test/json-rpc.test.ts +177 -0
- package/src/native-ui/shared-contracts/test/json-value.test.ts +62 -0
- package/src/native-ui/shared-contracts/test/native-refs.test.ts +95 -0
- package/src/native-ui/shared-contracts/test/thread-usage.test.ts +115 -0
- package/src/native-ui/shared-contracts/test/updates.test.ts +120 -0
- package/src/native-ui/shared-contracts/tsconfig.json +10 -0
- package/src/tsconfig.base.json +19 -0
|
@@ -0,0 +1,2000 @@
|
|
|
1
|
+
import {
|
|
2
|
+
harnessIdSchema,
|
|
3
|
+
hostThreadIdSchema,
|
|
4
|
+
type HarnessAccountListResult,
|
|
5
|
+
type HarnessSessionListParams,
|
|
6
|
+
type UpdateCheckResult,
|
|
7
|
+
type UpdateStatus,
|
|
8
|
+
} from "@codexhost/shared-contracts";
|
|
9
|
+
import { describe, expect, it, vi } from "vitest";
|
|
10
|
+
|
|
11
|
+
vi.mock("../../src/settings/icons.js", () => ({
|
|
12
|
+
createRendererSettingsIcon: () => "icon",
|
|
13
|
+
isRendererSettingsIconName: () => true,
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
import { RendererSettingsPageScope } from "../../src/settings/core.js";
|
|
17
|
+
import { mountHarnessAccounts } from "../../src/settings/harness-accounts.js";
|
|
18
|
+
import { rendererSettingsMessages } from "../../src/settings/localization.js";
|
|
19
|
+
import { createRendererModelClient } from "../../src/renderer-model-client.js";
|
|
20
|
+
import { RendererSessionImportUnavailableError } from "../../src/renderer-session-import-client.js";
|
|
21
|
+
const HARNESS_SESSION_LIST_METHOD = "codexhost/harness/session-import/list";
|
|
22
|
+
const HARNESS_SESSION_IMPORT_METHOD = "codexhost/harness/session-import/import";
|
|
23
|
+
import {
|
|
24
|
+
CODEXHOST_RELEASES_LATEST_URL,
|
|
25
|
+
createDefaultRendererSettingsPages,
|
|
26
|
+
} from "../../src/settings/pages.js";
|
|
27
|
+
import type {
|
|
28
|
+
RendererConnectionDiagnostics,
|
|
29
|
+
RendererConnectionSnapshot,
|
|
30
|
+
} from "../../src/settings/pages.js";
|
|
31
|
+
|
|
32
|
+
class FakeElement {
|
|
33
|
+
readonly children: unknown[] = [];
|
|
34
|
+
readonly dataset: Record<string, string> = {};
|
|
35
|
+
readonly attributes = new Map<string, string>();
|
|
36
|
+
readonly style: Record<string, string> = {};
|
|
37
|
+
readonly #listeners = new Map<string, (event?: unknown) => void>();
|
|
38
|
+
className = "";
|
|
39
|
+
hidden = false;
|
|
40
|
+
href = "";
|
|
41
|
+
rel = "";
|
|
42
|
+
target = "";
|
|
43
|
+
textContent = "";
|
|
44
|
+
title = "";
|
|
45
|
+
type = "";
|
|
46
|
+
value = "";
|
|
47
|
+
tabIndex = 0;
|
|
48
|
+
disabled = false;
|
|
49
|
+
focused = false;
|
|
50
|
+
scrollLeft = 0;
|
|
51
|
+
scrollWidth = 0;
|
|
52
|
+
clientWidth = 0;
|
|
53
|
+
|
|
54
|
+
constructor(
|
|
55
|
+
readonly tagName: string,
|
|
56
|
+
readonly ownerDocument: FakeDocument,
|
|
57
|
+
) {}
|
|
58
|
+
|
|
59
|
+
addEventListener(name: string, listener: (event?: unknown) => void): void {
|
|
60
|
+
this.#listeners.set(name, listener);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
removeEventListener(name: string): void {
|
|
64
|
+
this.#listeners.delete(name);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
append(...children: unknown[]): void {
|
|
68
|
+
this.children.push(...children);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
get childElementCount(): number {
|
|
72
|
+
return this.children.filter((child) => child instanceof FakeElement).length;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
dispatch(name: string, event?: unknown): void {
|
|
76
|
+
this.#listeners.get(name)?.(event);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
focus(): void {
|
|
80
|
+
this.focused = true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
getRootNode(): FakeDocument {
|
|
84
|
+
return this.ownerDocument;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
scrollBy(options: ScrollToOptions): void {
|
|
88
|
+
this.scrollLeft += Number(options.left ?? 0);
|
|
89
|
+
this.dispatch("scroll");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
scrollIntoView(): void {}
|
|
93
|
+
|
|
94
|
+
getAttribute(name: string): string | null {
|
|
95
|
+
return this.attributes.get(name) ?? null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
replaceChildren(...children: unknown[]): void {
|
|
99
|
+
this.children.splice(0, this.children.length, ...children);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
setAttribute(name: string, value: string): void {
|
|
103
|
+
this.attributes.set(name, value);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
class FakeDocument {
|
|
108
|
+
readonly clipboardWriteText = vi.fn(async () => undefined);
|
|
109
|
+
readonly defaultView: Window;
|
|
110
|
+
|
|
111
|
+
constructor(platform = "MacIntel") {
|
|
112
|
+
this.defaultView = {
|
|
113
|
+
navigator: {
|
|
114
|
+
clipboard: { writeText: this.clipboardWriteText },
|
|
115
|
+
platform,
|
|
116
|
+
userAgent: platform === "Win32" ? "Windows" : "Macintosh",
|
|
117
|
+
},
|
|
118
|
+
setTimeout: vi.fn(() => 0),
|
|
119
|
+
clearTimeout: vi.fn(),
|
|
120
|
+
addEventListener: vi.fn(),
|
|
121
|
+
removeEventListener: vi.fn(),
|
|
122
|
+
} as unknown as Window;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
createElement(tagName: string): FakeElement {
|
|
126
|
+
return new FakeElement(tagName, this);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
createElementNS(_namespace: string, tagName: string): FakeElement {
|
|
130
|
+
return new FakeElement(tagName, this);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function descendants(root: FakeElement): FakeElement[] {
|
|
135
|
+
return [
|
|
136
|
+
root,
|
|
137
|
+
...root.children.flatMap((child) => (child instanceof FakeElement ? descendants(child) : [])),
|
|
138
|
+
];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function visibleNotesText(root: FakeElement): string {
|
|
142
|
+
const parts: string[] = [];
|
|
143
|
+
const walk = (node: unknown): void => {
|
|
144
|
+
if (typeof node === "string") {
|
|
145
|
+
if (node) parts.push(node);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (!(node instanceof FakeElement)) return;
|
|
149
|
+
if (node.children.length === 0) {
|
|
150
|
+
if (node.textContent) parts.push(node.textContent);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
for (const child of node.children) walk(child);
|
|
154
|
+
};
|
|
155
|
+
walk(root);
|
|
156
|
+
return parts.join(" ");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function elementWithClass(root: FakeElement, className: string): FakeElement {
|
|
160
|
+
const element = descendants(root).find((candidate) =>
|
|
161
|
+
candidate.className.split(" ").includes(className),
|
|
162
|
+
);
|
|
163
|
+
if (!element) throw new Error(`Missing .${className}`);
|
|
164
|
+
return element;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function updateCheck(status: UpdateStatus | null = null): UpdateCheckResult {
|
|
168
|
+
return {
|
|
169
|
+
currentVersion: "1.2.2",
|
|
170
|
+
installation: "npm",
|
|
171
|
+
latestVersion: "1.2.3",
|
|
172
|
+
updateAvailable: true,
|
|
173
|
+
installationAvailable: true,
|
|
174
|
+
releaseNotes: "Safer updates",
|
|
175
|
+
releaseNotesUrl: "https://github.com/BytePioneer-AI/codex-host/releases/tag/v1.2.3",
|
|
176
|
+
status,
|
|
177
|
+
error: null,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function updateStatus(
|
|
182
|
+
phase: UpdateStatus["phase"],
|
|
183
|
+
installation: UpdateStatus["installation"] = "npm",
|
|
184
|
+
): UpdateStatus {
|
|
185
|
+
return {
|
|
186
|
+
version: "1.2.3",
|
|
187
|
+
installation,
|
|
188
|
+
phase,
|
|
189
|
+
updatedAt: 1_700_000_000,
|
|
190
|
+
error: null,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function deferred<T>(): {
|
|
195
|
+
readonly promise: Promise<T>;
|
|
196
|
+
readonly resolve: (value: T) => void;
|
|
197
|
+
} {
|
|
198
|
+
let resolve!: (value: T) => void;
|
|
199
|
+
const promise = new Promise<T>((nextResolve) => {
|
|
200
|
+
resolve = nextResolve;
|
|
201
|
+
});
|
|
202
|
+
return { promise, resolve };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function visibleText(root: FakeElement): string {
|
|
206
|
+
return descendants(root)
|
|
207
|
+
.map(({ textContent }) => textContent)
|
|
208
|
+
.filter(Boolean)
|
|
209
|
+
.join(" ");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
describe("Read-only Harness accounts", () => {
|
|
213
|
+
const result: HarnessAccountListResult = {
|
|
214
|
+
accounts: [
|
|
215
|
+
{
|
|
216
|
+
harnessId: harnessIdSchema.parse("grok"),
|
|
217
|
+
harnessName: "Grok Build",
|
|
218
|
+
email: "person@example.com",
|
|
219
|
+
credits: { usedPercent: 25, periodType: "weekly" },
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
};
|
|
223
|
+
it("shows only returned accounts with searchable read-only quota and shared display mode", async () => {
|
|
224
|
+
const document = new FakeDocument();
|
|
225
|
+
const content = document.createElement("main");
|
|
226
|
+
const scope = new RendererSettingsPageScope();
|
|
227
|
+
const listHarnessAccounts = vi.fn(async () => result);
|
|
228
|
+
const mounted = mountHarnessAccounts(
|
|
229
|
+
{
|
|
230
|
+
content: content as unknown as HTMLElement,
|
|
231
|
+
signal: scope.signal,
|
|
232
|
+
runLatest: (op, handlers) => scope.runLatest(op, handlers),
|
|
233
|
+
},
|
|
234
|
+
rendererSettingsMessages("en"),
|
|
235
|
+
() => ({ listHarnessAccounts }),
|
|
236
|
+
vi.fn(),
|
|
237
|
+
);
|
|
238
|
+
expect(descendants(content).find((node) => node.tagName === "section")?.hidden).toBe(true);
|
|
239
|
+
await mounted.refresh();
|
|
240
|
+
expect(visibleText(content)).toContain("Grok Build");
|
|
241
|
+
expect(visibleText(content)).toContain("person@example.com");
|
|
242
|
+
expect(descendants(content).filter((node) => node.tagName === "button")).toHaveLength(0);
|
|
243
|
+
expect(
|
|
244
|
+
descendants(content)
|
|
245
|
+
.find((node) => node.attributes.get("role") === "meter")
|
|
246
|
+
?.attributes.get("aria-valuenow"),
|
|
247
|
+
).toBe("75");
|
|
248
|
+
mounted.update("grok", "used");
|
|
249
|
+
expect(
|
|
250
|
+
descendants(content)
|
|
251
|
+
.find((node) => node.attributes.get("role") === "meter")
|
|
252
|
+
?.attributes.get("aria-valuenow"),
|
|
253
|
+
).toBe("25");
|
|
254
|
+
mounted.update("not-found", "used");
|
|
255
|
+
expect(descendants(content).filter((node) => node.tagName === "article")).toHaveLength(0);
|
|
256
|
+
listHarnessAccounts.mockResolvedValueOnce({ accounts: [] });
|
|
257
|
+
await mounted.refresh();
|
|
258
|
+
expect(descendants(content).find((node) => node.tagName === "section")?.hidden).toBe(true);
|
|
259
|
+
expect(visibleText(content)).not.toContain("person@example.com");
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("coalesces refreshes and discards late results after page disposal", async () => {
|
|
263
|
+
const document = new FakeDocument();
|
|
264
|
+
const content = document.createElement("main");
|
|
265
|
+
const scope = new RendererSettingsPageScope();
|
|
266
|
+
const pending = deferred<HarnessAccountListResult>();
|
|
267
|
+
const listHarnessAccounts = vi.fn(() => pending.promise);
|
|
268
|
+
const mounted = mountHarnessAccounts(
|
|
269
|
+
{
|
|
270
|
+
content: content as unknown as HTMLElement,
|
|
271
|
+
signal: scope.signal,
|
|
272
|
+
runLatest: (op, handlers) => scope.runLatest(op, handlers),
|
|
273
|
+
},
|
|
274
|
+
rendererSettingsMessages("en"),
|
|
275
|
+
() => ({ listHarnessAccounts }),
|
|
276
|
+
vi.fn(),
|
|
277
|
+
);
|
|
278
|
+
const refresh = mounted.refresh();
|
|
279
|
+
await mounted.refresh();
|
|
280
|
+
expect(listHarnessAccounts).toHaveBeenCalledOnce();
|
|
281
|
+
scope.dispose();
|
|
282
|
+
pending.resolve(result);
|
|
283
|
+
await refresh;
|
|
284
|
+
expect(visibleText(content)).not.toContain("person@example.com");
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
describe("Renderer Connections page", () => {
|
|
289
|
+
it("opens managed DSH Web only for the local Host and coalesces repeated clicks", async () => {
|
|
290
|
+
const opened = deferred<undefined>();
|
|
291
|
+
const diagnostics: RendererConnectionDiagnostics = {
|
|
292
|
+
snapshot: () => ({
|
|
293
|
+
adapter: { state: "ready", reason: "ready", modelUpdates: 1, hook: "request-bridge" },
|
|
294
|
+
hosts: [
|
|
295
|
+
{
|
|
296
|
+
hostId: "local",
|
|
297
|
+
active: true,
|
|
298
|
+
agents: [
|
|
299
|
+
{
|
|
300
|
+
agent: "deepseek-harness",
|
|
301
|
+
availability: "ready",
|
|
302
|
+
error: null,
|
|
303
|
+
webUiAvailable: true,
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
hostId: "remote-ssh-codex-managed:fixture",
|
|
309
|
+
active: false,
|
|
310
|
+
agents: [
|
|
311
|
+
{
|
|
312
|
+
agent: "deepseek-harness",
|
|
313
|
+
availability: "ready",
|
|
314
|
+
error: null,
|
|
315
|
+
webUiAvailable: true,
|
|
316
|
+
},
|
|
317
|
+
],
|
|
318
|
+
},
|
|
319
|
+
],
|
|
320
|
+
}),
|
|
321
|
+
refresh: vi.fn(() => Promise.resolve()),
|
|
322
|
+
openWebUi: vi.fn(() => opened.promise),
|
|
323
|
+
subscribe: vi.fn(() => () => undefined),
|
|
324
|
+
};
|
|
325
|
+
const page = createDefaultRendererSettingsPages(
|
|
326
|
+
rendererSettingsMessages("zh-CN"),
|
|
327
|
+
() => null,
|
|
328
|
+
() => diagnostics,
|
|
329
|
+
).find(({ id }) => id === "connections");
|
|
330
|
+
if (!page) throw new Error("Connections page is not registered");
|
|
331
|
+
const document = new FakeDocument();
|
|
332
|
+
const content = document.createElement("main");
|
|
333
|
+
const scope = new RendererSettingsPageScope();
|
|
334
|
+
const cleanup = page.mount({
|
|
335
|
+
content: content as unknown as HTMLElement,
|
|
336
|
+
signal: scope.signal,
|
|
337
|
+
runLatest: (operation, handlers) => scope.runLatest(operation, handlers),
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
const dshRow = descendants(content).find(
|
|
341
|
+
({ dataset }) => dataset.connectionItem === "deepseek-harness",
|
|
342
|
+
);
|
|
343
|
+
if (!dshRow) throw new Error("DeepSeek Harness row is not rendered");
|
|
344
|
+
dshRow.dispatch("click", { target: null });
|
|
345
|
+
const open = descendants(content).find(
|
|
346
|
+
({ dataset }) => dataset.connectionAction === "open-web-ui",
|
|
347
|
+
);
|
|
348
|
+
if (!open) throw new Error("DeepSeek Harness Web action is not rendered");
|
|
349
|
+
expect(visibleNotesText(open)).toContain("打开 DeepSeek Harness Web");
|
|
350
|
+
expect(descendants(content).some(({ href }) => href.includes("token="))).toBe(false);
|
|
351
|
+
|
|
352
|
+
open.dispatch("click");
|
|
353
|
+
open.dispatch("click");
|
|
354
|
+
expect(diagnostics.openWebUi).toHaveBeenCalledOnce();
|
|
355
|
+
expect(diagnostics.openWebUi).toHaveBeenCalledWith("local", "deepseek-harness");
|
|
356
|
+
expect(open.disabled).toBe(true);
|
|
357
|
+
opened.resolve(undefined);
|
|
358
|
+
await vi.waitFor(() => expect(open.disabled).toBe(false));
|
|
359
|
+
|
|
360
|
+
const remoteTab = descendants(content).find(
|
|
361
|
+
({ dataset }) => dataset.connectionHostTab === "remote-ssh-codex-managed:fixture",
|
|
362
|
+
);
|
|
363
|
+
if (!remoteTab) throw new Error("Remote Host tab is not rendered");
|
|
364
|
+
remoteTab.dispatch("click");
|
|
365
|
+
expect(
|
|
366
|
+
descendants(content).find(({ dataset }) => dataset.connectionAction === "open-web-ui"),
|
|
367
|
+
).toBeUndefined();
|
|
368
|
+
|
|
369
|
+
cleanup?.();
|
|
370
|
+
scope.dispose();
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("renders Host tabs, install actions, and error details", async () => {
|
|
374
|
+
const refreshRequest = deferred<undefined>();
|
|
375
|
+
const diagnostics: RendererConnectionDiagnostics = {
|
|
376
|
+
snapshot: vi.fn((): RendererConnectionSnapshot => ({
|
|
377
|
+
adapter: {
|
|
378
|
+
state: "ready",
|
|
379
|
+
reason: "ready",
|
|
380
|
+
modelUpdates: 1,
|
|
381
|
+
hook: "request-bridge",
|
|
382
|
+
},
|
|
383
|
+
hosts: [
|
|
384
|
+
{
|
|
385
|
+
hostId: "local",
|
|
386
|
+
active: true,
|
|
387
|
+
agents: [
|
|
388
|
+
{
|
|
389
|
+
agent: "pi",
|
|
390
|
+
availability: "error",
|
|
391
|
+
error: {
|
|
392
|
+
code: "processExited",
|
|
393
|
+
message: "pi exited with code 1",
|
|
394
|
+
retryable: true,
|
|
395
|
+
stage: "startup",
|
|
396
|
+
durationMs: 120,
|
|
397
|
+
stderrTail: "check ~/.pi/agent/settings.json",
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
agent: "deepseek-harness",
|
|
402
|
+
availability: "notInstalled",
|
|
403
|
+
error: {
|
|
404
|
+
code: "notInstalled",
|
|
405
|
+
message: "DSH is not installed",
|
|
406
|
+
retryable: false,
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
],
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
hostId: "remote-ssh-codex-managed:%E5%85%AC%E5%8F%B8",
|
|
413
|
+
active: false,
|
|
414
|
+
agents: [{ agent: "pi", availability: "ready", error: null }],
|
|
415
|
+
},
|
|
416
|
+
],
|
|
417
|
+
})),
|
|
418
|
+
refresh: vi.fn(() => refreshRequest.promise),
|
|
419
|
+
subscribe: vi.fn(() => () => undefined),
|
|
420
|
+
};
|
|
421
|
+
const page = createDefaultRendererSettingsPages(
|
|
422
|
+
rendererSettingsMessages("zh-CN"),
|
|
423
|
+
() => null,
|
|
424
|
+
() => diagnostics,
|
|
425
|
+
).find(({ id }) => id === "connections");
|
|
426
|
+
if (!page) throw new Error("Connections page is not registered");
|
|
427
|
+
|
|
428
|
+
const document = new FakeDocument();
|
|
429
|
+
const content = document.createElement("main");
|
|
430
|
+
const scope = new RendererSettingsPageScope();
|
|
431
|
+
const cleanup = page.mount({
|
|
432
|
+
content: content as unknown as HTMLElement,
|
|
433
|
+
signal: scope.signal,
|
|
434
|
+
runLatest: (operation, handlers) => scope.runLatest(operation, handlers),
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
expect(visibleText(content)).toContain("本地");
|
|
438
|
+
expect(
|
|
439
|
+
descendants(content).filter((candidate) =>
|
|
440
|
+
candidate.className.split(" ").includes("settings-connection-row__mark--logo"),
|
|
441
|
+
),
|
|
442
|
+
).toHaveLength(3);
|
|
443
|
+
expect(visibleText(content)).toContain("CH");
|
|
444
|
+
expect(visibleText(content)).toContain("公司");
|
|
445
|
+
expect(visibleText(content)).toContain("pi exited with code 1");
|
|
446
|
+
expect(visibleText(content)).toContain("~/.pi/agent/settings.json");
|
|
447
|
+
expect(visibleText(content)).toContain("startup");
|
|
448
|
+
const issueLink = descendants(content).find(
|
|
449
|
+
({ tagName, href }) =>
|
|
450
|
+
tagName === "a" && href === "https://github.com/BytePioneer-AI/codex-host/issues/new",
|
|
451
|
+
);
|
|
452
|
+
expect(issueLink).toBeDefined();
|
|
453
|
+
const copyButton = descendants(
|
|
454
|
+
elementWithClass(content, "settings-connection-error-log-header"),
|
|
455
|
+
).find(({ tagName }) => tagName === "button");
|
|
456
|
+
if (!copyButton) throw new Error("Copy error log button is not rendered");
|
|
457
|
+
copyButton.dispatch("click");
|
|
458
|
+
await vi.waitFor(() => expect(document.clipboardWriteText).toHaveBeenCalledOnce());
|
|
459
|
+
expect(document.clipboardWriteText).toHaveBeenCalledWith(
|
|
460
|
+
expect.stringContaining("host: local"),
|
|
461
|
+
);
|
|
462
|
+
await vi.waitFor(() => expect(visibleNotesText(content)).toContain("已复制"));
|
|
463
|
+
const refresh = descendants(content).find(
|
|
464
|
+
({ tagName, dataset }) => tagName === "button" && dataset.connectionAction === "refresh",
|
|
465
|
+
);
|
|
466
|
+
if (!refresh) throw new Error("Connection refresh button is not rendered");
|
|
467
|
+
refresh.dispatch("click");
|
|
468
|
+
expect(refresh.disabled).toBe(true);
|
|
469
|
+
expect(visibleNotesText(refresh)).toContain("正在诊断...");
|
|
470
|
+
expect(diagnostics.refresh).toHaveBeenCalledWith();
|
|
471
|
+
refreshRequest.resolve(undefined);
|
|
472
|
+
await vi.waitFor(() => expect(refresh.disabled).toBe(false));
|
|
473
|
+
expect(visibleNotesText(refresh)).toContain("重新诊断连接");
|
|
474
|
+
|
|
475
|
+
const installLink = descendants(content).find(
|
|
476
|
+
({ tagName, href }) =>
|
|
477
|
+
tagName === "a" && href === "https://deepseek-harness.github.io/deepseek-harness/",
|
|
478
|
+
);
|
|
479
|
+
expect(installLink).toMatchObject({
|
|
480
|
+
target: "_blank",
|
|
481
|
+
rel: "noopener noreferrer",
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
expect(visibleText(content)).toContain("查看错误");
|
|
485
|
+
const remoteTab = descendants(content).find(
|
|
486
|
+
({ tagName, dataset }) =>
|
|
487
|
+
tagName === "button" &&
|
|
488
|
+
dataset.connectionHostTab === "remote-ssh-codex-managed:%E5%85%AC%E5%8F%B8",
|
|
489
|
+
);
|
|
490
|
+
if (!remoteTab) throw new Error("Remote Host tab is not rendered");
|
|
491
|
+
remoteTab.dispatch("click");
|
|
492
|
+
const selectedPanel = descendants(content).find(
|
|
493
|
+
({ dataset }) => dataset.connectionHost === "remote-ssh-codex-managed:%E5%85%AC%E5%8F%B8",
|
|
494
|
+
);
|
|
495
|
+
expect(selectedPanel).toBeDefined();
|
|
496
|
+
expect(visibleText(content)).not.toContain("pi exited with code 1");
|
|
497
|
+
const selectedRemoteTab = descendants(content).find(
|
|
498
|
+
({ dataset, attributes }) =>
|
|
499
|
+
dataset.connectionHostTab === "remote-ssh-codex-managed:%E5%85%AC%E5%8F%B8" &&
|
|
500
|
+
attributes.get("aria-selected") === "true",
|
|
501
|
+
);
|
|
502
|
+
expect(selectedRemoteTab).toBeDefined();
|
|
503
|
+
|
|
504
|
+
const hostTabs = elementWithClass(content, "settings-connection-host-tabs");
|
|
505
|
+
hostTabs.clientWidth = 240;
|
|
506
|
+
hostTabs.scrollWidth = 720;
|
|
507
|
+
hostTabs.dispatch("scroll");
|
|
508
|
+
const scrollRight = descendants(content).find(
|
|
509
|
+
({ dataset }) => dataset.connectionHostScroll === "right",
|
|
510
|
+
);
|
|
511
|
+
if (!scrollRight) throw new Error("Host scroll button is not rendered");
|
|
512
|
+
expect(scrollRight.disabled).toBe(false);
|
|
513
|
+
scrollRight.dispatch("click");
|
|
514
|
+
expect(hostTabs.scrollLeft).toBeGreaterThan(0);
|
|
515
|
+
|
|
516
|
+
cleanup?.();
|
|
517
|
+
});
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
describe("Renderer Codex Accounts page", () => {
|
|
521
|
+
it("renders cached Accounts before live metadata refresh completes", async () => {
|
|
522
|
+
const refresh = Promise.withResolvers<{
|
|
523
|
+
accounts: Array<{
|
|
524
|
+
accountId: string;
|
|
525
|
+
label: string;
|
|
526
|
+
email: string;
|
|
527
|
+
codexHome: string;
|
|
528
|
+
active: boolean;
|
|
529
|
+
isDefault: boolean;
|
|
530
|
+
}>;
|
|
531
|
+
}>();
|
|
532
|
+
const cachedAccount = {
|
|
533
|
+
accountId: "default",
|
|
534
|
+
label: "Default",
|
|
535
|
+
codexHome: "/tmp/default",
|
|
536
|
+
active: true,
|
|
537
|
+
isDefault: true,
|
|
538
|
+
};
|
|
539
|
+
const client = {
|
|
540
|
+
listCodexAccounts: vi.fn(async () => ({ accounts: [cachedAccount] })),
|
|
541
|
+
refreshCodexAccounts: vi.fn(() => refresh.promise),
|
|
542
|
+
createCodexAccount: vi.fn(),
|
|
543
|
+
deleteCodexAccount: vi.fn(),
|
|
544
|
+
activateCodexAccount: vi.fn(),
|
|
545
|
+
startCodexAccountLogin: vi.fn(),
|
|
546
|
+
cancelCodexAccountLogin: vi.fn(),
|
|
547
|
+
};
|
|
548
|
+
const page = createDefaultRendererSettingsPages(
|
|
549
|
+
rendererSettingsMessages("en"),
|
|
550
|
+
() => null,
|
|
551
|
+
() => null,
|
|
552
|
+
() => client,
|
|
553
|
+
).find(({ id }) => id === "accounts");
|
|
554
|
+
if (!page) throw new Error("Accounts page is not registered");
|
|
555
|
+
|
|
556
|
+
const document = new FakeDocument();
|
|
557
|
+
const content = document.createElement("main");
|
|
558
|
+
const scope = new RendererSettingsPageScope();
|
|
559
|
+
page.mount({
|
|
560
|
+
content: content as unknown as HTMLElement,
|
|
561
|
+
signal: scope.signal,
|
|
562
|
+
runLatest: (operation, handlers) => scope.runLatest(operation, handlers),
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
await vi.waitFor(() => expect(visibleText(content)).toContain("Default"));
|
|
566
|
+
expect(client.refreshCodexAccounts).toHaveBeenCalledOnce();
|
|
567
|
+
|
|
568
|
+
refresh.resolve({ accounts: [{ ...cachedAccount, email: "cached@example.com" }] });
|
|
569
|
+
await vi.waitFor(() =>
|
|
570
|
+
expect(descendants(content).some((element) => element.title === "cached@example.com")).toBe(
|
|
571
|
+
true,
|
|
572
|
+
),
|
|
573
|
+
);
|
|
574
|
+
expect(visibleText(content)).toContain("cached");
|
|
575
|
+
expect(visibleText(content)).toContain("example.com");
|
|
576
|
+
expect(visibleText(content)).not.toContain("/tmp/default");
|
|
577
|
+
|
|
578
|
+
scope.dispose();
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
it("creates an isolated Account and starts sign-in without asking for a name", async () => {
|
|
582
|
+
const createdAccount = {
|
|
583
|
+
accountId: "work",
|
|
584
|
+
label: "Codex Account",
|
|
585
|
+
codexHome: "/tmp/work",
|
|
586
|
+
active: false,
|
|
587
|
+
isDefault: false,
|
|
588
|
+
};
|
|
589
|
+
const client = {
|
|
590
|
+
listCodexAccounts: vi.fn(async () => ({ accounts: [] })),
|
|
591
|
+
createCodexAccount: vi.fn(async () => ({ account: createdAccount })),
|
|
592
|
+
deleteCodexAccount: vi.fn(),
|
|
593
|
+
activateCodexAccount: vi.fn(),
|
|
594
|
+
startCodexAccountLogin: vi.fn(async ({ accountId }: { accountId: string }) => ({
|
|
595
|
+
accountId,
|
|
596
|
+
loginId: "login-1",
|
|
597
|
+
verificationUrl: "https://example.com/device",
|
|
598
|
+
userCode: "ABCD-EFGH",
|
|
599
|
+
})),
|
|
600
|
+
cancelCodexAccountLogin: vi.fn(async () => ({ cancelled: true })),
|
|
601
|
+
};
|
|
602
|
+
const page = createDefaultRendererSettingsPages(
|
|
603
|
+
rendererSettingsMessages("en"),
|
|
604
|
+
() => null,
|
|
605
|
+
() => null,
|
|
606
|
+
() => client,
|
|
607
|
+
).find(({ id }) => id === "accounts");
|
|
608
|
+
if (!page) throw new Error("Accounts page is not registered");
|
|
609
|
+
|
|
610
|
+
const document = new FakeDocument();
|
|
611
|
+
const content = document.createElement("main");
|
|
612
|
+
const scope = new RendererSettingsPageScope();
|
|
613
|
+
page.mount({
|
|
614
|
+
content: content as unknown as HTMLElement,
|
|
615
|
+
signal: scope.signal,
|
|
616
|
+
runLatest: (operation, handlers) => scope.runLatest(operation, handlers),
|
|
617
|
+
});
|
|
618
|
+
await vi.waitFor(() => expect(client.listCodexAccounts).toHaveBeenCalledTimes(1));
|
|
619
|
+
|
|
620
|
+
expect(
|
|
621
|
+
descendants(content)
|
|
622
|
+
.filter(({ tagName }) => tagName === "input")
|
|
623
|
+
.map(({ type }) => type),
|
|
624
|
+
).toEqual(["search"]);
|
|
625
|
+
const add = descendants(content).find(
|
|
626
|
+
({ tagName, children }) => tagName === "button" && children.includes("Add Account"),
|
|
627
|
+
);
|
|
628
|
+
add?.dispatch("click");
|
|
629
|
+
|
|
630
|
+
await vi.waitFor(() => expect(client.createCodexAccount).toHaveBeenCalledWith({}));
|
|
631
|
+
await vi.waitFor(() =>
|
|
632
|
+
expect(client.startCodexAccountLogin).toHaveBeenCalledWith({ accountId: "work" }),
|
|
633
|
+
);
|
|
634
|
+
await vi.waitFor(() => expect(visibleText(content)).toContain("ABCD-EFGH"));
|
|
635
|
+
|
|
636
|
+
scope.dispose();
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
it("allows deleting only non-default Accounts", async () => {
|
|
640
|
+
const deleteCodexAccount = vi.fn(async ({ accountId }: { accountId: string }) => ({
|
|
641
|
+
deletedAccountId: accountId,
|
|
642
|
+
}));
|
|
643
|
+
const client = {
|
|
644
|
+
listCodexAccounts: vi.fn(async () => ({
|
|
645
|
+
accounts: [
|
|
646
|
+
{
|
|
647
|
+
accountId: "default",
|
|
648
|
+
label: "Default",
|
|
649
|
+
codexHome: "/tmp/default",
|
|
650
|
+
active: true,
|
|
651
|
+
isDefault: true,
|
|
652
|
+
},
|
|
653
|
+
{
|
|
654
|
+
accountId: "work",
|
|
655
|
+
label: "Work",
|
|
656
|
+
codexHome: "/tmp/work",
|
|
657
|
+
active: false,
|
|
658
|
+
isDefault: false,
|
|
659
|
+
},
|
|
660
|
+
],
|
|
661
|
+
})),
|
|
662
|
+
createCodexAccount: vi.fn(),
|
|
663
|
+
deleteCodexAccount,
|
|
664
|
+
activateCodexAccount: vi.fn(),
|
|
665
|
+
startCodexAccountLogin: vi.fn(),
|
|
666
|
+
cancelCodexAccountLogin: vi.fn(),
|
|
667
|
+
};
|
|
668
|
+
const page = createDefaultRendererSettingsPages(
|
|
669
|
+
rendererSettingsMessages("en"),
|
|
670
|
+
() => null,
|
|
671
|
+
() => null,
|
|
672
|
+
() => client,
|
|
673
|
+
).find(({ id }) => id === "accounts");
|
|
674
|
+
if (!page) throw new Error("Accounts page is not registered");
|
|
675
|
+
|
|
676
|
+
const document = new FakeDocument();
|
|
677
|
+
document.defaultView.confirm = vi.fn(() => true);
|
|
678
|
+
const content = document.createElement("main");
|
|
679
|
+
const scope = new RendererSettingsPageScope();
|
|
680
|
+
page.mount({
|
|
681
|
+
content: content as unknown as HTMLElement,
|
|
682
|
+
signal: scope.signal,
|
|
683
|
+
runLatest: (operation, handlers) => scope.runLatest(operation, handlers),
|
|
684
|
+
});
|
|
685
|
+
await vi.waitFor(() => expect(visibleText(content)).toContain("Work"));
|
|
686
|
+
|
|
687
|
+
const deleteButtons = descendants(content).filter(
|
|
688
|
+
(element) =>
|
|
689
|
+
element.tagName === "button" && element.getAttribute("aria-label")?.startsWith("Delete:"),
|
|
690
|
+
);
|
|
691
|
+
expect(deleteButtons).toHaveLength(1);
|
|
692
|
+
deleteButtons[0]?.dispatch("click");
|
|
693
|
+
expect(document.defaultView.confirm).toHaveBeenCalledWith(
|
|
694
|
+
"Delete this Account and its local data? This cannot be undone.",
|
|
695
|
+
);
|
|
696
|
+
await vi.waitFor(() => expect(deleteCodexAccount).toHaveBeenCalledWith({ accountId: "work" }));
|
|
697
|
+
await vi.waitFor(() => expect(visibleText(content)).not.toContain("Work"));
|
|
698
|
+
expect(visibleText(content)).toContain("Default");
|
|
699
|
+
expect(visibleText(content)).not.toContain("/tmp/work");
|
|
700
|
+
|
|
701
|
+
scope.dispose();
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
it("renders usage cards for signed-in Accounts and omits unsigned quota and CODEX_HOME", async () => {
|
|
705
|
+
const inspectCodexAccountUsage = vi.fn(async ({ accountId }: { accountId: string }) => {
|
|
706
|
+
if (accountId !== "work") throw new Error(`unexpected account ${accountId}`);
|
|
707
|
+
return {
|
|
708
|
+
accountId,
|
|
709
|
+
usage: null,
|
|
710
|
+
accountCredits: {
|
|
711
|
+
usedPercent: 27,
|
|
712
|
+
periodType: "weekly" as const,
|
|
713
|
+
resetsAt: "2026-09-10T03:32:00.000Z",
|
|
714
|
+
productUsage: [
|
|
715
|
+
{
|
|
716
|
+
product: "GrokBuild",
|
|
717
|
+
usagePercent: 27,
|
|
718
|
+
resetsAt: "2026-09-10T03:32:00.000Z",
|
|
719
|
+
},
|
|
720
|
+
],
|
|
721
|
+
},
|
|
722
|
+
};
|
|
723
|
+
});
|
|
724
|
+
const client = {
|
|
725
|
+
listCodexAccounts: vi.fn(async () => ({
|
|
726
|
+
accounts: [
|
|
727
|
+
{
|
|
728
|
+
accountId: "work",
|
|
729
|
+
label: "Work",
|
|
730
|
+
email: "work@example.com",
|
|
731
|
+
codexHome: "/tmp/secret-home",
|
|
732
|
+
active: true,
|
|
733
|
+
isDefault: true,
|
|
734
|
+
},
|
|
735
|
+
{
|
|
736
|
+
accountId: "pending",
|
|
737
|
+
label: "Pending",
|
|
738
|
+
codexHome: "/tmp/pending-home",
|
|
739
|
+
active: false,
|
|
740
|
+
isDefault: false,
|
|
741
|
+
},
|
|
742
|
+
],
|
|
743
|
+
})),
|
|
744
|
+
inspectCodexAccountUsage,
|
|
745
|
+
createCodexAccount: vi.fn(),
|
|
746
|
+
deleteCodexAccount: vi.fn(),
|
|
747
|
+
activateCodexAccount: vi.fn(),
|
|
748
|
+
startCodexAccountLogin: vi.fn(),
|
|
749
|
+
cancelCodexAccountLogin: vi.fn(),
|
|
750
|
+
};
|
|
751
|
+
const page = createDefaultRendererSettingsPages(
|
|
752
|
+
rendererSettingsMessages("zh-CN"),
|
|
753
|
+
() => null,
|
|
754
|
+
() => null,
|
|
755
|
+
() => client,
|
|
756
|
+
).find(({ id }) => id === "accounts");
|
|
757
|
+
if (!page) throw new Error("Accounts page is not registered");
|
|
758
|
+
|
|
759
|
+
const document = new FakeDocument();
|
|
760
|
+
const content = document.createElement("main");
|
|
761
|
+
const scope = new RendererSettingsPageScope();
|
|
762
|
+
page.mount({
|
|
763
|
+
content: content as unknown as HTMLElement,
|
|
764
|
+
signal: scope.signal,
|
|
765
|
+
runLatest: (operation, handlers) => scope.runLatest(operation, handlers),
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
await vi.waitFor(() =>
|
|
769
|
+
expect(inspectCodexAccountUsage).toHaveBeenCalledWith({ accountId: "work" }),
|
|
770
|
+
);
|
|
771
|
+
expect(inspectCodexAccountUsage).toHaveBeenCalledTimes(1);
|
|
772
|
+
await vi.waitFor(() => expect(visibleText(content)).toContain("周额度"));
|
|
773
|
+
expect(visibleText(content)).toContain("Build");
|
|
774
|
+
expect(visibleText(content)).toContain("已用");
|
|
775
|
+
expect(visibleText(content)).toContain("work");
|
|
776
|
+
expect(visibleText(content)).toContain("example.com");
|
|
777
|
+
expect(visibleText(content)).toContain("Pending");
|
|
778
|
+
expect(visibleText(content)).not.toContain("work@example.com");
|
|
779
|
+
expect(visibleText(content)).not.toContain("/tmp/secret-home");
|
|
780
|
+
expect(visibleText(content)).not.toContain("/tmp/pending-home");
|
|
781
|
+
expect(
|
|
782
|
+
descendants(content).filter((element) =>
|
|
783
|
+
element.className.split(" ").includes("settings-account-usage"),
|
|
784
|
+
),
|
|
785
|
+
).toHaveLength(1);
|
|
786
|
+
|
|
787
|
+
scope.dispose();
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
it("hides login for valid Accounts and exposes device-code login for others", async () => {
|
|
791
|
+
let active = "personal";
|
|
792
|
+
const personal = { email: undefined as string | undefined };
|
|
793
|
+
let loginCompleted:
|
|
794
|
+
| ((result: {
|
|
795
|
+
accountId: string;
|
|
796
|
+
loginId: string;
|
|
797
|
+
success: boolean;
|
|
798
|
+
error: string | null;
|
|
799
|
+
}) => void)
|
|
800
|
+
| undefined;
|
|
801
|
+
const listCodexAccounts = vi.fn(async () => ({
|
|
802
|
+
accounts: [
|
|
803
|
+
{
|
|
804
|
+
accountId: "personal",
|
|
805
|
+
label: "Personal",
|
|
806
|
+
...(personal.email ? { email: personal.email } : {}),
|
|
807
|
+
codexHome: "/tmp/personal",
|
|
808
|
+
active: active === "personal",
|
|
809
|
+
isDefault: true,
|
|
810
|
+
},
|
|
811
|
+
{
|
|
812
|
+
accountId: "work",
|
|
813
|
+
label: "Work",
|
|
814
|
+
email: "work@example.com",
|
|
815
|
+
codexHome: "/tmp/work",
|
|
816
|
+
active: active === "work",
|
|
817
|
+
isDefault: false,
|
|
818
|
+
},
|
|
819
|
+
],
|
|
820
|
+
}));
|
|
821
|
+
const loginStart = Promise.withResolvers<{
|
|
822
|
+
accountId: string;
|
|
823
|
+
loginId: string;
|
|
824
|
+
verificationUrl: string;
|
|
825
|
+
userCode: string;
|
|
826
|
+
}>();
|
|
827
|
+
const client = {
|
|
828
|
+
listCodexAccounts,
|
|
829
|
+
refreshCodexAccounts: listCodexAccounts,
|
|
830
|
+
createCodexAccount: vi.fn(),
|
|
831
|
+
deleteCodexAccount: vi.fn(async ({ accountId }: { accountId: string }) => ({
|
|
832
|
+
deletedAccountId: accountId,
|
|
833
|
+
})),
|
|
834
|
+
activateCodexAccount: vi.fn(async ({ accountId }: { accountId: string }) => {
|
|
835
|
+
active = accountId;
|
|
836
|
+
return {
|
|
837
|
+
account: {
|
|
838
|
+
accountId,
|
|
839
|
+
label: "Work",
|
|
840
|
+
codexHome: "/tmp/work",
|
|
841
|
+
active: true,
|
|
842
|
+
isDefault: false,
|
|
843
|
+
},
|
|
844
|
+
};
|
|
845
|
+
}),
|
|
846
|
+
startCodexAccountLogin: vi.fn(() => loginStart.promise),
|
|
847
|
+
cancelCodexAccountLogin: vi.fn(async () => ({ cancelled: true })),
|
|
848
|
+
subscribeCodexAccountLogin: vi.fn((listener: typeof loginCompleted) => {
|
|
849
|
+
loginCompleted = listener;
|
|
850
|
+
return () => undefined;
|
|
851
|
+
}),
|
|
852
|
+
};
|
|
853
|
+
const page = createDefaultRendererSettingsPages(
|
|
854
|
+
rendererSettingsMessages("en"),
|
|
855
|
+
() => null,
|
|
856
|
+
() => null,
|
|
857
|
+
() => client,
|
|
858
|
+
).find(({ id }) => id === "accounts");
|
|
859
|
+
if (!page) throw new Error("Accounts page is not registered");
|
|
860
|
+
|
|
861
|
+
const document = new FakeDocument();
|
|
862
|
+
const openInBrowser = vi.fn(async () => undefined);
|
|
863
|
+
(
|
|
864
|
+
document.defaultView as Window & {
|
|
865
|
+
electronBridge: { sendMessageFromView: typeof openInBrowser };
|
|
866
|
+
}
|
|
867
|
+
).electronBridge = { sendMessageFromView: openInBrowser };
|
|
868
|
+
const content = document.createElement("main");
|
|
869
|
+
const scope = new RendererSettingsPageScope();
|
|
870
|
+
const cleanup = page.mount({
|
|
871
|
+
content: content as unknown as HTMLElement,
|
|
872
|
+
signal: scope.signal,
|
|
873
|
+
runLatest: (operation, handlers) => scope.runLatest(operation, handlers),
|
|
874
|
+
});
|
|
875
|
+
await vi.waitFor(() => expect(visibleText(content)).toContain("Personal"));
|
|
876
|
+
expect(visibleText(content)).toContain(
|
|
877
|
+
"Existing tasks keep the account they were created with",
|
|
878
|
+
);
|
|
879
|
+
expect(visibleText(content)).toContain("Enable device code authorization for Codex");
|
|
880
|
+
expect(visibleText(content)).not.toContain("token");
|
|
881
|
+
expect(
|
|
882
|
+
descendants(content).filter(
|
|
883
|
+
({ tagName, textContent }) => tagName === "button" && textContent === "Sign in",
|
|
884
|
+
),
|
|
885
|
+
).toHaveLength(1);
|
|
886
|
+
|
|
887
|
+
const useWork = descendants(content).find(
|
|
888
|
+
({ tagName, textContent }) => tagName === "button" && textContent === "Set as default",
|
|
889
|
+
);
|
|
890
|
+
useWork?.dispatch("click");
|
|
891
|
+
await vi.waitFor(() =>
|
|
892
|
+
expect(client.activateCodexAccount).toHaveBeenCalledWith({ accountId: "work" }),
|
|
893
|
+
);
|
|
894
|
+
await vi.waitFor(() => expect(visibleText(content)).toContain("Default"));
|
|
895
|
+
|
|
896
|
+
const signIn = descendants(content).find(
|
|
897
|
+
({ tagName, textContent }) => tagName === "button" && textContent === "Sign in",
|
|
898
|
+
);
|
|
899
|
+
signIn?.dispatch("click");
|
|
900
|
+
const signInButtons = descendants(content).filter(
|
|
901
|
+
({ tagName, textContent }) => tagName === "button" && textContent === "Sign in",
|
|
902
|
+
);
|
|
903
|
+
expect(signInButtons.every(({ disabled }) => disabled)).toBe(true);
|
|
904
|
+
signInButtons.at(-1)?.dispatch("click");
|
|
905
|
+
expect(client.startCodexAccountLogin).toHaveBeenCalledTimes(1);
|
|
906
|
+
loginStart.resolve({
|
|
907
|
+
accountId: "personal",
|
|
908
|
+
loginId: "login-1",
|
|
909
|
+
verificationUrl: "https://example.com/device",
|
|
910
|
+
userCode: "ABCD-EFGH",
|
|
911
|
+
});
|
|
912
|
+
await vi.waitFor(() => expect(visibleText(content)).toContain("ABCD-EFGH"));
|
|
913
|
+
expect(visibleText(content)).toContain("https://example.com/device");
|
|
914
|
+
const verificationLink = descendants(content).find(
|
|
915
|
+
({ tagName, href }) => tagName === "a" && href === "https://example.com/device",
|
|
916
|
+
);
|
|
917
|
+
const preventDefault = vi.fn();
|
|
918
|
+
verificationLink?.dispatch("click", { preventDefault });
|
|
919
|
+
expect(preventDefault).toHaveBeenCalledOnce();
|
|
920
|
+
expect(openInBrowser).toHaveBeenCalledWith({
|
|
921
|
+
type: "open-in-browser",
|
|
922
|
+
url: "https://example.com/device",
|
|
923
|
+
initiator: "open_in_browser_bridge",
|
|
924
|
+
openTarget: "external-browser",
|
|
925
|
+
source: "manual",
|
|
926
|
+
});
|
|
927
|
+
expect(
|
|
928
|
+
descendants(content).find(
|
|
929
|
+
({ tagName, textContent }) => tagName === "button" && textContent === "Sign in",
|
|
930
|
+
)?.disabled,
|
|
931
|
+
).toBe(true);
|
|
932
|
+
personal.email = "personal@example.com";
|
|
933
|
+
const refreshAfterLogin = vi.mocked(document.defaultView.setTimeout).mock.calls.at(-1)?.[0];
|
|
934
|
+
if (typeof refreshAfterLogin !== "function") {
|
|
935
|
+
throw new Error("Account login refresh was not scheduled");
|
|
936
|
+
}
|
|
937
|
+
refreshAfterLogin();
|
|
938
|
+
await vi.waitFor(() =>
|
|
939
|
+
expect(
|
|
940
|
+
descendants(content).some(
|
|
941
|
+
({ tagName, textContent }) => tagName === "button" && textContent === "Sign in",
|
|
942
|
+
),
|
|
943
|
+
).toBe(false),
|
|
944
|
+
);
|
|
945
|
+
expect(visibleText(content)).toContain("Sign-in completed");
|
|
946
|
+
expect(visibleText(content)).not.toContain("ABCD-EFGH");
|
|
947
|
+
expect(loginCompleted).toBeTypeOf("function");
|
|
948
|
+
|
|
949
|
+
cleanup?.();
|
|
950
|
+
scope.dispose();
|
|
951
|
+
});
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
describe("Renderer Updates page", () => {
|
|
955
|
+
it.each([
|
|
956
|
+
[updateStatus("prepared"), "正在准备更新..."],
|
|
957
|
+
[updateStatus("waiting-for-exit"), "正在等待应用退出..."],
|
|
958
|
+
[updateStatus("installing"), "正在通过 npm 安装..."],
|
|
959
|
+
[updateStatus("installing", "windows-installer"), "正在安装更新..."],
|
|
960
|
+
[updateStatus("restarting"), "正在重启以完成更新..."],
|
|
961
|
+
[updateStatus("failed"), "更新失败。"],
|
|
962
|
+
])("renders a distinct localized update status for $0.phase", async (status, expected) => {
|
|
963
|
+
const client = {
|
|
964
|
+
checkUpdate: vi.fn(async () => updateCheck(status)),
|
|
965
|
+
startUpdate: vi.fn(),
|
|
966
|
+
readUpdateStatus: vi.fn(async () => ({ status })),
|
|
967
|
+
};
|
|
968
|
+
const page = createDefaultRendererSettingsPages(
|
|
969
|
+
rendererSettingsMessages("zh-CN"),
|
|
970
|
+
() => client,
|
|
971
|
+
).find(({ id }) => id === "updates");
|
|
972
|
+
if (!page) throw new Error("Updates page is not registered");
|
|
973
|
+
|
|
974
|
+
const document = new FakeDocument();
|
|
975
|
+
const content = document.createElement("main");
|
|
976
|
+
const scope = new RendererSettingsPageScope();
|
|
977
|
+
const cleanup = page.mount({
|
|
978
|
+
content: content as unknown as HTMLElement,
|
|
979
|
+
signal: scope.signal,
|
|
980
|
+
runLatest: (operation, handlers) => scope.runLatest(operation, handlers),
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
await vi.waitFor(() => {
|
|
984
|
+
expect(visibleText(elementWithClass(content, "settings-update-panel"))).toContain(expected);
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
cleanup?.();
|
|
988
|
+
scope.dispose();
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
it("shows only the Update action before an update starts and ignores stale success state", async () => {
|
|
992
|
+
const client = {
|
|
993
|
+
checkUpdate: vi.fn(async () => updateCheck(updateStatus("succeeded"))),
|
|
994
|
+
startUpdate: vi.fn(),
|
|
995
|
+
readUpdateStatus: vi.fn(async () => ({ status: null })),
|
|
996
|
+
};
|
|
997
|
+
const page = createDefaultRendererSettingsPages(
|
|
998
|
+
rendererSettingsMessages("zh-CN"),
|
|
999
|
+
() => client,
|
|
1000
|
+
).find(({ id }) => id === "updates");
|
|
1001
|
+
if (!page) throw new Error("Updates page is not registered");
|
|
1002
|
+
|
|
1003
|
+
const document = new FakeDocument();
|
|
1004
|
+
const content = document.createElement("main");
|
|
1005
|
+
const scope = new RendererSettingsPageScope();
|
|
1006
|
+
const cleanup = page.mount({
|
|
1007
|
+
content: content as unknown as HTMLElement,
|
|
1008
|
+
signal: scope.signal,
|
|
1009
|
+
runLatest: (operation, handlers) => scope.runLatest(operation, handlers),
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
await vi.waitFor(() => {
|
|
1013
|
+
const panel = elementWithClass(content, "settings-update-panel");
|
|
1014
|
+
expect(visibleText(panel)).toContain("更新");
|
|
1015
|
+
expect(visibleText(panel)).not.toContain("更新安装成功");
|
|
1016
|
+
expect(visibleText(panel)).not.toContain("有新版本可用");
|
|
1017
|
+
});
|
|
1018
|
+
expect(client.startUpdate).not.toHaveBeenCalled();
|
|
1019
|
+
|
|
1020
|
+
cleanup?.();
|
|
1021
|
+
scope.dispose();
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
it("keeps a manual GitHub Releases download available before discovery and after update failure", async () => {
|
|
1025
|
+
const client = {
|
|
1026
|
+
checkUpdate: vi.fn(async () => updateCheck()),
|
|
1027
|
+
startUpdate: vi.fn(async () => {
|
|
1028
|
+
throw new Error("download failed");
|
|
1029
|
+
}),
|
|
1030
|
+
readUpdateStatus: vi.fn(async () => ({ status: null })),
|
|
1031
|
+
};
|
|
1032
|
+
const page = createDefaultRendererSettingsPages(undefined, () => client).find(
|
|
1033
|
+
({ id }) => id === "updates",
|
|
1034
|
+
);
|
|
1035
|
+
if (!page) throw new Error("Updates page is not registered");
|
|
1036
|
+
|
|
1037
|
+
const document = new FakeDocument();
|
|
1038
|
+
const content = document.createElement("main");
|
|
1039
|
+
const scope = new RendererSettingsPageScope();
|
|
1040
|
+
const cleanup = page.mount({
|
|
1041
|
+
content: content as unknown as HTMLElement,
|
|
1042
|
+
signal: scope.signal,
|
|
1043
|
+
runLatest: (operation, handlers) => scope.runLatest(operation, handlers),
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
const releaseLink = descendants(content).find(
|
|
1047
|
+
(candidate) =>
|
|
1048
|
+
candidate.tagName === "a" &&
|
|
1049
|
+
visibleNotesText(candidate).includes("Download from GitHub Releases"),
|
|
1050
|
+
);
|
|
1051
|
+
if (!releaseLink) throw new Error("GitHub Releases link is not rendered");
|
|
1052
|
+
expect(releaseLink.href).toBe(CODEXHOST_RELEASES_LATEST_URL);
|
|
1053
|
+
expect(releaseLink.target).toBe("_blank");
|
|
1054
|
+
expect(releaseLink.rel).toBe("noopener noreferrer");
|
|
1055
|
+
|
|
1056
|
+
await vi.waitFor(() => {
|
|
1057
|
+
expect(releaseLink.href).toBe(
|
|
1058
|
+
"https://github.com/BytePioneer-AI/codex-host/releases/tag/v1.2.3",
|
|
1059
|
+
);
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
const panel = elementWithClass(content, "settings-update-panel");
|
|
1063
|
+
const updateButton = descendants(panel).find(({ tagName }) => tagName === "button");
|
|
1064
|
+
if (!updateButton) throw new Error("Update command is not rendered");
|
|
1065
|
+
updateButton.dispatch("click");
|
|
1066
|
+
|
|
1067
|
+
await vi.waitFor(() => {
|
|
1068
|
+
expect(panel.dataset.updateState).toBe("failed");
|
|
1069
|
+
});
|
|
1070
|
+
expect(descendants(content)).toContain(releaseLink);
|
|
1071
|
+
expect(releaseLink.href).toBe(
|
|
1072
|
+
"https://github.com/BytePioneer-AI/codex-host/releases/tag/v1.2.3",
|
|
1073
|
+
);
|
|
1074
|
+
|
|
1075
|
+
cleanup?.();
|
|
1076
|
+
scope.dispose();
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
it.each([
|
|
1080
|
+
["npm" as const, "Windows 暂不支持自动更新。请退出 codexhost,在终端运行以下命令完成更新。"],
|
|
1081
|
+
[
|
|
1082
|
+
"windows-installer" as const,
|
|
1083
|
+
"Windows 暂不支持自动更新。请下载并运行适用于当前系统的安装包。",
|
|
1084
|
+
],
|
|
1085
|
+
])("renders manual Windows updates for %s installations", async (installation, expected) => {
|
|
1086
|
+
const client = {
|
|
1087
|
+
checkUpdate: vi.fn(async () => ({ ...updateCheck(), installation })),
|
|
1088
|
+
startUpdate: vi.fn(),
|
|
1089
|
+
readUpdateStatus: vi.fn(async () => ({ status: null })),
|
|
1090
|
+
};
|
|
1091
|
+
const page = createDefaultRendererSettingsPages(
|
|
1092
|
+
rendererSettingsMessages("zh-CN"),
|
|
1093
|
+
() => client,
|
|
1094
|
+
).find(({ id }) => id === "updates");
|
|
1095
|
+
if (!page) throw new Error("Updates page is not registered");
|
|
1096
|
+
|
|
1097
|
+
const document = new FakeDocument("Win32");
|
|
1098
|
+
const content = document.createElement("main");
|
|
1099
|
+
const scope = new RendererSettingsPageScope();
|
|
1100
|
+
const cleanup = page.mount({
|
|
1101
|
+
content: content as unknown as HTMLElement,
|
|
1102
|
+
signal: scope.signal,
|
|
1103
|
+
runLatest: (operation, handlers) => scope.runLatest(operation, handlers),
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
await vi.waitFor(() => {
|
|
1107
|
+
expect(visibleText(content)).toContain(expected);
|
|
1108
|
+
expect(visibleText(elementWithClass(content, "settings-update-panel"))).toContain(
|
|
1109
|
+
"Windows 暂不支持自动更新",
|
|
1110
|
+
);
|
|
1111
|
+
});
|
|
1112
|
+
expect(
|
|
1113
|
+
descendants(elementWithClass(content, "settings-update-panel")).find(
|
|
1114
|
+
({ tagName }) => tagName === "button",
|
|
1115
|
+
),
|
|
1116
|
+
).toBeUndefined();
|
|
1117
|
+
expect(client.startUpdate).not.toHaveBeenCalled();
|
|
1118
|
+
if (installation === "npm") {
|
|
1119
|
+
expect(visibleText(content)).toContain("npm install -g @codexhost/cli@latest");
|
|
1120
|
+
} else {
|
|
1121
|
+
const link = descendants(content).find(
|
|
1122
|
+
({ tagName, href }) =>
|
|
1123
|
+
tagName === "a" &&
|
|
1124
|
+
href ===
|
|
1125
|
+
"https://github.com/BytePioneer-AI/codex-host/releases/download/v1.2.3/codexhost-1.2.3-windows-x64.exe",
|
|
1126
|
+
);
|
|
1127
|
+
expect(link).toMatchObject({ target: "_blank", rel: "noopener noreferrer" });
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
cleanup?.();
|
|
1131
|
+
scope.dispose();
|
|
1132
|
+
});
|
|
1133
|
+
|
|
1134
|
+
it("renders the open-source project introduction on the About page", () => {
|
|
1135
|
+
const page = createDefaultRendererSettingsPages(rendererSettingsMessages("zh-CN")).find(
|
|
1136
|
+
({ id }) => id === "about",
|
|
1137
|
+
);
|
|
1138
|
+
if (!page) throw new Error("About page is not registered");
|
|
1139
|
+
|
|
1140
|
+
const document = new FakeDocument();
|
|
1141
|
+
const content = document.createElement("main");
|
|
1142
|
+
const scope = new RendererSettingsPageScope();
|
|
1143
|
+
const cleanup = page.mount({
|
|
1144
|
+
content: content as unknown as HTMLElement,
|
|
1145
|
+
signal: scope.signal,
|
|
1146
|
+
runLatest: (operation, handlers) => scope.runLatest(operation, handlers),
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
expect(visibleText(content)).toContain("在 Codex Desktop 中运行 Pi 和其他 Harness");
|
|
1150
|
+
expect(visibleText(content)).toContain(
|
|
1151
|
+
"我们认为 Codex Desktop 提供了目前最好的桌面开发交互体验",
|
|
1152
|
+
);
|
|
1153
|
+
expect(visibleText(content)).toContain("Claude Code 和 Pi Agent");
|
|
1154
|
+
expect(visibleText(content)).toContain("codexhost 是一个开源项目");
|
|
1155
|
+
expect(visibleText(content)).toContain("请给我们一个 Star");
|
|
1156
|
+
const repository = descendants(content).find(
|
|
1157
|
+
({ tagName, href }) =>
|
|
1158
|
+
tagName === "a" && href === "https://github.com/BytePioneer-AI/codex-host",
|
|
1159
|
+
);
|
|
1160
|
+
expect(repository).toMatchObject({ target: "_blank", rel: "noopener noreferrer" });
|
|
1161
|
+
expect(visibleNotesText(repository as FakeElement)).toContain(
|
|
1162
|
+
"https://github.com/BytePioneer-AI/codex-host",
|
|
1163
|
+
);
|
|
1164
|
+
|
|
1165
|
+
cleanup?.();
|
|
1166
|
+
scope.dispose();
|
|
1167
|
+
});
|
|
1168
|
+
|
|
1169
|
+
it("renders GitHub Release notes as structured Markdown", async () => {
|
|
1170
|
+
const client = {
|
|
1171
|
+
checkUpdate: vi.fn(async () => ({
|
|
1172
|
+
...updateCheck(),
|
|
1173
|
+
releaseNotes: "## 本次发布\n\n- 新增 Grok CLI adapter\n- 集成 DeepSeek Harness",
|
|
1174
|
+
})),
|
|
1175
|
+
startUpdate: vi.fn(),
|
|
1176
|
+
readUpdateStatus: vi.fn(async () => ({ status: null })),
|
|
1177
|
+
};
|
|
1178
|
+
const page = createDefaultRendererSettingsPages(
|
|
1179
|
+
rendererSettingsMessages("zh-CN"),
|
|
1180
|
+
() => client,
|
|
1181
|
+
).find(({ id }) => id === "updates");
|
|
1182
|
+
if (!page) throw new Error("Updates page is not registered");
|
|
1183
|
+
|
|
1184
|
+
const document = new FakeDocument();
|
|
1185
|
+
const content = document.createElement("main");
|
|
1186
|
+
const scope = new RendererSettingsPageScope();
|
|
1187
|
+
const cleanup = page.mount({
|
|
1188
|
+
content: content as unknown as HTMLElement,
|
|
1189
|
+
signal: scope.signal,
|
|
1190
|
+
runLatest: (operation, handlers) => scope.runLatest(operation, handlers),
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
await vi.waitFor(() => {
|
|
1194
|
+
expect(elementWithClass(content, "settings-update-notes").children[0]).toMatchObject({
|
|
1195
|
+
tagName: "h2",
|
|
1196
|
+
});
|
|
1197
|
+
});
|
|
1198
|
+
const panel = elementWithClass(content, "settings-update-panel");
|
|
1199
|
+
const controls = elementWithClass(content, "settings-update-controls");
|
|
1200
|
+
const notes = elementWithClass(content, "settings-update-notes");
|
|
1201
|
+
const updateButton = descendants(panel).find(({ tagName }) => tagName === "button");
|
|
1202
|
+
if (!updateButton) throw new Error("Update command is not rendered");
|
|
1203
|
+
// Status and the update action come first; the manual fallback stays visible
|
|
1204
|
+
// right below it, and release notes render last.
|
|
1205
|
+
expect(content.children.indexOf(panel)).toBeLessThan(content.children.indexOf(controls));
|
|
1206
|
+
expect(content.children.indexOf(controls)).toBeLessThan(
|
|
1207
|
+
content.children.indexOf(elementWithClass(content, "settings-update-notes-section")),
|
|
1208
|
+
);
|
|
1209
|
+
expect(descendants(panel)).toContain(updateButton);
|
|
1210
|
+
expect(descendants(panel)).not.toContain(notes);
|
|
1211
|
+
expect(notes.children.map((child) => (child as FakeElement).tagName)).toEqual(["h2", "ul"]);
|
|
1212
|
+
expect(visibleNotesText(notes)).toContain("本次发布");
|
|
1213
|
+
expect(visibleNotesText(notes)).toContain("新增 Grok CLI adapter");
|
|
1214
|
+
expect(visibleNotesText(notes)).not.toContain("##");
|
|
1215
|
+
expect(visibleNotesText(notes)).not.toContain("- 新增");
|
|
1216
|
+
|
|
1217
|
+
cleanup?.();
|
|
1218
|
+
scope.dispose();
|
|
1219
|
+
});
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1222
|
+
describe("Renderer Session Import page", () => {
|
|
1223
|
+
it("configures page size, searches all metadata, rejects stale searches and locks controls during import", async () => {
|
|
1224
|
+
const rows = Array.from({ length: 45 }, (_, index) => ({
|
|
1225
|
+
nativeSessionId: `session-${index}`,
|
|
1226
|
+
title: `Session ${index}`,
|
|
1227
|
+
cwd: "C:\\work",
|
|
1228
|
+
updatedAt: 1_000,
|
|
1229
|
+
running: null,
|
|
1230
|
+
}));
|
|
1231
|
+
const slow = deferred<{ candidates: typeof rows; total: number }>();
|
|
1232
|
+
const imported = deferred<{ threadId: ReturnType<typeof hostThreadIdSchema.parse> }>();
|
|
1233
|
+
const client = {
|
|
1234
|
+
listSessionImportSources: vi.fn(async () => ({
|
|
1235
|
+
harnesses: [{ harnessId: harnessIdSchema.parse("pi"), name: "Pi" }],
|
|
1236
|
+
})),
|
|
1237
|
+
listHarnessSessions: vi.fn(
|
|
1238
|
+
async ({ query = "", offset = 0, limit = 20 }: HarnessSessionListParams) => {
|
|
1239
|
+
if (query === "slow") return slow.promise;
|
|
1240
|
+
const matched = rows.filter((row) =>
|
|
1241
|
+
row.title.toLowerCase().includes(query.toLowerCase()),
|
|
1242
|
+
);
|
|
1243
|
+
return { candidates: matched.slice(offset, offset + limit), total: matched.length };
|
|
1244
|
+
},
|
|
1245
|
+
),
|
|
1246
|
+
importHarnessSession: vi.fn(() => imported.promise),
|
|
1247
|
+
};
|
|
1248
|
+
const page = createDefaultRendererSettingsPages(
|
|
1249
|
+
rendererSettingsMessages("en"),
|
|
1250
|
+
() => null,
|
|
1251
|
+
() => null,
|
|
1252
|
+
() => null,
|
|
1253
|
+
() => client,
|
|
1254
|
+
async () => undefined,
|
|
1255
|
+
).find(({ id }) => id === "session-import");
|
|
1256
|
+
if (!page) throw new Error("Import page missing");
|
|
1257
|
+
const content = new FakeDocument().createElement("main");
|
|
1258
|
+
const scope = new RendererSettingsPageScope();
|
|
1259
|
+
page.mount({
|
|
1260
|
+
content: content as unknown as HTMLElement,
|
|
1261
|
+
signal: scope.signal,
|
|
1262
|
+
runLatest: (operation, handlers) => scope.runLatest(operation, handlers),
|
|
1263
|
+
});
|
|
1264
|
+
const action = (name: string): FakeElement => {
|
|
1265
|
+
const element = descendants(content).find(
|
|
1266
|
+
({ dataset }) => dataset.sessionImportAction === name,
|
|
1267
|
+
);
|
|
1268
|
+
if (!element) throw new Error(`Missing control ${name}`);
|
|
1269
|
+
return element;
|
|
1270
|
+
};
|
|
1271
|
+
const visibleRows = () =>
|
|
1272
|
+
descendants(content).filter(({ dataset }) => dataset.sessionImportId !== undefined);
|
|
1273
|
+
const search = (query: string): void => {
|
|
1274
|
+
action("search-input").value = query;
|
|
1275
|
+
descendants(content)
|
|
1276
|
+
.find(({ tagName }) => tagName === "form")
|
|
1277
|
+
?.dispatch("submit", { preventDefault: vi.fn() });
|
|
1278
|
+
};
|
|
1279
|
+
await vi.waitFor(() => expect(visibleRows()).toHaveLength(20));
|
|
1280
|
+
expect(action("previous").disabled).toBe(true);
|
|
1281
|
+
expect(action("page-summary").textContent).toBe("Page 1 of 3 · 45 sessions");
|
|
1282
|
+
action("next").dispatch("click");
|
|
1283
|
+
await vi.waitFor(() => expect(visibleRows()[0]?.dataset.sessionImportId).toBe("session-20"));
|
|
1284
|
+
action("next").dispatch("click");
|
|
1285
|
+
await vi.waitFor(() => expect(visibleRows()).toHaveLength(5));
|
|
1286
|
+
expect(action("next").disabled).toBe(true);
|
|
1287
|
+
action("previous").dispatch("click");
|
|
1288
|
+
await vi.waitFor(() =>
|
|
1289
|
+
expect(action("page-summary").textContent).toBe("Page 2 of 3 · 45 sessions"),
|
|
1290
|
+
);
|
|
1291
|
+
action("page-size").value = "50";
|
|
1292
|
+
action("page-size").dispatch("change");
|
|
1293
|
+
await vi.waitFor(() => expect(visibleRows()).toHaveLength(45));
|
|
1294
|
+
expect(client.listHarnessSessions).toHaveBeenLastCalledWith({
|
|
1295
|
+
harnessId: "pi",
|
|
1296
|
+
query: "",
|
|
1297
|
+
offset: 0,
|
|
1298
|
+
limit: 50,
|
|
1299
|
+
});
|
|
1300
|
+
search("slow");
|
|
1301
|
+
await vi.waitFor(() =>
|
|
1302
|
+
expect(client.listHarnessSessions).toHaveBeenLastCalledWith({
|
|
1303
|
+
harnessId: "pi",
|
|
1304
|
+
query: "slow",
|
|
1305
|
+
offset: 0,
|
|
1306
|
+
limit: 50,
|
|
1307
|
+
}),
|
|
1308
|
+
);
|
|
1309
|
+
search("SESSION 32");
|
|
1310
|
+
await vi.waitFor(() => expect(visibleRows()).toHaveLength(1));
|
|
1311
|
+
expect(visibleRows()[0]?.dataset.sessionImportId).toBe("session-32");
|
|
1312
|
+
slow.resolve({ candidates: rows, total: 45 });
|
|
1313
|
+
await slow.promise;
|
|
1314
|
+
await Promise.resolve();
|
|
1315
|
+
expect(visibleRows()).toHaveLength(1);
|
|
1316
|
+
expect(action("page-summary").textContent).toBe("Page 1 of 1 · 1 sessions");
|
|
1317
|
+
search("absent");
|
|
1318
|
+
await vi.waitFor(() => expect(visibleText(content)).toContain("No sessions match"));
|
|
1319
|
+
expect(action("previous").disabled).toBe(true);
|
|
1320
|
+
expect(action("next").disabled).toBe(true);
|
|
1321
|
+
search("Session 32");
|
|
1322
|
+
await vi.waitFor(() => expect(visibleRows()).toHaveLength(1));
|
|
1323
|
+
action("import").dispatch("click");
|
|
1324
|
+
expect(action("search-input").disabled).toBe(true);
|
|
1325
|
+
expect(action("page-size").disabled).toBe(true);
|
|
1326
|
+
search("absent"); // Synthetic submission must not invalidate a pending import.
|
|
1327
|
+
imported.resolve({ threadId: hostThreadIdSchema.parse("imported") });
|
|
1328
|
+
await vi.waitFor(() => expect(action("search-input").disabled).toBe(false));
|
|
1329
|
+
expect(client.listHarnessSessions).toHaveBeenLastCalledWith({
|
|
1330
|
+
harnessId: "pi",
|
|
1331
|
+
query: "Session 32",
|
|
1332
|
+
offset: 0,
|
|
1333
|
+
limit: 50,
|
|
1334
|
+
});
|
|
1335
|
+
scope.dispose();
|
|
1336
|
+
});
|
|
1337
|
+
|
|
1338
|
+
it("returns to a valid page when refresh removes the current last page", async () => {
|
|
1339
|
+
const rows = Array.from({ length: 41 }, (_, index) => ({
|
|
1340
|
+
nativeSessionId: `row-${index}`,
|
|
1341
|
+
title: null,
|
|
1342
|
+
cwd: "C:\\work",
|
|
1343
|
+
updatedAt: 1,
|
|
1344
|
+
running: null,
|
|
1345
|
+
}));
|
|
1346
|
+
const client = {
|
|
1347
|
+
listSessionImportSources: vi.fn(async () => ({
|
|
1348
|
+
harnesses: [{ harnessId: harnessIdSchema.parse("pi"), name: "Pi" }],
|
|
1349
|
+
})),
|
|
1350
|
+
listHarnessSessions: vi.fn(async ({ offset = 0, limit = 20 }: HarnessSessionListParams) => ({
|
|
1351
|
+
candidates: rows.slice(offset, offset + limit),
|
|
1352
|
+
total: rows.length,
|
|
1353
|
+
})),
|
|
1354
|
+
importHarnessSession: vi.fn(),
|
|
1355
|
+
};
|
|
1356
|
+
const page = createDefaultRendererSettingsPages(
|
|
1357
|
+
rendererSettingsMessages("en"),
|
|
1358
|
+
() => null,
|
|
1359
|
+
() => null,
|
|
1360
|
+
() => null,
|
|
1361
|
+
() => client,
|
|
1362
|
+
).find(({ id }) => id === "session-import");
|
|
1363
|
+
if (!page) throw new Error("Import page missing");
|
|
1364
|
+
const content = new FakeDocument().createElement("main");
|
|
1365
|
+
const scope = new RendererSettingsPageScope();
|
|
1366
|
+
page.mount({
|
|
1367
|
+
content: content as unknown as HTMLElement,
|
|
1368
|
+
signal: scope.signal,
|
|
1369
|
+
runLatest: (operation, handlers) => scope.runLatest(operation, handlers),
|
|
1370
|
+
});
|
|
1371
|
+
const control = (name: string) =>
|
|
1372
|
+
descendants(content).find(({ dataset }) => dataset.sessionImportAction === name);
|
|
1373
|
+
await vi.waitFor(() => expect(control("page-summary")?.textContent).toContain("Page 1 of 3"));
|
|
1374
|
+
control("next")?.dispatch("click");
|
|
1375
|
+
await vi.waitFor(() => expect(control("page-summary")?.textContent).toContain("Page 2 of 3"));
|
|
1376
|
+
control("next")?.dispatch("click");
|
|
1377
|
+
await vi.waitFor(() => expect(control("page-summary")?.textContent).toContain("Page 3 of 3"));
|
|
1378
|
+
rows.splice(1);
|
|
1379
|
+
control("refresh")?.dispatch("click");
|
|
1380
|
+
await vi.waitFor(() =>
|
|
1381
|
+
expect(control("page-summary")?.textContent).toBe("Page 1 of 1 · 1 sessions"),
|
|
1382
|
+
);
|
|
1383
|
+
expect(client.listHarnessSessions).toHaveBeenLastCalledWith({
|
|
1384
|
+
harnessId: "pi",
|
|
1385
|
+
query: "",
|
|
1386
|
+
offset: 0,
|
|
1387
|
+
limit: 20,
|
|
1388
|
+
});
|
|
1389
|
+
expect(
|
|
1390
|
+
descendants(content).filter(({ dataset }) => dataset.sessionImportId !== undefined),
|
|
1391
|
+
).toHaveLength(1);
|
|
1392
|
+
scope.dispose();
|
|
1393
|
+
});
|
|
1394
|
+
|
|
1395
|
+
it("discovers Harness options, ignores stale Harness results, and imports Pi with an activity warning", async () => {
|
|
1396
|
+
const oldList = deferred<{ candidates: []; total: number }>();
|
|
1397
|
+
const client = {
|
|
1398
|
+
listSessionImportSources: vi.fn(async () => ({
|
|
1399
|
+
harnesses: [
|
|
1400
|
+
{ harnessId: harnessIdSchema.parse("deepseek-harness"), name: "DeepSeek Harness" },
|
|
1401
|
+
{ harnessId: harnessIdSchema.parse("pi"), name: "Pi" },
|
|
1402
|
+
],
|
|
1403
|
+
})),
|
|
1404
|
+
listHarnessSessions: vi.fn(async ({ harnessId }: { harnessId: string }) =>
|
|
1405
|
+
harnessId === "deepseek-harness"
|
|
1406
|
+
? oldList.promise
|
|
1407
|
+
: {
|
|
1408
|
+
total: 1,
|
|
1409
|
+
candidates: [
|
|
1410
|
+
{
|
|
1411
|
+
nativeSessionId: "pi-session",
|
|
1412
|
+
title: "Pi original",
|
|
1413
|
+
cwd: "C:\\work",
|
|
1414
|
+
running: null,
|
|
1415
|
+
updatedAt: 1_000,
|
|
1416
|
+
},
|
|
1417
|
+
],
|
|
1418
|
+
},
|
|
1419
|
+
),
|
|
1420
|
+
importHarnessSession: vi.fn(async () => ({
|
|
1421
|
+
threadId: hostThreadIdSchema.parse("pi-imported"),
|
|
1422
|
+
})),
|
|
1423
|
+
};
|
|
1424
|
+
const open = vi.fn(async () => undefined);
|
|
1425
|
+
const page = createDefaultRendererSettingsPages(
|
|
1426
|
+
rendererSettingsMessages("en"),
|
|
1427
|
+
() => null,
|
|
1428
|
+
() => null,
|
|
1429
|
+
() => null,
|
|
1430
|
+
() => client,
|
|
1431
|
+
open,
|
|
1432
|
+
).find(({ id }) => id === "session-import");
|
|
1433
|
+
if (!page) throw new Error("Session import page missing");
|
|
1434
|
+
const content = new FakeDocument().createElement("main");
|
|
1435
|
+
const scope = new RendererSettingsPageScope();
|
|
1436
|
+
page.mount({
|
|
1437
|
+
content: content as unknown as HTMLElement,
|
|
1438
|
+
signal: scope.signal,
|
|
1439
|
+
runLatest: (operation, handlers) => scope.runLatest(operation, handlers),
|
|
1440
|
+
});
|
|
1441
|
+
await vi.waitFor(() =>
|
|
1442
|
+
expect(client.listHarnessSessions).toHaveBeenCalledWith({
|
|
1443
|
+
harnessId: "deepseek-harness",
|
|
1444
|
+
query: "",
|
|
1445
|
+
offset: 0,
|
|
1446
|
+
limit: 20,
|
|
1447
|
+
}),
|
|
1448
|
+
);
|
|
1449
|
+
const options = descendants(content).filter(
|
|
1450
|
+
({ dataset }) => dataset.sessionImportHarnessOption !== undefined,
|
|
1451
|
+
);
|
|
1452
|
+
expect(options.map(({ textContent }) => textContent)).toEqual(["DeepSeek Harness", "Pi"]);
|
|
1453
|
+
options[1]?.dispatch("click");
|
|
1454
|
+
await vi.waitFor(() => expect(visibleText(content)).toContain("Pi original"));
|
|
1455
|
+
oldList.resolve({ candidates: [], total: 0 });
|
|
1456
|
+
await oldList.promise;
|
|
1457
|
+
await Promise.resolve();
|
|
1458
|
+
expect(visibleText(content)).toContain("Pi original");
|
|
1459
|
+
expect(visibleText(content)).toContain("Activity unknown");
|
|
1460
|
+
expect(visibleText(content)).toContain("close the session in its native client");
|
|
1461
|
+
const button = descendants(content).find(
|
|
1462
|
+
({ dataset }) => dataset.sessionImportAction === "import",
|
|
1463
|
+
);
|
|
1464
|
+
expect(button?.disabled).toBe(false);
|
|
1465
|
+
button?.dispatch("click");
|
|
1466
|
+
await vi.waitFor(() =>
|
|
1467
|
+
expect(client.importHarnessSession).toHaveBeenCalledWith({
|
|
1468
|
+
harnessId: "pi",
|
|
1469
|
+
nativeSessionId: "pi-session",
|
|
1470
|
+
}),
|
|
1471
|
+
);
|
|
1472
|
+
await vi.waitFor(() =>
|
|
1473
|
+
expect(open).toHaveBeenCalledWith("pi-imported", expect.any(AbortSignal)),
|
|
1474
|
+
);
|
|
1475
|
+
scope.dispose();
|
|
1476
|
+
});
|
|
1477
|
+
|
|
1478
|
+
it("renders loading and empty states, ignores an older list, and recovers through Refresh", async () => {
|
|
1479
|
+
const candidate = {
|
|
1480
|
+
nativeSessionId: "recovered-session",
|
|
1481
|
+
title: "Recovered session",
|
|
1482
|
+
updatedAt: 1_700_000_000_000,
|
|
1483
|
+
cwd: "C:\\work",
|
|
1484
|
+
running: false,
|
|
1485
|
+
};
|
|
1486
|
+
const first = deferred<{ candidates: (typeof candidate)[]; total: number }>();
|
|
1487
|
+
const second = deferred<{ candidates: (typeof candidate)[]; total: number }>();
|
|
1488
|
+
const client = {
|
|
1489
|
+
listSessionImportSources: vi.fn(async () => ({
|
|
1490
|
+
harnesses: [
|
|
1491
|
+
{ harnessId: harnessIdSchema.parse("deepseek-harness"), name: "DeepSeek Harness" },
|
|
1492
|
+
],
|
|
1493
|
+
})),
|
|
1494
|
+
listHarnessSessions: vi
|
|
1495
|
+
.fn()
|
|
1496
|
+
.mockImplementationOnce(() => first.promise)
|
|
1497
|
+
.mockImplementationOnce(() => second.promise)
|
|
1498
|
+
.mockRejectedValueOnce(new Error("private list detail"))
|
|
1499
|
+
.mockResolvedValueOnce({ candidates: [candidate], total: 1 }),
|
|
1500
|
+
importHarnessSession: vi.fn(),
|
|
1501
|
+
};
|
|
1502
|
+
const page = createDefaultRendererSettingsPages(
|
|
1503
|
+
rendererSettingsMessages("en"),
|
|
1504
|
+
() => null,
|
|
1505
|
+
() => null,
|
|
1506
|
+
() => null,
|
|
1507
|
+
() => client,
|
|
1508
|
+
).find(({ id }) => id === "session-import");
|
|
1509
|
+
if (!page) throw new Error("Session Import page is not registered");
|
|
1510
|
+
const document = new FakeDocument();
|
|
1511
|
+
const content = document.createElement("main");
|
|
1512
|
+
const scope = new RendererSettingsPageScope();
|
|
1513
|
+
page.mount({
|
|
1514
|
+
content: content as unknown as HTMLElement,
|
|
1515
|
+
signal: scope.signal,
|
|
1516
|
+
runLatest: (operation, handlers) => scope.runLatest(operation, handlers),
|
|
1517
|
+
});
|
|
1518
|
+
const refresh = descendants(content).find(
|
|
1519
|
+
({ dataset }) => dataset.sessionImportAction === "refresh",
|
|
1520
|
+
);
|
|
1521
|
+
if (!refresh) throw new Error("Session Import Refresh is not rendered");
|
|
1522
|
+
|
|
1523
|
+
expect(refresh.disabled).toBe(true);
|
|
1524
|
+
expect(visibleNotesText(refresh)).toContain("Loading local sessions");
|
|
1525
|
+
expect(visibleText(content)).toContain("Loading local sessions");
|
|
1526
|
+
|
|
1527
|
+
await vi.waitFor(() => expect(client.listHarnessSessions).toHaveBeenCalledOnce());
|
|
1528
|
+
// A synthetic second activation proves runLatest still rejects stale results even if
|
|
1529
|
+
// browser-level disabled handling is bypassed.
|
|
1530
|
+
refresh.dispatch("click");
|
|
1531
|
+
second.resolve({ candidates: [], total: 0 });
|
|
1532
|
+
await vi.waitFor(() => expect(visibleText(content)).toContain("No local sessions"));
|
|
1533
|
+
expect(refresh.disabled).toBe(false);
|
|
1534
|
+
|
|
1535
|
+
first.resolve({
|
|
1536
|
+
total: 1,
|
|
1537
|
+
candidates: [{ ...candidate, nativeSessionId: "stale-session", title: "Ignored stale" }],
|
|
1538
|
+
});
|
|
1539
|
+
await first.promise;
|
|
1540
|
+
await Promise.resolve();
|
|
1541
|
+
expect(visibleText(content)).toContain("No local sessions");
|
|
1542
|
+
expect(visibleText(content)).not.toContain("Ignored stale");
|
|
1543
|
+
|
|
1544
|
+
refresh.dispatch("click");
|
|
1545
|
+
await vi.waitFor(() => expect(visibleText(content)).toContain("could not be loaded"));
|
|
1546
|
+
expect(visibleText(content)).not.toContain("private list detail");
|
|
1547
|
+
|
|
1548
|
+
refresh.dispatch("click");
|
|
1549
|
+
await vi.waitFor(() => expect(visibleText(content)).toContain("Recovered session"));
|
|
1550
|
+
expect(client.listHarnessSessions).toHaveBeenCalledTimes(4);
|
|
1551
|
+
scope.dispose();
|
|
1552
|
+
});
|
|
1553
|
+
|
|
1554
|
+
it("lists local DSH Modern sessions and imports only an idle row", async () => {
|
|
1555
|
+
const client = {
|
|
1556
|
+
listSessionImportSources: vi.fn(async () => ({
|
|
1557
|
+
harnesses: [
|
|
1558
|
+
{ harnessId: harnessIdSchema.parse("deepseek-harness"), name: "DeepSeek Harness" },
|
|
1559
|
+
],
|
|
1560
|
+
})),
|
|
1561
|
+
listHarnessSessions: vi.fn(async () => ({
|
|
1562
|
+
total: 2,
|
|
1563
|
+
candidates: [
|
|
1564
|
+
{
|
|
1565
|
+
nativeSessionId: "idle-session-identifier-that-is-long",
|
|
1566
|
+
title: "既有会话",
|
|
1567
|
+
updatedAt: 1_700_000_000_000,
|
|
1568
|
+
cwd: "C:\\work\\idle",
|
|
1569
|
+
running: false,
|
|
1570
|
+
},
|
|
1571
|
+
{
|
|
1572
|
+
nativeSessionId: "running-session",
|
|
1573
|
+
title: null,
|
|
1574
|
+
updatedAt: 1_700_000_001_000,
|
|
1575
|
+
cwd: "C:\\work\\running",
|
|
1576
|
+
running: true,
|
|
1577
|
+
},
|
|
1578
|
+
],
|
|
1579
|
+
})),
|
|
1580
|
+
importHarnessSession: vi.fn(async () => ({
|
|
1581
|
+
threadId: hostThreadIdSchema.parse("imported-thread"),
|
|
1582
|
+
})),
|
|
1583
|
+
};
|
|
1584
|
+
const openImportedThread = vi.fn(async () => undefined);
|
|
1585
|
+
const page = createDefaultRendererSettingsPages(
|
|
1586
|
+
rendererSettingsMessages("zh-CN"),
|
|
1587
|
+
() => null,
|
|
1588
|
+
() => null,
|
|
1589
|
+
() => null,
|
|
1590
|
+
() => client,
|
|
1591
|
+
openImportedThread,
|
|
1592
|
+
).find(({ id }) => id === "session-import");
|
|
1593
|
+
if (!page) throw new Error("Session Import page is not registered");
|
|
1594
|
+
const document = new FakeDocument();
|
|
1595
|
+
const content = document.createElement("main");
|
|
1596
|
+
const scope = new RendererSettingsPageScope();
|
|
1597
|
+
page.mount({
|
|
1598
|
+
content: content as unknown as HTMLElement,
|
|
1599
|
+
signal: scope.signal,
|
|
1600
|
+
runLatest: (operation, handlers) => scope.runLatest(operation, handlers),
|
|
1601
|
+
});
|
|
1602
|
+
|
|
1603
|
+
await vi.waitFor(() =>
|
|
1604
|
+
expect(client.listHarnessSessions).toHaveBeenCalledWith({
|
|
1605
|
+
harnessId: "deepseek-harness",
|
|
1606
|
+
query: "",
|
|
1607
|
+
offset: 0,
|
|
1608
|
+
limit: 20,
|
|
1609
|
+
}),
|
|
1610
|
+
);
|
|
1611
|
+
await vi.waitFor(() => expect(visibleText(content)).toContain("既有会话"));
|
|
1612
|
+
expect(visibleText(content)).toContain("未命名会话");
|
|
1613
|
+
expect(visibleText(content)).toContain("运行中");
|
|
1614
|
+
const visualIdentity = descendants(content).find(
|
|
1615
|
+
({ attributes, textContent }) =>
|
|
1616
|
+
attributes.get("aria-hidden") === "true" && textContent.startsWith("会话 ID:"),
|
|
1617
|
+
);
|
|
1618
|
+
expect(visualIdentity?.textContent).not.toContain("idle-session-identifier-that-is-long");
|
|
1619
|
+
expect(visualIdentity?.title).toBe("idle-session-identifier-that-is-long");
|
|
1620
|
+
expect(descendants(content).find(({ tagName }) => tagName === "h2")?.textContent).toBe(
|
|
1621
|
+
"会话导入",
|
|
1622
|
+
);
|
|
1623
|
+
expect(visibleText(content)).toContain(
|
|
1624
|
+
"可选 Harness 来自本地 Host。运行状态未知时,请先在原生客户端关闭该会话再导入,避免同时写入。",
|
|
1625
|
+
);
|
|
1626
|
+
const harnessSelector = descendants(content).find(
|
|
1627
|
+
({ dataset }) => dataset.sessionImportHarness === "selector",
|
|
1628
|
+
);
|
|
1629
|
+
if (!harnessSelector) throw new Error("Session Import Harness selector is not rendered");
|
|
1630
|
+
const harnessOptions = descendants(harnessSelector).filter(
|
|
1631
|
+
({ dataset }) => dataset.sessionImportHarnessOption !== undefined,
|
|
1632
|
+
);
|
|
1633
|
+
expect(harnessOptions.map(({ textContent }) => textContent)).toEqual(["DeepSeek Harness"]);
|
|
1634
|
+
expect(
|
|
1635
|
+
harnessOptions.filter(({ disabled }) => !disabled).map(({ textContent }) => textContent),
|
|
1636
|
+
).toEqual(["DeepSeek Harness"]);
|
|
1637
|
+
expect(
|
|
1638
|
+
harnessOptions.find(({ attributes }) => attributes.get("aria-pressed") === "true")
|
|
1639
|
+
?.textContent,
|
|
1640
|
+
).toBe("DeepSeek Harness");
|
|
1641
|
+
for (const option of harnessOptions) option.dispatch("click");
|
|
1642
|
+
expect(client.listHarnessSessions).toHaveBeenCalledOnce();
|
|
1643
|
+
expect(client.importHarnessSession).not.toHaveBeenCalled();
|
|
1644
|
+
expect(
|
|
1645
|
+
descendants(content).find(
|
|
1646
|
+
({ className, textContent }) =>
|
|
1647
|
+
className === "settings-visually-hidden" &&
|
|
1648
|
+
textContent.includes("idle-session-identifier-that-is-long"),
|
|
1649
|
+
)?.textContent,
|
|
1650
|
+
).toContain("idle-session-identifier-that-is-long");
|
|
1651
|
+
expect(
|
|
1652
|
+
descendants(content).find(
|
|
1653
|
+
({ className, textContent }) =>
|
|
1654
|
+
className === "settings-visually-hidden" && textContent.startsWith("运行中:"),
|
|
1655
|
+
)?.textContent,
|
|
1656
|
+
).toBe("运行中: 请先在原生客户端关闭该会话,再刷新并导入。");
|
|
1657
|
+
|
|
1658
|
+
const actions = descendants(content).filter(
|
|
1659
|
+
({ dataset }) => dataset.sessionImportAction === "import",
|
|
1660
|
+
);
|
|
1661
|
+
expect(actions).toHaveLength(2);
|
|
1662
|
+
expect(actions[0]?.disabled).toBe(false);
|
|
1663
|
+
expect(actions[1]?.disabled).toBe(true);
|
|
1664
|
+
actions[1]?.dispatch("click");
|
|
1665
|
+
expect(client.importHarnessSession).not.toHaveBeenCalled();
|
|
1666
|
+
actions[0]?.dispatch("click");
|
|
1667
|
+
expect(descendants(content)).toContain(actions[0]);
|
|
1668
|
+
expect(actions[0]?.getAttribute("aria-disabled")).toBe("true");
|
|
1669
|
+
expect(actions[0]?.getAttribute("aria-busy")).toBe("true");
|
|
1670
|
+
actions[0]?.dispatch("click");
|
|
1671
|
+
await vi.waitFor(() => expect(client.importHarnessSession).toHaveBeenCalledOnce());
|
|
1672
|
+
expect(client.importHarnessSession).toHaveBeenCalledWith({
|
|
1673
|
+
harnessId: "deepseek-harness",
|
|
1674
|
+
nativeSessionId: "idle-session-identifier-that-is-long",
|
|
1675
|
+
});
|
|
1676
|
+
await vi.waitFor(() =>
|
|
1677
|
+
expect(openImportedThread).toHaveBeenCalledWith("imported-thread", expect.any(AbortSignal)),
|
|
1678
|
+
);
|
|
1679
|
+
scope.dispose();
|
|
1680
|
+
});
|
|
1681
|
+
|
|
1682
|
+
it("shows a localized focused error when import fails before commit", async () => {
|
|
1683
|
+
const client = {
|
|
1684
|
+
listSessionImportSources: vi.fn(async () => ({
|
|
1685
|
+
harnesses: [
|
|
1686
|
+
{ harnessId: harnessIdSchema.parse("deepseek-harness"), name: "DeepSeek Harness" },
|
|
1687
|
+
],
|
|
1688
|
+
})),
|
|
1689
|
+
listHarnessSessions: vi.fn(async () => ({
|
|
1690
|
+
total: 1,
|
|
1691
|
+
candidates: [
|
|
1692
|
+
{
|
|
1693
|
+
nativeSessionId: "idle-session",
|
|
1694
|
+
title: "Import candidate",
|
|
1695
|
+
updatedAt: 1_700_000_000_000,
|
|
1696
|
+
cwd: "C:\\work",
|
|
1697
|
+
running: false,
|
|
1698
|
+
},
|
|
1699
|
+
],
|
|
1700
|
+
})),
|
|
1701
|
+
importHarnessSession: vi.fn(async () => Promise.reject(new Error("private import detail"))),
|
|
1702
|
+
};
|
|
1703
|
+
const openImportedThread = vi.fn();
|
|
1704
|
+
const page = createDefaultRendererSettingsPages(
|
|
1705
|
+
rendererSettingsMessages("en"),
|
|
1706
|
+
() => null,
|
|
1707
|
+
() => null,
|
|
1708
|
+
() => null,
|
|
1709
|
+
() => client,
|
|
1710
|
+
openImportedThread,
|
|
1711
|
+
).find(({ id }) => id === "session-import");
|
|
1712
|
+
if (!page) throw new Error("Session Import page is not registered");
|
|
1713
|
+
const document = new FakeDocument();
|
|
1714
|
+
const content = document.createElement("main");
|
|
1715
|
+
const scope = new RendererSettingsPageScope();
|
|
1716
|
+
page.mount({
|
|
1717
|
+
content: content as unknown as HTMLElement,
|
|
1718
|
+
signal: scope.signal,
|
|
1719
|
+
runLatest: (operation, handlers) => scope.runLatest(operation, handlers),
|
|
1720
|
+
});
|
|
1721
|
+
await vi.waitFor(() => expect(visibleText(content)).toContain("Import candidate"));
|
|
1722
|
+
|
|
1723
|
+
descendants(content)
|
|
1724
|
+
.find(({ dataset }) => dataset.sessionImportAction === "import")
|
|
1725
|
+
?.dispatch("click");
|
|
1726
|
+
|
|
1727
|
+
await vi.waitFor(() => expect(visibleText(content)).toContain("could not be imported"));
|
|
1728
|
+
expect(elementWithClass(content, "settings-session-import-status").focused).toBe(true);
|
|
1729
|
+
expect(visibleText(content)).not.toContain("private import detail");
|
|
1730
|
+
expect(openImportedThread).not.toHaveBeenCalled();
|
|
1731
|
+
expect(
|
|
1732
|
+
descendants(content).find(({ dataset }) => dataset.sessionImportAction === "refresh")
|
|
1733
|
+
?.disabled,
|
|
1734
|
+
).toBe(false);
|
|
1735
|
+
scope.dispose();
|
|
1736
|
+
});
|
|
1737
|
+
|
|
1738
|
+
it("coalesces import across page remount and lets only the current scope navigate", async () => {
|
|
1739
|
+
const imported = deferred<unknown>();
|
|
1740
|
+
const candidate = {
|
|
1741
|
+
nativeSessionId: "remounted-session",
|
|
1742
|
+
title: "Remounted session",
|
|
1743
|
+
updatedAt: 1_700_000_000_000,
|
|
1744
|
+
cwd: "C:\\work",
|
|
1745
|
+
running: false,
|
|
1746
|
+
};
|
|
1747
|
+
const sendRequest = vi.fn((method: string): Promise<unknown> => {
|
|
1748
|
+
if (method === HARNESS_SESSION_LIST_METHOD) {
|
|
1749
|
+
return Promise.resolve({ candidates: [candidate], total: 1 });
|
|
1750
|
+
}
|
|
1751
|
+
if (method === HARNESS_SESSION_IMPORT_METHOD) return imported.promise;
|
|
1752
|
+
return Promise.reject(new Error(`Unexpected method: ${method}`));
|
|
1753
|
+
});
|
|
1754
|
+
const modelClient = createRendererModelClient([{ sendRequest }]);
|
|
1755
|
+
if (!modelClient?.listHarnessSessions || !modelClient.importHarnessSession) {
|
|
1756
|
+
throw new Error("DSH Modern Session client was not created");
|
|
1757
|
+
}
|
|
1758
|
+
const client = {
|
|
1759
|
+
listSessionImportSources: vi.fn(async () => ({
|
|
1760
|
+
harnesses: [
|
|
1761
|
+
{ harnessId: harnessIdSchema.parse("deepseek-harness"), name: "DeepSeek Harness" },
|
|
1762
|
+
],
|
|
1763
|
+
})),
|
|
1764
|
+
listHarnessSessions: modelClient.listHarnessSessions,
|
|
1765
|
+
importHarnessSession: modelClient.importHarnessSession,
|
|
1766
|
+
};
|
|
1767
|
+
const navigated: string[] = [];
|
|
1768
|
+
const openImportedThread = vi.fn(async (threadId: string, signal: AbortSignal) => {
|
|
1769
|
+
if (signal.aborted) throw Object.assign(new Error("aborted"), { name: "AbortError" });
|
|
1770
|
+
navigated.push(threadId);
|
|
1771
|
+
});
|
|
1772
|
+
const page = createDefaultRendererSettingsPages(
|
|
1773
|
+
rendererSettingsMessages("en"),
|
|
1774
|
+
() => null,
|
|
1775
|
+
() => null,
|
|
1776
|
+
() => null,
|
|
1777
|
+
() => client,
|
|
1778
|
+
openImportedThread,
|
|
1779
|
+
).find(({ id }) => id === "session-import");
|
|
1780
|
+
if (!page) throw new Error("Session Import page is not registered");
|
|
1781
|
+
|
|
1782
|
+
const firstDocument = new FakeDocument();
|
|
1783
|
+
const firstContent = firstDocument.createElement("main");
|
|
1784
|
+
const firstScope = new RendererSettingsPageScope();
|
|
1785
|
+
page.mount({
|
|
1786
|
+
content: firstContent as unknown as HTMLElement,
|
|
1787
|
+
signal: firstScope.signal,
|
|
1788
|
+
runLatest: (operation, handlers) => firstScope.runLatest(operation, handlers),
|
|
1789
|
+
});
|
|
1790
|
+
await vi.waitFor(() => expect(visibleText(firstContent)).toContain("Remounted session"));
|
|
1791
|
+
descendants(firstContent)
|
|
1792
|
+
.find(({ dataset }) => dataset.sessionImportAction === "import")
|
|
1793
|
+
?.dispatch("click");
|
|
1794
|
+
await vi.waitFor(() =>
|
|
1795
|
+
expect(
|
|
1796
|
+
sendRequest.mock.calls.filter(([method]) => method === HARNESS_SESSION_IMPORT_METHOD),
|
|
1797
|
+
).toHaveLength(1),
|
|
1798
|
+
);
|
|
1799
|
+
const staleContent = visibleText(firstContent);
|
|
1800
|
+
firstScope.dispose();
|
|
1801
|
+
|
|
1802
|
+
const currentDocument = new FakeDocument();
|
|
1803
|
+
const currentContent = currentDocument.createElement("main");
|
|
1804
|
+
const currentScope = new RendererSettingsPageScope();
|
|
1805
|
+
page.mount({
|
|
1806
|
+
content: currentContent as unknown as HTMLElement,
|
|
1807
|
+
signal: currentScope.signal,
|
|
1808
|
+
runLatest: (operation, handlers) => currentScope.runLatest(operation, handlers),
|
|
1809
|
+
});
|
|
1810
|
+
await vi.waitFor(() => expect(visibleText(currentContent)).toContain("Remounted session"));
|
|
1811
|
+
descendants(currentContent)
|
|
1812
|
+
.find(({ dataset }) => dataset.sessionImportAction === "import")
|
|
1813
|
+
?.dispatch("click");
|
|
1814
|
+
expect(
|
|
1815
|
+
sendRequest.mock.calls.filter(([method]) => method === HARNESS_SESSION_IMPORT_METHOD),
|
|
1816
|
+
).toHaveLength(1);
|
|
1817
|
+
|
|
1818
|
+
imported.resolve({ threadId: "imported-thread" });
|
|
1819
|
+
await vi.waitFor(() => expect(navigated).toEqual(["imported-thread"]));
|
|
1820
|
+
expect(visibleText(firstContent)).toBe(staleContent);
|
|
1821
|
+
currentScope.dispose();
|
|
1822
|
+
});
|
|
1823
|
+
|
|
1824
|
+
it("localizes unavailable and ordinary list failures without exposing their detail", async () => {
|
|
1825
|
+
const messages = rendererSettingsMessages("zh-CN");
|
|
1826
|
+
const document = new FakeDocument();
|
|
1827
|
+
for (const [error, expected] of [
|
|
1828
|
+
[new RendererSessionImportUnavailableError(), messages.sessionImportUnavailable],
|
|
1829
|
+
[new Error("private native failure detail"), messages.sessionImportLoadFailed],
|
|
1830
|
+
] as const) {
|
|
1831
|
+
const client = {
|
|
1832
|
+
listSessionImportSources: vi.fn(async () => ({
|
|
1833
|
+
harnesses: [
|
|
1834
|
+
{ harnessId: harnessIdSchema.parse("deepseek-harness"), name: "DeepSeek Harness" },
|
|
1835
|
+
],
|
|
1836
|
+
})),
|
|
1837
|
+
listHarnessSessions: vi.fn(async () => Promise.reject(error)),
|
|
1838
|
+
importHarnessSession: vi.fn(),
|
|
1839
|
+
};
|
|
1840
|
+
const page = createDefaultRendererSettingsPages(
|
|
1841
|
+
messages,
|
|
1842
|
+
() => null,
|
|
1843
|
+
() => null,
|
|
1844
|
+
() => null,
|
|
1845
|
+
() => client,
|
|
1846
|
+
).find(({ id }) => id === "session-import");
|
|
1847
|
+
if (!page) throw new Error("Session Import page is not registered");
|
|
1848
|
+
const content = document.createElement("main");
|
|
1849
|
+
const scope = new RendererSettingsPageScope();
|
|
1850
|
+
page.mount({
|
|
1851
|
+
content: content as unknown as HTMLElement,
|
|
1852
|
+
signal: scope.signal,
|
|
1853
|
+
runLatest: (operation, handlers) => scope.runLatest(operation, handlers),
|
|
1854
|
+
});
|
|
1855
|
+
|
|
1856
|
+
await vi.waitFor(() => expect(visibleText(content)).toContain(expected));
|
|
1857
|
+
expect(visibleText(content)).not.toContain(error.message);
|
|
1858
|
+
scope.dispose();
|
|
1859
|
+
}
|
|
1860
|
+
});
|
|
1861
|
+
|
|
1862
|
+
it("keeps project recovery actions after committed import navigation fails", async () => {
|
|
1863
|
+
const client = {
|
|
1864
|
+
listSessionImportSources: vi.fn(async () => ({
|
|
1865
|
+
harnesses: [
|
|
1866
|
+
{ harnessId: harnessIdSchema.parse("deepseek-harness"), name: "DeepSeek Harness" },
|
|
1867
|
+
],
|
|
1868
|
+
})),
|
|
1869
|
+
listHarnessSessions: vi.fn(async () => ({
|
|
1870
|
+
total: 1,
|
|
1871
|
+
candidates: [
|
|
1872
|
+
{
|
|
1873
|
+
nativeSessionId: "idle-session",
|
|
1874
|
+
title: null,
|
|
1875
|
+
updatedAt: 1_700_000_000_000,
|
|
1876
|
+
cwd: "C:\\work",
|
|
1877
|
+
running: false,
|
|
1878
|
+
},
|
|
1879
|
+
],
|
|
1880
|
+
})),
|
|
1881
|
+
importHarnessSession: vi.fn(async () => ({
|
|
1882
|
+
threadId: hostThreadIdSchema.parse("imported-thread"),
|
|
1883
|
+
})),
|
|
1884
|
+
};
|
|
1885
|
+
const openImportedThread = vi
|
|
1886
|
+
.fn()
|
|
1887
|
+
.mockRejectedValueOnce(new Error("private navigation detail"))
|
|
1888
|
+
.mockResolvedValueOnce(undefined);
|
|
1889
|
+
const page = createDefaultRendererSettingsPages(
|
|
1890
|
+
rendererSettingsMessages("en"),
|
|
1891
|
+
() => null,
|
|
1892
|
+
() => null,
|
|
1893
|
+
() => null,
|
|
1894
|
+
() => client,
|
|
1895
|
+
openImportedThread,
|
|
1896
|
+
).find(({ id }) => id === "session-import");
|
|
1897
|
+
if (!page) throw new Error("Session Import page is not registered");
|
|
1898
|
+
const document = new FakeDocument();
|
|
1899
|
+
const content = document.createElement("main");
|
|
1900
|
+
const scope = new RendererSettingsPageScope();
|
|
1901
|
+
page.mount({
|
|
1902
|
+
content: content as unknown as HTMLElement,
|
|
1903
|
+
signal: scope.signal,
|
|
1904
|
+
runLatest: (operation, handlers) => scope.runLatest(operation, handlers),
|
|
1905
|
+
});
|
|
1906
|
+
await vi.waitFor(() => expect(visibleText(content)).toContain("Untitled session"));
|
|
1907
|
+
|
|
1908
|
+
descendants(content)
|
|
1909
|
+
.find(({ dataset }) => dataset.sessionImportAction === "import")
|
|
1910
|
+
?.dispatch("click");
|
|
1911
|
+
|
|
1912
|
+
await vi.waitFor(() => expect(visibleText(content)).toContain("Session imported"));
|
|
1913
|
+
const recovery = elementWithClass(content, "settings-session-import-recovery");
|
|
1914
|
+
expect(recovery.focused).toBe(true);
|
|
1915
|
+
expect(visibleText(content)).toContain("C:\\work");
|
|
1916
|
+
expect(visibleText(content)).toContain("added as a project");
|
|
1917
|
+
expect(visibleText(content)).not.toContain("private navigation detail");
|
|
1918
|
+
|
|
1919
|
+
const copyPath = descendants(content).find(
|
|
1920
|
+
({ dataset }) => dataset.sessionImportAction === "copy-project-path",
|
|
1921
|
+
);
|
|
1922
|
+
if (!copyPath) throw new Error("Copy project path action is not rendered");
|
|
1923
|
+
copyPath.dispatch("click");
|
|
1924
|
+
await vi.waitFor(() => expect(document.clipboardWriteText).toHaveBeenCalledWith("C:\\work"));
|
|
1925
|
+
expect(visibleNotesText(copyPath)).toContain("Copied");
|
|
1926
|
+
|
|
1927
|
+
const retry = descendants(content).find(
|
|
1928
|
+
({ dataset }) => dataset.sessionImportAction === "retry-open",
|
|
1929
|
+
);
|
|
1930
|
+
if (!retry) throw new Error("Retry open action is not rendered");
|
|
1931
|
+
retry.dispatch("click");
|
|
1932
|
+
expect(retry.disabled).toBe(true);
|
|
1933
|
+
expect(retry.getAttribute("aria-busy")).toBe("true");
|
|
1934
|
+
expect(
|
|
1935
|
+
descendants(content).find(({ dataset }) => dataset.sessionImportAction === "refresh")
|
|
1936
|
+
?.disabled,
|
|
1937
|
+
).toBe(true);
|
|
1938
|
+
await vi.waitFor(() => expect(openImportedThread).toHaveBeenCalledTimes(2));
|
|
1939
|
+
expect(client.importHarnessSession).toHaveBeenCalledOnce();
|
|
1940
|
+
scope.dispose();
|
|
1941
|
+
});
|
|
1942
|
+
|
|
1943
|
+
it("shows an honest unavailable state without a local import client", () => {
|
|
1944
|
+
const page = createDefaultRendererSettingsPages(rendererSettingsMessages("en")).find(
|
|
1945
|
+
({ id }) => id === "session-import",
|
|
1946
|
+
);
|
|
1947
|
+
if (!page) throw new Error("Session Import page is not registered");
|
|
1948
|
+
const document = new FakeDocument();
|
|
1949
|
+
const content = document.createElement("main");
|
|
1950
|
+
const scope = new RendererSettingsPageScope();
|
|
1951
|
+
page.mount({
|
|
1952
|
+
content: content as unknown as HTMLElement,
|
|
1953
|
+
signal: scope.signal,
|
|
1954
|
+
runLatest: (operation, handlers) => scope.runLatest(operation, handlers),
|
|
1955
|
+
});
|
|
1956
|
+
|
|
1957
|
+
expect(visibleText(content)).toContain("Session import is unavailable for this local Harness");
|
|
1958
|
+
expect(
|
|
1959
|
+
descendants(content).filter(({ dataset }) => dataset.sessionImportAction === "import"),
|
|
1960
|
+
).toHaveLength(0);
|
|
1961
|
+
scope.dispose();
|
|
1962
|
+
});
|
|
1963
|
+
|
|
1964
|
+
it("ignores a Session list that resolves after the settings page is disposed", async () => {
|
|
1965
|
+
const listed = deferred<{ candidates: never[]; total: number }>();
|
|
1966
|
+
const client = {
|
|
1967
|
+
listSessionImportSources: vi.fn(async () => ({
|
|
1968
|
+
harnesses: [
|
|
1969
|
+
{ harnessId: harnessIdSchema.parse("deepseek-harness"), name: "DeepSeek Harness" },
|
|
1970
|
+
],
|
|
1971
|
+
})),
|
|
1972
|
+
listHarnessSessions: vi.fn(() => listed.promise),
|
|
1973
|
+
importHarnessSession: vi.fn(),
|
|
1974
|
+
};
|
|
1975
|
+
const page = createDefaultRendererSettingsPages(
|
|
1976
|
+
rendererSettingsMessages("en"),
|
|
1977
|
+
() => null,
|
|
1978
|
+
() => null,
|
|
1979
|
+
() => null,
|
|
1980
|
+
() => client,
|
|
1981
|
+
).find(({ id }) => id === "session-import");
|
|
1982
|
+
if (!page) throw new Error("Session Import page is not registered");
|
|
1983
|
+
const document = new FakeDocument();
|
|
1984
|
+
const content = document.createElement("main");
|
|
1985
|
+
const scope = new RendererSettingsPageScope();
|
|
1986
|
+
page.mount({
|
|
1987
|
+
content: content as unknown as HTMLElement,
|
|
1988
|
+
signal: scope.signal,
|
|
1989
|
+
runLatest: (operation, handlers) => scope.runLatest(operation, handlers),
|
|
1990
|
+
});
|
|
1991
|
+
const before = visibleText(content);
|
|
1992
|
+
|
|
1993
|
+
scope.dispose();
|
|
1994
|
+
listed.resolve({ candidates: [], total: 0 });
|
|
1995
|
+
await Promise.resolve();
|
|
1996
|
+
await Promise.resolve();
|
|
1997
|
+
|
|
1998
|
+
expect(visibleText(content)).toBe(before);
|
|
1999
|
+
});
|
|
2000
|
+
});
|