@cuna_labs/cli 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 +201 -0
- package/NOTICE +9 -0
- package/README.md +169 -0
- package/THIRD_PARTY_NOTICES.md +29 -0
- package/dist/api/capability-evidence.d.ts +19 -0
- package/dist/api/capability-evidence.d.ts.map +1 -0
- package/dist/api/capability-evidence.js +38 -0
- package/dist/api/capability-evidence.js.map +1 -0
- package/dist/api/client.d.ts +167 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/client.js +937 -0
- package/dist/api/client.js.map +1 -0
- package/dist/api/contracts.d.ts +345 -0
- package/dist/api/contracts.d.ts.map +1 -0
- package/dist/api/contracts.js +976 -0
- package/dist/api/contracts.js.map +1 -0
- package/dist/api/execution-workspaces.d.ts +17 -0
- package/dist/api/execution-workspaces.d.ts.map +1 -0
- package/dist/api/execution-workspaces.js +46 -0
- package/dist/api/execution-workspaces.js.map +1 -0
- package/dist/api/http.d.ts +76 -0
- package/dist/api/http.d.ts.map +1 -0
- package/dist/api/http.js +838 -0
- package/dist/api/http.js.map +1 -0
- package/dist/api/managed-executions.d.ts +37 -0
- package/dist/api/managed-executions.d.ts.map +1 -0
- package/dist/api/managed-executions.js +80 -0
- package/dist/api/managed-executions.js.map +1 -0
- package/dist/api/observer-v2-schema.d.ts +206 -0
- package/dist/api/observer-v2-schema.d.ts.map +1 -0
- package/dist/api/observer-v2-schema.js +264 -0
- package/dist/api/observer-v2-schema.js.map +1 -0
- package/dist/api/observer-v2.d.ts +33 -0
- package/dist/api/observer-v2.d.ts.map +1 -0
- package/dist/api/observer-v2.js +60 -0
- package/dist/api/observer-v2.js.map +1 -0
- package/dist/api/owner-observe-grants-v2-schema.d.ts +678 -0
- package/dist/api/owner-observe-grants-v2-schema.d.ts.map +1 -0
- package/dist/api/owner-observe-grants-v2-schema.js +887 -0
- package/dist/api/owner-observe-grants-v2-schema.js.map +1 -0
- package/dist/api/owner-observe-grants-v2.d.ts +268 -0
- package/dist/api/owner-observe-grants-v2.d.ts.map +1 -0
- package/dist/api/owner-observe-grants-v2.js +604 -0
- package/dist/api/owner-observe-grants-v2.js.map +1 -0
- package/dist/api/provider-v2-schema.d.ts +293 -0
- package/dist/api/provider-v2-schema.d.ts.map +1 -0
- package/dist/api/provider-v2-schema.js +381 -0
- package/dist/api/provider-v2-schema.js.map +1 -0
- package/dist/api/provider-v2.d.ts +55 -0
- package/dist/api/provider-v2.d.ts.map +1 -0
- package/dist/api/provider-v2.js +54 -0
- package/dist/api/provider-v2.js.map +1 -0
- package/dist/api/remote-workspace.d.ts +29 -0
- package/dist/api/remote-workspace.d.ts.map +1 -0
- package/dist/api/remote-workspace.js +62 -0
- package/dist/api/remote-workspace.js.map +1 -0
- package/dist/api/supervisor-live-update.d.ts +248 -0
- package/dist/api/supervisor-live-update.d.ts.map +1 -0
- package/dist/api/supervisor-live-update.js +434 -0
- package/dist/api/supervisor-live-update.js.map +1 -0
- package/dist/api/terminal-reason.d.ts +5 -0
- package/dist/api/terminal-reason.d.ts.map +1 -0
- package/dist/api/terminal-reason.js +12 -0
- package/dist/api/terminal-reason.js.map +1 -0
- package/dist/auth/browser-handoff.d.ts +59 -0
- package/dist/auth/browser-handoff.d.ts.map +1 -0
- package/dist/auth/browser-handoff.js +33 -0
- package/dist/auth/browser-handoff.js.map +1 -0
- package/dist/auth/browser.d.ts +11 -0
- package/dist/auth/browser.d.ts.map +1 -0
- package/dist/auth/browser.js +47 -0
- package/dist/auth/browser.js.map +1 -0
- package/dist/auth/human-client.d.ts +39 -0
- package/dist/auth/human-client.d.ts.map +1 -0
- package/dist/auth/human-client.js +66 -0
- package/dist/auth/human-client.js.map +1 -0
- package/dist/auth/human-contracts.d.ts +52 -0
- package/dist/auth/human-contracts.d.ts.map +1 -0
- package/dist/auth/human-contracts.js +229 -0
- package/dist/auth/human-contracts.js.map +1 -0
- package/dist/auth/human-session.d.ts +48 -0
- package/dist/auth/human-session.d.ts.map +1 -0
- package/dist/auth/human-session.js +902 -0
- package/dist/auth/human-session.js.map +1 -0
- package/dist/auth/onboarding-state.d.ts +31 -0
- package/dist/auth/onboarding-state.d.ts.map +1 -0
- package/dist/auth/onboarding-state.js +52 -0
- package/dist/auth/onboarding-state.js.map +1 -0
- package/dist/auth/pkce.d.ts +15 -0
- package/dist/auth/pkce.d.ts.map +1 -0
- package/dist/auth/pkce.js +19 -0
- package/dist/auth/pkce.js.map +1 -0
- package/dist/bin/cuna.d.ts +3 -0
- package/dist/bin/cuna.d.ts.map +1 -0
- package/dist/bin/cuna.js +4 -0
- package/dist/bin/cuna.js.map +1 -0
- package/dist/build-identity.d.ts +20 -0
- package/dist/build-identity.d.ts.map +1 -0
- package/dist/build-identity.js +77 -0
- package/dist/build-identity.js.map +1 -0
- package/dist/cli/command-help.d.ts +9 -0
- package/dist/cli/command-help.d.ts.map +1 -0
- package/dist/cli/command-help.js +324 -0
- package/dist/cli/command-help.js.map +1 -0
- package/dist/cli/help.d.ts +40 -0
- package/dist/cli/help.d.ts.map +1 -0
- package/dist/cli/help.js +256 -0
- package/dist/cli/help.js.map +1 -0
- package/dist/cli/output.d.ts +28 -0
- package/dist/cli/output.d.ts.map +1 -0
- package/dist/cli/output.js +95 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/cli/parser.d.ts +65 -0
- package/dist/cli/parser.d.ts.map +1 -0
- package/dist/cli/parser.js +294 -0
- package/dist/cli/parser.js.map +1 -0
- package/dist/cli/process-entrypoint.d.ts +18 -0
- package/dist/cli/process-entrypoint.d.ts.map +1 -0
- package/dist/cli/process-entrypoint.js +26 -0
- package/dist/cli/process-entrypoint.js.map +1 -0
- package/dist/cli/run.d.ts +114 -0
- package/dist/cli/run.d.ts.map +1 -0
- package/dist/cli/run.js +1969 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/commands/commands.d.ts +74 -0
- package/dist/commands/commands.d.ts.map +1 -0
- package/dist/commands/commands.js +2814 -0
- package/dist/commands/commands.js.map +1 -0
- package/dist/config/config.d.ts +86 -0
- package/dist/config/config.d.ts.map +1 -0
- package/dist/config/config.js +323 -0
- package/dist/config/config.js.map +1 -0
- package/dist/config/infra-contract-witness.d.ts +29 -0
- package/dist/config/infra-contract-witness.d.ts.map +1 -0
- package/dist/config/infra-contract-witness.js +30 -0
- package/dist/config/infra-contract-witness.js.map +1 -0
- package/dist/core/browser-url.d.ts +2 -0
- package/dist/core/browser-url.d.ts.map +1 -0
- package/dist/core/browser-url.js +20 -0
- package/dist/core/browser-url.js.map +1 -0
- package/dist/core/deployed-wire-compatibility.d.ts +15 -0
- package/dist/core/deployed-wire-compatibility.d.ts.map +1 -0
- package/dist/core/deployed-wire-compatibility.js +15 -0
- package/dist/core/deployed-wire-compatibility.js.map +1 -0
- package/dist/core/errors.d.ts +59 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +82 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/exit-codes.d.ts +68 -0
- package/dist/core/exit-codes.d.ts.map +1 -0
- package/dist/core/exit-codes.js +84 -0
- package/dist/core/exit-codes.js.map +1 -0
- package/dist/core/instant.d.ts +60 -0
- package/dist/core/instant.d.ts.map +1 -0
- package/dist/core/instant.js +90 -0
- package/dist/core/instant.js.map +1 -0
- package/dist/core/namespace.d.ts +171 -0
- package/dist/core/namespace.d.ts.map +1 -0
- package/dist/core/namespace.js +227 -0
- package/dist/core/namespace.js.map +1 -0
- package/dist/core/observation-budget.d.ts +185 -0
- package/dist/core/observation-budget.d.ts.map +1 -0
- package/dist/core/observation-budget.js +209 -0
- package/dist/core/observation-budget.js.map +1 -0
- package/dist/core/product-web.d.ts +81 -0
- package/dist/core/product-web.d.ts.map +1 -0
- package/dist/core/product-web.js +87 -0
- package/dist/core/product-web.js.map +1 -0
- package/dist/core/validation.d.ts +90 -0
- package/dist/core/validation.d.ts.map +1 -0
- package/dist/core/validation.js +199 -0
- package/dist/core/validation.js.map +1 -0
- package/dist/credentials/contracts.d.ts +82 -0
- package/dist/credentials/contracts.d.ts.map +1 -0
- package/dist/credentials/contracts.js +2 -0
- package/dist/credentials/contracts.js.map +1 -0
- package/dist/credentials/errors.d.ts +19 -0
- package/dist/credentials/errors.d.ts.map +1 -0
- package/dist/credentials/errors.js +16 -0
- package/dist/credentials/errors.js.map +1 -0
- package/dist/credentials/index.d.ts +6 -0
- package/dist/credentials/index.d.ts.map +1 -0
- package/dist/credentials/index.js +6 -0
- package/dist/credentials/index.js.map +1 -0
- package/dist/credentials/local-session.d.ts +101 -0
- package/dist/credentials/local-session.d.ts.map +1 -0
- package/dist/credentials/local-session.js +839 -0
- package/dist/credentials/local-session.js.map +1 -0
- package/dist/credentials/secret-material.d.ts +17 -0
- package/dist/credentials/secret-material.d.ts.map +1 -0
- package/dist/credentials/secret-material.js +49 -0
- package/dist/credentials/secret-material.js.map +1 -0
- package/dist/credentials/vault.d.ts +41 -0
- package/dist/credentials/vault.d.ts.map +1 -0
- package/dist/credentials/vault.js +719 -0
- package/dist/credentials/vault.js.map +1 -0
- package/dist/daemon/endpoint.d.ts +29 -0
- package/dist/daemon/endpoint.d.ts.map +1 -0
- package/dist/daemon/endpoint.js +66 -0
- package/dist/daemon/endpoint.js.map +1 -0
- package/dist/daemon/journal.d.ts +62 -0
- package/dist/daemon/journal.d.ts.map +1 -0
- package/dist/daemon/journal.js +299 -0
- package/dist/daemon/journal.js.map +1 -0
- package/dist/daemon/lease.d.ts +25 -0
- package/dist/daemon/lease.d.ts.map +1 -0
- package/dist/daemon/lease.js +94 -0
- package/dist/daemon/lease.js.map +1 -0
- package/dist/daemon/lifecycle.d.ts +14 -0
- package/dist/daemon/lifecycle.d.ts.map +1 -0
- package/dist/daemon/lifecycle.js +29 -0
- package/dist/daemon/lifecycle.js.map +1 -0
- package/dist/daemon/protocol.d.ts +65 -0
- package/dist/daemon/protocol.d.ts.map +1 -0
- package/dist/daemon/protocol.js +194 -0
- package/dist/daemon/protocol.js.map +1 -0
- package/dist/daemon/supervisor.d.ts +29 -0
- package/dist/daemon/supervisor.d.ts.map +1 -0
- package/dist/daemon/supervisor.js +87 -0
- package/dist/daemon/supervisor.js.map +1 -0
- package/dist/daemon/views.d.ts +31 -0
- package/dist/daemon/views.d.ts.map +1 -0
- package/dist/daemon/views.js +86 -0
- package/dist/daemon/views.js.map +1 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +38 -0
- package/dist/index.js.map +1 -0
- package/dist/journey/api-effects.d.ts +59 -0
- package/dist/journey/api-effects.d.ts.map +1 -0
- package/dist/journey/api-effects.js +386 -0
- package/dist/journey/api-effects.js.map +1 -0
- package/dist/journey/derived-identity.d.ts +91 -0
- package/dist/journey/derived-identity.d.ts.map +1 -0
- package/dist/journey/derived-identity.js +123 -0
- package/dist/journey/derived-identity.js.map +1 -0
- package/dist/journey/index.d.ts +6 -0
- package/dist/journey/index.d.ts.map +1 -0
- package/dist/journey/index.js +6 -0
- package/dist/journey/index.js.map +1 -0
- package/dist/journey/intent.d.ts +36 -0
- package/dist/journey/intent.d.ts.map +1 -0
- package/dist/journey/intent.js +191 -0
- package/dist/journey/intent.js.map +1 -0
- package/dist/journey/orchestrator.d.ts +140 -0
- package/dist/journey/orchestrator.d.ts.map +1 -0
- package/dist/journey/orchestrator.js +415 -0
- package/dist/journey/orchestrator.js.map +1 -0
- package/dist/journey/provider-launch-intent.d.ts +12 -0
- package/dist/journey/provider-launch-intent.d.ts.map +1 -0
- package/dist/journey/provider-launch-intent.js +40 -0
- package/dist/journey/provider-launch-intent.js.map +1 -0
- package/dist/journey/remote-workspace.d.ts +34 -0
- package/dist/journey/remote-workspace.d.ts.map +1 -0
- package/dist/journey/remote-workspace.js +91 -0
- package/dist/journey/remote-workspace.js.map +1 -0
- package/dist/journey/root-entry.d.ts +56 -0
- package/dist/journey/root-entry.d.ts.map +1 -0
- package/dist/journey/root-entry.js +19 -0
- package/dist/journey/root-entry.js.map +1 -0
- package/dist/journey/selection.d.ts +165 -0
- package/dist/journey/selection.d.ts.map +1 -0
- package/dist/journey/selection.js +565 -0
- package/dist/journey/selection.js.map +1 -0
- package/dist/journey/session-failure.d.ts +4 -0
- package/dist/journey/session-failure.d.ts.map +1 -0
- package/dist/journey/session-failure.js +28 -0
- package/dist/journey/session-failure.js.map +1 -0
- package/dist/journey/workspace-effects.d.ts +30 -0
- package/dist/journey/workspace-effects.d.ts.map +1 -0
- package/dist/journey/workspace-effects.js +348 -0
- package/dist/journey/workspace-effects.js.map +1 -0
- package/dist/local-actions/broker.d.ts +32 -0
- package/dist/local-actions/broker.d.ts.map +1 -0
- package/dist/local-actions/broker.js +400 -0
- package/dist/local-actions/broker.js.map +1 -0
- package/dist/local-actions/browser-action.d.ts +78 -0
- package/dist/local-actions/browser-action.d.ts.map +1 -0
- package/dist/local-actions/browser-action.js +307 -0
- package/dist/local-actions/browser-action.js.map +1 -0
- package/dist/local-actions/callback-relay.d.ts +45 -0
- package/dist/local-actions/callback-relay.d.ts.map +1 -0
- package/dist/local-actions/callback-relay.js +265 -0
- package/dist/local-actions/callback-relay.js.map +1 -0
- package/dist/local-actions/clipboard.d.ts +3 -0
- package/dist/local-actions/clipboard.d.ts.map +1 -0
- package/dist/local-actions/clipboard.js +18 -0
- package/dist/local-actions/clipboard.js.map +1 -0
- package/dist/local-actions/contracts.d.ts +63 -0
- package/dist/local-actions/contracts.d.ts.map +1 -0
- package/dist/local-actions/contracts.js +18 -0
- package/dist/local-actions/contracts.js.map +1 -0
- package/dist/local-actions/desktop-presentation.d.ts +141 -0
- package/dist/local-actions/desktop-presentation.d.ts.map +1 -0
- package/dist/local-actions/desktop-presentation.js +253 -0
- package/dist/local-actions/desktop-presentation.js.map +1 -0
- package/dist/local-actions/device-selection.d.ts +43 -0
- package/dist/local-actions/device-selection.d.ts.map +1 -0
- package/dist/local-actions/device-selection.js +154 -0
- package/dist/local-actions/device-selection.js.map +1 -0
- package/dist/local-actions/executor.d.ts +49 -0
- package/dist/local-actions/executor.d.ts.map +1 -0
- package/dist/local-actions/executor.js +329 -0
- package/dist/local-actions/executor.js.map +1 -0
- package/dist/local-actions/file-transfers.d.ts +141 -0
- package/dist/local-actions/file-transfers.d.ts.map +1 -0
- package/dist/local-actions/file-transfers.js +446 -0
- package/dist/local-actions/file-transfers.js.map +1 -0
- package/dist/local-actions/foreground-notifications.d.ts +45 -0
- package/dist/local-actions/foreground-notifications.d.ts.map +1 -0
- package/dist/local-actions/foreground-notifications.js +163 -0
- package/dist/local-actions/foreground-notifications.js.map +1 -0
- package/dist/local-actions/git-signing.d.ts +39 -0
- package/dist/local-actions/git-signing.d.ts.map +1 -0
- package/dist/local-actions/git-signing.js +135 -0
- package/dist/local-actions/git-signing.js.map +1 -0
- package/dist/local-actions/index.d.ts +19 -0
- package/dist/local-actions/index.d.ts.map +1 -0
- package/dist/local-actions/index.js +19 -0
- package/dist/local-actions/index.js.map +1 -0
- package/dist/local-actions/local-service-actions.d.ts +87 -0
- package/dist/local-actions/local-service-actions.d.ts.map +1 -0
- package/dist/local-actions/local-service-actions.js +347 -0
- package/dist/local-actions/local-service-actions.js.map +1 -0
- package/dist/local-actions/loopback.d.ts +5 -0
- package/dist/local-actions/loopback.d.ts.map +1 -0
- package/dist/local-actions/loopback.js +15 -0
- package/dist/local-actions/loopback.js.map +1 -0
- package/dist/local-actions/policy-store.d.ts +7 -0
- package/dist/local-actions/policy-store.d.ts.map +1 -0
- package/dist/local-actions/policy-store.js +70 -0
- package/dist/local-actions/policy-store.js.map +1 -0
- package/dist/local-actions/policy.d.ts +21 -0
- package/dist/local-actions/policy.d.ts.map +1 -0
- package/dist/local-actions/policy.js +43 -0
- package/dist/local-actions/policy.js.map +1 -0
- package/dist/local-actions/private-port-forward.d.ts +36 -0
- package/dist/local-actions/private-port-forward.d.ts.map +1 -0
- package/dist/local-actions/private-port-forward.js +154 -0
- package/dist/local-actions/private-port-forward.js.map +1 -0
- package/dist/local-actions/provider-mcp-config.d.ts +101 -0
- package/dist/local-actions/provider-mcp-config.d.ts.map +1 -0
- package/dist/local-actions/provider-mcp-config.js +166 -0
- package/dist/local-actions/provider-mcp-config.js.map +1 -0
- package/dist/local-actions/providers.d.ts +12 -0
- package/dist/local-actions/providers.d.ts.map +1 -0
- package/dist/local-actions/providers.js +42 -0
- package/dist/local-actions/providers.js.map +1 -0
- package/dist/local-actions/schemas.d.ts +4 -0
- package/dist/local-actions/schemas.d.ts.map +1 -0
- package/dist/local-actions/schemas.js +314 -0
- package/dist/local-actions/schemas.js.map +1 -0
- package/dist/local-actions/sensitive-consent.d.ts +32 -0
- package/dist/local-actions/sensitive-consent.d.ts.map +1 -0
- package/dist/local-actions/sensitive-consent.js +47 -0
- package/dist/local-actions/sensitive-consent.js.map +1 -0
- package/dist/machines/command-launch.d.ts +17 -0
- package/dist/machines/command-launch.d.ts.map +1 -0
- package/dist/machines/command-launch.js +52 -0
- package/dist/machines/command-launch.js.map +1 -0
- package/dist/machines/execution-receipt.d.ts +18 -0
- package/dist/machines/execution-receipt.d.ts.map +1 -0
- package/dist/machines/execution-receipt.js +87 -0
- package/dist/machines/execution-receipt.js.map +1 -0
- package/dist/machines/executions-screen.d.ts +6 -0
- package/dist/machines/executions-screen.d.ts.map +1 -0
- package/dist/machines/executions-screen.js +410 -0
- package/dist/machines/executions-screen.js.map +1 -0
- package/dist/machines/explorer.d.ts +75 -0
- package/dist/machines/explorer.d.ts.map +1 -0
- package/dist/machines/explorer.js +1726 -0
- package/dist/machines/explorer.js.map +1 -0
- package/dist/machines/live-supervisor-update.d.ts +325 -0
- package/dist/machines/live-supervisor-update.d.ts.map +1 -0
- package/dist/machines/live-supervisor-update.js +513 -0
- package/dist/machines/live-supervisor-update.js.map +1 -0
- package/dist/machines/machine-first.d.ts +123 -0
- package/dist/machines/machine-first.d.ts.map +1 -0
- package/dist/machines/machine-first.js +119 -0
- package/dist/machines/machine-first.js.map +1 -0
- package/dist/machines/opencode-supervisor.d.ts +51 -0
- package/dist/machines/opencode-supervisor.d.ts.map +1 -0
- package/dist/machines/opencode-supervisor.js +124 -0
- package/dist/machines/opencode-supervisor.js.map +1 -0
- package/dist/machines/pagination.d.ts +4 -0
- package/dist/machines/pagination.d.ts.map +1 -0
- package/dist/machines/pagination.js +7 -0
- package/dist/machines/pagination.js.map +1 -0
- package/dist/machines/provider-availability.d.ts +42 -0
- package/dist/machines/provider-availability.d.ts.map +1 -0
- package/dist/machines/provider-availability.js +82 -0
- package/dist/machines/provider-availability.js.map +1 -0
- package/dist/machines/provider-screen.d.ts +12 -0
- package/dist/machines/provider-screen.d.ts.map +1 -0
- package/dist/machines/provider-screen.js +129 -0
- package/dist/machines/provider-screen.js.map +1 -0
- package/dist/machines/session-actionability.d.ts +86 -0
- package/dist/machines/session-actionability.d.ts.map +1 -0
- package/dist/machines/session-actionability.js +125 -0
- package/dist/machines/session-actionability.js.map +1 -0
- package/dist/machines/session-visibility.d.ts +139 -0
- package/dist/machines/session-visibility.d.ts.map +1 -0
- package/dist/machines/session-visibility.js +109 -0
- package/dist/machines/session-visibility.js.map +1 -0
- package/dist/platform/adapter.d.ts +62 -0
- package/dist/platform/adapter.d.ts.map +1 -0
- package/dist/platform/adapter.js +240 -0
- package/dist/platform/adapter.js.map +1 -0
- package/dist/platform/support.d.ts +12 -0
- package/dist/platform/support.d.ts.map +1 -0
- package/dist/platform/support.js +25 -0
- package/dist/platform/support.js.map +1 -0
- package/dist/pty/contracts.d.ts +45 -0
- package/dist/pty/contracts.d.ts.map +1 -0
- package/dist/pty/contracts.js +2 -0
- package/dist/pty/contracts.js.map +1 -0
- package/dist/pty/evidence-gate.d.ts +14 -0
- package/dist/pty/evidence-gate.d.ts.map +1 -0
- package/dist/pty/evidence-gate.js +35 -0
- package/dist/pty/evidence-gate.js.map +1 -0
- package/dist/pty/index.d.ts +5 -0
- package/dist/pty/index.d.ts.map +1 -0
- package/dist/pty/index.js +5 -0
- package/dist/pty/index.js.map +1 -0
- package/dist/pty/node-host-terminal.d.ts +14 -0
- package/dist/pty/node-host-terminal.d.ts.map +1 -0
- package/dist/pty/node-host-terminal.js +173 -0
- package/dist/pty/node-host-terminal.js.map +1 -0
- package/dist/pty/node-process.d.ts +26 -0
- package/dist/pty/node-process.d.ts.map +1 -0
- package/dist/pty/node-process.js +102 -0
- package/dist/pty/node-process.js.map +1 -0
- package/dist/runtime/api-terminal-control-plane.d.ts +7 -0
- package/dist/runtime/api-terminal-control-plane.d.ts.map +1 -0
- package/dist/runtime/api-terminal-control-plane.js +95 -0
- package/dist/runtime/api-terminal-control-plane.js.map +1 -0
- package/dist/runtime/backoff.d.ts +17 -0
- package/dist/runtime/backoff.d.ts.map +1 -0
- package/dist/runtime/backoff.js +61 -0
- package/dist/runtime/backoff.js.map +1 -0
- package/dist/runtime/boundary.d.ts +171 -0
- package/dist/runtime/boundary.d.ts.map +1 -0
- package/dist/runtime/boundary.js +1858 -0
- package/dist/runtime/boundary.js.map +1 -0
- package/dist/runtime/capability-gate.d.ts +25 -0
- package/dist/runtime/capability-gate.d.ts.map +1 -0
- package/dist/runtime/capability-gate.js +77 -0
- package/dist/runtime/capability-gate.js.map +1 -0
- package/dist/runtime/contracts.d.ts +25 -0
- package/dist/runtime/contracts.d.ts.map +1 -0
- package/dist/runtime/contracts.js +66 -0
- package/dist/runtime/contracts.js.map +1 -0
- package/dist/runtime/errors.d.ts +20 -0
- package/dist/runtime/errors.d.ts.map +1 -0
- package/dist/runtime/errors.js +22 -0
- package/dist/runtime/errors.js.map +1 -0
- package/dist/runtime/index.d.ts +9 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +9 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/node-foreground-session.d.ts +50 -0
- package/dist/runtime/node-foreground-session.d.ts.map +1 -0
- package/dist/runtime/node-foreground-session.js +431 -0
- package/dist/runtime/node-foreground-session.js.map +1 -0
- package/dist/runtime/node-websocket-connector.d.ts +6 -0
- package/dist/runtime/node-websocket-connector.d.ts.map +1 -0
- package/dist/runtime/node-websocket-connector.js +344 -0
- package/dist/runtime/node-websocket-connector.js.map +1 -0
- package/dist/runtime/observer-screen.d.ts +6 -0
- package/dist/runtime/observer-screen.d.ts.map +1 -0
- package/dist/runtime/observer-screen.js +144 -0
- package/dist/runtime/observer-screen.js.map +1 -0
- package/dist/runtime/owner-grant-operations.d.ts +136 -0
- package/dist/runtime/owner-grant-operations.d.ts.map +1 -0
- package/dist/runtime/owner-grant-operations.js +216 -0
- package/dist/runtime/owner-grant-operations.js.map +1 -0
- package/dist/runtime/owner-grants-screen.d.ts +40 -0
- package/dist/runtime/owner-grants-screen.d.ts.map +1 -0
- package/dist/runtime/owner-grants-screen.js +987 -0
- package/dist/runtime/owner-grants-screen.js.map +1 -0
- package/dist/runtime/terminal-transport.d.ts +85 -0
- package/dist/runtime/terminal-transport.d.ts.map +1 -0
- package/dist/runtime/terminal-transport.js +152 -0
- package/dist/runtime/terminal-transport.js.map +1 -0
- package/dist/sync/continuous-sync-supervisor.d.ts +107 -0
- package/dist/sync/continuous-sync-supervisor.d.ts.map +1 -0
- package/dist/sync/continuous-sync-supervisor.js +1117 -0
- package/dist/sync/continuous-sync-supervisor.js.map +1 -0
- package/dist/sync/index.d.ts +9 -0
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/index.js +9 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/sync/journal.d.ts +57 -0
- package/dist/sync/journal.d.ts.map +1 -0
- package/dist/sync/journal.js +685 -0
- package/dist/sync/journal.js.map +1 -0
- package/dist/sync/revisions.d.ts +118 -0
- package/dist/sync/revisions.d.ts.map +1 -0
- package/dist/sync/revisions.js +364 -0
- package/dist/sync/revisions.js.map +1 -0
- package/dist/sync/supervisor.d.ts +107 -0
- package/dist/sync/supervisor.d.ts.map +1 -0
- package/dist/sync/supervisor.js +246 -0
- package/dist/sync/supervisor.js.map +1 -0
- package/dist/sync/workspace-sync-client.d.ts +18 -0
- package/dist/sync/workspace-sync-client.d.ts.map +1 -0
- package/dist/sync/workspace-sync-client.js +150 -0
- package/dist/sync/workspace-sync-client.js.map +1 -0
- package/dist/sync/workspace-sync-coordinator.d.ts +76 -0
- package/dist/sync/workspace-sync-coordinator.d.ts.map +1 -0
- package/dist/sync/workspace-sync-coordinator.js +621 -0
- package/dist/sync/workspace-sync-coordinator.js.map +1 -0
- package/dist/sync/workspace-sync-product-service.d.ts +79 -0
- package/dist/sync/workspace-sync-product-service.d.ts.map +1 -0
- package/dist/sync/workspace-sync-product-service.js +398 -0
- package/dist/sync/workspace-sync-product-service.js.map +1 -0
- package/dist/sync/workspace-sync-protocol.d.ts +145 -0
- package/dist/sync/workspace-sync-protocol.d.ts.map +1 -0
- package/dist/sync/workspace-sync-protocol.js +347 -0
- package/dist/sync/workspace-sync-protocol.js.map +1 -0
- package/dist/terminal/appbar.d.ts +40 -0
- package/dist/terminal/appbar.d.ts.map +1 -0
- package/dist/terminal/appbar.js +55 -0
- package/dist/terminal/appbar.js.map +1 -0
- package/dist/terminal/cell-width.d.ts +3 -0
- package/dist/terminal/cell-width.d.ts.map +1 -0
- package/dist/terminal/cell-width.js +53 -0
- package/dist/terminal/cell-width.js.map +1 -0
- package/dist/terminal/codec.d.ts +107 -0
- package/dist/terminal/codec.d.ts.map +1 -0
- package/dist/terminal/codec.js +538 -0
- package/dist/terminal/codec.js.map +1 -0
- package/dist/terminal/foreground.d.ts +135 -0
- package/dist/terminal/foreground.d.ts.map +1 -0
- package/dist/terminal/foreground.js +2097 -0
- package/dist/terminal/foreground.js.map +1 -0
- package/dist/terminal/generated/terminal-wire-v1.d.ts +26 -0
- package/dist/terminal/generated/terminal-wire-v1.d.ts.map +1 -0
- package/dist/terminal/generated/terminal-wire-v1.js +27 -0
- package/dist/terminal/generated/terminal-wire-v1.js.map +1 -0
- package/dist/terminal/mode.d.ts +33 -0
- package/dist/terminal/mode.d.ts.map +1 -0
- package/dist/terminal/mode.js +99 -0
- package/dist/terminal/mode.js.map +1 -0
- package/dist/terminal/passthrough.d.ts +47 -0
- package/dist/terminal/passthrough.d.ts.map +1 -0
- package/dist/terminal/passthrough.js +666 -0
- package/dist/terminal/passthrough.js.map +1 -0
- package/dist/terminal/resume.d.ts +58 -0
- package/dist/terminal/resume.d.ts.map +1 -0
- package/dist/terminal/resume.js +185 -0
- package/dist/terminal/resume.js.map +1 -0
- package/dist/terminal/viewport.d.ts +93 -0
- package/dist/terminal/viewport.d.ts.map +1 -0
- package/dist/terminal/viewport.js +266 -0
- package/dist/terminal/viewport.js.map +1 -0
- package/dist/terminal/workbench.d.ts +32 -0
- package/dist/terminal/workbench.d.ts.map +1 -0
- package/dist/terminal/workbench.js +316 -0
- package/dist/terminal/workbench.js.map +1 -0
- package/dist/terminal/xterm-vte.d.ts +49 -0
- package/dist/terminal/xterm-vte.d.ts.map +1 -0
- package/dist/terminal/xterm-vte.js +602 -0
- package/dist/terminal/xterm-vte.js.map +1 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/dist/workspace/binding-path.d.ts +17 -0
- package/dist/workspace/binding-path.d.ts.map +1 -0
- package/dist/workspace/binding-path.js +91 -0
- package/dist/workspace/binding-path.js.map +1 -0
- package/dist/workspace/binding-store.d.ts +107 -0
- package/dist/workspace/binding-store.d.ts.map +1 -0
- package/dist/workspace/binding-store.js +783 -0
- package/dist/workspace/binding-store.js.map +1 -0
- package/dist/workspace/binding.d.ts +35 -0
- package/dist/workspace/binding.d.ts.map +1 -0
- package/dist/workspace/binding.js +70 -0
- package/dist/workspace/binding.js.map +1 -0
- package/dist/workspace/errors.d.ts +4 -0
- package/dist/workspace/errors.d.ts.map +1 -0
- package/dist/workspace/errors.js +19 -0
- package/dist/workspace/errors.js.map +1 -0
- package/dist/workspace/exclusion.d.ts +21 -0
- package/dist/workspace/exclusion.d.ts.map +1 -0
- package/dist/workspace/exclusion.js +243 -0
- package/dist/workspace/exclusion.js.map +1 -0
- package/dist/workspace/index.d.ts +8 -0
- package/dist/workspace/index.d.ts.map +1 -0
- package/dist/workspace/index.js +8 -0
- package/dist/workspace/index.js.map +1 -0
- package/dist/workspace/lifecycle.d.ts +56 -0
- package/dist/workspace/lifecycle.d.ts.map +1 -0
- package/dist/workspace/lifecycle.js +94 -0
- package/dist/workspace/lifecycle.js.map +1 -0
- package/dist/workspace/manifest.d.ts +47 -0
- package/dist/workspace/manifest.d.ts.map +1 -0
- package/dist/workspace/manifest.js +243 -0
- package/dist/workspace/manifest.js.map +1 -0
- package/dist/workspace/paths.d.ts +14 -0
- package/dist/workspace/paths.d.ts.map +1 -0
- package/dist/workspace/paths.js +67 -0
- package/dist/workspace/paths.js.map +1 -0
- package/dist/workspace/schema.d.ts +15 -0
- package/dist/workspace/schema.d.ts.map +1 -0
- package/dist/workspace/schema.js +33 -0
- package/dist/workspace/schema.js.map +1 -0
- package/dist/workspace/selection-screen.d.ts +4 -0
- package/dist/workspace/selection-screen.d.ts.map +1 -0
- package/dist/workspace/selection-screen.js +216 -0
- package/dist/workspace/selection-screen.js.map +1 -0
- package/dist/workspace/selection-service.d.ts +16 -0
- package/dist/workspace/selection-service.d.ts.map +1 -0
- package/dist/workspace/selection-service.js +65 -0
- package/dist/workspace/selection-service.js.map +1 -0
- package/node_modules/@xterm/headless/README.md +31 -0
- package/node_modules/@xterm/headless/lib-headless/xterm-headless.js +2 -0
- package/node_modules/@xterm/headless/lib-headless/xterm-headless.js.map +1 -0
- package/node_modules/@xterm/headless/lib-headless/xterm-headless.mjs +46 -0
- package/node_modules/@xterm/headless/lib-headless/xterm-headless.mjs.map +7 -0
- package/node_modules/@xterm/headless/package.json +29 -0
- package/node_modules/@xterm/headless/typings/xterm-headless.d.ts +1385 -0
- package/package.json +75 -0
|
@@ -0,0 +1,2097 @@
|
|
|
1
|
+
import { copyLocalText } from "../local-actions/clipboard.js";
|
|
2
|
+
import { admitProviderAuthUrl, ProviderBrowserActionDetector, ProviderOAuthPasteGuard, } from "../local-actions/browser-action.js";
|
|
3
|
+
import { LOCAL_ACTION_PROTOCOL_VERSION, LocalActionBroker, digestLocalActionArguments, sameLocalActionIdentity, } from "../local-actions/index.js";
|
|
4
|
+
import { RuntimeBoundaryError, runtimeFailure, terminalHistoryGap } from "../runtime/errors.js";
|
|
5
|
+
import { assertCanonicalUuid } from "../core/validation.js";
|
|
6
|
+
import { buildAppbarModel } from "./appbar.js";
|
|
7
|
+
import { renderWorkbenchFrame } from "./workbench.js";
|
|
8
|
+
import { ViewportRegistry } from "./viewport.js";
|
|
9
|
+
import { XtermViewportAdapter } from "./xterm-vte.js";
|
|
10
|
+
const ESCAPE_PREFIX = 0x1d;
|
|
11
|
+
export const HISTORICAL_INPUT_NOTICE = "Prior input uncertain · not resent";
|
|
12
|
+
const INTERRUPT = 0x03;
|
|
13
|
+
const FLOW_RESUME = 0x11;
|
|
14
|
+
const FLOW_PAUSE = 0x13;
|
|
15
|
+
const REMOTE_INTERRUPT = 0x63;
|
|
16
|
+
const REMOTE_FLOW_RESUME = 0x71;
|
|
17
|
+
const REMOTE_FLOW_PAUSE = 0x73;
|
|
18
|
+
const REMOTE_REDRAW = Uint8Array.of(0x0c);
|
|
19
|
+
const TAB_FIRST = 0x31;
|
|
20
|
+
const TAB_LAST = 0x34;
|
|
21
|
+
const NEXT_TAB = 0x6e;
|
|
22
|
+
const DETACH = 0x64;
|
|
23
|
+
const HELP = 0x3f;
|
|
24
|
+
const RETRY = 0x72;
|
|
25
|
+
const TAKE_WRITER = 0x77; // Ctrl+] w: take the terminal's one writing seat
|
|
26
|
+
const RETAINED_SIGN_IN = 0x61; // Ctrl+] a: inspect the last retained sign-in link
|
|
27
|
+
const RESIZE_COALESCE_MS = 50;
|
|
28
|
+
const DISCONNECT_FRAME_MS = 30;
|
|
29
|
+
const DISCONNECTING_FRAMES = Object.freeze([
|
|
30
|
+
"✦ Disconnecting...",
|
|
31
|
+
"✧ Disconnecting...",
|
|
32
|
+
"✦ Disconnecting...",
|
|
33
|
+
]);
|
|
34
|
+
const STARTUP_CLOSE_FRAMES = Object.freeze(["✦ Closing Cuna...", "✧ Closing Cuna...", "✓ Closed."]);
|
|
35
|
+
const ATTACHING_FRAMES = Object.freeze(["◐", "◓", "◑", "◒"]);
|
|
36
|
+
const ATTACHING_PROGRESS = Object.freeze(["━╺━━━━", "━━╺━━━", "━━━╺━━", "━━━━╺━", "━━━━━╺", "━━━━╸━", "━━━╸━━", "━━╸━━━"]);
|
|
37
|
+
const ATTACHING_FRAME_MS = 90;
|
|
38
|
+
// Three progress frames plus one confirmation frame: 120ms by default and
|
|
39
|
+
// never more than one second under an injected/test cadence.
|
|
40
|
+
const MAX_DISCONNECT_FRAME_MS = 250;
|
|
41
|
+
const INPUT_WITHHELD_NOTICE = "Reconnecting · input was not sent. Retry after terminal attached.";
|
|
42
|
+
const FLOW_CONTROL_NOTICE = "Terminal output kept active · Ctrl+] s sends Ctrl+S remotely.";
|
|
43
|
+
const RECONNECT_FAILED_NOTICE = "Reconnect failed · Ctrl+] r retries · Ctrl+C disconnects.";
|
|
44
|
+
// Automatic recovery back-off: 100 ms doubling, capped at 5 s per wait, ten
|
|
45
|
+
// attempts, about 26 s in total. Three attempts (~0.7 s) gave up while the
|
|
46
|
+
// gateway was still resetting a terminal view after a host resize, leaving
|
|
47
|
+
// the person on "Reconnect failed" although a fresh attach a minute later
|
|
48
|
+
// succeeded (2026-09-15, session 7d73bf08). The bound stays explicit; a
|
|
49
|
+
// non-retryable refusal still stops the loop on its first occurrence.
|
|
50
|
+
const DEFAULT_RECONNECT_ATTEMPTS = 10;
|
|
51
|
+
const BRACKETED_PASTE_START = Uint8Array.of(0x1b, 0x5b, 0x32, 0x30, 0x30, 0x7e);
|
|
52
|
+
const BRACKETED_PASTE_END = Uint8Array.of(0x1b, 0x5b, 0x32, 0x30, 0x31, 0x7e);
|
|
53
|
+
const CLAUDE_LOCAL_ACTION_KINDS = Object.freeze(["browser.open"]);
|
|
54
|
+
const CODEX_LOCAL_ACTION_KINDS = Object.freeze(["browser.open", "auth.device.present"]);
|
|
55
|
+
const NO_LOCAL_ACTION_KINDS = Object.freeze([]);
|
|
56
|
+
export const MAX_FOREGROUND_PENDING_INPUT_BYTES = 1_048_576;
|
|
57
|
+
export class ForegroundTerminalCoordinator {
|
|
58
|
+
#options;
|
|
59
|
+
#registry = new ViewportRegistry();
|
|
60
|
+
#tabs = new Map();
|
|
61
|
+
#clock;
|
|
62
|
+
#runtime;
|
|
63
|
+
#lease;
|
|
64
|
+
#state = "idle";
|
|
65
|
+
#activeTabId;
|
|
66
|
+
#removeInput;
|
|
67
|
+
#removeResize;
|
|
68
|
+
#resizeTimer;
|
|
69
|
+
#renderTail = Promise.resolve();
|
|
70
|
+
/** The last complete frame the host accepted; undefined whenever the host screen is not known to show it. */
|
|
71
|
+
#lastHostFrame;
|
|
72
|
+
#inputTail = Promise.resolve();
|
|
73
|
+
#copyDetectors = new Map();
|
|
74
|
+
#copyLinks = new Map();
|
|
75
|
+
#prefixPending = false;
|
|
76
|
+
#prefixTarget;
|
|
77
|
+
#pasteActive = false;
|
|
78
|
+
#pasteStartMatch = 0;
|
|
79
|
+
#pasteEndMatch = 0;
|
|
80
|
+
#stopPromise;
|
|
81
|
+
#seatNotice;
|
|
82
|
+
#stopStarted;
|
|
83
|
+
#resolveStopStarted;
|
|
84
|
+
#lifetimeAbort = new AbortController();
|
|
85
|
+
#reconnectTasks = new Map();
|
|
86
|
+
#recoverableReconnectFailures = new Map();
|
|
87
|
+
#outputTails = new Map();
|
|
88
|
+
#localDetachTabIds = new Set();
|
|
89
|
+
#detachedSessions = [];
|
|
90
|
+
#browserDetectors = new Map();
|
|
91
|
+
#retainedBrowserDetectors = new Map();
|
|
92
|
+
#retainedBrowserCandidates = new Map();
|
|
93
|
+
#retainedPendingRequestId;
|
|
94
|
+
#oauthPasteGuards = new Map();
|
|
95
|
+
#oauthPrefixTimers = new Map();
|
|
96
|
+
#handledBrowserUrls = new Set();
|
|
97
|
+
#browserRequests = new Map();
|
|
98
|
+
#remoteLocalActionTabs = new Map();
|
|
99
|
+
#pendingRemoteLocalActionResults = new Map();
|
|
100
|
+
#remoteLocalActionResultSends = new Map();
|
|
101
|
+
#remoteLocalActionResultTasks = new Set();
|
|
102
|
+
#localActionBroker;
|
|
103
|
+
#removeAbort;
|
|
104
|
+
#pendingInputBytes = 0;
|
|
105
|
+
#helpVisible = false;
|
|
106
|
+
#pendingBrowserAction;
|
|
107
|
+
#pendingBrowserActionTabId;
|
|
108
|
+
#browserNotice;
|
|
109
|
+
#browserOpening = false;
|
|
110
|
+
#terminalFailure;
|
|
111
|
+
#stateRenderRunning = false;
|
|
112
|
+
#stateRenderDirty = false;
|
|
113
|
+
#startupDetached = false;
|
|
114
|
+
#closingTabId;
|
|
115
|
+
#disconnectNotice;
|
|
116
|
+
#attachingAnimationTimer;
|
|
117
|
+
#attachingFrame = 0;
|
|
118
|
+
constructor(options) {
|
|
119
|
+
const resizeCoalesceMs = options.resizeCoalesceMs ?? RESIZE_COALESCE_MS;
|
|
120
|
+
if (!Number.isSafeInteger(resizeCoalesceMs) || resizeCoalesceMs < 1 || resizeCoalesceMs > 1_000) {
|
|
121
|
+
throw new RangeError("Foreground resize coalescing must be between 1 and 1000 milliseconds.");
|
|
122
|
+
}
|
|
123
|
+
const reconnectAttempts = options.reconnectAttempts ?? DEFAULT_RECONNECT_ATTEMPTS;
|
|
124
|
+
const reconnectBaseDelayMs = options.reconnectBaseDelayMs ?? 100;
|
|
125
|
+
const disconnectFrameMs = options.disconnectFrameMs ?? DISCONNECT_FRAME_MS;
|
|
126
|
+
if (!Number.isSafeInteger(reconnectAttempts) || reconnectAttempts < 1 || reconnectAttempts > 10) {
|
|
127
|
+
throw new RangeError("Foreground reconnect attempts must be between 1 and 10.");
|
|
128
|
+
}
|
|
129
|
+
if (!Number.isSafeInteger(reconnectBaseDelayMs) || reconnectBaseDelayMs < 1 || reconnectBaseDelayMs > 5_000) {
|
|
130
|
+
throw new RangeError("Foreground reconnect delay must be between 1 and 5000 milliseconds.");
|
|
131
|
+
}
|
|
132
|
+
if (!Number.isSafeInteger(disconnectFrameMs) || disconnectFrameMs < 1 || disconnectFrameMs > MAX_DISCONNECT_FRAME_MS) {
|
|
133
|
+
throw new RangeError("Foreground disconnect frame duration must be between 1 and 250 milliseconds.");
|
|
134
|
+
}
|
|
135
|
+
this.#options = Object.freeze({ ...options, resizeCoalesceMs, reconnectAttempts, reconnectBaseDelayMs, disconnectFrameMs });
|
|
136
|
+
this.#clock = options.clock ?? Date.now;
|
|
137
|
+
this.#localActionBroker = new LocalActionBroker({
|
|
138
|
+
clock: this.#clock,
|
|
139
|
+
isIdentityLive: (identity) => this.#isLocalActionIdentityLive(identity),
|
|
140
|
+
onChange: (snapshot) => {
|
|
141
|
+
if (["succeeded", "failed", "denied", "expired", "cancelled"].includes(snapshot.state)) {
|
|
142
|
+
this.#browserRequests.delete(snapshot.request.id);
|
|
143
|
+
this.#queueRemoteLocalActionResult(snapshot);
|
|
144
|
+
}
|
|
145
|
+
void this.#render().catch(() => undefined);
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
let resolveStopStarted = () => undefined;
|
|
149
|
+
this.#stopStarted = new Promise((resolve) => {
|
|
150
|
+
resolveStopStarted = resolve;
|
|
151
|
+
});
|
|
152
|
+
this.#resolveStopStarted = resolveStopStarted;
|
|
153
|
+
}
|
|
154
|
+
get state() {
|
|
155
|
+
return this.#state;
|
|
156
|
+
}
|
|
157
|
+
get failure() {
|
|
158
|
+
return this.#terminalFailure ?? this.#recoverableReconnectFailures.values().next().value;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* AgentSessions the person detached from on purpose (Ctrl+] d or Ctrl+C),
|
|
162
|
+
* each confirmed by the runtime. A detach never terminates the remote
|
|
163
|
+
* process, so the runner tells the person how to come back once the host
|
|
164
|
+
* terminal is theirs again. Tabs detached by cleanup are not listed.
|
|
165
|
+
*/
|
|
166
|
+
get detachedSessions() {
|
|
167
|
+
return Object.freeze([...this.#detachedSessions]);
|
|
168
|
+
}
|
|
169
|
+
bindRuntime(runtime) {
|
|
170
|
+
if (this.#runtime !== undefined)
|
|
171
|
+
throw runtimeFailure("session_conflict", "The foreground runtime is already bound.");
|
|
172
|
+
if (this.#state !== "idle")
|
|
173
|
+
throw runtimeFailure("session_conflict", "The foreground runtime must be bound before startup.");
|
|
174
|
+
this.#runtime = runtime;
|
|
175
|
+
}
|
|
176
|
+
runtimeCallbacks() {
|
|
177
|
+
return Object.freeze({
|
|
178
|
+
onTerminalReady: async (snapshot) => await this.#terminalReady(snapshot),
|
|
179
|
+
onTerminalViewStarted: async (event) => await this.#terminalViewStarted(event),
|
|
180
|
+
onTerminalGeometry: async (event) => await this.#terminalGeometry(event.snapshot, event.signal),
|
|
181
|
+
onTerminalOutput: async (event) => await this.#queueTerminalOutput(event),
|
|
182
|
+
onTerminalState: (snapshot) => this.#terminalState(snapshot),
|
|
183
|
+
localActionKinds: (agentSessionId) => this.#localActionKindsForSession(agentSessionId),
|
|
184
|
+
onLocalActionFrame: async (event) => await this.#localActionFrame(event),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
async start(intents, signal) {
|
|
188
|
+
if (this.#state !== "idle")
|
|
189
|
+
throw runtimeFailure("session_conflict", "The foreground terminal already started.");
|
|
190
|
+
const runtime = this.#requireRuntime();
|
|
191
|
+
validateIntents(intents);
|
|
192
|
+
this.#pendingIntents = Object.freeze(intents.map((intent) => Object.freeze({ ...intent })));
|
|
193
|
+
this.#state = "starting";
|
|
194
|
+
try {
|
|
195
|
+
if (signal?.aborted)
|
|
196
|
+
throw runtimeFailure("terminal_disconnected", "Foreground terminal startup was cancelled.");
|
|
197
|
+
if (signal !== undefined) {
|
|
198
|
+
const onAbort = () => {
|
|
199
|
+
this.#recordFailure(runtimeFailure("terminal_disconnected", "Foreground terminal execution was cancelled."));
|
|
200
|
+
void this.stop().catch(() => { this.#state = "failed"; });
|
|
201
|
+
};
|
|
202
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
203
|
+
this.#removeAbort = () => signal.removeEventListener("abort", onAbort);
|
|
204
|
+
}
|
|
205
|
+
admitForegroundDimensions(this.#options.host.dimensions());
|
|
206
|
+
const lease = await this.#options.host.acquire();
|
|
207
|
+
if (signal?.aborted || this.#state !== "starting") {
|
|
208
|
+
await lease.restore();
|
|
209
|
+
throw runtimeFailure("terminal_disconnected", "Foreground terminal startup was cancelled.");
|
|
210
|
+
}
|
|
211
|
+
this.#lease = lease;
|
|
212
|
+
this.#removeInput = this.#options.host.onInput((bytes) => this.#queueInput(bytes));
|
|
213
|
+
this.#removeResize = this.#options.host.onResize(() => this.#queueResize());
|
|
214
|
+
const dimensions = admitForegroundDimensions(this.#options.host.dimensions());
|
|
215
|
+
await this.#renderAttaching(intents.length, dimensions);
|
|
216
|
+
this.#startAttachingAnimation(intents.length);
|
|
217
|
+
const attachSignal = signal === undefined
|
|
218
|
+
? this.#lifetimeAbort.signal
|
|
219
|
+
: AbortSignal.any([signal, this.#lifetimeAbort.signal]);
|
|
220
|
+
for (const intent of intents) {
|
|
221
|
+
if (attachSignal.aborted)
|
|
222
|
+
throw runtimeFailure("terminal_disconnected", "Foreground terminal startup was cancelled.");
|
|
223
|
+
this.#startAttachingAnimation(intents.length);
|
|
224
|
+
const attachDimensions = admitForegroundDimensions(this.#options.host.dimensions());
|
|
225
|
+
const snapshot = await runtime.attach({
|
|
226
|
+
tabId: intent.tabId,
|
|
227
|
+
agentSessionId: intent.agentSessionId,
|
|
228
|
+
columns: attachDimensions.columns,
|
|
229
|
+
rows: remoteRows(attachDimensions.rows),
|
|
230
|
+
...(intent.attachmentAdmission === undefined
|
|
231
|
+
? {}
|
|
232
|
+
: { expectedAdmission: intent.attachmentAdmission }),
|
|
233
|
+
signal: attachSignal,
|
|
234
|
+
});
|
|
235
|
+
if (this.#startupDetached || this.#state !== "starting") {
|
|
236
|
+
await runtime.detach(snapshot.tabId);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const tab = this.#tabs.get(intent.tabId);
|
|
240
|
+
if (tab === undefined) {
|
|
241
|
+
throw runtimeFailure("terminal_protocol_error", "The terminal became active before its fenced viewport was installed.");
|
|
242
|
+
}
|
|
243
|
+
tab.snapshot = snapshot;
|
|
244
|
+
this.#activeTabId ??= intent.tabId;
|
|
245
|
+
await this.#reconcileGeometry(snapshot, true);
|
|
246
|
+
await this.#render();
|
|
247
|
+
}
|
|
248
|
+
this.#stopAttachingAnimation();
|
|
249
|
+
this.#state = "active";
|
|
250
|
+
await this.#render();
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
const startupDetached = this.#startupDetached;
|
|
254
|
+
if (!startupDetached)
|
|
255
|
+
this.#state = "failed";
|
|
256
|
+
try {
|
|
257
|
+
await this.stop();
|
|
258
|
+
}
|
|
259
|
+
catch (cleanupError) {
|
|
260
|
+
throw new AggregateError([error, cleanupError], "Foreground terminal startup and cleanup both failed.");
|
|
261
|
+
}
|
|
262
|
+
if (startupDetached)
|
|
263
|
+
return;
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
async stop() {
|
|
268
|
+
if (this.#stopPromise !== undefined)
|
|
269
|
+
return await this.#stopPromise;
|
|
270
|
+
this.#resolveStopStarted();
|
|
271
|
+
this.#stopPromise = this.#stopNow();
|
|
272
|
+
try {
|
|
273
|
+
return await this.#stopPromise;
|
|
274
|
+
}
|
|
275
|
+
finally {
|
|
276
|
+
if (this.#state === "failed")
|
|
277
|
+
this.#stopPromise = undefined;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
async waitForStop() {
|
|
281
|
+
await this.#stopStarted;
|
|
282
|
+
const attempt = this.#stopPromise;
|
|
283
|
+
if (attempt === undefined) {
|
|
284
|
+
throw runtimeFailure("terminal_disconnected", "Foreground terminal cleanup did not start.");
|
|
285
|
+
}
|
|
286
|
+
await attempt;
|
|
287
|
+
if (this.#state !== "stopped") {
|
|
288
|
+
throw runtimeFailure("terminal_disconnected", "Foreground terminal cleanup did not complete.");
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
async #stopNow() {
|
|
292
|
+
if (this.#state === "stopped")
|
|
293
|
+
return;
|
|
294
|
+
this.#state = "stopping";
|
|
295
|
+
this.#stopAttachingAnimation();
|
|
296
|
+
this.#lifetimeAbort.abort();
|
|
297
|
+
this.#removeAbort?.();
|
|
298
|
+
this.#removeAbort = undefined;
|
|
299
|
+
if (this.#resizeTimer !== undefined)
|
|
300
|
+
clearTimeout(this.#resizeTimer);
|
|
301
|
+
for (const timer of this.#oauthPrefixTimers.values())
|
|
302
|
+
clearTimeout(timer);
|
|
303
|
+
this.#oauthPrefixTimers.clear();
|
|
304
|
+
this.#resizeTimer = undefined;
|
|
305
|
+
this.#removeInput?.();
|
|
306
|
+
this.#removeResize?.();
|
|
307
|
+
this.#removeInput = undefined;
|
|
308
|
+
this.#removeResize = undefined;
|
|
309
|
+
const failures = [];
|
|
310
|
+
const runtime = this.#runtime;
|
|
311
|
+
for (const tab of this.#tabs.values()) {
|
|
312
|
+
this.#localActionBroker.cancelBinding(this.#localActionIdentity(tab.intent, tab.snapshot), "foreground_stopped");
|
|
313
|
+
}
|
|
314
|
+
try {
|
|
315
|
+
await Promise.all(this.#remoteLocalActionResultTasks);
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
failures.push(error);
|
|
319
|
+
}
|
|
320
|
+
if (runtime !== undefined) {
|
|
321
|
+
for (const tabId of this.#tabs.keys()) {
|
|
322
|
+
try {
|
|
323
|
+
await runtime.detach(tabId);
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
failures.push(error);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
try {
|
|
331
|
+
await Promise.all(this.#outputTails.values());
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
failures.push(error);
|
|
335
|
+
}
|
|
336
|
+
this.#outputTails.clear();
|
|
337
|
+
try {
|
|
338
|
+
await this.#inputTail;
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
failures.push(error);
|
|
342
|
+
}
|
|
343
|
+
for (const tab of this.#tabs.values())
|
|
344
|
+
tab.viewport.dispose();
|
|
345
|
+
this.#tabs.clear();
|
|
346
|
+
this.#browserDetectors.clear();
|
|
347
|
+
this.#copyDetectors.clear();
|
|
348
|
+
this.#copyLinks.clear();
|
|
349
|
+
this.#retainedBrowserDetectors.clear();
|
|
350
|
+
this.#retainedBrowserCandidates.clear();
|
|
351
|
+
this.#retainedPendingRequestId = undefined;
|
|
352
|
+
this.#oauthPasteGuards.clear();
|
|
353
|
+
this.#browserRequests.clear();
|
|
354
|
+
this.#remoteLocalActionTabs.clear();
|
|
355
|
+
this.#pendingRemoteLocalActionResults.clear();
|
|
356
|
+
this.#remoteLocalActionResultSends.clear();
|
|
357
|
+
this.#remoteLocalActionResultTasks.clear();
|
|
358
|
+
this.#pendingBrowserAction = undefined;
|
|
359
|
+
this.#pendingBrowserActionTabId = undefined;
|
|
360
|
+
this.#browserNotice = undefined;
|
|
361
|
+
this.#pendingIntents = Object.freeze([]);
|
|
362
|
+
this.#pendingInputBytes = 0;
|
|
363
|
+
this.#reconnectTasks.clear();
|
|
364
|
+
try {
|
|
365
|
+
await this.#renderTail;
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
failures.push(error);
|
|
369
|
+
}
|
|
370
|
+
if (this.#lease !== undefined) {
|
|
371
|
+
try {
|
|
372
|
+
await this.#lease.restore();
|
|
373
|
+
this.#lease = undefined;
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
failures.push(error);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
this.#state = failures.length === 0 ? "stopped" : "failed";
|
|
380
|
+
if (failures.length > 0)
|
|
381
|
+
throw new AggregateError(failures, "Foreground terminal cleanup was incomplete.");
|
|
382
|
+
}
|
|
383
|
+
async #terminalReady(snapshot) {
|
|
384
|
+
// `onTerminalReady` is the precise boundary at which remote bytes may be
|
|
385
|
+
// rendered. Stop the local timer first, then drain its last queued paint;
|
|
386
|
+
// this guarantees the loading chrome never races or interleaves with PTY
|
|
387
|
+
// output.
|
|
388
|
+
this.#stopAttachingAnimation();
|
|
389
|
+
const runtime = this.#requireRuntime();
|
|
390
|
+
const intent = this.#findIntent(snapshot.tabId, snapshot.agentSessionId);
|
|
391
|
+
const previous = this.#tabs.get(snapshot.tabId);
|
|
392
|
+
await this.#outputTails.get(snapshot.tabId);
|
|
393
|
+
await this.#renderTail;
|
|
394
|
+
if (this.#lifetimeAbort.signal.aborted ||
|
|
395
|
+
(this.#state !== "starting" && this.#state !== "active")) {
|
|
396
|
+
throw runtimeFailure("terminal_disconnected", "Terminal readiness arrived after foreground ownership ended.");
|
|
397
|
+
}
|
|
398
|
+
if (previous !== undefined)
|
|
399
|
+
this.#forgetSeatNoticeOnSeatChange(previous.snapshot, snapshot);
|
|
400
|
+
const dimensions = admitForegroundDimensions(this.#options.host.dimensions());
|
|
401
|
+
this.#copyLinks.delete(snapshot.tabId);
|
|
402
|
+
this.#copyDetectors.set(snapshot.tabId, ["codex", "claude-code"].map(provider => new ProviderBrowserActionDetector({ copyOnly: true, provider, agentSessionId: snapshot.agentSessionId, processEpoch: snapshot.processEpoch, fencingGeneration: snapshot.fencingGeneration, clock: this.#clock })));
|
|
403
|
+
this.#retainedBrowserDetectors.delete(snapshot.tabId);
|
|
404
|
+
this.#retainedBrowserCandidates.delete(snapshot.tabId);
|
|
405
|
+
if (intent.localBrowserActions === true &&
|
|
406
|
+
// OpenCode owns `/connect` inside its remote TUI. PTY text is display
|
|
407
|
+
// only, never authority to open a local browser; only the currently
|
|
408
|
+
// supported Claude/Codex flows may request that foreground action.
|
|
409
|
+
(intent.agent === "claude-code" || intent.agent === "codex")) {
|
|
410
|
+
this.#browserDetectors.set(snapshot.tabId, new ProviderBrowserActionDetector({
|
|
411
|
+
provider: intent.agent,
|
|
412
|
+
agentSessionId: snapshot.agentSessionId,
|
|
413
|
+
processEpoch: snapshot.processEpoch,
|
|
414
|
+
fencingGeneration: snapshot.fencingGeneration,
|
|
415
|
+
clock: this.#clock,
|
|
416
|
+
}));
|
|
417
|
+
this.#retainedBrowserDetectors.set(snapshot.tabId, new ProviderBrowserActionDetector({
|
|
418
|
+
provider: intent.agent,
|
|
419
|
+
agentSessionId: snapshot.agentSessionId,
|
|
420
|
+
processEpoch: snapshot.processEpoch,
|
|
421
|
+
fencingGeneration: snapshot.fencingGeneration,
|
|
422
|
+
clock: this.#clock,
|
|
423
|
+
}));
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
this.#browserDetectors.delete(snapshot.tabId);
|
|
427
|
+
}
|
|
428
|
+
this.#localActionBroker.cancelStaleForIdentity(this.#localActionIdentity(intent, snapshot));
|
|
429
|
+
// A replacement process or fencing generation is a hard authority
|
|
430
|
+
// boundary. Results from the previous binding must never be replayed into
|
|
431
|
+
// the new terminal, even if their old socket later becomes writable.
|
|
432
|
+
this.#fenceRemoteLocalActionResults(snapshot.tabId, this.#localActionIdentity(intent, snapshot));
|
|
433
|
+
if (this.#pendingBrowserActionTabId === snapshot.tabId &&
|
|
434
|
+
this.#pendingBrowserAction?.fencingGeneration !== snapshot.fencingGeneration) {
|
|
435
|
+
this.#pendingBrowserAction = undefined;
|
|
436
|
+
this.#pendingBrowserActionTabId = undefined;
|
|
437
|
+
this.#browserNotice = "Browser request expired after the terminal changed.";
|
|
438
|
+
}
|
|
439
|
+
const guardedRequest = this.#pendingBrowserActionTabId === snapshot.tabId
|
|
440
|
+
? this.#pendingBrowserAction
|
|
441
|
+
: undefined;
|
|
442
|
+
if (guardedRequest?.fencingGeneration !== snapshot.fencingGeneration) {
|
|
443
|
+
this.#setOAuthPasteGuard(snapshot.tabId);
|
|
444
|
+
}
|
|
445
|
+
const binding = {
|
|
446
|
+
userId: snapshot.userId,
|
|
447
|
+
machineId: snapshot.machineId,
|
|
448
|
+
agentSessionId: snapshot.agentSessionId,
|
|
449
|
+
processEpoch: snapshot.processEpoch,
|
|
450
|
+
fencingGeneration: snapshot.fencingGeneration,
|
|
451
|
+
};
|
|
452
|
+
// A resumed stream contains only output after the retained cursor. Keep
|
|
453
|
+
// those earlier cells, while retiring the old query-response authority.
|
|
454
|
+
const viewport = previous?.viewport ?? new XtermViewportAdapter({
|
|
455
|
+
tabId: snapshot.tabId,
|
|
456
|
+
binding,
|
|
457
|
+
columns: dimensions.columns,
|
|
458
|
+
rows: remoteRows(dimensions.rows),
|
|
459
|
+
registry: this.#registry,
|
|
460
|
+
onTerminalResponse: async (response) => await runtime.sendTerminalResponse(response),
|
|
461
|
+
clock: this.#clock,
|
|
462
|
+
});
|
|
463
|
+
if (previous !== undefined) {
|
|
464
|
+
const oldBinding = viewport.snapshot().binding;
|
|
465
|
+
if (snapshot.terminalView === undefined && !sameSnapshotBinding(snapshot, oldBinding))
|
|
466
|
+
await viewport.rebind(binding);
|
|
467
|
+
if (this.#lifetimeAbort.signal.aborted || this.#tabs.get(snapshot.tabId) !== previous ||
|
|
468
|
+
(this.#state !== "starting" && this.#state !== "active")) {
|
|
469
|
+
throw runtimeFailure("terminal_disconnected", "Terminal rebinding completed after foreground ownership ended.");
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
this.#tabs.set(snapshot.tabId, { intent, snapshot, viewport });
|
|
473
|
+
// A reconnect for the same binding may have dropped the outcome after the
|
|
474
|
+
// local side effect completed. The server ACK is the only condition that
|
|
475
|
+
// clears the cache, so resubmit it once this exact attachment is ready.
|
|
476
|
+
this.#resendPendingRemoteLocalActionResults(snapshot.tabId, this.#localActionIdentity(intent, snapshot));
|
|
477
|
+
}
|
|
478
|
+
async #terminalViewStarted(event) {
|
|
479
|
+
const { snapshot, signal } = event;
|
|
480
|
+
const tab = this.#tabs.get(snapshot.tabId);
|
|
481
|
+
if (signal.aborted || tab === undefined || tab.snapshot.fencingGeneration !== snapshot.fencingGeneration)
|
|
482
|
+
throw runtimeFailure("terminal_disconnected", "Terminal view reset targets a retired attachment.");
|
|
483
|
+
const abort = () => { if (this.#tabs.get(snapshot.tabId) === tab)
|
|
484
|
+
tab.viewport.dispose(); };
|
|
485
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
486
|
+
try {
|
|
487
|
+
const current = tab.viewport.snapshot();
|
|
488
|
+
if (current.binding.fencingGeneration === snapshot.fencingGeneration) {
|
|
489
|
+
if (current.outputSequence !== 0n || current.replayCursor !== 0n)
|
|
490
|
+
throw runtimeFailure("terminal_protocol_error", "Initial terminal view is not empty.");
|
|
491
|
+
await raceAbort(tab.viewport.resize(event.columns, event.rows), signal);
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
await raceAbort(tab.viewport.resetForCurrentView({
|
|
495
|
+
userId: snapshot.userId, machineId: snapshot.machineId,
|
|
496
|
+
agentSessionId: snapshot.agentSessionId, processEpoch: snapshot.processEpoch,
|
|
497
|
+
fencingGeneration: snapshot.fencingGeneration,
|
|
498
|
+
}, event.columns, event.rows), signal);
|
|
499
|
+
}
|
|
500
|
+
if (signal.aborted || this.#tabs.get(snapshot.tabId) !== tab)
|
|
501
|
+
throw runtimeFailure("terminal_disconnected", "Terminal view reset completed after retirement.");
|
|
502
|
+
}
|
|
503
|
+
catch (error) {
|
|
504
|
+
abort();
|
|
505
|
+
throw error;
|
|
506
|
+
}
|
|
507
|
+
finally {
|
|
508
|
+
signal.removeEventListener("abort", abort);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
async #terminalGeometry(snapshot, signal) {
|
|
512
|
+
const tab = this.#tabs.get(snapshot.tabId);
|
|
513
|
+
if (tab === undefined || !sameSnapshotBinding(tab.snapshot, snapshot) ||
|
|
514
|
+
snapshot.geometry === null || snapshot.geometry.writerEpoch !== snapshot.writerEpoch ||
|
|
515
|
+
snapshot.writerEpoch !== tab.snapshot.writerEpoch) {
|
|
516
|
+
throw runtimeFailure("grant_scope_mismatch", "Terminal geometry targets an unbound foreground viewport.");
|
|
517
|
+
}
|
|
518
|
+
if (signal.aborted)
|
|
519
|
+
throw signal.reason;
|
|
520
|
+
// Only observers adopt remote dimensions. The writer owns local fit and
|
|
521
|
+
// sends its own fenced RESIZE; an older size notice cannot undo that fit.
|
|
522
|
+
if (snapshot.accessMode === "observer") {
|
|
523
|
+
try {
|
|
524
|
+
await raceAbort(tab.viewport.resize(snapshot.geometry.columns, snapshot.geometry.rows), signal);
|
|
525
|
+
}
|
|
526
|
+
catch (error) {
|
|
527
|
+
tab.viewport.dispose();
|
|
528
|
+
throw error;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
if (signal.aborted || this.#tabs.get(snapshot.tabId) !== tab)
|
|
532
|
+
return;
|
|
533
|
+
tab.snapshot = snapshot;
|
|
534
|
+
await this.#render();
|
|
535
|
+
}
|
|
536
|
+
async #queueTerminalOutput(event) {
|
|
537
|
+
const previous = this.#outputTails.get(event.tabId) ?? Promise.resolve();
|
|
538
|
+
const operation = previous.then(async () => await this.#terminalOutput(event));
|
|
539
|
+
const tail = operation.then(() => undefined, () => undefined);
|
|
540
|
+
this.#outputTails.set(event.tabId, tail);
|
|
541
|
+
try {
|
|
542
|
+
await operation;
|
|
543
|
+
}
|
|
544
|
+
finally {
|
|
545
|
+
if (this.#outputTails.get(event.tabId) === tail)
|
|
546
|
+
this.#outputTails.delete(event.tabId);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
async #terminalOutput(event) {
|
|
550
|
+
const tab = this.#tabs.get(event.tabId);
|
|
551
|
+
if (tab === undefined || tab.intent.agentSessionId !== event.agentSessionId || !sameSnapshotBinding(tab.snapshot, event.binding)) {
|
|
552
|
+
throw runtimeFailure("grant_scope_mismatch", "Terminal output targets an unbound foreground viewport.");
|
|
553
|
+
}
|
|
554
|
+
// Retained output remains visible but cannot acquire fresh local-action
|
|
555
|
+
// authority. Do not feed it into the streaming detector: a historical
|
|
556
|
+
// prefix must never combine with a live suffix into a new request.
|
|
557
|
+
for (const detector of this.#copyDetectors.get(event.tabId) ?? []) {
|
|
558
|
+
const candidate = detector.push(event.bytes).at(-1);
|
|
559
|
+
if (candidate !== undefined)
|
|
560
|
+
this.#copyLinks.set(event.tabId, candidate);
|
|
561
|
+
}
|
|
562
|
+
const detected = event.provenance === "live"
|
|
563
|
+
? this.#browserDetectors.get(event.tabId)?.push(event.bytes) ?? [] : [];
|
|
564
|
+
if (event.provenance !== "live") {
|
|
565
|
+
const retained = this.#retainedBrowserDetectors.get(event.tabId)?.push(event.bytes) ?? [];
|
|
566
|
+
const latest = retained.at(-1);
|
|
567
|
+
if (latest !== undefined)
|
|
568
|
+
this.#retainedBrowserCandidates.set(event.tabId, latest);
|
|
569
|
+
}
|
|
570
|
+
for (const request of detected)
|
|
571
|
+
this.#enqueueBrowserAction(event.tabId, tab, request);
|
|
572
|
+
this.#promoteBrowserAction();
|
|
573
|
+
await raceAbort(tab.viewport.write(event.bytes, event.sequence, event.sequence), event.signal);
|
|
574
|
+
const current = this.#tabs.get(event.tabId);
|
|
575
|
+
if (event.signal.aborted || current !== tab || !sameSnapshotBinding(tab.snapshot, event.binding))
|
|
576
|
+
return;
|
|
577
|
+
await this.#render();
|
|
578
|
+
}
|
|
579
|
+
#terminalState(snapshot) {
|
|
580
|
+
const tab = this.#tabs.get(snapshot.tabId);
|
|
581
|
+
if (tab !== undefined) {
|
|
582
|
+
this.#forgetSeatNoticeOnSeatChange(tab.snapshot, snapshot);
|
|
583
|
+
tab.snapshot = snapshot;
|
|
584
|
+
if (snapshot.state === "active" &&
|
|
585
|
+
(this.#browserNotice === INPUT_WITHHELD_NOTICE || this.#browserNotice === RECONNECT_FAILED_NOTICE)) {
|
|
586
|
+
this.#browserNotice = undefined;
|
|
587
|
+
}
|
|
588
|
+
if (this.#localDetachTabIds.has(snapshot.tabId) &&
|
|
589
|
+
(snapshot.state === "failed" || snapshot.state === "interrupted" || snapshot.state === "closed" || snapshot.state === "detached")) {
|
|
590
|
+
// A local detach may close the wire before the runtime publishes its
|
|
591
|
+
// final state. #detachTab owns its success/failure decision and closing
|
|
592
|
+
// frame, so callbacks cannot restore the host ahead of that sequence.
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
if (snapshot.state === "failed" || snapshot.state === "closed" || snapshot.state === "detached") {
|
|
596
|
+
if (snapshot.state === "failed") {
|
|
597
|
+
this.#recordFailure(snapshot.reason === "terminal_history_gap" ? terminalHistoryGap(snapshot.agentSessionId) : snapshot.reason === "terminal_input_recovery_required"
|
|
598
|
+
? runtimeFailure("terminal_protocol_error", "Terminal input requires recovery. Earlier input will not be resent automatically.", {
|
|
599
|
+
retryable: false, safeDetails: { reason: snapshot.reason },
|
|
600
|
+
})
|
|
601
|
+
: snapshot.reason === "terminal_protocol_error"
|
|
602
|
+
? runtimeFailure("terminal_protocol_error", snapshot.remoteReason === undefined
|
|
603
|
+
? "Cuna could not safely process the terminal stream. Inspect this session before reconnecting."
|
|
604
|
+
: `The remote terminal ended the stream: ${snapshot.remoteReason}. Inspect this session before reconnecting.`, {
|
|
605
|
+
retryable: false,
|
|
606
|
+
...(snapshot.remoteReason === undefined ? {} : { safeDetails: { reason: snapshot.remoteReason } }),
|
|
607
|
+
})
|
|
608
|
+
: runtimeFailure("terminal_disconnected", "A foreground AgentSession terminal failed."));
|
|
609
|
+
}
|
|
610
|
+
this.#localActionBroker.cancelBinding(this.#localActionIdentity(tab.intent, tab.snapshot), "terminal_detached");
|
|
611
|
+
tab.viewport.dispose();
|
|
612
|
+
this.#tabs.delete(snapshot.tabId);
|
|
613
|
+
if (this.#activeTabId === snapshot.tabId) {
|
|
614
|
+
const replacement = [...this.#tabs.keys()][0];
|
|
615
|
+
this.#activeTabId = replacement;
|
|
616
|
+
if (replacement !== undefined) {
|
|
617
|
+
this.#requireRuntime().switchActive(replacement);
|
|
618
|
+
this.#registry.select(replacement);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
if (this.#state !== "active")
|
|
624
|
+
return;
|
|
625
|
+
if (this.#tabs.size === 0) {
|
|
626
|
+
void this.stop().catch(() => { this.#state = "failed"; });
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
if (snapshot.state === "interrupted" && !this.#reconnectTasks.has(snapshot.tabId)) {
|
|
630
|
+
this.#startRecovery(snapshot.tabId);
|
|
631
|
+
}
|
|
632
|
+
this.#queueStateRender();
|
|
633
|
+
}
|
|
634
|
+
async #recoverTab(tabId) {
|
|
635
|
+
const attempts = this.#options.reconnectAttempts ?? DEFAULT_RECONNECT_ATTEMPTS;
|
|
636
|
+
const baseDelayMs = this.#options.reconnectBaseDelayMs ?? 100;
|
|
637
|
+
let lastFailure;
|
|
638
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
639
|
+
if (this.#state !== "active" || this.#lifetimeAbort.signal.aborted)
|
|
640
|
+
return;
|
|
641
|
+
await abortableDelay(Math.min(baseDelayMs * (2 ** attempt), 5_000), this.#lifetimeAbort.signal);
|
|
642
|
+
if (this.#state !== "active" || this.#lifetimeAbort.signal.aborted)
|
|
643
|
+
return;
|
|
644
|
+
try {
|
|
645
|
+
const snapshot = await this.#requireRuntime().reconnect({ tabId, signal: this.#lifetimeAbort.signal });
|
|
646
|
+
await this.#reconcileGeometry(snapshot, false);
|
|
647
|
+
this.#recoverableReconnectFailures.delete(tabId);
|
|
648
|
+
if (this.#browserNotice === INPUT_WITHHELD_NOTICE || this.#browserNotice === RECONNECT_FAILED_NOTICE) {
|
|
649
|
+
this.#browserNotice = undefined;
|
|
650
|
+
}
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
catch (error) {
|
|
654
|
+
lastFailure = error;
|
|
655
|
+
if (this.#lifetimeAbort.signal.aborted || this.#state !== "active")
|
|
656
|
+
return;
|
|
657
|
+
if (error instanceof RuntimeBoundaryError && !error.retryable)
|
|
658
|
+
break;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
if (this.#state === "active") {
|
|
662
|
+
this.#recoverableReconnectFailures.set(tabId, lastFailure ?? runtimeFailure("terminal_disconnected", "Automatic terminal reconnection was exhausted.", { retryable: true }));
|
|
663
|
+
this.#browserNotice = RECONNECT_FAILED_NOTICE;
|
|
664
|
+
await this.#render();
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
#queueInput(bytes) {
|
|
668
|
+
if (bytes.byteLength < 1)
|
|
669
|
+
return;
|
|
670
|
+
if (this.#state === "starting" && bytes.includes(INTERRUPT)) {
|
|
671
|
+
if (this.#startupDetached)
|
|
672
|
+
return;
|
|
673
|
+
this.#startupDetached = true;
|
|
674
|
+
this.#stopAttachingAnimation();
|
|
675
|
+
// There is no foreground viewport until the first attach completes, so
|
|
676
|
+
// the normal workbench close renderer has nothing to paint here. Queue a
|
|
677
|
+
// short, host-owned close acknowledgement before restoration instead.
|
|
678
|
+
// This keeps Ctrl-C single-press and visible even while authority is
|
|
679
|
+
// still being checked.
|
|
680
|
+
this.#queueStartupCloseFeedback();
|
|
681
|
+
void this.stop().catch(() => { this.#state = "failed"; });
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
if (this.#closingTabId !== undefined)
|
|
685
|
+
return;
|
|
686
|
+
if (bytes.byteLength > MAX_FOREGROUND_PENDING_INPUT_BYTES || this.#pendingInputBytes + bytes.byteLength > MAX_FOREGROUND_PENDING_INPUT_BYTES) {
|
|
687
|
+
this.#recordFailure(runtimeFailure("terminal_protocol_error", "Foreground terminal input exceeded its bounded queue."));
|
|
688
|
+
void this.stop().catch(() => { this.#state = "failed"; });
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
const payload = bytes.slice();
|
|
692
|
+
const receiptTarget = this.#captureInputTarget();
|
|
693
|
+
if (this.#state === "active" &&
|
|
694
|
+
receiptTarget === undefined &&
|
|
695
|
+
!(payload.byteLength === 1 && payload[0] === INTERRUPT) &&
|
|
696
|
+
!this.#prefixPending &&
|
|
697
|
+
payload[0] !== ESCAPE_PREFIX) {
|
|
698
|
+
if (this.#pendingBrowserActionTabId === this.#activeTabId) {
|
|
699
|
+
this.#pendingBrowserAction = undefined;
|
|
700
|
+
this.#pendingBrowserActionTabId = undefined;
|
|
701
|
+
}
|
|
702
|
+
this.#browserNotice = INPUT_WITHHELD_NOTICE;
|
|
703
|
+
void this.#render().catch(() => undefined);
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
if (payload.byteLength === 1 &&
|
|
707
|
+
payload[0] === INTERRUPT &&
|
|
708
|
+
!this.#prefixPending &&
|
|
709
|
+
receiptTarget !== undefined) {
|
|
710
|
+
// Record local detach at receipt time. Input is serialized, so the
|
|
711
|
+
// transport may publish interrupted before #detachTab runs.
|
|
712
|
+
this.#localDetachTabIds.add(receiptTarget.tabId);
|
|
713
|
+
this.#pasteActive = false;
|
|
714
|
+
this.#pasteStartMatch = 0;
|
|
715
|
+
this.#pasteEndMatch = 0;
|
|
716
|
+
this.#closingTabId = receiptTarget.tabId;
|
|
717
|
+
this.#disconnectNotice = DISCONNECTING_FRAMES[0];
|
|
718
|
+
// Feedback is receipt-time, not input-tail-time. A prior admitted remote
|
|
719
|
+
// write may still be draining, but Ctrl-C must visibly acknowledge that
|
|
720
|
+
// the local close intent was accepted.
|
|
721
|
+
void this.#render().catch(() => undefined);
|
|
722
|
+
}
|
|
723
|
+
this.#pendingInputBytes += payload.byteLength;
|
|
724
|
+
const operation = this.#inputTail.then(async () => {
|
|
725
|
+
try {
|
|
726
|
+
await this.#routeInput(payload, receiptTarget);
|
|
727
|
+
}
|
|
728
|
+
finally {
|
|
729
|
+
this.#pendingInputBytes -= payload.byteLength;
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
this.#inputTail = operation.catch((error) => {
|
|
733
|
+
if (error instanceof RuntimeBoundaryError && error.code === "terminal_observer") {
|
|
734
|
+
// Typing into an observed terminal is refused, not fatal: the seat is
|
|
735
|
+
// someone else's. Say so on the notice line and keep observing.
|
|
736
|
+
const snapshot = this.#activeTabId === undefined ? undefined : this.#tabs.get(this.#activeTabId)?.snapshot;
|
|
737
|
+
this.#seatNotice = snapshot === undefined ? "Read-only · input was not sent."
|
|
738
|
+
: writerCapabilityRefusal(snapshot) ?? "Input not sent · press Ctrl+] then w to take control";
|
|
739
|
+
this.#helpVisible = false;
|
|
740
|
+
void this.#render().catch(() => undefined);
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
if (error instanceof RuntimeBoundaryError && (error.code === "terminal_disconnected" || error.code === "session_unknown")) {
|
|
744
|
+
if (this.#pendingBrowserActionTabId === this.#activeTabId) {
|
|
745
|
+
this.#pendingBrowserAction = undefined;
|
|
746
|
+
this.#pendingBrowserActionTabId = undefined;
|
|
747
|
+
}
|
|
748
|
+
this.#browserNotice = INPUT_WITHHELD_NOTICE;
|
|
749
|
+
void this.#render().catch(() => undefined);
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
this.#recordFailure(error);
|
|
753
|
+
void this.stop().catch(() => { this.#state = "failed"; });
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
async #routeInput(bytes, receiptTarget, releasedPrefix = false) {
|
|
757
|
+
const runtime = this.#requireRuntime();
|
|
758
|
+
if (bytes.includes(INTERRUPT)) {
|
|
759
|
+
const active = this.#pendingBrowserAction === undefined
|
|
760
|
+
? undefined
|
|
761
|
+
: this.#localActionBroker.get(this.#pendingBrowserAction.id);
|
|
762
|
+
if (active !== undefined)
|
|
763
|
+
this.#localActionBroker.cancelBinding(active.request.identity, "user_interrupt");
|
|
764
|
+
this.#pendingBrowserAction = undefined;
|
|
765
|
+
this.#pendingBrowserActionTabId = undefined;
|
|
766
|
+
this.#browserNotice = undefined;
|
|
767
|
+
}
|
|
768
|
+
else if (!releasedPrefix && await this.#routeBrowserActionInput(bytes, receiptTarget)) {
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
const guarded = releasedPrefix ? { bytes, blocked: false } : this.#guardProviderOAuthPaste(bytes, receiptTarget);
|
|
772
|
+
if (guarded.blocked) {
|
|
773
|
+
this.#browserNotice = "That is the sign-in link, not the code. Approve access in the browser, then paste only the code shown on the final page.";
|
|
774
|
+
await this.#render();
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
if (guarded.bytes.byteLength === 0)
|
|
778
|
+
return;
|
|
779
|
+
bytes = guarded.bytes;
|
|
780
|
+
if (this.#browserNotice !== undefined)
|
|
781
|
+
this.#browserNotice = undefined;
|
|
782
|
+
let target = this.#prefixPending ? this.#prefixTarget : receiptTarget;
|
|
783
|
+
let remote = [];
|
|
784
|
+
const flush = async () => {
|
|
785
|
+
if (remote.length === 0)
|
|
786
|
+
return;
|
|
787
|
+
if (target === undefined)
|
|
788
|
+
throw runtimeFailure("session_unknown", "No foreground terminal tab is active.");
|
|
789
|
+
const payload = Uint8Array.from(remote);
|
|
790
|
+
remote = [];
|
|
791
|
+
await runtime.sendInput(payload, target.tabId, target.binding);
|
|
792
|
+
};
|
|
793
|
+
for (const byte of bytes) {
|
|
794
|
+
if (this.#pasteActive) {
|
|
795
|
+
remote.push(byte);
|
|
796
|
+
const matched = advanceSequence(BRACKETED_PASTE_END, byte, this.#pasteEndMatch);
|
|
797
|
+
if (matched === BRACKETED_PASTE_END.length) {
|
|
798
|
+
this.#pasteActive = false;
|
|
799
|
+
this.#pasteEndMatch = 0;
|
|
800
|
+
}
|
|
801
|
+
else {
|
|
802
|
+
this.#pasteEndMatch = matched;
|
|
803
|
+
}
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
this.#pasteStartMatch = advanceSequence(BRACKETED_PASTE_START, byte, this.#pasteStartMatch);
|
|
807
|
+
if (this.#pasteStartMatch === BRACKETED_PASTE_START.length) {
|
|
808
|
+
this.#pasteActive = true;
|
|
809
|
+
this.#pasteStartMatch = 0;
|
|
810
|
+
remote.push(byte);
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
if (!this.#prefixPending) {
|
|
814
|
+
if (byte === FLOW_PAUSE) {
|
|
815
|
+
await flush();
|
|
816
|
+
if (target === undefined)
|
|
817
|
+
throw runtimeFailure("session_unknown", "No foreground terminal tab is active.");
|
|
818
|
+
await runtime.sendInput(Uint8Array.of(FLOW_RESUME), target.tabId, target.binding);
|
|
819
|
+
this.#browserNotice = FLOW_CONTROL_NOTICE;
|
|
820
|
+
await this.#render();
|
|
821
|
+
}
|
|
822
|
+
else if (byte === INTERRUPT) {
|
|
823
|
+
await flush();
|
|
824
|
+
await this.#detachTab(target?.tabId);
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
else if (byte === ESCAPE_PREFIX) {
|
|
828
|
+
await flush();
|
|
829
|
+
this.#prefixPending = true;
|
|
830
|
+
this.#prefixTarget = target;
|
|
831
|
+
}
|
|
832
|
+
else {
|
|
833
|
+
remote.push(byte);
|
|
834
|
+
}
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
this.#prefixPending = false;
|
|
838
|
+
const chordTarget = this.#prefixTarget;
|
|
839
|
+
this.#prefixTarget = undefined;
|
|
840
|
+
if (byte === ESCAPE_PREFIX) {
|
|
841
|
+
target = chordTarget ?? target;
|
|
842
|
+
remote.push(ESCAPE_PREFIX);
|
|
843
|
+
}
|
|
844
|
+
else if (byte === REMOTE_INTERRUPT) {
|
|
845
|
+
target = chordTarget ?? target;
|
|
846
|
+
remote.push(INTERRUPT);
|
|
847
|
+
}
|
|
848
|
+
else if (byte === REMOTE_FLOW_PAUSE) {
|
|
849
|
+
target = chordTarget ?? target;
|
|
850
|
+
remote.push(FLOW_PAUSE);
|
|
851
|
+
}
|
|
852
|
+
else if (byte === REMOTE_FLOW_RESUME) {
|
|
853
|
+
target = chordTarget ?? target;
|
|
854
|
+
remote.push(FLOW_RESUME);
|
|
855
|
+
}
|
|
856
|
+
else if (byte >= TAB_FIRST && byte <= TAB_LAST) {
|
|
857
|
+
await flush();
|
|
858
|
+
this.#selectByIndex(byte - TAB_FIRST);
|
|
859
|
+
target = this.#captureInputTarget();
|
|
860
|
+
}
|
|
861
|
+
else if (byte === NEXT_TAB) {
|
|
862
|
+
await flush();
|
|
863
|
+
this.#selectNext();
|
|
864
|
+
target = this.#captureInputTarget();
|
|
865
|
+
}
|
|
866
|
+
else if (byte === DETACH) {
|
|
867
|
+
await flush();
|
|
868
|
+
await this.#detachTab(chordTarget?.tabId);
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
else if (byte === HELP) {
|
|
872
|
+
await flush();
|
|
873
|
+
this.#helpVisible = !this.#helpVisible;
|
|
874
|
+
await this.#render();
|
|
875
|
+
}
|
|
876
|
+
else if (byte === RETRY) {
|
|
877
|
+
await flush();
|
|
878
|
+
this.#retryActiveTab();
|
|
879
|
+
}
|
|
880
|
+
else if (byte === TAKE_WRITER) {
|
|
881
|
+
await flush();
|
|
882
|
+
this.#takeWriterActiveTab();
|
|
883
|
+
}
|
|
884
|
+
else if (byte === 0x79) {
|
|
885
|
+
await flush();
|
|
886
|
+
await this.#copySignInLink(chordTarget);
|
|
887
|
+
}
|
|
888
|
+
else if (byte === RETAINED_SIGN_IN) {
|
|
889
|
+
await flush();
|
|
890
|
+
await this.#requestRetainedSignIn();
|
|
891
|
+
}
|
|
892
|
+
else {
|
|
893
|
+
this.#helpVisible = false;
|
|
894
|
+
target = chordTarget ?? target;
|
|
895
|
+
remote.push(ESCAPE_PREFIX, byte);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
await flush();
|
|
899
|
+
}
|
|
900
|
+
async #routeBrowserActionInput(bytes, target) {
|
|
901
|
+
const request = this.#pendingBrowserAction;
|
|
902
|
+
if (request === undefined ||
|
|
903
|
+
target === undefined ||
|
|
904
|
+
target.tabId !== this.#activeTabId ||
|
|
905
|
+
target.tabId !== this.#pendingBrowserActionTabId)
|
|
906
|
+
return false;
|
|
907
|
+
if (request.agentSessionId !== target.binding.agentSessionId ||
|
|
908
|
+
request.processEpoch !== target.binding.processEpoch ||
|
|
909
|
+
request.fencingGeneration !== target.binding.fencingGeneration) {
|
|
910
|
+
const tracked = this.#localActionBroker.get(request.id);
|
|
911
|
+
if (tracked !== undefined)
|
|
912
|
+
this.#localActionBroker.cancelBinding(tracked.request.identity);
|
|
913
|
+
this.#pendingBrowserAction = undefined;
|
|
914
|
+
this.#pendingBrowserActionTabId = undefined;
|
|
915
|
+
this.#browserNotice = "Browser request expired after the terminal changed.";
|
|
916
|
+
await this.#render();
|
|
917
|
+
return true;
|
|
918
|
+
}
|
|
919
|
+
if (this.#clock() >= request.expiresAt) {
|
|
920
|
+
this.#localActionBroker.expire();
|
|
921
|
+
this.#pendingBrowserAction = undefined;
|
|
922
|
+
this.#pendingBrowserActionTabId = undefined;
|
|
923
|
+
this.#handledBrowserUrls.add(this.#browserRequestKey(request));
|
|
924
|
+
this.#browserNotice = request.id === this.#retainedPendingRequestId
|
|
925
|
+
? "Local permission expired · Ctrl+] a to inspect again · request a new link if the provider rejects it"
|
|
926
|
+
: "Browser authentication request expired. Retry from the provider.";
|
|
927
|
+
await this.#render();
|
|
928
|
+
return true;
|
|
929
|
+
}
|
|
930
|
+
// Permission decisions are deliberately single-key only. A bracketed
|
|
931
|
+
// paste, an opaque provider code beginning with "d", or any multi-byte
|
|
932
|
+
// input is PTY data and must never be reinterpreted as a local decision.
|
|
933
|
+
if (bytes.byteLength !== 1)
|
|
934
|
+
return false;
|
|
935
|
+
const decision = bytes[0];
|
|
936
|
+
if (decision === 0x1b || decision === 0x64 || decision === 0x44) {
|
|
937
|
+
this.#localActionBroker.decide(request.id, false);
|
|
938
|
+
this.#pendingBrowserAction = undefined;
|
|
939
|
+
this.#pendingBrowserActionTabId = undefined;
|
|
940
|
+
this.#handledBrowserUrls.add(this.#browserRequestKey(request));
|
|
941
|
+
this.#browserNotice = request.type === "auth.device.present"
|
|
942
|
+
? "Device sign-in denied. The provider page remains in the cloud terminal."
|
|
943
|
+
: "Browser authentication denied. The provider URL remains in the terminal.";
|
|
944
|
+
await this.#render();
|
|
945
|
+
this.#promoteBrowserAction();
|
|
946
|
+
return true;
|
|
947
|
+
}
|
|
948
|
+
if (decision !== 0x0d && decision !== 0x6f && decision !== 0x4f) {
|
|
949
|
+
return false;
|
|
950
|
+
}
|
|
951
|
+
if (this.#browserOpening)
|
|
952
|
+
return true;
|
|
953
|
+
this.#browserOpening = true;
|
|
954
|
+
// Remote MCP requests retain their exact mcp:<kind> scope. Passing the
|
|
955
|
+
// local detector scope here would widen it and makes the broker reject a
|
|
956
|
+
// valid, fenced request before the browser can open.
|
|
957
|
+
this.#localActionBroker.decide(request.id, true);
|
|
958
|
+
this.#browserNotice = `Opening ${new URL(request.url).hostname} in your local browser...`;
|
|
959
|
+
await this.#render();
|
|
960
|
+
void this.#openBrowser(request).catch(() => undefined);
|
|
961
|
+
return true;
|
|
962
|
+
}
|
|
963
|
+
async #openBrowser(request) {
|
|
964
|
+
try {
|
|
965
|
+
const browser = this.#options.browser;
|
|
966
|
+
if (browser === undefined)
|
|
967
|
+
throw new Error("browser unavailable");
|
|
968
|
+
await browser.open(request.url);
|
|
969
|
+
const tracked = this.#localActionBroker.get(request.id);
|
|
970
|
+
if (tracked?.state !== "executing")
|
|
971
|
+
return;
|
|
972
|
+
this.#pendingBrowserAction = undefined;
|
|
973
|
+
this.#pendingBrowserActionTabId = undefined;
|
|
974
|
+
this.#handledBrowserUrls.add(this.#browserRequestKey(request));
|
|
975
|
+
if (request.type === "auth.device.present") {
|
|
976
|
+
// This confirms only local presentation. The remote provider owns its
|
|
977
|
+
// device-code polling and Cuna must not claim that it is signed in.
|
|
978
|
+
this.#localActionBroker.complete(request.id, tracked.request.identity, "succeeded", Object.freeze({ awaitingProvider: true }));
|
|
979
|
+
this.#browserNotice = `${providerName(request.provider)} sign-in opened · complete it there · waiting for confirmation…`;
|
|
980
|
+
}
|
|
981
|
+
else {
|
|
982
|
+
this.#localActionBroker.awaitingRemoteCompletion(request.id);
|
|
983
|
+
for (const [tabId, tab] of this.#tabs) {
|
|
984
|
+
if (tab.snapshot.agentSessionId === request.agentSessionId &&
|
|
985
|
+
tab.snapshot.processEpoch === request.processEpoch &&
|
|
986
|
+
tab.snapshot.fencingGeneration === request.fencingGeneration) {
|
|
987
|
+
this.#oauthPasteGuards.get(tabId)?.beginCodeCapture();
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
this.#browserNotice = request.id === this.#retainedPendingRequestId
|
|
991
|
+
? "Retained link opened · validity unknown · if rejected, request a new sign-in link in the provider terminal"
|
|
992
|
+
: "Browser opened locally. Complete authentication there, then return here.";
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
catch {
|
|
996
|
+
const tracked = this.#localActionBroker.get(request.id);
|
|
997
|
+
if (tracked !== undefined) {
|
|
998
|
+
this.#localActionBroker.complete(request.id, tracked.request.identity, "failed", undefined, "browser_open_failed");
|
|
999
|
+
}
|
|
1000
|
+
this.#pendingBrowserAction = undefined;
|
|
1001
|
+
this.#pendingBrowserActionTabId = undefined;
|
|
1002
|
+
this.#handledBrowserUrls.add(this.#browserRequestKey(request));
|
|
1003
|
+
this.#browserNotice = "Could not open the local browser. Use the provider URL shown below.";
|
|
1004
|
+
}
|
|
1005
|
+
finally {
|
|
1006
|
+
this.#browserOpening = false;
|
|
1007
|
+
}
|
|
1008
|
+
await this.#render();
|
|
1009
|
+
this.#promoteBrowserAction();
|
|
1010
|
+
}
|
|
1011
|
+
async #copySignInLink(target) {
|
|
1012
|
+
const tab = target === undefined ? undefined : this.#tabs.get(target.tabId);
|
|
1013
|
+
const link = target === undefined ? undefined : this.#copyLinks.get(target.tabId);
|
|
1014
|
+
if (target === undefined || target.tabId !== this.#activeTabId || tab === undefined || link === undefined ||
|
|
1015
|
+
link.agentSessionId !== tab.snapshot.agentSessionId || link.processEpoch !== tab.snapshot.processEpoch ||
|
|
1016
|
+
link.fencingGeneration !== tab.snapshot.fencingGeneration) {
|
|
1017
|
+
this.#browserNotice = "No hay enlace para copiar. Solicita un enlace de acceso en el agente.";
|
|
1018
|
+
}
|
|
1019
|
+
else {
|
|
1020
|
+
try {
|
|
1021
|
+
await (this.#options.copyText ?? copyLocalText)(link.url);
|
|
1022
|
+
this.#browserNotice = "Enlace copiado al portapapeles local. Si caducó, solicita uno nuevo.";
|
|
1023
|
+
}
|
|
1024
|
+
catch {
|
|
1025
|
+
this.#browserNotice = "No se pudo copiar el enlace al portapapeles local. Inténtalo de nuevo.";
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
await this.#render();
|
|
1029
|
+
}
|
|
1030
|
+
async #requestRetainedSignIn() {
|
|
1031
|
+
const tabId = this.#activeTabId;
|
|
1032
|
+
const tab = tabId === undefined ? undefined : this.#tabs.get(tabId);
|
|
1033
|
+
const candidate = tabId === undefined ? undefined : this.#retainedBrowserCandidates.get(tabId);
|
|
1034
|
+
if (tabId === undefined || tab === undefined || this.#pendingBrowserAction !== undefined)
|
|
1035
|
+
return;
|
|
1036
|
+
if (candidate === undefined || candidate.fencingGeneration !== tab.snapshot.fencingGeneration ||
|
|
1037
|
+
candidate.agentSessionId !== tab.snapshot.agentSessionId || candidate.processEpoch !== tab.snapshot.processEpoch) {
|
|
1038
|
+
this.#browserNotice = "No retained sign-in link. Request a new link in the provider terminal.";
|
|
1039
|
+
await this.#render();
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
// Renew only the local permission request, never the provider challenge.
|
|
1043
|
+
// Re-admit the URL against the provider allowlist and current binding.
|
|
1044
|
+
const detector = new ProviderBrowserActionDetector({
|
|
1045
|
+
provider: candidate.provider, agentSessionId: candidate.agentSessionId,
|
|
1046
|
+
processEpoch: candidate.processEpoch, fencingGeneration: candidate.fencingGeneration, clock: this.#clock,
|
|
1047
|
+
});
|
|
1048
|
+
const request = detector.push(new TextEncoder().encode(candidate.url))[0];
|
|
1049
|
+
if (request !== undefined) {
|
|
1050
|
+
this.#handledBrowserUrls.delete(this.#browserRequestKey(request));
|
|
1051
|
+
this.#enqueueBrowserAction(tabId, tab, request);
|
|
1052
|
+
this.#retainedPendingRequestId = request.id;
|
|
1053
|
+
this.#promoteBrowserAction();
|
|
1054
|
+
}
|
|
1055
|
+
await this.#render();
|
|
1056
|
+
}
|
|
1057
|
+
#enqueueBrowserAction(tabId, tab, request) {
|
|
1058
|
+
if (request.fencingGeneration !== tab.snapshot.fencingGeneration || this.#handledBrowserUrls.has(this.#browserRequestKey(request)))
|
|
1059
|
+
return;
|
|
1060
|
+
const action = request.type === "browser.open"
|
|
1061
|
+
? Object.freeze({
|
|
1062
|
+
version: LOCAL_ACTION_PROTOCOL_VERSION,
|
|
1063
|
+
id: request.id,
|
|
1064
|
+
identity: this.#localActionIdentity(tab.intent, tab.snapshot),
|
|
1065
|
+
provider: request.provider,
|
|
1066
|
+
kind: "browser.open",
|
|
1067
|
+
arguments: Object.freeze({ url: request.url }),
|
|
1068
|
+
argumentsDigest: digestLocalActionArguments({ url: request.url }),
|
|
1069
|
+
requestedScope: "provider-auth",
|
|
1070
|
+
createdAt: request.detectedAt,
|
|
1071
|
+
expiresAt: request.expiresAt,
|
|
1072
|
+
nonce: request.nonce,
|
|
1073
|
+
})
|
|
1074
|
+
: Object.freeze({
|
|
1075
|
+
version: LOCAL_ACTION_PROTOCOL_VERSION,
|
|
1076
|
+
id: request.id,
|
|
1077
|
+
identity: this.#localActionIdentity(tab.intent, tab.snapshot),
|
|
1078
|
+
provider: "codex",
|
|
1079
|
+
kind: "auth.device.present",
|
|
1080
|
+
arguments: Object.freeze({ verificationUri: request.url, userCode: request.userCode }),
|
|
1081
|
+
argumentsDigest: digestLocalActionArguments({ verificationUri: request.url, userCode: request.userCode }),
|
|
1082
|
+
requestedScope: "provider-auth",
|
|
1083
|
+
createdAt: request.detectedAt,
|
|
1084
|
+
expiresAt: request.expiresAt,
|
|
1085
|
+
nonce: request.nonce,
|
|
1086
|
+
});
|
|
1087
|
+
try {
|
|
1088
|
+
this.#localActionBroker.submit(action);
|
|
1089
|
+
}
|
|
1090
|
+
catch {
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
this.#browserRequests.set(request.id, request);
|
|
1094
|
+
if (request.type === "browser.open") {
|
|
1095
|
+
this.#setOAuthPasteGuard(tabId, new ProviderOAuthPasteGuard(request));
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
async #localActionFrame(event) {
|
|
1099
|
+
if (event.frame.type === "local_action_result") {
|
|
1100
|
+
this.#acknowledgeRemoteLocalActionResult(event.tabId, event.payload);
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
if (event.frame.type !== "local_action_request")
|
|
1104
|
+
return;
|
|
1105
|
+
const raw = event.payload.request;
|
|
1106
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
1107
|
+
throw runtimeFailure("terminal_protocol_error", "The local action request payload is malformed.");
|
|
1108
|
+
}
|
|
1109
|
+
const request = raw;
|
|
1110
|
+
const tab = this.#tabs.get(event.tabId);
|
|
1111
|
+
if (tab === undefined)
|
|
1112
|
+
throw runtimeFailure("terminal_disconnected", "The local action targets an unavailable tab.");
|
|
1113
|
+
let browserRequest;
|
|
1114
|
+
if (request.kind === "browser.open") {
|
|
1115
|
+
const url = request.arguments.url;
|
|
1116
|
+
if ((request.provider !== "claude-code" && request.provider !== "codex") ||
|
|
1117
|
+
typeof url !== "string") {
|
|
1118
|
+
throw runtimeFailure("capability_unsupported", "This browser action was not negotiated by the foreground.");
|
|
1119
|
+
}
|
|
1120
|
+
const admitted = admitProviderAuthUrl(request.provider, url);
|
|
1121
|
+
if (admitted === undefined) {
|
|
1122
|
+
throw runtimeFailure("terminal_protocol_error", "The browser action URL is outside Cuna's provider descriptor.");
|
|
1123
|
+
}
|
|
1124
|
+
browserRequest = Object.freeze({
|
|
1125
|
+
id: request.id,
|
|
1126
|
+
type: "browser.open",
|
|
1127
|
+
provider: request.provider,
|
|
1128
|
+
agentSessionId: request.identity.agentSessionId,
|
|
1129
|
+
processEpoch: request.identity.processEpoch,
|
|
1130
|
+
fencingGeneration: request.identity.fencingGeneration,
|
|
1131
|
+
url: admitted.href,
|
|
1132
|
+
origin: admitted.origin,
|
|
1133
|
+
nonce: request.nonce,
|
|
1134
|
+
detectedAt: request.createdAt,
|
|
1135
|
+
expiresAt: request.expiresAt,
|
|
1136
|
+
state: "pending_permission",
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
else if (request.kind === "auth.device.present" && request.provider === "codex") {
|
|
1140
|
+
const verificationUri = request.arguments.verificationUri;
|
|
1141
|
+
const userCode = request.arguments.userCode;
|
|
1142
|
+
if (typeof verificationUri !== "string" || typeof userCode !== "string") {
|
|
1143
|
+
throw runtimeFailure("terminal_protocol_error", "The Codex device request is malformed.");
|
|
1144
|
+
}
|
|
1145
|
+
const admitted = admitProviderAuthUrl("codex", verificationUri);
|
|
1146
|
+
if (admitted === undefined) {
|
|
1147
|
+
throw runtimeFailure("terminal_protocol_error", "The Codex device URI is outside Cuna's provider descriptor.");
|
|
1148
|
+
}
|
|
1149
|
+
browserRequest = Object.freeze({
|
|
1150
|
+
id: request.id,
|
|
1151
|
+
type: "auth.device.present",
|
|
1152
|
+
provider: "codex",
|
|
1153
|
+
agentSessionId: request.identity.agentSessionId,
|
|
1154
|
+
processEpoch: request.identity.processEpoch,
|
|
1155
|
+
fencingGeneration: request.identity.fencingGeneration,
|
|
1156
|
+
url: admitted.href,
|
|
1157
|
+
origin: admitted.origin,
|
|
1158
|
+
userCode,
|
|
1159
|
+
nonce: request.nonce,
|
|
1160
|
+
detectedAt: request.createdAt,
|
|
1161
|
+
expiresAt: request.expiresAt,
|
|
1162
|
+
state: "pending_permission",
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
else {
|
|
1166
|
+
throw runtimeFailure("capability_unsupported", "This local action kind was not negotiated by the foreground.");
|
|
1167
|
+
}
|
|
1168
|
+
this.#remoteLocalActionTabs.set(request.id, event.tabId);
|
|
1169
|
+
this.#browserRequests.set(request.id, browserRequest);
|
|
1170
|
+
let admitted;
|
|
1171
|
+
try {
|
|
1172
|
+
admitted = this.#localActionBroker.submit(request);
|
|
1173
|
+
}
|
|
1174
|
+
catch (error) {
|
|
1175
|
+
this.#remoteLocalActionTabs.delete(request.id);
|
|
1176
|
+
this.#browserRequests.delete(request.id);
|
|
1177
|
+
throw error;
|
|
1178
|
+
}
|
|
1179
|
+
if (["succeeded", "failed", "denied", "expired", "cancelled"].includes(admitted.state)) {
|
|
1180
|
+
// The bridge may replay a completed request after a transport break.
|
|
1181
|
+
// Reuse its immutable result and wait for the exact ACK rather than
|
|
1182
|
+
// executing the local side effect a second time.
|
|
1183
|
+
this.#queueRemoteLocalActionResult(admitted);
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
if (browserRequest.type === "browser.open") {
|
|
1187
|
+
this.#setOAuthPasteGuard(event.tabId, new ProviderOAuthPasteGuard(browserRequest));
|
|
1188
|
+
}
|
|
1189
|
+
this.#promoteBrowserAction();
|
|
1190
|
+
await this.#render();
|
|
1191
|
+
}
|
|
1192
|
+
#acknowledgeRemoteLocalActionResult(tabId, payload) {
|
|
1193
|
+
if (payload.message !== "ack" ||
|
|
1194
|
+
typeof payload.requestId !== "string" ||
|
|
1195
|
+
typeof payload.argumentDigest !== "string") {
|
|
1196
|
+
throw runtimeFailure("terminal_protocol_error", "The local action acknowledgement is malformed.");
|
|
1197
|
+
}
|
|
1198
|
+
const pending = this.#pendingRemoteLocalActionResults.get(payload.requestId);
|
|
1199
|
+
// ACKs are idempotent. An old result, a result from another foreground
|
|
1200
|
+
// process, or a request with a different canonical argument digest cannot
|
|
1201
|
+
// discharge this foreground's cached outcome.
|
|
1202
|
+
if (pending === undefined ||
|
|
1203
|
+
pending.tabId !== tabId ||
|
|
1204
|
+
pending.snapshot.request.argumentsDigest !== payload.argumentDigest)
|
|
1205
|
+
return;
|
|
1206
|
+
const tab = this.#tabs.get(tabId);
|
|
1207
|
+
if (tab === undefined ||
|
|
1208
|
+
!sameLocalActionIdentity(pending.snapshot.request.identity, this.#localActionIdentity(tab.intent, tab.snapshot)))
|
|
1209
|
+
return;
|
|
1210
|
+
this.#pendingRemoteLocalActionResults.delete(payload.requestId);
|
|
1211
|
+
if (this.#remoteLocalActionTabs.get(payload.requestId) === tabId) {
|
|
1212
|
+
this.#remoteLocalActionTabs.delete(payload.requestId);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
async #sendRemoteLocalActionResult(snapshot) {
|
|
1216
|
+
const tabId = this.#remoteLocalActionTabs.get(snapshot.request.id);
|
|
1217
|
+
if (tabId === undefined || snapshot.result === undefined)
|
|
1218
|
+
return;
|
|
1219
|
+
const existing = this.#pendingRemoteLocalActionResults.get(snapshot.request.id);
|
|
1220
|
+
if (existing === undefined) {
|
|
1221
|
+
this.#pendingRemoteLocalActionResults.set(snapshot.request.id, Object.freeze({ tabId, snapshot }));
|
|
1222
|
+
}
|
|
1223
|
+
else if (existing.tabId !== tabId ||
|
|
1224
|
+
existing.snapshot.request.argumentsDigest !== snapshot.request.argumentsDigest ||
|
|
1225
|
+
!sameLocalActionIdentity(existing.snapshot.request.identity, snapshot.request.identity)) {
|
|
1226
|
+
// Request ids are single-use within the broker. A different binding or
|
|
1227
|
+
// digest is a replay collision and must never replace the result already
|
|
1228
|
+
// waiting for its matching acknowledgement.
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
await this.#sendPendingRemoteLocalActionResult(snapshot.request.id);
|
|
1232
|
+
}
|
|
1233
|
+
async #sendPendingRemoteLocalActionResult(requestId) {
|
|
1234
|
+
const pending = this.#pendingRemoteLocalActionResults.get(requestId);
|
|
1235
|
+
if (pending === undefined || pending.snapshot.result === undefined)
|
|
1236
|
+
return;
|
|
1237
|
+
const tab = this.#tabs.get(pending.tabId);
|
|
1238
|
+
if (tab === undefined ||
|
|
1239
|
+
!sameLocalActionIdentity(pending.snapshot.request.identity, this.#localActionIdentity(tab.intent, tab.snapshot)))
|
|
1240
|
+
return;
|
|
1241
|
+
const runtime = this.#runtime;
|
|
1242
|
+
if (runtime === undefined)
|
|
1243
|
+
return;
|
|
1244
|
+
await runtime.sendLocalActionControl("local_action_result", Object.freeze({
|
|
1245
|
+
message: "outcome",
|
|
1246
|
+
requestId,
|
|
1247
|
+
argumentDigest: pending.snapshot.request.argumentsDigest,
|
|
1248
|
+
result: pending.snapshot.result,
|
|
1249
|
+
}), pending.tabId);
|
|
1250
|
+
}
|
|
1251
|
+
#queueRemoteLocalActionResult(snapshot) {
|
|
1252
|
+
const requestId = snapshot.request.id;
|
|
1253
|
+
if (this.#remoteLocalActionResultSends.has(requestId))
|
|
1254
|
+
return;
|
|
1255
|
+
const task = this.#sendRemoteLocalActionResult(snapshot);
|
|
1256
|
+
this.#remoteLocalActionResultSends.set(requestId, task);
|
|
1257
|
+
this.#remoteLocalActionResultTasks.add(task);
|
|
1258
|
+
void task.catch((error) => {
|
|
1259
|
+
if (this.#state !== "stopping" && this.#state !== "stopped")
|
|
1260
|
+
this.#recordFailure(error);
|
|
1261
|
+
}).finally(() => {
|
|
1262
|
+
this.#remoteLocalActionResultTasks.delete(task);
|
|
1263
|
+
if (this.#remoteLocalActionResultSends.get(requestId) === task) {
|
|
1264
|
+
this.#remoteLocalActionResultSends.delete(requestId);
|
|
1265
|
+
}
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
#resendPendingRemoteLocalActionResults(tabId, identity) {
|
|
1269
|
+
for (const pending of this.#pendingRemoteLocalActionResults.values()) {
|
|
1270
|
+
if (pending.tabId !== tabId || !sameLocalActionIdentity(pending.snapshot.request.identity, identity))
|
|
1271
|
+
continue;
|
|
1272
|
+
this.#queueRemoteLocalActionResult(pending.snapshot);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
#fenceRemoteLocalActionResults(tabId, identity) {
|
|
1276
|
+
for (const [requestId, pending] of this.#pendingRemoteLocalActionResults) {
|
|
1277
|
+
if (pending.tabId === tabId && !sameLocalActionIdentity(pending.snapshot.request.identity, identity)) {
|
|
1278
|
+
this.#pendingRemoteLocalActionResults.delete(requestId);
|
|
1279
|
+
if (this.#remoteLocalActionTabs.get(requestId) === tabId)
|
|
1280
|
+
this.#remoteLocalActionTabs.delete(requestId);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
#promoteBrowserAction() {
|
|
1285
|
+
const current = this.#localActionBroker.current();
|
|
1286
|
+
if (current?.state !== "pending_user") {
|
|
1287
|
+
this.#pendingBrowserAction = undefined;
|
|
1288
|
+
this.#pendingBrowserActionTabId = undefined;
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
const request = this.#browserRequests.get(current.request.id);
|
|
1292
|
+
if (request === undefined)
|
|
1293
|
+
return;
|
|
1294
|
+
this.#pendingBrowserAction = request;
|
|
1295
|
+
this.#pendingBrowserActionTabId = [...this.#tabs.entries()].find(([, tab]) => tab.snapshot.agentSessionId === request.agentSessionId &&
|
|
1296
|
+
tab.snapshot.processEpoch === request.processEpoch &&
|
|
1297
|
+
tab.snapshot.fencingGeneration === request.fencingGeneration)?.[0];
|
|
1298
|
+
this.#browserNotice = undefined;
|
|
1299
|
+
}
|
|
1300
|
+
#localActionIdentity(_intent, snapshot) {
|
|
1301
|
+
return Object.freeze({
|
|
1302
|
+
userId: snapshot.userId,
|
|
1303
|
+
deviceId: this.#options.deviceId ?? "cli-foreground",
|
|
1304
|
+
machineId: snapshot.machineId,
|
|
1305
|
+
workspaceBindingId: snapshot.workspaceBindingId ?? null,
|
|
1306
|
+
workspaceBindingGeneration: snapshot.workspaceBindingGeneration ?? null,
|
|
1307
|
+
agentSessionId: snapshot.agentSessionId,
|
|
1308
|
+
processEpoch: snapshot.processEpoch,
|
|
1309
|
+
fencingGeneration: snapshot.fencingGeneration,
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
#isLocalActionIdentityLive(identity) {
|
|
1313
|
+
for (const tab of this.#tabs.values()) {
|
|
1314
|
+
if (sameLocalActionIdentity(identity, this.#localActionIdentity(tab.intent, tab.snapshot)))
|
|
1315
|
+
return true;
|
|
1316
|
+
}
|
|
1317
|
+
return false;
|
|
1318
|
+
}
|
|
1319
|
+
#browserRequestKey(request) {
|
|
1320
|
+
return `${request.agentSessionId}:${request.processEpoch}:${request.fencingGeneration}:${request.type}:${request.url}`;
|
|
1321
|
+
}
|
|
1322
|
+
#setOAuthPasteGuard(tabId, guard) {
|
|
1323
|
+
const timer = this.#oauthPrefixTimers.get(tabId);
|
|
1324
|
+
if (timer !== undefined)
|
|
1325
|
+
clearTimeout(timer);
|
|
1326
|
+
this.#oauthPrefixTimers.delete(tabId);
|
|
1327
|
+
this.#oauthPasteGuards.get(tabId)?.reset();
|
|
1328
|
+
if (guard === undefined)
|
|
1329
|
+
this.#oauthPasteGuards.delete(tabId);
|
|
1330
|
+
else
|
|
1331
|
+
this.#oauthPasteGuards.set(tabId, guard);
|
|
1332
|
+
}
|
|
1333
|
+
#guardProviderOAuthPaste(bytes, target) {
|
|
1334
|
+
if (target === undefined || target.tabId !== this.#activeTabId) {
|
|
1335
|
+
return { bytes, blocked: false };
|
|
1336
|
+
}
|
|
1337
|
+
const guard = this.#oauthPasteGuards.get(target.tabId);
|
|
1338
|
+
if (guard === undefined)
|
|
1339
|
+
return { bytes, blocked: false };
|
|
1340
|
+
const result = guard.push(bytes);
|
|
1341
|
+
const previousTimer = this.#oauthPrefixTimers.get(target.tabId);
|
|
1342
|
+
if (!guard.hasPendingPrefix && previousTimer !== undefined) {
|
|
1343
|
+
clearTimeout(previousTimer);
|
|
1344
|
+
this.#oauthPrefixTimers.delete(target.tabId);
|
|
1345
|
+
}
|
|
1346
|
+
else if (this.#state === "active" && guard.hasPendingPrefix && previousTimer === undefined) {
|
|
1347
|
+
const timer = setTimeout(() => {
|
|
1348
|
+
this.#oauthPrefixTimers.delete(target.tabId);
|
|
1349
|
+
const operation = this.#inputTail.then(async () => {
|
|
1350
|
+
const tab = this.#tabs.get(target.tabId);
|
|
1351
|
+
if (this.#state !== "active" || tab === undefined || tab.snapshot.state !== "active" || this.#activeTabId !== target.tabId ||
|
|
1352
|
+
this.#oauthPasteGuards.get(target.tabId) !== guard || !sameSnapshotBinding(tab.snapshot, target.binding))
|
|
1353
|
+
return;
|
|
1354
|
+
const prefix = guard.releasePendingPrefix();
|
|
1355
|
+
if (prefix.byteLength > 0)
|
|
1356
|
+
await this.#routeInput(prefix, target, true);
|
|
1357
|
+
});
|
|
1358
|
+
this.#inputTail = operation.catch((error) => {
|
|
1359
|
+
if (error instanceof RuntimeBoundaryError &&
|
|
1360
|
+
["session_unknown", "terminal_disconnected", "terminal_observer"].includes(error.code))
|
|
1361
|
+
return;
|
|
1362
|
+
this.#recordFailure(error);
|
|
1363
|
+
void this.stop().catch(() => { this.#state = "failed"; });
|
|
1364
|
+
});
|
|
1365
|
+
}, 35);
|
|
1366
|
+
timer.unref?.();
|
|
1367
|
+
this.#oauthPrefixTimers.set(target.tabId, timer);
|
|
1368
|
+
}
|
|
1369
|
+
if (result.forward.length === 1)
|
|
1370
|
+
return { bytes: result.forward[0], blocked: result.blocked };
|
|
1371
|
+
if (result.forward.length === 0)
|
|
1372
|
+
return { bytes: new Uint8Array(), blocked: result.blocked };
|
|
1373
|
+
const length = result.forward.reduce((total, chunk) => total + chunk.byteLength, 0);
|
|
1374
|
+
const joined = new Uint8Array(length);
|
|
1375
|
+
let offset = 0;
|
|
1376
|
+
for (const chunk of result.forward) {
|
|
1377
|
+
joined.set(chunk, offset);
|
|
1378
|
+
offset += chunk.byteLength;
|
|
1379
|
+
}
|
|
1380
|
+
return { bytes: joined, blocked: result.blocked };
|
|
1381
|
+
}
|
|
1382
|
+
#selectByIndex(index) {
|
|
1383
|
+
const tabId = [...this.#tabs.keys()][index];
|
|
1384
|
+
if (tabId === undefined)
|
|
1385
|
+
return;
|
|
1386
|
+
this.#select(tabId);
|
|
1387
|
+
}
|
|
1388
|
+
#selectNext() {
|
|
1389
|
+
const ids = [...this.#tabs.keys()];
|
|
1390
|
+
if (ids.length === 0)
|
|
1391
|
+
return;
|
|
1392
|
+
const current = this.#activeTabId === undefined ? -1 : ids.indexOf(this.#activeTabId);
|
|
1393
|
+
this.#select(ids[(current + 1) % ids.length] ?? ids[0] ?? "");
|
|
1394
|
+
}
|
|
1395
|
+
#select(tabId) {
|
|
1396
|
+
if (!this.#tabs.has(tabId))
|
|
1397
|
+
return;
|
|
1398
|
+
this.#requireRuntime().switchActive(tabId);
|
|
1399
|
+
this.#registry.select(tabId);
|
|
1400
|
+
this.#activeTabId = tabId;
|
|
1401
|
+
void this.#render().catch((error) => {
|
|
1402
|
+
this.#recordFailure(error);
|
|
1403
|
+
void this.stop().catch(() => { this.#state = "failed"; });
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
async #detachTab(tabId = this.#activeTabId) {
|
|
1407
|
+
if (tabId === undefined)
|
|
1408
|
+
return;
|
|
1409
|
+
this.#localDetachTabIds.add(tabId);
|
|
1410
|
+
const animate = this.#closingTabId === tabId;
|
|
1411
|
+
const departing = this.#tabs.get(tabId);
|
|
1412
|
+
if (departing !== undefined) {
|
|
1413
|
+
this.#localActionBroker.cancelBinding(this.#localActionIdentity(departing.intent, departing.snapshot), "terminal_detached");
|
|
1414
|
+
await Promise.allSettled(this.#remoteLocalActionResultTasks);
|
|
1415
|
+
}
|
|
1416
|
+
const detachOutcome = this.#requireRuntime().detach(tabId).then(() => Object.freeze({ ok: true }), (error) => Object.freeze({ ok: false, error }));
|
|
1417
|
+
if (animate)
|
|
1418
|
+
await this.#animateDisconnecting();
|
|
1419
|
+
const outcome = await detachOutcome;
|
|
1420
|
+
if (!outcome.ok) {
|
|
1421
|
+
this.#disconnectNotice = undefined;
|
|
1422
|
+
this.#closingTabId = undefined;
|
|
1423
|
+
this.#localDetachTabIds.delete(tabId);
|
|
1424
|
+
this.#recordFailure(outcome.error);
|
|
1425
|
+
const remaining = this.#tabs.get(tabId);
|
|
1426
|
+
if (remaining?.snapshot.state === "failed" || remaining?.snapshot.state === "interrupted") {
|
|
1427
|
+
this.#terminalState(remaining.snapshot);
|
|
1428
|
+
}
|
|
1429
|
+
// Cleanup is deliberately asynchronous because it drains this input
|
|
1430
|
+
// operation before restoring the host. The recorded detach error remains
|
|
1431
|
+
// observable to the foreground runner after cleanup.
|
|
1432
|
+
void this.stop().catch(() => { this.#state = "failed"; });
|
|
1433
|
+
throw outcome.error;
|
|
1434
|
+
}
|
|
1435
|
+
// Recorded only once the runtime confirmed the detach: a failed detach
|
|
1436
|
+
// above must never be announced as a session that keeps running.
|
|
1437
|
+
if (departing !== undefined) {
|
|
1438
|
+
this.#detachedSessions.push(Object.freeze({
|
|
1439
|
+
agentSessionId: departing.intent.agentSessionId,
|
|
1440
|
+
label: departing.intent.label,
|
|
1441
|
+
}));
|
|
1442
|
+
}
|
|
1443
|
+
try {
|
|
1444
|
+
if (animate && this.#tabs.size === 1) {
|
|
1445
|
+
this.#disconnectNotice = "✓ Disconnected.";
|
|
1446
|
+
await this.#renderDisconnectNotice();
|
|
1447
|
+
await abortableDelay(this.#options.disconnectFrameMs ?? DISCONNECT_FRAME_MS, this.#lifetimeAbort.signal);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
finally {
|
|
1451
|
+
this.#disconnectNotice = undefined;
|
|
1452
|
+
this.#closingTabId = undefined;
|
|
1453
|
+
}
|
|
1454
|
+
this.#localDetachTabIds.delete(tabId);
|
|
1455
|
+
this.#browserDetectors.delete(tabId);
|
|
1456
|
+
this.#copyDetectors.delete(tabId);
|
|
1457
|
+
this.#copyLinks.delete(tabId);
|
|
1458
|
+
this.#retainedBrowserDetectors.delete(tabId);
|
|
1459
|
+
this.#retainedBrowserCandidates.delete(tabId);
|
|
1460
|
+
if (this.#pendingBrowserActionTabId === tabId) {
|
|
1461
|
+
this.#pendingBrowserAction = undefined;
|
|
1462
|
+
this.#pendingBrowserActionTabId = undefined;
|
|
1463
|
+
this.#browserNotice = undefined;
|
|
1464
|
+
}
|
|
1465
|
+
this.#setOAuthPasteGuard(tabId);
|
|
1466
|
+
// The runtime boundary publishes a detached snapshot before resolving. Keep
|
|
1467
|
+
// this fallback so the coordinator contract remains safe with any runtime
|
|
1468
|
+
// implementation that resolves detach without publishing a state event.
|
|
1469
|
+
const remaining = this.#tabs.get(tabId);
|
|
1470
|
+
if (remaining !== undefined) {
|
|
1471
|
+
remaining.viewport.dispose();
|
|
1472
|
+
this.#tabs.delete(tabId);
|
|
1473
|
+
}
|
|
1474
|
+
if (this.#activeTabId === tabId)
|
|
1475
|
+
this.#activeTabId = undefined;
|
|
1476
|
+
if (this.#tabs.size === 0) {
|
|
1477
|
+
// Do not await stop from inside the serialized input operation: cleanup
|
|
1478
|
+
// intentionally waits for that operation to drain before host restore.
|
|
1479
|
+
void this.stop().catch(() => { this.#state = "failed"; });
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
if (this.#activeTabId === undefined) {
|
|
1483
|
+
const replacement = this.#tabs.keys().next().value;
|
|
1484
|
+
if (replacement !== undefined) {
|
|
1485
|
+
this.#requireRuntime().switchActive(replacement);
|
|
1486
|
+
this.#registry.select(replacement);
|
|
1487
|
+
this.#activeTabId = replacement;
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
await this.#render();
|
|
1491
|
+
}
|
|
1492
|
+
async #animateDisconnecting() {
|
|
1493
|
+
for (const notice of DISCONNECTING_FRAMES) {
|
|
1494
|
+
this.#disconnectNotice = notice;
|
|
1495
|
+
await this.#renderDisconnectNotice();
|
|
1496
|
+
await abortableDelay(this.#options.disconnectFrameMs ?? DISCONNECT_FRAME_MS, this.#lifetimeAbort.signal);
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
async #renderDisconnectNotice() {
|
|
1500
|
+
// Closing feedback is best-effort decoration. A host paint failure must not
|
|
1501
|
+
// turn a confirmed remote detach into a failed detach or mask its result.
|
|
1502
|
+
try {
|
|
1503
|
+
await this.#render();
|
|
1504
|
+
}
|
|
1505
|
+
catch { /* restore still owns terminal cleanup */ }
|
|
1506
|
+
}
|
|
1507
|
+
#captureInputTarget() {
|
|
1508
|
+
const tabId = this.#activeTabId;
|
|
1509
|
+
if (tabId === undefined)
|
|
1510
|
+
return undefined;
|
|
1511
|
+
const tab = this.#tabs.get(tabId);
|
|
1512
|
+
if (tab === undefined || tab.snapshot.state !== "active" || tab.snapshot.terminalView?.ready === false)
|
|
1513
|
+
return undefined;
|
|
1514
|
+
return Object.freeze({
|
|
1515
|
+
tabId,
|
|
1516
|
+
binding: Object.freeze({
|
|
1517
|
+
userId: tab.snapshot.userId,
|
|
1518
|
+
machineId: tab.snapshot.machineId,
|
|
1519
|
+
agentSessionId: tab.snapshot.agentSessionId,
|
|
1520
|
+
processEpoch: tab.snapshot.processEpoch,
|
|
1521
|
+
fencingGeneration: tab.snapshot.fencingGeneration,
|
|
1522
|
+
}),
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
#queueResize() {
|
|
1526
|
+
if (this.#state !== "active")
|
|
1527
|
+
return;
|
|
1528
|
+
// Output also requests reconciliation while geometry differs. Preserve the
|
|
1529
|
+
// first deadline so continuous output cannot postpone every paint; the
|
|
1530
|
+
// callback reads the latest host dimensions when it runs.
|
|
1531
|
+
if (this.#resizeTimer !== undefined)
|
|
1532
|
+
return;
|
|
1533
|
+
this.#resizeTimer = setTimeout(() => {
|
|
1534
|
+
this.#resizeTimer = undefined;
|
|
1535
|
+
void this.#applyResize().catch((error) => {
|
|
1536
|
+
this.#recordFailure(error);
|
|
1537
|
+
void this.stop().catch(() => { this.#state = "failed"; });
|
|
1538
|
+
});
|
|
1539
|
+
}, this.#options.resizeCoalesceMs);
|
|
1540
|
+
this.#resizeTimer.unref();
|
|
1541
|
+
}
|
|
1542
|
+
async #applyResize() {
|
|
1543
|
+
// The host reflows or clears its alternate screen on resize; the last
|
|
1544
|
+
// frame no longer describes what it shows.
|
|
1545
|
+
this.#lastHostFrame = undefined;
|
|
1546
|
+
const dimensions = admitForegroundDimensions(this.#options.host.dimensions());
|
|
1547
|
+
const rows = remoteRows(dimensions.rows);
|
|
1548
|
+
for (const [tabId, tab] of this.#tabs) {
|
|
1549
|
+
if (tab.snapshot.accessMode !== "observer")
|
|
1550
|
+
await tab.viewport.resize(dimensions.columns, rows);
|
|
1551
|
+
// An observer renders at the writer's dimensions; it never resizes the
|
|
1552
|
+
// PTY. The gateway would close its attachment on the first RESIZE.
|
|
1553
|
+
if (tab.snapshot.resizeCapability === "live" &&
|
|
1554
|
+
tab.snapshot.state === "active" &&
|
|
1555
|
+
tab.snapshot.accessMode !== "observer") {
|
|
1556
|
+
await this.#requireRuntime().resize(dimensions.columns, rows, tabId);
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
await this.#render();
|
|
1560
|
+
}
|
|
1561
|
+
async #reconcileGeometry(snapshot, repaintAfterReplay) {
|
|
1562
|
+
const tab = this.#tabs.get(snapshot.tabId);
|
|
1563
|
+
if (tab === undefined ||
|
|
1564
|
+
snapshot.state !== "active" ||
|
|
1565
|
+
snapshot.resizeCapability !== "live" ||
|
|
1566
|
+
// An observer neither resizes the PTY nor asks it to repaint: both are
|
|
1567
|
+
// writer actions the gateway closes an observer's attachment for.
|
|
1568
|
+
snapshot.accessMode === "observer" ||
|
|
1569
|
+
tab.snapshot.accessMode === "observer" ||
|
|
1570
|
+
tab.snapshot.state !== "active" ||
|
|
1571
|
+
tab.snapshot.resizeCapability !== "live" ||
|
|
1572
|
+
!sameSnapshotBinding(tab.snapshot, snapshot))
|
|
1573
|
+
return;
|
|
1574
|
+
// Host geometry can change while attach or reconnect is awaiting remote
|
|
1575
|
+
// readiness. Re-read it only after the fenced viewport exists, then bring
|
|
1576
|
+
// the local VTE and the exact live remote binding to the same dimensions.
|
|
1577
|
+
const dimensions = admitForegroundDimensions(this.#options.host.dimensions());
|
|
1578
|
+
const rows = remoteRows(dimensions.rows);
|
|
1579
|
+
await tab.viewport.resize(dimensions.columns, rows);
|
|
1580
|
+
const current = this.#tabs.get(snapshot.tabId);
|
|
1581
|
+
if (current !== tab ||
|
|
1582
|
+
current.snapshot.state !== "active" ||
|
|
1583
|
+
current.snapshot.resizeCapability !== "live" ||
|
|
1584
|
+
!sameSnapshotBinding(current.snapshot, snapshot))
|
|
1585
|
+
return;
|
|
1586
|
+
const runtime = this.#requireRuntime();
|
|
1587
|
+
// Canonical views supply a complete fresh rendering stream. Legacy repaint
|
|
1588
|
+
// input must not enter their provider, especially before view readiness.
|
|
1589
|
+
if (repaintAfterReplay && snapshot.terminalView === undefined) {
|
|
1590
|
+
// A fullscreen provider may have painted before initial attach, so
|
|
1591
|
+
// replay can contain only later cursor-relative deltas. Trigger SIGWINCH
|
|
1592
|
+
// with a bounded row bounce, then restore the exact isolated viewport.
|
|
1593
|
+
// Keeping columns stable avoids introducing a temporary wrap topology.
|
|
1594
|
+
const bounceRows = rows === 1 ? 2 : rows - 1;
|
|
1595
|
+
await runtime.resize(dimensions.columns, bounceRows, snapshot.tabId);
|
|
1596
|
+
const afterBounce = this.#tabs.get(snapshot.tabId);
|
|
1597
|
+
if (afterBounce !== tab ||
|
|
1598
|
+
afterBounce.snapshot.state !== "active" ||
|
|
1599
|
+
!sameSnapshotBinding(afterBounce.snapshot, snapshot))
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
await runtime.resize(dimensions.columns, rows, snapshot.tabId);
|
|
1603
|
+
if (repaintAfterReplay && snapshot.terminalView === undefined) {
|
|
1604
|
+
const afterResize = this.#tabs.get(snapshot.tabId);
|
|
1605
|
+
if (afterResize !== tab ||
|
|
1606
|
+
afterResize.snapshot.state !== "active" ||
|
|
1607
|
+
!sameSnapshotBinding(afterResize.snapshot, snapshot))
|
|
1608
|
+
return;
|
|
1609
|
+
// Claude Code and Codex do not always repaint an already-running TUI from
|
|
1610
|
+
// SIGWINCH alone. Ctrl+L is their standard terminal redraw request; send
|
|
1611
|
+
// it once after initial replay, never on routine resize or reconnect.
|
|
1612
|
+
await runtime.sendInput(REMOTE_REDRAW, snapshot.tabId, Object.freeze({
|
|
1613
|
+
userId: snapshot.userId,
|
|
1614
|
+
machineId: snapshot.machineId,
|
|
1615
|
+
agentSessionId: snapshot.agentSessionId,
|
|
1616
|
+
processEpoch: snapshot.processEpoch,
|
|
1617
|
+
fencingGeneration: snapshot.fencingGeneration,
|
|
1618
|
+
}));
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
async #render() {
|
|
1622
|
+
const operation = this.#renderTail.then(async () => {
|
|
1623
|
+
const activeTabId = this.#activeTabId;
|
|
1624
|
+
if (activeTabId === undefined || this.#tabs.size === 0)
|
|
1625
|
+
return;
|
|
1626
|
+
const dimensions = admitForegroundDimensions(this.#options.host.dimensions());
|
|
1627
|
+
const activeViewport = this.#tabs.get(activeTabId)?.viewport.snapshot();
|
|
1628
|
+
if (activeViewport === undefined ||
|
|
1629
|
+
(this.#tabs.get(activeTabId)?.snapshot.accessMode !== "observer" &&
|
|
1630
|
+
(activeViewport.columns !== dimensions.columns || activeViewport.rows !== remoteRows(dimensions.rows)))) {
|
|
1631
|
+
// A host resize becomes observable before the coalesced local VTE and
|
|
1632
|
+
// remote PTY resize completes. Rendering the old, wider viewport into
|
|
1633
|
+
// the new frame would either clip trusted state or reject a valid row
|
|
1634
|
+
// as oversized. Keep the last complete frame visible; #applyResize
|
|
1635
|
+
// renders again after every viewport reaches the admitted dimensions.
|
|
1636
|
+
this.#queueResize();
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
const tabs = [...this.#tabs.values()].map((tab) => Object.freeze({
|
|
1640
|
+
id: tab.intent.tabId,
|
|
1641
|
+
label: tab.intent.label,
|
|
1642
|
+
agent: tab.intent.agent,
|
|
1643
|
+
viewport: tab.snapshot.accessMode === "observer"
|
|
1644
|
+
? tab.viewport.snapshotForHost(dimensions.columns, remoteRows(dimensions.rows))
|
|
1645
|
+
: tab.viewport.snapshot(),
|
|
1646
|
+
}));
|
|
1647
|
+
const frame = renderWorkbenchFrame({
|
|
1648
|
+
columns: dimensions.columns,
|
|
1649
|
+
rows: dimensions.rows,
|
|
1650
|
+
activeTabId,
|
|
1651
|
+
tabs,
|
|
1652
|
+
...(this.#copyLinks.has(activeTabId) ? { action: "Copiar enlace · Ctrl+] y" } : {}),
|
|
1653
|
+
appbar: this.#options.appbar?.() ?? runtimeAppbar(this.#clock(), this.#tabs.get(activeTabId)?.snapshot, this.#tabs.get(activeTabId)?.intent),
|
|
1654
|
+
color: this.#options.color ?? true,
|
|
1655
|
+
...(this.#disconnectNotice !== undefined
|
|
1656
|
+
? { notice: this.#disconnectNotice }
|
|
1657
|
+
: this.#tabs.get(activeTabId)?.snapshot.state === "active" && this.#tabs.get(activeTabId)?.snapshot.terminalView?.ready === false
|
|
1658
|
+
? { notice: "Restoring terminal\u2026" }
|
|
1659
|
+
: this.#browserNotice !== undefined
|
|
1660
|
+
? { notice: this.#tabs.get(activeTabId)?.snapshot.historicalInputUncertainty === true &&
|
|
1661
|
+
(this.#browserNotice === INPUT_WITHHELD_NOTICE || this.#browserNotice === RECONNECT_FAILED_NOTICE)
|
|
1662
|
+
? `${HISTORICAL_INPUT_NOTICE} · ${this.#browserNotice}` : this.#browserNotice }
|
|
1663
|
+
: this.#pendingBrowserAction !== undefined && this.#pendingBrowserActionTabId === activeTabId
|
|
1664
|
+
? {
|
|
1665
|
+
notice: this.#pendingBrowserAction.type === "auth.device.present"
|
|
1666
|
+
? `${providerName(this.#pendingBrowserAction.provider)} requests device sign-in · code ${this.#pendingBrowserAction.userCode} · Enter/o open · d/Esc deny`
|
|
1667
|
+
: this.#pendingBrowserAction.id === this.#retainedPendingRequestId
|
|
1668
|
+
? "Retained sign-in link; validity unknown · Enter/o open · d/Esc deny"
|
|
1669
|
+
: `${providerName(this.#pendingBrowserAction.provider)} requests browser authentication · Enter/o open · d/Esc deny`,
|
|
1670
|
+
}
|
|
1671
|
+
: this.#helpVisible
|
|
1672
|
+
? { notice: "Keys: Ctrl+C detach | " + (process.platform === "win32" ? "Select text + Ctrl+Shift+C copy | Ctrl+Shift+V paste | " : "") + "Ctrl+S keep active | Ctrl+] c/s/q remote | 1-4 tab | n next | r retry" +
|
|
1673
|
+
(this.#tabs.get(activeTabId)?.snapshot.accessMode === "observer" &&
|
|
1674
|
+
writerCapabilityRefusal(this.#tabs.get(activeTabId).snapshot) === undefined
|
|
1675
|
+
? writerCapabilityNeedsRefresh(this.#tabs.get(activeTabId).snapshot) ? " | w recheck control" : " | w take control"
|
|
1676
|
+
: "") + " | a retained sign-in link | d detach" }
|
|
1677
|
+
: this.#seatNoticeFor(this.#tabs.get(activeTabId)?.snapshot) !== undefined
|
|
1678
|
+
? { notice: this.#seatNoticeFor(this.#tabs.get(activeTabId)?.snapshot) }
|
|
1679
|
+
: this.#retainedBrowserCandidates.has(activeTabId)
|
|
1680
|
+
? { notice: "Sign-in link in history · Ctrl+] a to inspect · if rejected, request a new link in the provider" }
|
|
1681
|
+
: {}),
|
|
1682
|
+
});
|
|
1683
|
+
// Every accepted keystroke and every output frame renders a complete
|
|
1684
|
+
// absolute-addressed frame. Most of them repaint exactly what the host
|
|
1685
|
+
// already shows (measured 2026-09-15: three of four frames per typed
|
|
1686
|
+
// key changed no row). A byte-identical frame is not written again;
|
|
1687
|
+
// anything else that touches the host clears this memory first.
|
|
1688
|
+
if (this.#lastHostFrame !== undefined && sameBytes(this.#lastHostFrame, frame.bytes))
|
|
1689
|
+
return;
|
|
1690
|
+
this.#lastHostFrame = undefined;
|
|
1691
|
+
await this.#options.host.write(frame.bytes);
|
|
1692
|
+
this.#lastHostFrame = frame.bytes;
|
|
1693
|
+
});
|
|
1694
|
+
this.#renderTail = operation.then(() => undefined, () => undefined);
|
|
1695
|
+
await operation;
|
|
1696
|
+
}
|
|
1697
|
+
#findIntent(tabId, agentSessionId) {
|
|
1698
|
+
const existing = this.#tabs.get(tabId)?.intent;
|
|
1699
|
+
if (existing !== undefined)
|
|
1700
|
+
return existing;
|
|
1701
|
+
const intent = this.#pendingIntents.find((candidate) => candidate.tabId === tabId);
|
|
1702
|
+
if (intent === undefined || intent.agentSessionId !== agentSessionId) {
|
|
1703
|
+
throw runtimeFailure("grant_scope_mismatch", "Terminal readiness targets an unknown foreground intent.");
|
|
1704
|
+
}
|
|
1705
|
+
return intent;
|
|
1706
|
+
}
|
|
1707
|
+
/**
|
|
1708
|
+
* RTP negotiation happens before `onTerminalReady`, so fall back to the
|
|
1709
|
+
* immutable startup intents. An unknown attachment gets no local actions.
|
|
1710
|
+
*/
|
|
1711
|
+
#localActionKindsForSession(agentSessionId) {
|
|
1712
|
+
const existing = [...this.#tabs.values()].find((tab) => tab.intent.agentSessionId === agentSessionId)?.intent;
|
|
1713
|
+
const intent = existing ?? this.#pendingIntents.find((candidate) => candidate.agentSessionId === agentSessionId);
|
|
1714
|
+
if (intent?.agent === "claude-code")
|
|
1715
|
+
return CLAUDE_LOCAL_ACTION_KINDS;
|
|
1716
|
+
if (intent?.agent === "codex")
|
|
1717
|
+
return CODEX_LOCAL_ACTION_KINDS;
|
|
1718
|
+
// OpenCode owns `/connect` and `/models` in its remote TUI. It must not
|
|
1719
|
+
// advertise a local-action protocol that could be mistaken for a browser
|
|
1720
|
+
// or device-auth handoff.
|
|
1721
|
+
return NO_LOCAL_ACTION_KINDS;
|
|
1722
|
+
}
|
|
1723
|
+
#pendingIntents = Object.freeze([]);
|
|
1724
|
+
#requireRuntime() {
|
|
1725
|
+
if (this.#runtime === undefined)
|
|
1726
|
+
throw runtimeFailure("control_plane_unavailable", "No foreground terminal runtime is bound.");
|
|
1727
|
+
return this.#runtime;
|
|
1728
|
+
}
|
|
1729
|
+
async #renderAttaching(count, dimensions) {
|
|
1730
|
+
const color = this.#options.color ?? true;
|
|
1731
|
+
const top = padTrustedLine(` CUNA ATTACHING ${count} EXACT AGENTSESSION${count === 1 ? "" : "S"}`, dimensions.columns);
|
|
1732
|
+
const indicator = `${ATTACHING_FRAMES[this.#attachingFrame % ATTACHING_FRAMES.length]} Checking terminal authority ${ATTACHING_PROGRESS[this.#attachingFrame % ATTACHING_PROGRESS.length]}`;
|
|
1733
|
+
const detail = padTrustedLine(` ${indicator} · Ctrl-C cancels`, dimensions.columns);
|
|
1734
|
+
const text = [
|
|
1735
|
+
"\u001b[?25l\u001b[H\u001b[2J",
|
|
1736
|
+
color ? "\u001b[48;2;235;86;37m\u001b[38;2;255;255;255m" : "",
|
|
1737
|
+
top,
|
|
1738
|
+
color ? "\u001b[0m" : "",
|
|
1739
|
+
dimensions.rows > 1 ? "\r\n" : "",
|
|
1740
|
+
dimensions.rows > 1 && color ? "\u001b[48;2;121;48;25m\u001b[38;2;224;210;203m" : "",
|
|
1741
|
+
dimensions.rows > 1 ? detail : "",
|
|
1742
|
+
color ? "\u001b[0m" : "",
|
|
1743
|
+
].join("");
|
|
1744
|
+
this.#lastHostFrame = undefined;
|
|
1745
|
+
await this.#options.host.write(new TextEncoder().encode(text));
|
|
1746
|
+
}
|
|
1747
|
+
#startAttachingAnimation(count) {
|
|
1748
|
+
if (this.#state !== "starting" || this.#startupDetached)
|
|
1749
|
+
return;
|
|
1750
|
+
if (this.#attachingAnimationTimer !== undefined)
|
|
1751
|
+
return;
|
|
1752
|
+
this.#attachingAnimationTimer = setInterval(() => {
|
|
1753
|
+
if (this.#state !== "starting" || this.#startupDetached) {
|
|
1754
|
+
this.#stopAttachingAnimation();
|
|
1755
|
+
return;
|
|
1756
|
+
}
|
|
1757
|
+
this.#attachingFrame = (this.#attachingFrame + 1) % ATTACHING_PROGRESS.length;
|
|
1758
|
+
this.#queueAttachingRender(count);
|
|
1759
|
+
}, ATTACHING_FRAME_MS);
|
|
1760
|
+
this.#attachingAnimationTimer.unref();
|
|
1761
|
+
}
|
|
1762
|
+
#stopAttachingAnimation() {
|
|
1763
|
+
if (this.#attachingAnimationTimer === undefined)
|
|
1764
|
+
return;
|
|
1765
|
+
clearInterval(this.#attachingAnimationTimer);
|
|
1766
|
+
this.#attachingAnimationTimer = undefined;
|
|
1767
|
+
}
|
|
1768
|
+
#queueAttachingRender(count) {
|
|
1769
|
+
const operation = this.#renderTail.then(async () => {
|
|
1770
|
+
if (this.#state !== "starting" || this.#startupDetached)
|
|
1771
|
+
return;
|
|
1772
|
+
await this.#renderAttaching(count, admitForegroundDimensions(this.#options.host.dimensions()));
|
|
1773
|
+
});
|
|
1774
|
+
this.#renderTail = operation.then(() => undefined, () => undefined);
|
|
1775
|
+
void operation.catch((error) => {
|
|
1776
|
+
this.#recordFailure(error);
|
|
1777
|
+
if (this.#state === "starting")
|
|
1778
|
+
void this.stop().catch(() => { this.#state = "failed"; });
|
|
1779
|
+
});
|
|
1780
|
+
}
|
|
1781
|
+
#queueStartupCloseFeedback() {
|
|
1782
|
+
const operation = this.#renderTail.then(async () => {
|
|
1783
|
+
const dimensions = admitForegroundDimensions(this.#options.host.dimensions());
|
|
1784
|
+
for (let index = 0; index < STARTUP_CLOSE_FRAMES.length; index += 1) {
|
|
1785
|
+
const notice = STARTUP_CLOSE_FRAMES[index];
|
|
1786
|
+
if (notice === undefined)
|
|
1787
|
+
continue;
|
|
1788
|
+
try {
|
|
1789
|
+
await this.#renderStartupClose(notice, dimensions);
|
|
1790
|
+
}
|
|
1791
|
+
catch {
|
|
1792
|
+
// Decorative shutdown feedback must never prevent the host lease
|
|
1793
|
+
// from being restored after a local interrupt.
|
|
1794
|
+
return;
|
|
1795
|
+
}
|
|
1796
|
+
if (index + 1 < STARTUP_CLOSE_FRAMES.length) {
|
|
1797
|
+
await new Promise((resolve) => setTimeout(resolve, this.#options.disconnectFrameMs ?? DISCONNECT_FRAME_MS));
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
});
|
|
1801
|
+
this.#renderTail = operation.then(() => undefined, () => undefined);
|
|
1802
|
+
}
|
|
1803
|
+
async #renderStartupClose(notice, dimensions) {
|
|
1804
|
+
const color = this.#options.color ?? true;
|
|
1805
|
+
const top = padTrustedLine(" CUNA CLOSING", dimensions.columns);
|
|
1806
|
+
const detail = padTrustedLine(` ${notice}`, dimensions.columns);
|
|
1807
|
+
const text = [
|
|
1808
|
+
"\u001b[?25l\u001b[H\u001b[2J",
|
|
1809
|
+
color ? "\u001b[48;2;235;86;37m\u001b[38;2;255;255;255m" : "",
|
|
1810
|
+
top,
|
|
1811
|
+
color ? "\u001b[0m" : "",
|
|
1812
|
+
dimensions.rows > 1 ? "\r\n" : "",
|
|
1813
|
+
dimensions.rows > 1 && color ? "\u001b[48;2;121;48;25m\u001b[38;2;224;210;203m" : "",
|
|
1814
|
+
dimensions.rows > 1 ? detail : "",
|
|
1815
|
+
color ? "\u001b[0m" : "",
|
|
1816
|
+
].join("");
|
|
1817
|
+
this.#lastHostFrame = undefined;
|
|
1818
|
+
await this.#options.host.write(new TextEncoder().encode(text));
|
|
1819
|
+
}
|
|
1820
|
+
#recordFailure(error) {
|
|
1821
|
+
this.#terminalFailure ??= error;
|
|
1822
|
+
}
|
|
1823
|
+
#queueStateRender() {
|
|
1824
|
+
this.#stateRenderDirty = true;
|
|
1825
|
+
if (this.#stateRenderRunning)
|
|
1826
|
+
return;
|
|
1827
|
+
this.#stateRenderRunning = true;
|
|
1828
|
+
void (async () => {
|
|
1829
|
+
try {
|
|
1830
|
+
while (this.#stateRenderDirty && this.#state === "active") {
|
|
1831
|
+
this.#stateRenderDirty = false;
|
|
1832
|
+
await this.#render();
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
catch (error) {
|
|
1836
|
+
this.#recordFailure(error);
|
|
1837
|
+
if (this.#state === "active")
|
|
1838
|
+
await this.stop();
|
|
1839
|
+
}
|
|
1840
|
+
finally {
|
|
1841
|
+
this.#stateRenderRunning = false;
|
|
1842
|
+
if (this.#stateRenderDirty && this.#state === "active")
|
|
1843
|
+
this.#queueStateRender();
|
|
1844
|
+
}
|
|
1845
|
+
})().catch((error) => {
|
|
1846
|
+
this.#recordFailure(error);
|
|
1847
|
+
this.#state = "failed";
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1850
|
+
#startRecovery(tabId) {
|
|
1851
|
+
if (this.#reconnectTasks.has(tabId))
|
|
1852
|
+
return;
|
|
1853
|
+
const recovery = this.#recoverTab(tabId).catch(async (error) => {
|
|
1854
|
+
this.#recordFailure(error);
|
|
1855
|
+
if (this.#state === "active")
|
|
1856
|
+
await this.stop();
|
|
1857
|
+
});
|
|
1858
|
+
this.#reconnectTasks.set(tabId, recovery);
|
|
1859
|
+
void recovery.finally(() => this.#reconnectTasks.delete(tabId)).catch(() => { this.#state = "failed"; });
|
|
1860
|
+
}
|
|
1861
|
+
#retryActiveTab() {
|
|
1862
|
+
const tabId = this.#activeTabId;
|
|
1863
|
+
if (tabId === undefined || this.#tabs.get(tabId)?.snapshot.state !== "interrupted")
|
|
1864
|
+
return;
|
|
1865
|
+
this.#startRecovery(tabId);
|
|
1866
|
+
}
|
|
1867
|
+
/**
|
|
1868
|
+
* Ctrl+] w: ask for the terminal's one writing seat. The seat itself moves
|
|
1869
|
+
* only when the server's writer_epoch notice arrives; until then the tab
|
|
1870
|
+
* keeps observing, and the notice line says which of the two is true.
|
|
1871
|
+
*/
|
|
1872
|
+
#takeWriterActiveTab() {
|
|
1873
|
+
const tabId = this.#activeTabId;
|
|
1874
|
+
const runtime = this.#runtime;
|
|
1875
|
+
if (tabId === undefined || runtime === undefined)
|
|
1876
|
+
return;
|
|
1877
|
+
const tab = this.#tabs.get(tabId);
|
|
1878
|
+
if (tab === undefined || tab.snapshot.state !== "active")
|
|
1879
|
+
return;
|
|
1880
|
+
// New action feedback replaces help; the user can reopen help afterward.
|
|
1881
|
+
this.#helpVisible = false;
|
|
1882
|
+
if (tab.snapshot.accessMode === "writer") {
|
|
1883
|
+
this.#seatNotice = "You already hold this terminal's writer seat.";
|
|
1884
|
+
void this.#render().catch(() => undefined);
|
|
1885
|
+
return;
|
|
1886
|
+
}
|
|
1887
|
+
const refusal = writerCapabilityRefusal(tab.snapshot);
|
|
1888
|
+
if (refusal !== undefined) {
|
|
1889
|
+
this.#seatNotice = refusal;
|
|
1890
|
+
void this.#render().catch(() => undefined);
|
|
1891
|
+
return;
|
|
1892
|
+
}
|
|
1893
|
+
this.#seatNotice = writerCapabilityNeedsRefresh(tab.snapshot) ? "Checking control…" : "Taking control…";
|
|
1894
|
+
void this.#render().catch(() => undefined);
|
|
1895
|
+
void runtime.takeWriter({ tabId, signal: this.#lifetimeAbort.signal }).then(() => { this.#seatNotice = undefined; }, (error) => {
|
|
1896
|
+
this.#seatNotice = writerTransferFailureNotice(error);
|
|
1897
|
+
this.#helpVisible = false;
|
|
1898
|
+
}).finally(() => { void this.#render().catch(() => undefined); });
|
|
1899
|
+
}
|
|
1900
|
+
/**
|
|
1901
|
+
* A seat notice describes one moment: a refused keystroke, a pending or
|
|
1902
|
+
* refused take-control. It must not outlive the seat it described. Forget
|
|
1903
|
+
* it when the published seat (mode or reason) changes; keep it across a
|
|
1904
|
+
* publish that changes nothing about the seat, such as a heartbeat, so a
|
|
1905
|
+
* "Could not take control" line is still readable.
|
|
1906
|
+
*/
|
|
1907
|
+
#forgetSeatNoticeOnSeatChange(before, after) {
|
|
1908
|
+
if (this.#seatNotice === undefined)
|
|
1909
|
+
return;
|
|
1910
|
+
if (before.accessMode !== after.accessMode || before.reason !== after.reason ||
|
|
1911
|
+
before.writerTransferCapability?.supported !== after.writerTransferCapability?.supported ||
|
|
1912
|
+
before.writerTransferCapability?.reasonCode !== after.writerTransferCapability?.reasonCode)
|
|
1913
|
+
this.#seatNotice = undefined;
|
|
1914
|
+
}
|
|
1915
|
+
#seatNoticeFor(snapshot) {
|
|
1916
|
+
const historical = snapshot?.historicalInputUncertainty === true ? HISTORICAL_INPUT_NOTICE : undefined;
|
|
1917
|
+
const withHistory = (notice) => historical === undefined ? notice :
|
|
1918
|
+
notice === undefined ? historical : `${historical} · ${notice}`;
|
|
1919
|
+
if (this.#seatNotice !== undefined)
|
|
1920
|
+
return withHistory(this.#seatNotice);
|
|
1921
|
+
if (snapshot === undefined || snapshot.state !== "active" || snapshot.accessMode !== "observer")
|
|
1922
|
+
return historical;
|
|
1923
|
+
const refusal = writerCapabilityRefusal(snapshot);
|
|
1924
|
+
if (refusal !== undefined)
|
|
1925
|
+
return withHistory(refusal);
|
|
1926
|
+
if (writerCapabilityNeedsRefresh(snapshot))
|
|
1927
|
+
return withHistory("Observing (read-only) · Press Ctrl+] then w to recheck control");
|
|
1928
|
+
return withHistory(snapshot.reason === "writer_transferred"
|
|
1929
|
+
? "Control moved to another client · Press Ctrl+] then w to take it back"
|
|
1930
|
+
: snapshot.geometry == null
|
|
1931
|
+
? "Observing (read-only) · geometry unknown · Ctrl+] then w: control"
|
|
1932
|
+
: "Observing (read-only) · Press Ctrl+] then w to take control");
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
function validateIntents(intents) {
|
|
1936
|
+
admitForegroundSessionIds(intents.map((intent) => intent.agentSessionId));
|
|
1937
|
+
if (new Set(intents.map((intent) => intent.tabId)).size !== intents.length)
|
|
1938
|
+
throw new RangeError("Foreground tab IDs must be unique.");
|
|
1939
|
+
for (const intent of intents) {
|
|
1940
|
+
if (!/^[A-Za-z0-9._:-]{1,256}$/u.test(intent.tabId)) {
|
|
1941
|
+
throw new RangeError("Foreground tab IDs must use the local identifier grammar.");
|
|
1942
|
+
}
|
|
1943
|
+
if (intent.label.length < 1 || intent.label.length > 64)
|
|
1944
|
+
throw new RangeError("Foreground tab labels must contain 1 through 64 characters.");
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
export function admitForegroundSessionIds(sessionIds) {
|
|
1948
|
+
if (sessionIds.length < 1 || sessionIds.length > 4) {
|
|
1949
|
+
throw new RangeError("Foreground mode supports one through four active AgentSessions.");
|
|
1950
|
+
}
|
|
1951
|
+
if (new Set(sessionIds).size !== sessionIds.length) {
|
|
1952
|
+
throw new RangeError("Each foreground tab must bind a distinct AgentSession.");
|
|
1953
|
+
}
|
|
1954
|
+
for (const sessionId of sessionIds) {
|
|
1955
|
+
assertCanonicalUuid(sessionId, "AgentSession ID");
|
|
1956
|
+
}
|
|
1957
|
+
return Object.freeze([...sessionIds]);
|
|
1958
|
+
}
|
|
1959
|
+
export function admitForegroundDimensions(input) {
|
|
1960
|
+
if (!Number.isSafeInteger(input.columns) || !Number.isSafeInteger(input.rows) || input.columns < 20 || input.rows < 3 || input.columns > 1_000 || input.rows > 1_000) {
|
|
1961
|
+
throw new RangeError("The foreground host terminal dimensions are outside supported bounds.");
|
|
1962
|
+
}
|
|
1963
|
+
return Object.freeze({ columns: input.columns, rows: input.rows });
|
|
1964
|
+
}
|
|
1965
|
+
function sameBytes(left, right) {
|
|
1966
|
+
if (left.byteLength !== right.byteLength)
|
|
1967
|
+
return false;
|
|
1968
|
+
for (let index = 0; index < left.byteLength; index += 1) {
|
|
1969
|
+
if (left[index] !== right[index])
|
|
1970
|
+
return false;
|
|
1971
|
+
}
|
|
1972
|
+
return true;
|
|
1973
|
+
}
|
|
1974
|
+
function remoteRows(hostRows) {
|
|
1975
|
+
return Math.max(1, hostRows - (hostRows >= 5 ? 2 : 1));
|
|
1976
|
+
}
|
|
1977
|
+
function unknownAppbar(now) {
|
|
1978
|
+
return buildAppbarModel({
|
|
1979
|
+
now,
|
|
1980
|
+
machineLifecycle: [],
|
|
1981
|
+
agentSessionLifecycle: [],
|
|
1982
|
+
attachment: [],
|
|
1983
|
+
providerAuthentication: [],
|
|
1984
|
+
workspaceSync: [],
|
|
1985
|
+
});
|
|
1986
|
+
}
|
|
1987
|
+
function runtimeAppbar(now, snapshot, intent) {
|
|
1988
|
+
if (snapshot === undefined)
|
|
1989
|
+
return unknownAppbar(now);
|
|
1990
|
+
const evidence = {
|
|
1991
|
+
source: "foreground_terminal_runtime",
|
|
1992
|
+
observedAt: snapshot.heartbeatObservedAt,
|
|
1993
|
+
expiresAt: snapshot.heartbeatExpiresAt,
|
|
1994
|
+
correlationId: snapshot.viewId,
|
|
1995
|
+
};
|
|
1996
|
+
return buildAppbarModel({
|
|
1997
|
+
now,
|
|
1998
|
+
machineLifecycle: [],
|
|
1999
|
+
// A terminal heartbeat proves attachment health, not the supervisor-owned
|
|
2000
|
+
// AgentSession lifecycle. Only independently supplied supervisor evidence
|
|
2001
|
+
// may populate the session projection.
|
|
2002
|
+
agentSessionLifecycle: intent?.agentSessionLifecycle === undefined
|
|
2003
|
+
? []
|
|
2004
|
+
: [intent.agentSessionLifecycle],
|
|
2005
|
+
attachment: [{ ...evidence, value: snapshot.state === "active" ? "attached" : snapshot.state }],
|
|
2006
|
+
providerAuthentication: intent?.providerAuthentication === undefined
|
|
2007
|
+
? []
|
|
2008
|
+
: [intent.providerAuthentication],
|
|
2009
|
+
workspaceSync: [],
|
|
2010
|
+
});
|
|
2011
|
+
}
|
|
2012
|
+
function advanceSequence(sequence, byte, matched) {
|
|
2013
|
+
if (byte === sequence[matched])
|
|
2014
|
+
return matched + 1;
|
|
2015
|
+
return byte === sequence[0] ? 1 : 0;
|
|
2016
|
+
}
|
|
2017
|
+
function sameSnapshotBinding(snapshot, binding) {
|
|
2018
|
+
return snapshot.userId === binding.userId &&
|
|
2019
|
+
snapshot.machineId === binding.machineId &&
|
|
2020
|
+
snapshot.agentSessionId === binding.agentSessionId &&
|
|
2021
|
+
snapshot.processEpoch === binding.processEpoch &&
|
|
2022
|
+
snapshot.fencingGeneration === binding.fencingGeneration;
|
|
2023
|
+
}
|
|
2024
|
+
function padTrustedLine(value, columns) {
|
|
2025
|
+
return value.slice(0, columns).padEnd(columns, " ");
|
|
2026
|
+
}
|
|
2027
|
+
function providerName(provider) {
|
|
2028
|
+
if (provider === "claude-code")
|
|
2029
|
+
return "Claude Code";
|
|
2030
|
+
return "Codex";
|
|
2031
|
+
}
|
|
2032
|
+
async function raceAbort(operation, signal) {
|
|
2033
|
+
if (signal.aborted)
|
|
2034
|
+
throw signal.reason instanceof Error ? signal.reason : new Error("Terminal output was cancelled.");
|
|
2035
|
+
let onAbort;
|
|
2036
|
+
try {
|
|
2037
|
+
return await Promise.race([
|
|
2038
|
+
operation,
|
|
2039
|
+
new Promise((_resolve, reject) => {
|
|
2040
|
+
onAbort = () => reject(signal.reason instanceof Error ? signal.reason : new Error("Terminal output was cancelled."));
|
|
2041
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2042
|
+
}),
|
|
2043
|
+
]);
|
|
2044
|
+
}
|
|
2045
|
+
finally {
|
|
2046
|
+
if (onAbort !== undefined)
|
|
2047
|
+
signal.removeEventListener("abort", onAbort);
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
async function abortableDelay(milliseconds, signal) {
|
|
2051
|
+
if (signal.aborted)
|
|
2052
|
+
return;
|
|
2053
|
+
await new Promise((resolve) => {
|
|
2054
|
+
// Awaited by reconnect recovery: the delay must be able to elapse even when
|
|
2055
|
+
// no other handle keeps the event loop alive (Linux exits early otherwise).
|
|
2056
|
+
const timer = setTimeout(done, milliseconds);
|
|
2057
|
+
const onAbort = () => done();
|
|
2058
|
+
function done() {
|
|
2059
|
+
clearTimeout(timer);
|
|
2060
|
+
signal.removeEventListener("abort", onAbort);
|
|
2061
|
+
resolve();
|
|
2062
|
+
}
|
|
2063
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2064
|
+
});
|
|
2065
|
+
}
|
|
2066
|
+
function writerCapabilityNeedsRefresh(snapshot) {
|
|
2067
|
+
const capability = snapshot.writerTransferCapability;
|
|
2068
|
+
return capability !== undefined && Number.isFinite(capability.expiresAt) && capability.expiresAt <= Date.now() &&
|
|
2069
|
+
(capability.supported || capability.reasonCode === "capability_snapshot_expired");
|
|
2070
|
+
}
|
|
2071
|
+
function writerCapabilityRefusal(snapshot) {
|
|
2072
|
+
const capability = snapshot.writerTransferCapability;
|
|
2073
|
+
// This enables only the refresh action. Runtime.takeWriter discovers and
|
|
2074
|
+
// admits fresh, scoped evidence before it can dispatch a transfer request.
|
|
2075
|
+
if (writerCapabilityNeedsRefresh(snapshot))
|
|
2076
|
+
return undefined;
|
|
2077
|
+
if (capability?.supported === true && capability.expiresAt > Date.now())
|
|
2078
|
+
return undefined;
|
|
2079
|
+
return `Control unavailable: ${capability?.reasonCode ?? (capability?.supported ? "capability_snapshot_expired" : "capability_unknown")}`;
|
|
2080
|
+
}
|
|
2081
|
+
function writerTransferFailureNotice(error) {
|
|
2082
|
+
if (error instanceof RuntimeBoundaryError && error.code.startsWith("capability_")) {
|
|
2083
|
+
return `Control unavailable: ${String(error.safeDetails?.reason_code ?? error.code)}`;
|
|
2084
|
+
}
|
|
2085
|
+
const reason = typeof error === "object" && error !== null
|
|
2086
|
+
? error.details?.reason : undefined;
|
|
2087
|
+
if (reason === "terminal_writer_cancelled")
|
|
2088
|
+
return "Control transfer cancelled. Read the terminal state before trying again.";
|
|
2089
|
+
if (reason === "terminal_writer_transfer_in_progress")
|
|
2090
|
+
return "Control transfer is pending. Keep observing; retry checks the same request.";
|
|
2091
|
+
if (reason === "terminal_writer_operation_mismatch")
|
|
2092
|
+
return "Control request does not match its operation. Read the terminal state before trying again.";
|
|
2093
|
+
if (reason === "terminal_writer_outcome_unknown")
|
|
2094
|
+
return "Control outcome is unconfirmed. Keep observing; retry checks the same request.";
|
|
2095
|
+
return `Could not take control: ${error instanceof Error ? error.message : String(error)}`;
|
|
2096
|
+
}
|
|
2097
|
+
//# sourceMappingURL=foreground.js.map
|