diff --git a/src/main/ipc/pty/ipc/codex-shared-server.ts b/src/main/ipc/pty/ipc/codex-shared-server.ts index 41cde5b4df2..84bb7c419cc 100644 --- a/src/main/ipc/pty/ipc/codex-shared-server.ts +++ b/src/main/ipc/pty/ipc/codex-shared-server.ts @@ -1,3 +1,4 @@ +import { isRemoteRuntimePtyId } from '../../../../shared/remote-runtime-pty-id' import { getPtyIpc } from '../../pty-host-bindings' import { parseAppSshPtyId } from '../../../providers/ssh-pty-id' import { @@ -34,7 +35,7 @@ async function findLocalPaneRootPid(deps: Deps, id: unknown): Promise 512 || - value.startsWith('remote:') || + isRemoteRuntimePtyId(value) || parseAppSshPtyId(value) ) { return false @@ -144,7 +145,7 @@ export function installPtyInspectIpcHandlers(deps: { ) ipcMain.handle('pty:hasPty', async (_event, args: { id: string }): Promise => { - if (typeof args?.id !== 'string' || args.id.startsWith('remote:')) { + if (typeof args?.id !== 'string' || isRemoteRuntimePtyId(args.id)) { // Why: same routing hazard pty:kill guards against — ptyOwnership never holds // a runtime terminal handle and parseAppSshPtyId ignores it, so the lookup // falls through to the local provider and its "not in my table" reads as an @@ -203,7 +204,7 @@ export function installPtyInspectIpcHandlers(deps: { } ) => { // Why: same routing hazard as pty:hasPty — an unroutable id must read as client-only unverifiable, not as a local-provider answer or a raised IPC error. - if (typeof args?.id !== 'string' || !args.id || args.id.startsWith('remote:')) { + if (typeof args?.id !== 'string' || !args.id || isRemoteRuntimePtyId(args.id)) { return clientOnlyUnverifiableInspection('terminal_gone') } // Why: the pre-swap LocalPtyProvider does not own restored daemon ids, so @@ -309,7 +310,7 @@ export function installPtyInspectIpcHandlers(deps: { async (_event, args: { ptyId?: unknown }): Promise => { if ( typeof args?.ptyId !== 'string' || - !args.ptyId.startsWith('remote:') || + !isRemoteRuntimePtyId(args.ptyId) || args.ptyId.length > 512 ) { return diff --git a/src/main/ipc/pty/ipc/renderer-kill.ts b/src/main/ipc/pty/ipc/renderer-kill.ts index 10a0ffa476e..fd25379c9b4 100644 --- a/src/main/ipc/pty/ipc/renderer-kill.ts +++ b/src/main/ipc/pty/ipc/renderer-kill.ts @@ -1,3 +1,4 @@ +import { isRemoteRuntimePtyId } from '../../../../shared/remote-runtime-pty-id' import { getPtyIpc } from '../../pty-host-bindings' import type { Store } from '../../../persistence' import type { OrcaRuntimeService } from '../../../runtime/orca-runtime' @@ -52,7 +53,7 @@ async function stopRendererOwnedPtyAs( args: { id: string; keepHistory?: boolean }, intentionalStop: TerminalIntentionalStopKind | null ): Promise { - if (typeof args?.id !== 'string' || !args.id || args.id.startsWith('remote:')) { + if (typeof args?.id !== 'string' || !args.id || isRemoteRuntimePtyId(args.id)) { // Why: runtime terminal handles belong to terminal.close; unowned PTY routing could target the local provider. throw new Error('Invalid PTY provider id') } diff --git a/src/main/ipc/pty/runtime/operations.ts b/src/main/ipc/pty/runtime/operations.ts index a815601ee30..a61e1caface 100644 --- a/src/main/ipc/pty/runtime/operations.ts +++ b/src/main/ipc/pty/runtime/operations.ts @@ -1,3 +1,4 @@ +import { isRemoteRuntimePtyId } from '../../../../shared/remote-runtime-pty-id' import type { IPtyProvider } from '../../../providers/types' import { LocalPtyProvider } from '../../../providers/local-pty-provider' import { parseAppSshPtyId } from '../../../providers/ssh-pty-id' @@ -88,7 +89,7 @@ export async function probePtyLivenessFromRuntimeController( try { // Why: no locally routed provider can authoritatively answer for a // remote host's PTY, so remote-scoped ids stay unknown, never absent. - if (ptyId.startsWith('remote:')) { + if (isRemoteRuntimePtyId(ptyId)) { return null } const connectionId = ptyOwnership.get(ptyId) ?? parseAppSshPtyId(ptyId)?.connectionId @@ -235,7 +236,7 @@ export function hasPtyFromRuntimeController( try { // Why: no locally routed provider can authoritatively answer for a // remote host's PTY, so remote-scoped ids stay unknown, never absent. - if (ptyId.startsWith('remote:')) { + if (isRemoteRuntimePtyId(ptyId)) { return null } const connectionId = ptyOwnership.get(ptyId) ?? parseAppSshPtyId(ptyId)?.connectionId diff --git a/src/main/runtime/orca-runtime-write-orchestration-pointer-pty.ts b/src/main/runtime/orca-runtime-write-orchestration-pointer-pty.ts index 244b88f52fd..93e5927d126 100644 --- a/src/main/runtime/orca-runtime-write-orchestration-pointer-pty.ts +++ b/src/main/runtime/orca-runtime-write-orchestration-pointer-pty.ts @@ -1,4 +1,5 @@ // @ts-nocheck -- mechanically split from OrcaRuntimeService; behavior is covered by AST equivalence and characterization tests. +import { isRemoteRuntimePtyId } from '../../shared/remote-runtime-pty-id' import { OrcaRuntimeWithRefreshFloatingWorkspacePtyLiveness } from './orca-runtime-refresh-floating-workspace-pty-liveness' import { writeOrchestrationPointerWithSettlement } from './orchestration/mailbox-pointer-pty-write' import type { WriteSettlement } from '../../shared/pty-write-settlement' @@ -127,7 +128,7 @@ export class OrcaRuntimeWithWriteOrchestrationPointerPty extends OrcaRuntimeWith provenLivePtyIds !== null && leaf.ptyId !== null && !provenLivePtyIds.has(leaf.ptyId) && - !leaf.ptyId.startsWith('remote:') && + !isRemoteRuntimePtyId(leaf.ptyId) && parseAppSshPtyId(leaf.ptyId) === null && this.ptyController?.hasPty?.(leaf.ptyId) !== true return { diff --git a/src/renderer/src/components/terminal-pane/pane-restart-transport-handoff.ts b/src/renderer/src/components/terminal-pane/pane-restart-transport-handoff.ts index 93eea063ed4..dd1b850cd42 100644 --- a/src/renderer/src/components/terminal-pane/pane-restart-transport-handoff.ts +++ b/src/renderer/src/components/terminal-pane/pane-restart-transport-handoff.ts @@ -1,5 +1,5 @@ +import { isRemoteRuntimePtyId } from '../../../../shared/remote-runtime-pty-id' import type { PtyTransport } from './pty-transport-types' -import { isRemoteRuntimePtyId } from './pty-connection/paired-parked-terminal-restore' /** * Releases a pane's transport for a restart and returns the PTY its replacement spawn must name. diff --git a/src/renderer/src/components/terminal-pane/pty-connection/apply-reattach-payload.ts b/src/renderer/src/components/terminal-pane/pty-connection/apply-reattach-payload.ts index 3f33132074d..c8f215d8310 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/apply-reattach-payload.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/apply-reattach-payload.ts @@ -1,3 +1,4 @@ +import { isRemoteRuntimePtyId } from '../../../../../shared/remote-runtime-pty-id' import { waitForTerminalReplayWritesParsed } from '../replay-guard' import { POST_REPLAY_MODE_RESET, @@ -12,7 +13,6 @@ import { shouldSkipAltFrameForWidthMismatch } from '../terminal-snapshot-replay-paint' -import { isRemoteRuntimePtyId } from './paired-parked-terminal-restore' import { restoredSnapshotPaintsPrintableContent } from '../restored-snapshot-coverage' import { resolveSshReconnectModelPaint } from './resolve-ssh-reconnect-model-paint' import { fitReattachedPaneToGrid, noteReattachAltFrameSkip } from './reattach-grid-fit' diff --git a/src/renderer/src/components/terminal-pane/pty-connection/deferred-session-attach.ts b/src/renderer/src/components/terminal-pane/pty-connection/deferred-session-attach.ts index 726744b7729..e8a37774de7 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/deferred-session-attach.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/deferred-session-attach.ts @@ -1,9 +1,9 @@ +import { isRemoteRuntimePtyId } from '../../../../../shared/remote-runtime-pty-id' import { useAppStore } from '@/store' import { isRuntimeOwnedSshTargetId } from '../../../../../shared/execution-host' import { resolveSshPaneConnectGate } from '../ssh-pane-connect-gate' import { waitForUserInitiatedSshConnect, waitForSshConnection } from './ssh-session-connect' -import { isRemoteRuntimePtyId } from './paired-parked-terminal-restore' import { isSshSessionGoneError } from './pty-connect-limits' import { toProcessExitStartup } from './process-exit-startup' diff --git a/src/renderer/src/components/terminal-pane/pty-connection/deferred-session-reattach-choice.ts b/src/renderer/src/components/terminal-pane/pty-connection/deferred-session-reattach-choice.ts index 7ee801dddfb..ab11a81ea2c 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/deferred-session-reattach-choice.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/deferred-session-reattach-choice.ts @@ -1,3 +1,4 @@ +import { isRemoteRuntimePtyId } from '../../../../../shared/remote-runtime-pty-id' import { scheduleRuntimeGraphSync } from '@/runtime/sync-runtime-graph' import { useAppStore } from '@/store' import { isWebTerminalSurfaceTabId } from '@/runtime/web-terminal-surface-id' @@ -9,7 +10,6 @@ import { recordPtyConnectDiagnostic } from './pty-connect-limits' import { - isRemoteRuntimePtyId, canRestorePairedParkedTerminal, isSessionOwnedByWorktree } from './paired-parked-terminal-restore' diff --git a/src/renderer/src/components/terminal-pane/pty-connection/deferred-session-reattach-connect.ts b/src/renderer/src/components/terminal-pane/pty-connection/deferred-session-reattach-connect.ts index 1b1cdb92e0c..356a2c8c583 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/deferred-session-reattach-connect.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/deferred-session-reattach-connect.ts @@ -1,6 +1,6 @@ +import { isRemoteRuntimePtyId } from '../../../../../shared/remote-runtime-pty-id' import { warnTerminalLifecycleAnomaly } from '../terminal-lifecycle-diagnostics' import { isSshSessionGoneError, recordPtyConnectDiagnostic } from './pty-connect-limits' -import { isRemoteRuntimePtyId } from './paired-parked-terminal-restore' import { toProcessExitStartup } from './process-exit-startup' import { recoverUnverifiableDirectSshReattach } from './direct-ssh-reattach-recovery' import type { ConnectPanePtySession } from './connect-pane-pty-session' diff --git a/src/renderer/src/components/terminal-pane/pty-connection/direct-ssh-retry-status.ts b/src/renderer/src/components/terminal-pane/pty-connection/direct-ssh-retry-status.ts index a494d08fc80..ab777b16df3 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/direct-ssh-retry-status.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/direct-ssh-retry-status.ts @@ -1,3 +1,4 @@ +import { isRemoteRuntimePtyId } from '../../../../../shared/remote-runtime-pty-id' import { reportWorkerTerminalUserInput } from '@/lib/worker-terminal-takeover-report' import { useAppStore } from '@/store' import { getLocalProjectExecutionRuntimeContext } from '@/lib/local-preflight-context' @@ -28,7 +29,6 @@ import { } from '../renderer-owned-agent-status-registry' import { DIRECT_SSH_PANE_RETRY_SETTLEMENT_TIMEOUT_MS } from './pty-connect-limits' -import { isRemoteRuntimePtyId } from './paired-parked-terminal-restore' import { resolveLatestAgentDoneStartedAt } from './agent-done-started-at' import { rendererAgentStatusObservations } from '@/lib/renderer-agent-status-observations' diff --git a/src/renderer/src/components/terminal-pane/pty-connection/empty-reattach-retire-evidence.ts b/src/renderer/src/components/terminal-pane/pty-connection/empty-reattach-retire-evidence.ts index eaa7638df6f..c19b5e5f09e 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/empty-reattach-retire-evidence.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/empty-reattach-retire-evidence.ts @@ -1,6 +1,6 @@ +import { isRemoteRuntimePtyId } from '../../../../../shared/remote-runtime-pty-id' import type { SleepingAgentSessionRecord } from '../../../../../shared/agent-session-resume' import type { ColdRestoreAgentResumeStartup } from './fresh-spawn-types' -import { isRemoteRuntimePtyId } from './paired-parked-terminal-restore' // Why not activationTreatsNoteAsFinished: a live+done note is a finished turn's idle anchor for a running pane, not sleep. export function isHibernationDoneRecord(record: SleepingAgentSessionRecord): boolean { diff --git a/src/renderer/src/components/terminal-pane/pty-connection/foreground-output-refresh.ts b/src/renderer/src/components/terminal-pane/pty-connection/foreground-output-refresh.ts index 9c1b45051be..fb4acf69654 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/foreground-output-refresh.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/foreground-output-refresh.ts @@ -1,3 +1,4 @@ +import { isRemoteRuntimePtyId } from '../../../../../shared/remote-runtime-pty-id' import { recordTerminalFreezeBreadcrumb } from '../terminal-freeze-breadcrumbs' import { redactPtyIdForDiagnostics } from '../../../../../shared/pty-delivery-diagnostics' import { @@ -26,7 +27,6 @@ import { shouldWritePtyOutputForeground, consumeInactiveForegroundImmediateBudget } from './foreground-output-scan' -import { isRemoteRuntimePtyId } from './paired-parked-terminal-restore' import type { ConnectPanePtySession } from './connect-pane-pty-session' diff --git a/src/renderer/src/components/terminal-pane/pty-connection/fresh-spawn-start.ts b/src/renderer/src/components/terminal-pane/pty-connection/fresh-spawn-start.ts index 7a238ef95df..ef85357e771 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/fresh-spawn-start.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/fresh-spawn-start.ts @@ -1,3 +1,4 @@ +import { isRemoteRuntimePtyId } from '../../../../../shared/remote-runtime-pty-id' import { useAppStore } from '@/store' import { PROCESS_BOUNDARY_GROUND } from '../../../../../shared/terminal-mode-reset-profiles' import { hasPtySerializer } from '../pty-buffer-serializer' @@ -7,7 +8,6 @@ import { settleSpawnThatLeftPaneUnbound } from './unbound-pane-spawn-recovery' import { STARTUP_CWD_FALLBACK_NOTICE } from './startup-cwd-fallback-notice' import { pendingSpawnByPaneKey, pendingSpawnGenerationByPaneKey } from './pty-connect-limits' import { shouldWritePtyOutputForeground } from './foreground-output-scan' -import { isRemoteRuntimePtyId } from './paired-parked-terminal-restore' import { toProcessExitStartup } from './process-exit-startup' import type { PendingStartupCommand, diff --git a/src/renderer/src/components/terminal-pane/pty-connection/hidden-output-restore-drain.ts b/src/renderer/src/components/terminal-pane/pty-connection/hidden-output-restore-drain.ts index ea4c48f43f2..b5b5c4e9992 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/hidden-output-restore-drain.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/hidden-output-restore-drain.ts @@ -1,3 +1,4 @@ +import { isRemoteRuntimePtyId } from '../../../../../shared/remote-runtime-pty-id' import { recordTerminalFreezeBreadcrumb } from '../terminal-freeze-breadcrumbs' import { redactPtyIdForDiagnostics } from '../../../../../shared/pty-delivery-diagnostics' import { cancelScheduledHiddenOutputRestore } from '../hidden-output-restore-scheduler' @@ -8,7 +9,6 @@ import { HIDDEN_OUTPUT_RESTORE_REMOTE_REARM_MAX } from './hidden-output-restore-limits' import { shouldWritePtyOutputForeground } from './foreground-output-scan' -import { isRemoteRuntimePtyId } from './paired-parked-terminal-restore' import type { ConnectPanePtySession } from './connect-pane-pty-session' diff --git a/src/renderer/src/components/terminal-pane/pty-connection/hidden-output-restore-snapshot.ts b/src/renderer/src/components/terminal-pane/pty-connection/hidden-output-restore-snapshot.ts index a64209a71cd..4db74de58b6 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/hidden-output-restore-snapshot.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/hidden-output-restore-snapshot.ts @@ -1,3 +1,4 @@ +import { isRemoteRuntimePtyId } from '../../../../../shared/remote-runtime-pty-id' import { safeFit, safeFitAndThen } from '@/lib/pane-manager/pane-tree-ops' import { getFitOverrideForPty } from '@/lib/pane-manager/mobile-fit-overrides' import { waitForTerminalReplayWritesParsed } from '../replay-guard' @@ -24,7 +25,6 @@ import { import { HIDDEN_OUTPUT_RESTORE_UNAVAILABLE_WARNING } from './hidden-output-restore-limits' import { shouldWritePtyOutputForeground } from './foreground-output-scan' -import { isRemoteRuntimePtyId } from './paired-parked-terminal-restore' import { restoredSnapshotPaintsPrintableContent } from '../restored-snapshot-coverage' import { recordTerminalFreezeBreadcrumb } from '../terminal-freeze-breadcrumbs' diff --git a/src/renderer/src/components/terminal-pane/pty-connection/hidden-output-snapshot-serialize.ts b/src/renderer/src/components/terminal-pane/pty-connection/hidden-output-snapshot-serialize.ts index e44c7a1661b..7f880916de7 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/hidden-output-snapshot-serialize.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/hidden-output-snapshot-serialize.ts @@ -1,9 +1,9 @@ +import { isRemoteRuntimePtyId } from '../../../../../shared/remote-runtime-pty-id' import { isHostAnsweredSnapshotRetryCause } from '@/runtime/remote-runtime-terminal-multiplexer' import { onTerminalScrollIntentFollowOutput } from '@/lib/pane-manager/terminal-scroll-intent' import { shouldWritePtyOutputForeground } from './foreground-output-scan' import { readE2eHiddenSnapshotOverride } from './e2e-terminal-pty-harness' -import { isRemoteRuntimePtyId } from './paired-parked-terminal-restore' import type { PtyBufferSnapshot } from '../pty-transport' import type { ConnectPanePtySession } from './connect-pane-pty-session' diff --git a/src/renderer/src/components/terminal-pane/pty-connection/paired-parked-terminal-restore.ts b/src/renderer/src/components/terminal-pane/pty-connection/paired-parked-terminal-restore.ts index db3e52b6179..7a38a6a08c3 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/paired-parked-terminal-restore.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/paired-parked-terminal-restore.ts @@ -1,16 +1,11 @@ import { useAppStore } from '@/store' import { TERMINAL_PAIRED_PARKING_RUNTIME_CAPABILITY } from '../../../../../shared/protocol-version' import { getRemoteRuntimePtyEnvironmentId } from '@/runtime/runtime-terminal-stream' -import { REMOTE_PTY_ID_PREFIX } from './pty-connect-limits' import { isRuntimeHostContactRevoked, lastVerifiedRuntimeStatus } from '../../../../../shared/runtime-host-status' -export function isRemoteRuntimePtyId(ptyId: string | null | undefined): boolean { - return typeof ptyId === 'string' && ptyId.startsWith(REMOTE_PTY_ID_PREFIX) -} - export function canRestorePairedParkedTerminal(ptyId: string): boolean { const environmentId = getRemoteRuntimePtyEnvironmentId(ptyId) if (environmentId === null) { diff --git a/src/renderer/src/components/terminal-pane/pty-connection/pane-pty-visibility-bind.ts b/src/renderer/src/components/terminal-pane/pty-connection/pane-pty-visibility-bind.ts index 1e3b8194a28..6442345755f 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/pane-pty-visibility-bind.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/pane-pty-visibility-bind.ts @@ -1,3 +1,4 @@ +import { isRemoteRuntimePtyId } from '../../../../../shared/remote-runtime-pty-id' import { captureNotificationTransportOwner } from '@/attention/notification-subject-owner' import { scheduleRuntimeGraphSync } from '@/runtime/sync-runtime-graph' import { useAppStore } from '@/store' @@ -12,7 +13,6 @@ import { isAgentTaskCompleteNotificationEnabled, isAgentTaskCompleteTrackingEnabled } from './agent-task-complete-settings' -import { isRemoteRuntimePtyId } from './paired-parked-terminal-restore' import { shouldIgnoreStalePanePtyLayoutBinding } from './pane-pty-layout-binding' import type { ConnectPanePtySession } from './connect-pane-pty-session' diff --git a/src/renderer/src/components/terminal-pane/pty-connection/pane-serializer-settle.ts b/src/renderer/src/components/terminal-pane/pty-connection/pane-serializer-settle.ts index 843350a6ec6..a9eec4111a3 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/pane-serializer-settle.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/pane-serializer-settle.ts @@ -1,3 +1,4 @@ +import { isRemoteRuntimePtyId } from '../../../../../shared/remote-runtime-pty-id' import { useAppStore } from '@/store' import type { SessionRestoredBannerReason } from '../session-restored-banner-pane-state' import { hasPtySerializer } from '../pty-buffer-serializer' @@ -12,7 +13,6 @@ import { writeTerminalPastePtyInput } from '../terminal-pty-paste-writer' import { createPasteReadinessTimeoutNotice } from '@/lib/launch-agent-paste-timeout-notice' import { STARTUP_DRAFT_PASTE_QUIET_MS } from './pty-connect-limits' -import { isRemoteRuntimePtyId } from './paired-parked-terminal-restore' import type { ConnectPanePtySession } from './connect-pane-pty-session' diff --git a/src/renderer/src/components/terminal-pane/pty-connection/park-reveal-snapshot-verdict.ts b/src/renderer/src/components/terminal-pane/pty-connection/park-reveal-snapshot-verdict.ts index 5bfa786d3bf..c6aa5ea7455 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/park-reveal-snapshot-verdict.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/park-reveal-snapshot-verdict.ts @@ -1,3 +1,4 @@ +import { isRemoteRuntimePtyId } from '../../../../../shared/remote-runtime-pty-id' import { RESET_GRAPHIC_RENDITION } from '../../../../../shared/terminal-mode-reset-profiles' import { redactPtyIdForDiagnostics } from '../../../../../shared/pty-delivery-diagnostics' import { writeTerminalOutput } from '@/lib/pane-manager/pane-terminal-output-scheduler' @@ -7,7 +8,6 @@ import type { PtyBufferSnapshot } from '../pty-transport' import type { ConnectPanePtySession } from './connect-pane-pty-session' import { PARK_REVEAL_NO_HOST_IMAGE_WARNING } from './hidden-output-restore-limits' import type { HiddenOutputSnapshotResult } from './hidden-output-snapshot-serialize' -import { isRemoteRuntimePtyId } from './paired-parked-terminal-restore' /** Which restore budget an unverifiable probe is charged to; null when the loop keeps its own count (legacy hosts). */ export type ParkRevealRetryLedger = 'host' | 'local' | null diff --git a/src/renderer/src/components/terminal-pane/pty-connection/pty-connect-limits.ts b/src/renderer/src/components/terminal-pane/pty-connection/pty-connect-limits.ts index f100c374395..bb2a62f1ae5 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/pty-connect-limits.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/pty-connect-limits.ts @@ -24,7 +24,6 @@ export function isSshSessionGoneError(error: unknown): boolean { } // Why: relay requests expire at 30s; leave one second for their fallback before re-arming locally. export const DIRECT_SSH_PANE_RETRY_SETTLEMENT_TIMEOUT_MS = 31_000 -export const REMOTE_PTY_ID_PREFIX = 'remote:' export const PTY_CONNECT_DIAG_LIMIT = 200 export const MANUAL_AGENT_COMMAND_MAX_CHARS = 4096 export const STARTUP_DRAFT_PASTE_QUIET_MS = 1500 diff --git a/src/renderer/src/components/terminal-pane/pty-connection/pty-input-forward.ts b/src/renderer/src/components/terminal-pane/pty-connection/pty-input-forward.ts index cbaf010940c..e58bd64bde8 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/pty-input-forward.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/pty-input-forward.ts @@ -1,3 +1,4 @@ +import { isRemoteRuntimePtyId } from '../../../../../shared/remote-runtime-pty-id' import type { ManagedPaneInternal } from '@/lib/pane-manager/pane-manager-types' import { subscribeToTerminalInputData } from '../terminal-user-input-signal' import { installTerminalImeCompositionRoute } from '../terminal-ime-composition-route' @@ -20,7 +21,6 @@ import { import { FOREGROUND_GRID_DRIFT_CHECK_MIN_MS } from './foreground-output-budgets' import { TERMINAL_FOCUS_IN_SEQUENCE, TERMINAL_FOCUS_OUT_SEQUENCE } from './foreground-output-scan' -import { isRemoteRuntimePtyId } from './paired-parked-terminal-restore' import { isCodexPaneStale } from './codex-pane-stale' import { installTerminalSelectionFitGuard } from '../terminal-selection-fit-guard' import { initializePaneGeometry, readPaneSize } from './read-pane-size' diff --git a/src/renderer/src/components/terminal-pane/pty-connection/pty-input-recovery.ts b/src/renderer/src/components/terminal-pane/pty-connection/pty-input-recovery.ts index 37faa8d2cea..74b1ac10cbc 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/pty-input-recovery.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/pty-input-recovery.ts @@ -1,3 +1,4 @@ +import { isRemoteRuntimePtyId } from '../../../../../shared/remote-runtime-pty-id' import { useAppStore } from '@/store' import { createIpcPtyTransport } from '../pty-transport' import { createRemoteRuntimePtyTransport } from '../remote-runtime-pty-transport' @@ -19,7 +20,6 @@ import { installTerminalCapabilityReplyHandlers } from '../terminal-capability-replies' -import { isRemoteRuntimePtyId } from './paired-parked-terminal-restore' import { TRANSPORT_CONNECT_SETTLE_GRACE_MS } from './pty-connect-limits' import { buildPaneTransportOptions } from './pane-transport-options' import { buffersInputOnlyForSshReattach } from './ssh-reattach-input-buffering' diff --git a/src/renderer/src/components/terminal-pane/pty-connection/pty-resize-geometry.ts b/src/renderer/src/components/terminal-pane/pty-connection/pty-resize-geometry.ts index 62dcb4b7a83..61a47c777e8 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/pty-resize-geometry.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/pty-resize-geometry.ts @@ -1,3 +1,4 @@ +import { isRemoteRuntimePtyId } from '../../../../../shared/remote-runtime-pty-id' import type { ManagedPaneInternal } from '@/lib/pane-manager/pane-manager-types' import { safeFit } from '@/lib/pane-manager/pane-tree-ops' import { requestStablePaneFit } from '@/lib/pane-manager/pane-fit-resize-observer' @@ -8,7 +9,6 @@ import { shouldClaimRemoteDesktopViewport } from '../remote-desktop-viewport-cla import { deferTerminalGeometryMutationDuringRebuild } from '@/lib/pane-manager/terminal-scroll-intent-rebuild' import { waitForStableStartupGrid } from '../terminal-startup-grid-settle' -import { isRemoteRuntimePtyId } from './paired-parked-terminal-restore' import { isSetupSplitGeometryReady } from './setup-split-geometry' import type { ConnectPanePtySession } from './connect-pane-pty-session' diff --git a/src/renderer/src/components/terminal-pane/pty-connection/reattach-grid-fit.ts b/src/renderer/src/components/terminal-pane/pty-connection/reattach-grid-fit.ts index e2445bb2b85..3b750b61b03 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/reattach-grid-fit.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/reattach-grid-fit.ts @@ -1,7 +1,7 @@ +import { isRemoteRuntimePtyId } from '../../../../../shared/remote-runtime-pty-id' import { safeFitAndThen } from '@/lib/pane-manager/pane-tree-ops' import { getFitOverrideForPty } from '@/lib/pane-manager/mobile-fit-overrides' -import { isRemoteRuntimePtyId } from './paired-parked-terminal-restore' import type { ReattachPayloadContext } from './reattach-payload-context' import type { ReattachPayloadSession } from './reattach-payload-session' diff --git a/src/renderer/src/components/terminal-pane/pty-connection/reattach-result-handler.ts b/src/renderer/src/components/terminal-pane/pty-connection/reattach-result-handler.ts index f141b671952..d4869afeadc 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/reattach-result-handler.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/reattach-result-handler.ts @@ -1,3 +1,4 @@ +import { isRemoteRuntimePtyId } from '../../../../../shared/remote-runtime-pty-id' import { scheduleRuntimeGraphSync } from '@/runtime/sync-runtime-graph' import type { PtyBufferSnapshot, PtyConnectResult } from '../pty-transport' import { warnTerminalLifecycleAnomaly } from '../terminal-lifecycle-diagnostics' @@ -9,7 +10,6 @@ import { parseAppSshPtyId } from '../../../../../shared/ssh-pty-id' import { resolveHiddenRestoreScrollbackRows } from '../terminal-hidden-restore-scrollback' import { shouldIgnoreStalePanePtyLayoutBinding } from './pane-pty-layout-binding' -import { isRemoteRuntimePtyId } from './paired-parked-terminal-restore' import { hasEmptyReattachRetireEvidence } from './empty-reattach-retire-evidence' import type { ColdRestoreAgentResumeStartup } from './fresh-spawn-types' diff --git a/src/renderer/src/components/terminal-pane/pty-connection/run-deferred-connect.ts b/src/renderer/src/components/terminal-pane/pty-connection/run-deferred-connect.ts index b02b9dfcef1..02ee7d4f084 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/run-deferred-connect.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/run-deferred-connect.ts @@ -1,3 +1,4 @@ +import { isRemoteRuntimePtyId } from '../../../../../shared/remote-runtime-pty-id' import { createTerminalZeroDimensionsMessage } from '../../../../../shared/terminal-zero-dimensions-diagnostic' import { isWorktreeRemovalFenceError } from '../../../../../shared/worktree/removal-fence-error' import { safeFit } from '@/lib/pane-manager/pane-tree-ops' @@ -5,7 +6,6 @@ import { useAppStore } from '@/store' import { createCodexBackfillErrorDetector } from '../codex-backfill-error-detector' import { hasWorktreeSleepIntent, onWorktreeSleepIntentCleared } from '@/lib/worktree-sleep-intent' -import { isRemoteRuntimePtyId } from './paired-parked-terminal-restore' import { recordPtyConnectDiagnostic } from './pty-connect-limits' import { findTerminalTabForPane } from './terminal-tab-id' diff --git a/src/renderer/src/components/terminal-pane/pty-connection/session-reconcile-dispose.ts b/src/renderer/src/components/terminal-pane/pty-connection/session-reconcile-dispose.ts index a48a56d9c7a..f7a6b4bee1a 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/session-reconcile-dispose.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/session-reconcile-dispose.ts @@ -10,7 +10,7 @@ import { discardTerminalOutput } from '@/lib/pane-manager/pane-terminal-output-s import { getProviderSessionClaimKey } from '@/lib/sleeping-agent-pane-ownership' import { releaseRendererPtyVisibilityClaim } from '../pty-renderer-delivery-claims' -import { REMOTE_PTY_ID_PREFIX } from './pty-connect-limits' +import { isRemoteRuntimePtyId } from '../../../../../shared/remote-runtime-pty-id' import { SHIFT_ENTER_RECONFIRM_IDLE_MS } from './foreground-output-scan' import type { PanePtyBinding } from './pane-pty-binding' @@ -51,7 +51,7 @@ export function installSessionReconcileDispose(session: ConnectPanePtySession): if ( !requestedPtyId || requestedPtyId === session.handledExitPtyId || - requestedPtyId.startsWith(REMOTE_PTY_ID_PREFIX) || + isRemoteRuntimePtyId(requestedPtyId) || session.transport.getConnectionId?.() != null ) { return diff --git a/src/renderer/src/components/terminal-pane/pty-connection/terminal-keydown-fit.ts b/src/renderer/src/components/terminal-pane/pty-connection/terminal-keydown-fit.ts index 46e944dd99f..99accd330a8 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/terminal-keydown-fit.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/terminal-keydown-fit.ts @@ -1,3 +1,4 @@ +import { isRemoteRuntimePtyId } from '../../../../../shared/remote-runtime-pty-id' import { makePaneKey } from '../../../../../shared/stable-pane-id' import { useAppStore } from '@/store' import { safeFit } from '@/lib/pane-manager/pane-tree-ops' @@ -12,7 +13,6 @@ import { registerTerminalSideEffectFactConsumer } from '../terminal-side-effect- import { isAgentTaskCompleteTrackingEnabled } from './agent-task-complete-settings' import { isAgentProcessInspectionCostly } from '../agent-process-inspection-cost' -import { isRemoteRuntimePtyId } from './paired-parked-terminal-restore' import { isRemoteExecutionHostPtyId } from '../remote-execution-host-pty' import type { ConnectPanePtySession } from './connect-pane-pty-session' diff --git a/src/renderer/src/components/terminal-pane/terminal-ctrl-arrow-conpty.ts b/src/renderer/src/components/terminal-pane/terminal-ctrl-arrow-conpty.ts index d2bc848886b..d59026604b7 100644 --- a/src/renderer/src/components/terminal-pane/terminal-ctrl-arrow-conpty.ts +++ b/src/renderer/src/components/terminal-pane/terminal-ctrl-arrow-conpty.ts @@ -1,5 +1,6 @@ import type { ExecutionHostId } from '../../../../shared/execution-host' import { LOCAL_EXECUTION_HOST_ID } from '../../../../shared/execution-host' +import { isRemoteRuntimePtyId } from '../../../../shared/remote-runtime-pty-id' import { getConnectionIdFromState } from '@/lib/connection-context' import { getExecutionHostIdForWorktree, @@ -9,8 +10,6 @@ import { isLocalNativeWindowsConpty } from '@/lib/pane-manager/windows-pty-compa import type { PaneCwdMap } from './resolve-split-cwd' import type { PtyTransport } from './pty-transport-types' -const REMOTE_RUNTIME_PTY_ID_PREFIX = 'remote:' - type TerminalTabShellState = { tabsByWorktree: Record< string, @@ -39,10 +38,6 @@ type TerminalCtrlArrowConptyArgs = { transport: TerminalCtrlArrowConptyTransport | null } -function isRemoteRuntimePtyId(ptyId: string): boolean { - return ptyId.startsWith(REMOTE_RUNTIME_PTY_ID_PREFIX) -} - export function isLocalWindowsConptyPaneForCtrlArrow({ isWindows, userAgent, @@ -59,7 +54,7 @@ export function isLocalWindowsConptyPaneForCtrlArrow({ } const ptyId = transport?.getPtyId() ?? null - if (ptyId !== null && isRemoteRuntimePtyId(ptyId)) { + if (isRemoteRuntimePtyId(ptyId)) { return false } diff --git a/src/renderer/src/components/terminal-pane/terminal-dead-session-reconcile.ts b/src/renderer/src/components/terminal-pane/terminal-dead-session-reconcile.ts index 7c9fe8a23fc..3b2e8bc527f 100644 --- a/src/renderer/src/components/terminal-pane/terminal-dead-session-reconcile.ts +++ b/src/renderer/src/components/terminal-pane/terminal-dead-session-reconcile.ts @@ -4,7 +4,7 @@ // this routes such panes through the same exit teardown an observed exit runs. // See design-docs/terminal-dead-pane-on-bg-exit.md. -const REMOTE_PTY_ID_PREFIX = 'remote:' +import { isRemoteRuntimePtyId } from '../../../../shared/remote-runtime-pty-id' /** * A pane binding that exposes its bound transport identity plus a reconcile @@ -42,7 +42,7 @@ export function shouldReconcileDeadSession(args: { if (ptyId === null || ptyId === undefined) { return false } - if (ptyId.startsWith(REMOTE_PTY_ID_PREFIX)) { + if (isRemoteRuntimePtyId(ptyId)) { return false } // Why: only local/daemon-backed ids (connectionId null/undefined) are diff --git a/src/renderer/src/components/terminal-pane/terminal-pane-lifecycle-primitives.ts b/src/renderer/src/components/terminal-pane/terminal-pane-lifecycle-primitives.ts index 385c07cf7c0..645a81a64ce 100644 --- a/src/renderer/src/components/terminal-pane/terminal-pane-lifecycle-primitives.ts +++ b/src/renderer/src/components/terminal-pane/terminal-pane-lifecycle-primitives.ts @@ -1,3 +1,4 @@ +import { isRemoteRuntimePtyId } from '../../../../shared/remote-runtime-pty-id' import type { Terminal } from '@xterm/xterm' import type { TerminalLayoutSnapshot, TerminalTab } from '../../../../shared/terminal-tab-types' import type { PtyPaneStartup } from './pty-connection-types' @@ -46,7 +47,7 @@ export function reportActiveRendererPtyForPane( ): void { for (const [paneId, transport] of paneTransports) { const ptyId = transport.getPtyId() - if (!ptyId || ptyId.startsWith('remote:')) { + if (!ptyId || isRemoteRuntimePtyId(ptyId)) { continue } window.api.pty.setActiveRendererPty?.(ptyId, activePaneId === paneId) diff --git a/src/renderer/src/components/terminal-pane/terminal-paste-runtime.test.ts b/src/renderer/src/components/terminal-pane/terminal-paste-runtime.test.ts index 32a3bc72e85..12090d30ffd 100644 --- a/src/renderer/src/components/terminal-pane/terminal-paste-runtime.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-paste-runtime.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { isRemoteRuntimePastePtyId, resolveTerminalPasteRuntime } from './terminal-paste-runtime' +import { resolveTerminalPasteRuntime } from './terminal-paste-runtime' describe('terminal paste runtime', () => { it('uses the platform-local runtime when no SSH identity is available', () => { @@ -244,10 +244,4 @@ describe('terminal paste runtime', () => { kind: 'remote-runtime' }) }) - - it('recognizes remote runtime PTY ids', () => { - expect(isRemoteRuntimePastePtyId('remote:terminal-1')).toBe(true) - expect(isRemoteRuntimePastePtyId('pty-1')).toBe(false) - expect(isRemoteRuntimePastePtyId(null)).toBe(false) - }) }) diff --git a/src/renderer/src/components/terminal-pane/terminal-paste-runtime.ts b/src/renderer/src/components/terminal-pane/terminal-paste-runtime.ts index 2100c3bddce..da6175c745c 100644 --- a/src/renderer/src/components/terminal-pane/terminal-paste-runtime.ts +++ b/src/renderer/src/components/terminal-pane/terminal-paste-runtime.ts @@ -1,7 +1,6 @@ import type { TerminalPasteRuntime } from './terminal-paste-model' import { parseWslUncPath } from '../../../../shared/wsl-paths' - -const REMOTE_PTY_ID_PREFIX = 'remote:' +import { isRemoteRuntimePtyId } from '../../../../shared/remote-runtime-pty-id' type TerminalPasteRuntimeTransport = { getConnectionId?: () => string | null | undefined @@ -34,7 +33,7 @@ export function resolveTerminalPasteRuntime({ }: ResolveTerminalPasteRuntimeArgs): TerminalPasteRuntime { const windowsConpty = isWindowsConpty === undefined ? {} : { isWindowsConpty } - if (isRemoteRuntimePastePtyId(ptyId)) { + if (isRemoteRuntimePtyId(ptyId)) { return { platform, runtimeKey: `remote:${ptyId}`, kind: 'remote-runtime', ...windowsConpty } } @@ -62,10 +61,6 @@ export function resolveTerminalPasteRuntime({ return { platform, runtimeKey: `local:${platform}`, kind: 'local', ...windowsConpty } } -export function isRemoteRuntimePastePtyId(ptyId: string | null | undefined): boolean { - return typeof ptyId === 'string' && ptyId.startsWith(REMOTE_PTY_ID_PREFIX) -} - function resolveWslRuntimeKey( metadata: | { diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects.ts index 994df83d2b0..1c98e382bce 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects.ts @@ -1,3 +1,4 @@ +import { isRemoteRuntimePtyId } from '../../../../shared/remote-runtime-pty-id' import { useEffect, useRef } from 'react' import { FOCUS_TERMINAL_PANE_EVENT, @@ -52,7 +53,7 @@ function reportRendererPtyVisibility( ): void { for (const transport of paneTransports.values()) { const ptyId = transport.getPtyId() - if (!ptyId || ptyId.startsWith('remote:')) { + if (!ptyId || isRemoteRuntimePtyId(ptyId)) { // Why: remote-runtime PTYs use a relay path outside main's local // renderer-visibility registry, so reporting them here is misleading. continue @@ -190,7 +191,7 @@ export function useTerminalPaneGlobalEffects({ useEffect(() => { const ptyId = isActive && isVisible && isWorktreeActive ? activeLeafPtyId : null - if (!ptyId || ptyId.startsWith('remote:')) { + if (!ptyId || isRemoteRuntimePtyId(ptyId)) { return } // Why: main uses this as a scheduler hint only, so the foreground pane's diff --git a/src/renderer/src/lib/agent-status-connection-ownership.ts b/src/renderer/src/lib/agent-status-connection-ownership.ts index 4caa0d36422..e5b6475cdc0 100644 --- a/src/renderer/src/lib/agent-status-connection-ownership.ts +++ b/src/renderer/src/lib/agent-status-connection-ownership.ts @@ -1,3 +1,4 @@ +import { isRemoteRuntimePtyId } from '../../../shared/remote-runtime-pty-id' import { parseAppSshPtyId } from '../../../shared/ssh-pty-id' import { parsePaneKey } from '../../../shared/stable-pane-id' import { parseRemoteRuntimePtyId } from '@/runtime/runtime-terminal-stream' @@ -51,7 +52,7 @@ export function resolveAgentStatusConnectionRouting(args: { } return { connectionId: null } } - if (ptyId.startsWith('remote:')) { + if (isRemoteRuntimePtyId(ptyId)) { return undefined } diff --git a/src/renderer/src/lib/automation-terminal-ownership.ts b/src/renderer/src/lib/automation-terminal-ownership.ts index 9b6ec37d77b..07753434220 100644 --- a/src/renderer/src/lib/automation-terminal-ownership.ts +++ b/src/renderer/src/lib/automation-terminal-ownership.ts @@ -1,3 +1,4 @@ +import { isRemoteRuntimePtyId } from '../../../shared/remote-runtime-pty-id' import { useAppStore } from '@/store' import type { AppState } from '@/store/types' import type { TerminalTab } from '../../../shared/terminal-tab-types' @@ -113,7 +114,7 @@ export function createAutomationTerminalOwnership( consumed = true unsubscribe() observeTakeover() - if (args.runtimeKind !== 'desktop' || args.ptyId.startsWith('remote:') || userTookOver) { + if (args.runtimeKind !== 'desktop' || isRemoteRuntimePtyId(args.ptyId) || userTookOver) { return false } const state = args.store.getState() diff --git a/src/renderer/src/runtime/runtime-terminal-inspection.ts b/src/renderer/src/runtime/runtime-terminal-inspection.ts index b6b50ed28fa..9592988aee1 100644 --- a/src/renderer/src/runtime/runtime-terminal-inspection.ts +++ b/src/renderer/src/runtime/runtime-terminal-inspection.ts @@ -24,15 +24,12 @@ import type { TerminalInputKind } from '../../../shared/terminal-input-kind' export type RuntimeTerminalProcessInspection = TerminalProcessInspection -const REMOTE_PTY_ID_PREFIX = 'remote:' const DESKTOP_RUNTIME_CLIENT = { id: 'orca-desktop', type: 'desktop' } as const function isRuntimePtyInputTooLarge(data: string): boolean | Promise { return isTerminalInputTooLargeWithDeferredMeasurement(data) } -export function isRemoteRuntimePtyId(ptyId: string): boolean { - return ptyId.startsWith(REMOTE_PTY_ID_PREFIX) -} +export { isRemoteRuntimePtyId } from '../../../shared/remote-runtime-pty-id' function isRemoteInspectionPtyId(ptyId: string): boolean { return getRemoteRuntimePtyEnvironmentId(ptyId) !== null || parseAppSshPtyId(ptyId) !== null diff --git a/src/renderer/src/runtime/sync-runtime-graph/mobile-session-surfaces.ts b/src/renderer/src/runtime/sync-runtime-graph/mobile-session-surfaces.ts index 216d2dd0448..fb9500636f2 100644 --- a/src/renderer/src/runtime/sync-runtime-graph/mobile-session-surfaces.ts +++ b/src/renderer/src/runtime/sync-runtime-graph/mobile-session-surfaces.ts @@ -4,7 +4,7 @@ import { isClaudeManagementTitle } from '../../../../shared/agent-detection' import { isTerminalLeafId, makePaneKey } from '../../../../shared/stable-pane-id' import { isWebTerminalSurfaceTabId } from '../../../../shared/terminal-surface-id' import type { Tab } from '../../../../shared/tab-types' -import { parseRemoteRuntimePtyId } from '../runtime-terminal-stream' +import { isRemoteRuntimePtyId } from '../../../../shared/remote-runtime-pty-id' import { isNativeChatTabWideFallbackSafe, nativeChatLaunchAgentForLeaf, @@ -16,10 +16,6 @@ export function mobileTerminalSurfaceId(parentTabId: string, leafId: string): st return `${parentTabId}::${leafId}` } -export function isRemoteRuntimePtyId(ptyId: string | null | undefined): boolean { - return typeof ptyId === 'string' && parseRemoteRuntimePtyId(ptyId) !== null -} - export function isWebOnlyMirroredTerminalTab( tab: Pick[number], 'id' | 'ptyId'>, layout: AppState['terminalLayoutsByTabId'][string] | undefined diff --git a/src/renderer/src/store/repos/repo-removal.ts b/src/renderer/src/store/repos/repo-removal.ts index 1943b3dd722..c042d46493f 100644 --- a/src/renderer/src/store/repos/repo-removal.ts +++ b/src/renderer/src/store/repos/repo-removal.ts @@ -1,3 +1,4 @@ +import { isRemoteRuntimePtyId } from '../../../../shared/remote-runtime-pty-id' import type { StateCreator } from 'zustand' import { toast } from 'sonner' import type { AppState } from '../types' @@ -151,7 +152,7 @@ export function createRepoRemovalActions( for (const tab of tabs) { killedTabIds.add(tab.id) for (const ptyId of get().ptyIdsByTabId[tab.id] ?? []) { - if (!ptyId.startsWith('remote:')) { + if (!isRemoteRuntimePtyId(ptyId)) { window.api.pty.kill(ptyId) } } diff --git a/src/renderer/src/store/slices/terminal-tab-retirement.ts b/src/renderer/src/store/slices/terminal-tab-retirement.ts index e2e034fa6af..0276541b7da 100644 --- a/src/renderer/src/store/slices/terminal-tab-retirement.ts +++ b/src/renderer/src/store/slices/terminal-tab-retirement.ts @@ -1,3 +1,4 @@ +import { isRemoteRuntimePtyId } from '../../../../shared/remote-runtime-pty-id' import type { SleepingAgentSessionRecord } from '../../../../shared/agent-session-resume' import type { AppState } from '../types' import { @@ -228,7 +229,7 @@ export function buildTerminalTabRetirementPlans( environmentId: remote.environmentId?.trim() || null, handle: remote.handle }) - } else if (ptyId.startsWith('remote:')) { + } else if (isRemoteRuntimePtyId(ptyId)) { unroutablePtyIds.push(ptyId) } else if (providerOwnership.kind !== 'local-or-ssh') { // Why: HUB-native wake hints are not paired-client PTY ids; wait for pane resolution instead of killing the same-looking local id. diff --git a/src/renderer/src/store/terminals/terminal-pane-hibernation.ts b/src/renderer/src/store/terminals/terminal-pane-hibernation.ts index edade70fcf4..beccf4cd049 100644 --- a/src/renderer/src/store/terminals/terminal-pane-hibernation.ts +++ b/src/renderer/src/store/terminals/terminal-pane-hibernation.ts @@ -1,3 +1,4 @@ +import { isRemoteRuntimePtyId } from '../../../../shared/remote-runtime-pty-id' import { parsePaneKey } from '../../../../shared/stable-pane-id' import { restorePtyDataHandlersAfterFailedShutdown, @@ -154,7 +155,7 @@ export function createTerminalPaneHibernationActions( for (const snapshot of unregisterPtyDataHandlers(rendererShutdownPtyIds) ?? []) { snapshot.commit?.() } - } else if (!opts.ptyId.startsWith('remote:')) { + } else if (!isRemoteRuntimePtyId(opts.ptyId)) { // Why: pty.kill can flush final data before exit; unregister first so stale handlers can't fire phantom notifications during hibernation. const handlerSnapshots = unregisterPtyDataHandlers(rendererShutdownPtyIds) ?? [] try { diff --git a/src/renderer/src/store/terminals/terminal-pty-bindings.ts b/src/renderer/src/store/terminals/terminal-pty-bindings.ts index 499ea63e8de..f78c8019e74 100644 --- a/src/renderer/src/store/terminals/terminal-pty-bindings.ts +++ b/src/renderer/src/store/terminals/terminal-pty-bindings.ts @@ -1,3 +1,4 @@ +import { isRemoteRuntimePtyId } from '../../../../shared/remote-runtime-pty-id' import { parseAppSshPtyId } from '../../../../shared/ssh-pty-id' import { parseRemoteRuntimePtyId, toRemoteRuntimePtyId } from '@/runtime/runtime-terminal-stream' import { isTerminalTabPresent } from '../slices/terminal-tab-retirement' @@ -5,8 +6,7 @@ import type { TerminalSlice, TerminalStoreGet, TerminalStoreSet } from './termin import { consumePendingActivationSpawn, getPendingActivationSpawnCount, - isCurrentDirectSshAuthority, - isRemoteRuntimePtyId + isCurrentDirectSshAuthority } from './terminal-pty-identities' import { omitUnverifiedPtyLossTabIds } from './terminal-unverified-pty-loss' import { clearWorktreeSleepIntent } from '@/lib/worktree-sleep-intent' diff --git a/src/renderer/src/store/terminals/terminal-pty-identities.ts b/src/renderer/src/store/terminals/terminal-pty-identities.ts index 52b29a29876..08cd8dc9865 100644 --- a/src/renderer/src/store/terminals/terminal-pty-identities.ts +++ b/src/renderer/src/store/terminals/terminal-pty-identities.ts @@ -2,13 +2,8 @@ import type { AppState } from '../types' import type { TerminalLayoutSnapshot, TerminalTab } from '../../../../shared/terminal-tab-types' import type { DirectSshAuthority } from '../../../../shared/ssh-types' import { parseLegacyNumericPaneKey, parsePaneKey } from '../../../../shared/stable-pane-id' -import { parseRemoteRuntimePtyId } from '@/runtime/runtime-terminal-stream' import { resolveDirectSshTerminalWorkspaceKeys } from '../slices/direct-ssh-terminal-workspace-scope' -export function isRemoteRuntimePtyId(ptyId: string | null | undefined): boolean { - return typeof ptyId === 'string' && parseRemoteRuntimePtyId(ptyId) !== null -} - export function isCurrentDirectSshAuthority( state: AppState, authority: DirectSshAuthority diff --git a/src/renderer/src/store/terminals/terminal-pty-release.ts b/src/renderer/src/store/terminals/terminal-pty-release.ts index 6a88cc0e85d..fe9b25ceb12 100644 --- a/src/renderer/src/store/terminals/terminal-pty-release.ts +++ b/src/renderer/src/store/terminals/terminal-pty-release.ts @@ -1,11 +1,11 @@ +import { isRemoteRuntimePtyId } from '../../../../shared/remote-runtime-pty-id' import { parseAppSshPtyId } from '../../../../shared/ssh-pty-id' import { hasWorktreeSleepIntent } from '@/lib/worktree-sleep-intent' import type { TerminalSlice, TerminalStoreGet, TerminalStoreSet } from './terminal-state' import { consumePendingActivationSpawn, getPendingActivationSpawnCount, - isCurrentDirectSshAuthority, - isRemoteRuntimePtyId + isCurrentDirectSshAuthority } from './terminal-pty-identities' export function createTerminalPtyReleaseActions( diff --git a/src/renderer/src/store/terminals/terminal-shutdown-guards.ts b/src/renderer/src/store/terminals/terminal-shutdown-guards.ts index 0eba0fda927..2f89610b45b 100644 --- a/src/renderer/src/store/terminals/terminal-shutdown-guards.ts +++ b/src/renderer/src/store/terminals/terminal-shutdown-guards.ts @@ -1,3 +1,4 @@ +import { isRemoteRuntimePtyId } from '../../../../shared/remote-runtime-pty-id' import type { TerminalTab } from '../../../../shared/terminal-tab-types' import { restorePtyDataHandlersAfterFailedShutdown, @@ -134,13 +135,13 @@ export function createTerminalShutdownGuardController({ stoppedPtyIds: string[] failure?: PromiseRejectedResult }> => { - const localPtyIds = rendererShutdownPtyIds.filter((ptyId) => !ptyId.startsWith('remote:')) + const localPtyIds = rendererShutdownPtyIds.filter((ptyId) => !isRemoteRuntimePtyId(ptyId)) const results = await Promise.allSettled( localPtyIds.map((ptyId) => window.api.pty.kill(ptyId, { keepHistory: keepIdentifiers })) ) const stoppedPtyIds = [ ...(runtimeEnvironmentId - ? rendererShutdownPtyIds.filter((ptyId) => ptyId.startsWith('remote:')) + ? rendererShutdownPtyIds.filter((ptyId) => isRemoteRuntimePtyId(ptyId)) : []), ...localPtyIds.filter((_, index) => results[index]?.status === 'fulfilled') ] diff --git a/src/shared/remote-runtime-pty-id.test.ts b/src/shared/remote-runtime-pty-id.test.ts new file mode 100644 index 00000000000..28fbec3e422 --- /dev/null +++ b/src/shared/remote-runtime-pty-id.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from 'vitest' +import { + isRemoteRuntimePtyId, + parseRemoteRuntimePtyId, + toRemoteRuntimePtyId +} from './remote-runtime-pty-id' +import { getPtyExecutionHost } from './terminal-execution-host' + +describe('isRemoteRuntimePtyId', () => { + it('recognizes minted paired-runtime ids, with and without an owner', () => { + expect(isRemoteRuntimePtyId(toRemoteRuntimePtyId('h1', 'env 1'))).toBe(true) + expect(isRemoteRuntimePtyId(toRemoteRuntimePtyId('h1'))).toBe(true) + }) + + it('rejects local, SSH and missing ids', () => { + expect(isRemoteRuntimePtyId('pty-1')).toBe(false) + expect(isRemoteRuntimePtyId('ssh:conn@@pty-1')).toBe(false) + expect(isRemoteRuntimePtyId('')).toBe(false) + expect(isRemoteRuntimePtyId(null)).toBe(false) + expect(isRemoteRuntimePtyId(undefined)).toBe(false) + }) + + // Pins the merge of the prefix-only and parse-based copies: an unparseable `remote:` id still + // runs off this host, matching getPtyExecutionHost's 'foreign' verdict. + it('treats an unparseable remote id as remote, never local', () => { + const malformed = 'remote:%E0%A4%A@@handle' + expect(parseRemoteRuntimePtyId(malformed)).toBeNull() + expect(isRemoteRuntimePtyId(malformed)).toBe(true) + expect(getPtyExecutionHost(malformed)).toBe('foreign') + }) +}) diff --git a/src/shared/remote-runtime-pty-id.ts b/src/shared/remote-runtime-pty-id.ts index 66c32612a35..cdb38a32b1c 100644 --- a/src/shared/remote-runtime-pty-id.ts +++ b/src/shared/remote-runtime-pty-id.ts @@ -16,8 +16,13 @@ export function toRemoteRuntimePtyId(handle: string, environmentId?: string | nu return `${REMOTE_PTY_ID_PREFIX}${encodeURIComponent(owner)}${REMOTE_PTY_OWNER_SEPARATOR}${encodeURIComponent(handle)}` } +// Why prefix-only: a `remote:` id that fails to parse still runs off this host, never locally. +export function isRemoteRuntimePtyId(ptyId: string | null | undefined): boolean { + return typeof ptyId === 'string' && ptyId.startsWith(REMOTE_PTY_ID_PREFIX) +} + export function parseRemoteRuntimePtyId(ptyId: string): RemoteRuntimePtyIdParts | null { - if (!ptyId.startsWith(REMOTE_PTY_ID_PREFIX)) { + if (!isRemoteRuntimePtyId(ptyId)) { return null } const rest = ptyId.slice(REMOTE_PTY_ID_PREFIX.length) diff --git a/src/shared/terminal-execution-host.ts b/src/shared/terminal-execution-host.ts index 908c8e9fe98..e1ee181f25c 100644 --- a/src/shared/terminal-execution-host.ts +++ b/src/shared/terminal-execution-host.ts @@ -3,7 +3,7 @@ import { toSshExecutionHostId, type ExecutionHostId } from './execution-host' -import { parseRemoteRuntimePtyId } from './remote-runtime-pty-id' +import { parseRemoteRuntimePtyId, isRemoteRuntimePtyId } from './remote-runtime-pty-id' import { parseAppSshPtyId } from './ssh-pty-id' /** 'foreign' = the id proves the PTY runs off this host but cannot name where. */ @@ -30,7 +30,7 @@ export function getPtyExecutionHost(ptyId: string | null | undefined): PtyExecut ? toRuntimeExecutionHostId(environmentId) : 'foreign' } - if (ptyId.startsWith('ssh:') || ptyId.startsWith('remote:')) { + if (ptyId.startsWith('ssh:') || isRemoteRuntimePtyId(ptyId)) { return 'foreign' } return null