diff --git a/src/renderer/src/components/terminal-pane/TerminalErrorToast.tsx b/src/renderer/src/components/terminal-pane/TerminalErrorToast.tsx index 66e9647f27a..3ef1f44a924 100644 --- a/src/renderer/src/components/terminal-pane/TerminalErrorToast.tsx +++ b/src/renderer/src/components/terminal-pane/TerminalErrorToast.tsx @@ -34,48 +34,34 @@ const PANE_OWNER_UNVERIFIED_MARKER = 'terminal_pane_owner_unverified' // translated here rather than at the source -- otherwise the banner mixes English with the // localized chrome around it (#9194). const REMOTE_TERMINAL_CLOSED_MARKER = 'Remote terminal was closed.' -// Why one source: the test and replace forms must match the same token, and a lone /g regex carries -// lastIndex state across .test() calls. Capture the leading boundary so replacement can restore it. -const TERMINAL_HOST_GONE_SOURCE = '(^|[^a-z0-9_])terminal_host_gone(?=$|[^a-z0-9_])' -const TERMINAL_HOST_GONE_PATTERN = new RegExp(TERMINAL_HOST_GONE_SOURCE) -const TERMINAL_HOST_GONE_REPLACE_PATTERN = new RegExp(TERMINAL_HOST_GONE_SOURCE, 'g') +// Why: a lone /g regex carries lastIndex across .test() calls, so each token gets a stateless test form and a /g replace form. +function tokenPattern(source: string): { match: RegExp; replaceAll: RegExp } { + return { match: new RegExp(source), replaceAll: new RegExp(source, 'g') } +} +// Capture the leading boundary so replacement can restore it. +const TERMINAL_HOST_GONE = tokenPattern('(^|[^a-z0-9_])terminal_host_gone(?=$|[^a-z0-9_])') const LEGACY_TERMINAL_HOST_GONE_PATTERN = /(^|[^a-z])connect (?:ENOENT|ECONNREFUSED) [^\r\n]*orca-terminal-host-v[^\r\n]*/i // A reattach the host answered "no such session" for: the SSH provider's expiry token, the relay's // raw not-found string when nothing mapped it, or a daemon generation old enough to still refuse a // pane respawning onto an id it is tearing down (#18046). None proves the shell died — the copy -// says only that this pane lost its session. Same lastIndex hazard as above. -const UNREATTACHABLE_SESSION_SOURCES = [ +// says only that this pane lost its session. +const UNREATTACHABLE_SESSIONS = [ 'SSH_SESSION_EXPIRED:[ \\t]*\\S*(?:[ \\t]+SSH_PTY_IDENTITY_MISMATCH)?', 'PTY "[^"\\r\\n]*" not found(?: \\(identity mismatch\\))?', '(?:SessionNotFoundError: )?Session not found: \\S+' -] +].map(tokenPattern) // The relay answered and proved the shell is still running — only its output delivery was retired. // Deliberately NOT one of the sources above: that copy says to open a new terminal, which here -// abandons a live agent. Same lastIndex hazard, so keep the test and replace forms separate. -const SOURCE_RESTORE_REQUIRED_SOURCE = +// abandons a live agent. +const SOURCE_RESTORE_REQUIRED = tokenPattern( 'SSH_PTY_SOURCE_RESTORE_REQUIRED(?::[ \\t]*\\S*(?:[ \\t]+\\S+)?)?' -const SOURCE_RESTORE_REQUIRED_PATTERN = new RegExp(SOURCE_RESTORE_REQUIRED_SOURCE) -const SOURCE_RESTORE_REQUIRED_REPLACE_PATTERN = new RegExp(SOURCE_RESTORE_REQUIRED_SOURCE, 'g') +) // An older Orca build's relay may still run this terminal, and this build cannot reach it. Not one of // the sources above: that copy implies the session is gone. -const HELD_BY_PREVIOUS_RELAY_SOURCE = 'SSH_PTY_HELD_BY_PREVIOUS_RELAY(?::[ \\t]*\\S*)?' -const HELD_BY_PREVIOUS_RELAY_PATTERN = new RegExp(HELD_BY_PREVIOUS_RELAY_SOURCE) +const HELD_BY_PREVIOUS_RELAY = tokenPattern('SSH_PTY_HELD_BY_PREVIOUS_RELAY(?::[ \\t]*\\S*)?') // The pane's saved session is owned by another host connection, e.g. an older build's relay tab. -const OWNER_HOST_MISMATCH_SOURCE = 'terminal_pane_owner_host_mismatch' -const OWNER_HOST_MISMATCH_PATTERN = new RegExp(OWNER_HOST_MISMATCH_SOURCE) -const OWNER_HOST_MISMATCH_REPLACE_PATTERN = new RegExp(OWNER_HOST_MISMATCH_SOURCE, 'g') -const HELD_BY_PREVIOUS_RELAY_REPLACE_PATTERN = new RegExp(HELD_BY_PREVIOUS_RELAY_SOURCE, 'g') -const UNREATTACHABLE_SESSION_PATTERNS = UNREATTACHABLE_SESSION_SOURCES.map( - (source) => new RegExp(source) -) -const UNREATTACHABLE_SESSION_REPLACE_PATTERNS = UNREATTACHABLE_SESSION_SOURCES.map( - (source) => new RegExp(source, 'g') -) - -function isSshError(error: string): boolean { - return isSshReconnectOwnedTerminalError(error) -} +const OWNER_HOST_MISMATCH = tokenPattern('terminal_pane_owner_host_mismatch') /** A single error line the SSH reconnect banner already covers — hide instead of stacking under/over it. */ export function isSshReconnectOwnedTerminalError(error: string): boolean { @@ -106,18 +92,20 @@ export function isExplainedTerminalError(error: string): boolean { .split('\n') .some( (line) => - TERMINAL_HOST_GONE_PATTERN.test(line) || LEGACY_TERMINAL_HOST_GONE_PATTERN.test(line) || - SOURCE_RESTORE_REQUIRED_PATTERN.test(line) || - HELD_BY_PREVIOUS_RELAY_PATTERN.test(line) || - OWNER_HOST_MISMATCH_PATTERN.test(line) || - UNREATTACHABLE_SESSION_PATTERNS.some((pattern) => pattern.test(line)) + [ + TERMINAL_HOST_GONE, + SOURCE_RESTORE_REQUIRED, + HELD_BY_PREVIOUS_RELAY, + OWNER_HOST_MISMATCH, + ...UNREATTACHABLE_SESSIONS + ].some((pattern) => pattern.match.test(line)) ) } /** A terminal the previous Orca version's relay runs on the host; this client's details say nothing about it. */ export function isHeldByPreviousRelayError(error: string): boolean { - return HELD_BY_PREVIOUS_RELAY_PATTERN.test(error) + return HELD_BY_PREVIOUS_RELAY.match.test(error) } export function isPaneOwnerUnverifiedError(error: string): boolean { @@ -131,8 +119,8 @@ function humanizeUnreattachableSession(error: string): string { "Orca couldn't reattach to this pane's terminal session on the host. Open a new terminal to continue." ) // Why a replacer: a translation containing `$&` or `$1` would otherwise be read as a substitution. - return UNREATTACHABLE_SESSION_REPLACE_PATTERNS.reduce( - (message, pattern) => message.replace(pattern, () => explanation), + return UNREATTACHABLE_SESSIONS.reduce( + (message, pattern) => message.replace(pattern.replaceAll, () => explanation), error ) } @@ -165,19 +153,19 @@ export function humanizeTerminalError(error: string): string { ) humanized = humanized.replaceAll(PANE_OWNER_UNVERIFIED_MARKER, () => explanation) } - humanized = humanized.replace(SOURCE_RESTORE_REQUIRED_REPLACE_PATTERN, () => + humanized = humanized.replace(SOURCE_RESTORE_REQUIRED.replaceAll, () => translate( 'auto.components.terminal.pane.TerminalErrorToast.sourceRestoring', 'Reconnecting this terminal — its output is being re-established. The session is still running.' ) ) - humanized = humanized.replace(HELD_BY_PREVIOUS_RELAY_REPLACE_PATTERN, () => + humanized = humanized.replace(HELD_BY_PREVIOUS_RELAY.replaceAll, () => translate( 'auto.components.terminal.pane.TerminalErrorToast.heldByPreviousRelay', 'This terminal is still running on the host under the previous Orca version, which this version cannot connect to. It keeps running until it exits. Open a new terminal to keep working here.' ) ) - humanized = humanized.replace(OWNER_HOST_MISMATCH_REPLACE_PATTERN, () => + humanized = humanized.replace(OWNER_HOST_MISMATCH.replaceAll, () => translate( 'auto.components.terminal.pane.TerminalErrorToast.ownerHostMismatch', "This terminal's saved session belongs to another host connection, so Orca can't reattach it here. Open a new terminal to continue." @@ -203,7 +191,7 @@ export function humanizeTerminalError(error: string): string { .split('\n') .map((line) => line - .replace(TERMINAL_HOST_GONE_REPLACE_PATTERN, (_match, prefix: string) => + .replace(TERMINAL_HOST_GONE.replaceAll, (_match, prefix: string) => prefix.concat(explanation) ) .replace(LEGACY_TERMINAL_HOST_GONE_PATTERN, (_match, prefix: string) => @@ -227,7 +215,7 @@ export function TerminalErrorToast({ onRestartDaemon?: () => void onRetry?: () => Promise }): React.JSX.Element { - const ssh = isSshError(error) + const ssh = isSshReconnectOwnedTerminalError(error) // Why: the client's OS and shell describe neither the host nor its shell, and the renderer knows neither. const showClientEnvironment = paneOnClient && !ssh && !isHeldByPreviousRelayError(error) const paneOwnerUnverified = isPaneOwnerUnverifiedError(error) diff --git a/src/renderer/src/components/terminal-pane/TerminalPaneSurface.tsx b/src/renderer/src/components/terminal-pane/TerminalPaneSurface.tsx index 16425f3fb12..895d1d1e306 100644 --- a/src/renderer/src/components/terminal-pane/TerminalPaneSurface.tsx +++ b/src/renderer/src/components/terminal-pane/TerminalPaneSurface.tsx @@ -27,7 +27,8 @@ import { } from './TerminalPaneRuntimePortals' import type { TerminalPaneController } from './use-terminal-pane-controller' import { useAppStore } from '@/store' -import { isTerminalPaneOnClient } from './terminal-pane-client-host' +import { getKnownExecutionHostIdForWorktree } from '@/lib/worktree-runtime-owner' +import { LOCAL_EXECUTION_HOST_ID } from '../../../../shared/execution-host' export function TerminalPaneSurface({ controller @@ -118,7 +119,10 @@ export function TerminalPaneSurface({ visibleTerminalError, worktreeId } = controller - const paneOnClient = useAppStore((state) => isTerminalPaneOnClient(state, worktreeId)) + // Why: an SSH, runtime, or not-yet-known host must not be described with this client's OS/shell. + const paneOnClient = useAppStore( + (state) => getKnownExecutionHostIdForWorktree(state, worktreeId) === LOCAL_EXECUTION_HOST_ID + ) return ( <> diff --git a/src/renderer/src/components/terminal-pane/pty-preconnect-input-buffer.ts b/src/renderer/src/components/terminal-pane/pty-preconnect-input-buffer.ts index 211800efea9..3a54d425651 100644 --- a/src/renderer/src/components/terminal-pane/pty-preconnect-input-buffer.ts +++ b/src/renderer/src/components/terminal-pane/pty-preconnect-input-buffer.ts @@ -24,7 +24,7 @@ type BufferedInput = PtyPreconnectInputEntry & { signal?: AbortSignal } -type PreconnectInputWriter = { +export type PreconnectInputWriter = { isCurrent: () => boolean sendInput: (data: string, inputKind: TerminalInputKind) => boolean sendInputImmediate: (data: string) => boolean diff --git a/src/renderer/src/components/terminal-pane/remote-runtime-recovery-input-hold.ts b/src/renderer/src/components/terminal-pane/remote-runtime-recovery-input-hold.ts index a275e886d0d..79cde127cfb 100644 --- a/src/renderer/src/components/terminal-pane/remote-runtime-recovery-input-hold.ts +++ b/src/renderer/src/components/terminal-pane/remote-runtime-recovery-input-hold.ts @@ -2,6 +2,7 @@ import type { TerminalInputKind } from '../../../../shared/terminal-input-kind' import { createPtyPreconnectInputBuffer, type AcceptedInputOptions, + type PreconnectInputWriter, type PtyPreconnectInputBuffer } from './pty-preconnect-input-buffer' @@ -11,17 +12,9 @@ export type RemoteRuntimeInputEndpoint = { incarnationId: string | null } -type HeldInputWriter = { - isCurrent: () => boolean - sendInput: (data: string, inputKind: TerminalInputKind) => boolean - sendInputImmediate: (data: string) => boolean - sendInputAccepted: ( - data: string, - inputKind: TerminalInputKind, - options?: AcceptedInputOptions - ) => Promise - continuesAfterFailedWrite?: () => boolean -} +// Why: release() always supplies an accepted-write path, so the hold requires what the buffer only accepts. +type HeldInputWriter = PreconnectInputWriter & + Required> export type RemoteRuntimeRecoveryInputHold = { isHolding: () => boolean diff --git a/src/renderer/src/components/terminal-pane/terminal-pane-client-host.test.ts b/src/renderer/src/components/terminal-pane/terminal-pane-client-host.test.ts deleted file mode 100644 index 49159c6ca01..00000000000 --- a/src/renderer/src/components/terminal-pane/terminal-pane-client-host.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { describe, expect, it } from 'vitest' -import type { WorktreeRuntimeOwnerState } from '@/lib/worktree-runtime-owner-state' -import { isTerminalPaneOnClient } from './terminal-pane-client-host' - -const state: WorktreeRuntimeOwnerState = { - repos: [ - { id: 'local-repo', executionHostId: 'local' }, - { id: 'ssh-repo', connectionId: 'box', executionHostId: 'ssh:box' }, - { id: 'managed-repo', executionHostId: 'runtime:env-1' } - ], - worktreesByRepo: {} -} - -describe('whether a terminal pane runs on this client', () => { - it('is true only for a local workspace', () => { - expect(isTerminalPaneOnClient(state, 'local-repo::/home/me/app')).toBe(true) - expect(isTerminalPaneOnClient(state, 'ssh-repo::/srv/app')).toBe(false) - expect(isTerminalPaneOnClient(state, 'managed-repo::/root/repo')).toBe(false) - }) - - it('is false while the workspace host is still unknown', () => { - expect(isTerminalPaneOnClient(state, 'not-loaded::/srv/app')).toBe(false) - }) -}) diff --git a/src/renderer/src/components/terminal-pane/terminal-pane-client-host.ts b/src/renderer/src/components/terminal-pane/terminal-pane-client-host.ts deleted file mode 100644 index 67c834425f4..00000000000 --- a/src/renderer/src/components/terminal-pane/terminal-pane-client-host.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { getKnownExecutionHostIdForWorktree } from '@/lib/worktree-runtime-owner' -import type { WorktreeRuntimeOwnerState } from '@/lib/worktree-runtime-owner-state' -import { LOCAL_EXECUTION_HOST_ID } from '../../../../shared/execution-host' - -/** - * True only when the pane's shell runs on this client. An SSH or remote host, or one not yet - * known, must not be described with this client's OS and shell. - */ -export function isTerminalPaneOnClient( - state: WorktreeRuntimeOwnerState, - worktreeId: string | null | undefined -): boolean { - return getKnownExecutionHostIdForWorktree(state, worktreeId) === LOCAL_EXECUTION_HOST_ID -} diff --git a/src/renderer/src/components/terminal-pane/terminal-pane-menu-paste.ts b/src/renderer/src/components/terminal-pane/terminal-pane-menu-paste.ts index 841a2e3eac5..bf918d94da4 100644 --- a/src/renderer/src/components/terminal-pane/terminal-pane-menu-paste.ts +++ b/src/renderer/src/components/terminal-pane/terminal-pane-menu-paste.ts @@ -3,19 +3,14 @@ import type { PtyTransport } from './pty-transport' import { requestNativeChatCoverPaste } from './native-chat-cover-paste' import { getConnectionId } from '@/lib/connection-context' import { getRuntimeEnvironmentIdForWorktree } from '@/lib/worktree-runtime-owner' -import { pasteTerminalText } from './terminal-bracketed-paste' +import { getShortcutPlatform } from '@/lib/shortcut-platform' import { pasteTerminalClipboard } from './terminal-clipboard-paste' -import { - executeTerminalPastePlan, - planTerminalPasteWithYield, - type TerminalPasteSource, - type TerminalPasteTextOptions -} from './terminal-paste-coordinator' +import type { TerminalPasteSource, TerminalPasteTextOptions } from './terminal-paste-coordinator' import { formatTerminalPasteExecutionError } from './terminal-paste-errors' import { resolveTerminalPasteRuntime } from './terminal-paste-runtime' import { getTerminalPasteSshRemotePlatform } from './terminal-paste-ssh-platform' import { isTerminalPanePasteTargetCurrent } from './terminal-paste-target-state' -import { writeTerminalPastePtyInput } from './terminal-pty-paste-writer' +import { pasteTextIntoTerminalPane } from './terminal-pane-paste-dispatch' import { scheduleImagePasteWebglAtlasRecovery } from './terminal-webgl-atlas-recovery' import { useAppStore } from '@/store' import { resolveProtectedMultilinePasteOptionsForPane } from './terminal-agent-paste-bracketing' @@ -31,13 +26,6 @@ export type TerminalPaneMenuPasteContext = { onPasteError: (message: string) => void } -export const getTerminalPaneMenuShortcutPlatform = (): NodeJS.Platform => { - if (navigator.userAgent.includes('Mac')) { - return 'darwin' - } - return navigator.userAgent.includes('Windows') ? 'win32' : 'linux' -} - const isPanePasteTargetMounted = ( context: TerminalPaneMenuPasteContext, pane: ManagedPane, @@ -64,35 +52,25 @@ export const executeTerminalPaneMenuPasteText = async ( const connectionId = getConnectionId(context.worktreeId) ?? null const transport = context.paneTransportsRef.current.get(pane.id) const ptyId = transport?.getPtyId() ?? null - const shortcutPlatform = getTerminalPaneMenuShortcutPlatform() - const plan = await planTerminalPasteWithYield({ + const isTargetMounted = (): boolean => isPanePasteTargetMounted(context, pane, transport, ptyId) + const execution = await pasteTextIntoTerminalPane({ + pane, text, source, - target: { - kind: 'terminal', - paneId: pane.id, - leafId: pane.leafId, + ptyId, + runtime: resolveTerminalPasteRuntime({ + platform: getShortcutPlatform(), ptyId, - runtime: resolveTerminalPasteRuntime({ - platform: shortcutPlatform, - ptyId, - connectionId, - remotePlatform: getTerminalPasteSshRemotePlatform(connectionId), - transport, - isWindowsConpty: context.forceBracketedMultilineTextPaste - }) - }, - forceBracketedPaste: options?.forceBracketedPaste, - forceBracketedPasteForMultiline: options?.forceBracketedPasteForMultiline, - windowsInputRecordNewline: options?.windowsInputRecordNewline, - terminalBracketedPasteMode: pane.terminal.modes.bracketedPasteMode - }) - const execution = await executeTerminalPastePlan(plan, { - pasteText: (pasteText, pasteOptions) => - pasteTerminalText(pane.terminal, pasteText, pasteOptions), - writePty: (data, signal) => writeTerminalPastePtyInput(transport, data, 'driving', signal), - isTargetCurrent: () => isPanePasteTargetMounted(context, pane, transport, ptyId), - canContinue: () => isPanePasteTargetMounted(context, pane, transport, ptyId) + connectionId, + remotePlatform: getTerminalPasteSshRemotePlatform(connectionId), + transport, + isWindowsConpty: context.forceBracketedMultilineTextPaste + }), + transport, + inputKind: 'driving', + isTargetCurrent: isTargetMounted, + canContinue: isTargetMounted, + planOptions: options }) if (execution.status !== 'pasted') { context.onPasteError(formatTerminalPasteExecutionError(execution.reason)) @@ -131,7 +109,7 @@ export const pasteTerminalPaneMenuClipboard = async ( protectedMultilineTextPasteOptions: resolveProtectedMultilinePasteOptionsForPane({ isWindowsClient: forceBracketedMultilineTextPaste, hostPlatform: resolveTerminalInputHostPlatform({ - clientPlatform: getTerminalPaneMenuShortcutPlatform(), + clientPlatform: getShortcutPlatform(), state, worktreeId, transport diff --git a/src/renderer/src/components/terminal-pane/terminal-pane-paste-dispatch.ts b/src/renderer/src/components/terminal-pane/terminal-pane-paste-dispatch.ts new file mode 100644 index 00000000000..275315fdbe6 --- /dev/null +++ b/src/renderer/src/components/terminal-pane/terminal-pane-paste-dispatch.ts @@ -0,0 +1,56 @@ +import { pasteTerminalText } from './terminal-bracketed-paste' +import { + executeTerminalPastePlan, + planTerminalPasteWithYield, + type TerminalPasteExecutionResult, + type TerminalPasteRuntime, + type TerminalPasteSource, + type TerminalPasteTextOptions +} from './terminal-paste-coordinator' +import { writeTerminalPastePtyInput } from './terminal-pty-paste-writer' + +type PasteTextIntoTerminalPaneArgs = { + pane: { id: number; leafId: string; terminal: Parameters[0] } + text: string + source: TerminalPasteSource + ptyId: string | null + runtime: TerminalPasteRuntime + transport: Parameters[0] + inputKind: 'driving' | 'launch' + isTargetCurrent: () => boolean + canContinue: () => boolean + planOptions?: Pick< + TerminalPasteTextOptions, + 'forceBracketedPaste' | 'forceBracketedPasteForMultiline' | 'windowsInputRecordNewline' + > +} + +export async function pasteTextIntoTerminalPane({ + pane, + text, + source, + ptyId, + runtime, + transport, + inputKind, + isTargetCurrent, + canContinue, + planOptions +}: PasteTextIntoTerminalPaneArgs): Promise { + const plan = await planTerminalPasteWithYield({ + text, + source, + target: { kind: 'terminal', paneId: pane.id, leafId: pane.leafId, ptyId, runtime }, + forceBracketedPaste: planOptions?.forceBracketedPaste, + forceBracketedPasteForMultiline: planOptions?.forceBracketedPasteForMultiline, + windowsInputRecordNewline: planOptions?.windowsInputRecordNewline, + terminalBracketedPasteMode: pane.terminal.modes?.bracketedPasteMode === true + }) + return executeTerminalPastePlan(plan, { + pasteText: (pasteText, pasteOptions) => + pasteTerminalText(pane.terminal, pasteText, pasteOptions), + writePty: (data, signal) => writeTerminalPastePtyInput(transport, data, inputKind, signal), + isTargetCurrent, + canContinue + }) +} diff --git a/src/renderer/src/components/terminal-pane/terminal-pane-paste-execution.ts b/src/renderer/src/components/terminal-pane/terminal-pane-paste-execution.ts index a8978a154c6..0a808c5f1db 100644 --- a/src/renderer/src/components/terminal-pane/terminal-pane-paste-execution.ts +++ b/src/renderer/src/components/terminal-pane/terminal-pane-paste-execution.ts @@ -3,20 +3,14 @@ import type { ManagedPane } from '@/lib/pane-manager/pane-manager' import type { PtyTransport } from './pty-transport' import { getConnectionId } from '@/lib/connection-context' import { getRuntimeEnvironmentIdForWorktree } from '@/lib/worktree-runtime-owner' -import { - executeTerminalPastePlan, - planTerminalPasteWithYield, - type TerminalPasteSource, - type TerminalPasteTextOptions -} from './terminal-paste-coordinator' +import type { TerminalPasteSource, TerminalPasteTextOptions } from './terminal-paste-coordinator' import { resolveTerminalPasteRuntime } from './terminal-paste-runtime' import { getTerminalPasteSshRemotePlatform } from './terminal-paste-ssh-platform' import { isTerminalPanePasteFocusCurrent, isTerminalPanePasteTargetCurrent } from './terminal-paste-target-state' -import { pasteTerminalText } from './terminal-bracketed-paste' -import { writeTerminalPastePtyInput } from './terminal-pty-paste-writer' +import { pasteTextIntoTerminalPane } from './terminal-pane-paste-dispatch' import { formatTerminalPasteExecutionError } from './terminal-paste-errors' import { recordTerminalUserInputForLeaf } from './terminal-input-activity' import { scheduleImagePasteWebglAtlasRecovery } from './terminal-webgl-atlas-recovery' @@ -69,31 +63,21 @@ export function createTerminalPanePasteExecution( const ptyId = transport?.getPtyId() ?? null const keyboardOwnedPaste = source === 'keyboard' || source === 'paste-event' || source === 'app-menu' - const plan = await planTerminalPasteWithYield({ + const execution = await pasteTextIntoTerminalPane({ + pane, text, source, - target: { - kind: 'terminal', - paneId: pane.id, - leafId: pane.leafId, + ptyId, + runtime: resolveTerminalPasteRuntime({ + platform: shortcutPlatform, ptyId, - runtime: resolveTerminalPasteRuntime({ - platform: shortcutPlatform, - ptyId, - connectionId, - remotePlatform: getTerminalPasteSshRemotePlatform(connectionId), - transport, - isWindowsConpty: forceBracketedMultilineTextPaste - }) - }, - forceBracketedPaste: options?.forceBracketedPaste, - forceBracketedPasteForMultiline: options?.forceBracketedPasteForMultiline, - terminalBracketedPasteMode: pane.terminal.modes.bracketedPasteMode - }) - const execution = await executeTerminalPastePlan(plan, { - pasteText: (pasteText, pasteOptions) => - pasteTerminalText(pane.terminal, pasteText, pasteOptions), - writePty: (data, signal) => writeTerminalPastePtyInput(transport, data, 'driving', signal), + connectionId, + remotePlatform: getTerminalPasteSshRemotePlatform(connectionId), + transport, + isWindowsConpty: forceBracketedMultilineTextPaste + }), + transport, + inputKind: 'driving', isTargetCurrent: () => { if (!isPanePasteTargetMounted(pane, transport, ptyId)) { return false @@ -104,7 +88,8 @@ export function createTerminalPanePasteExecution( paneContainer: pane.container }) }, - canContinue: () => isPanePasteTargetMounted(pane, transport, ptyId) + canContinue: () => isPanePasteTargetMounted(pane, transport, ptyId), + planOptions: options }) if (execution.status !== 'pasted') { setTerminalError(formatTerminalPasteExecutionError(execution.reason)) diff --git a/src/renderer/src/components/terminal-pane/terminal-programmatic-text-paste.ts b/src/renderer/src/components/terminal-pane/terminal-programmatic-text-paste.ts index 294a87e7ea0..7699355a77a 100644 --- a/src/renderer/src/components/terminal-pane/terminal-programmatic-text-paste.ts +++ b/src/renderer/src/components/terminal-pane/terminal-programmatic-text-paste.ts @@ -2,13 +2,12 @@ import type { PasteTerminalTextDetail } from '@/constants/terminal' import type { PaneManager } from '@/lib/pane-manager/pane-manager' import type { PtyTransport } from './pty-transport' import { getConnectionId } from '@/lib/connection-context' -import { pasteTerminalText } from './terminal-bracketed-paste' +import { getShortcutPlatform } from '@/lib/shortcut-platform' import { recordTerminalUserInputForLeaf } from './terminal-input-activity' -import { executeTerminalPastePlan, planTerminalPasteWithYield } from './terminal-paste-coordinator' +import { pasteTextIntoTerminalPane } from './terminal-pane-paste-dispatch' import { resolveTerminalPasteRuntime } from './terminal-paste-runtime' import { getTerminalPasteSshRemotePlatform } from './terminal-paste-ssh-platform' import { isTerminalPanePasteTargetCurrent } from './terminal-paste-target-state' -import { writeTerminalPastePtyInput } from './terminal-pty-paste-writer' type HandleTerminalProgrammaticTextPasteArgs = { detail: PasteTerminalTextDetail | undefined @@ -43,62 +42,37 @@ export function handleTerminalProgrammaticTextPaste({ const paneTransports = getPaneTransports() const transport = paneTransports.get(pane.id) const ptyId = transport?.getPtyId() ?? null - const platform = getShortcutPlatform() const connectionId = getConnectionId(worktreeId) ?? null - void planTerminalPasteWithYield({ - text: detail.text, - source: 'programmatic', - target: { - kind: 'terminal', + const isTargetCurrent = (): boolean => + isTerminalPanePasteTargetCurrent({ + manager: getManager(), + paneTransports: getPaneTransports(), paneId: pane.id, leafId: pane.leafId, - ptyId, - runtime: resolveTerminalPasteRuntime({ - platform, - ptyId, - connectionId, - remotePlatform: getTerminalPasteSshRemotePlatform(connectionId), - transport - }) - }, - terminalBracketedPasteMode: pane.terminal.modes?.bracketedPasteMode === true - }) - .then((plan) => - executeTerminalPastePlan(plan, { - pasteText: (text, options) => pasteTerminalText(pane.terminal, text, options), - writePty: (data, signal) => writeTerminalPastePtyInput(transport, data, 'driving', signal), - isTargetCurrent: () => - isTerminalPanePasteTargetCurrent({ - manager: getManager(), - paneTransports: getPaneTransports(), - paneId: pane.id, - leafId: pane.leafId, - transport, - ptyId - }), - canContinue: () => - isTerminalPanePasteTargetCurrent({ - manager: getManager(), - paneTransports: getPaneTransports(), - paneId: pane.id, - leafId: pane.leafId, - transport, - ptyId - }) - }) - ) - .then((result) => { - if (result.status !== 'pasted') { - return - } - recordTerminalUserInputForLeaf(tabId, pane.leafId) - pane.terminal.focus() + transport, + ptyId }) -} - -function getShortcutPlatform(userAgent = globalThis.navigator?.userAgent ?? ''): NodeJS.Platform { - if (userAgent.includes('Mac')) { - return 'darwin' - } - return userAgent.includes('Windows') ? 'win32' : 'linux' + void pasteTextIntoTerminalPane({ + pane, + text: detail.text, + source: 'programmatic', + ptyId, + runtime: resolveTerminalPasteRuntime({ + platform: getShortcutPlatform(), + ptyId, + connectionId, + remotePlatform: getTerminalPasteSshRemotePlatform(connectionId), + transport + }), + transport, + inputKind: 'driving', + isTargetCurrent, + canContinue: isTargetCurrent + }).then((result) => { + if (result.status !== 'pasted') { + return + } + recordTerminalUserInputForLeaf(tabId, pane.leafId) + pane.terminal.focus() + }) } diff --git a/src/renderer/src/components/terminal-pane/terminal-startup-command-paste.ts b/src/renderer/src/components/terminal-pane/terminal-startup-command-paste.ts index 837330ed443..240c619ad99 100644 --- a/src/renderer/src/components/terminal-pane/terminal-startup-command-paste.ts +++ b/src/renderer/src/components/terminal-pane/terminal-startup-command-paste.ts @@ -1,12 +1,10 @@ import type { PtyTransport } from './pty-transport' -import { pasteTerminalText } from './terminal-bracketed-paste' -import { - executeTerminalPastePlan, - planTerminalPasteWithYield, - type TerminalPasteExecutionResult, - type TerminalPasteRuntime +import type { pasteTerminalText } from './terminal-bracketed-paste' +import type { + TerminalPasteExecutionResult, + TerminalPasteRuntime } from './terminal-paste-coordinator' -import { writeTerminalPastePtyInput } from './terminal-pty-paste-writer' +import { pasteTextIntoTerminalPane } from './terminal-pane-paste-dispatch' type StartupCommandPane = { id: number @@ -32,22 +30,14 @@ export async function executeTerminalStartupCommandPaste({ isTargetCurrent }: ExecuteTerminalStartupCommandPasteArgs): Promise { const isCurrent = (): boolean => isTargetCurrent?.(ptyId) ?? true - const plan = await planTerminalPasteWithYield({ + return pasteTextIntoTerminalPane({ + pane, text: command, source: 'programmatic', - target: { - kind: 'terminal', - paneId: pane.id, - leafId: pane.leafId, - ptyId, - runtime - }, - terminalBracketedPasteMode: pane.terminal.modes?.bracketedPasteMode === true - }) - - return executeTerminalPastePlan(plan, { - pasteText: (text, options) => pasteTerminalText(pane.terminal, text, options), - writePty: (data, signal) => writeTerminalPastePtyInput(transport, data, 'launch', signal), + ptyId, + runtime, + transport, + inputKind: 'launch', isTargetCurrent: isCurrent, canContinue: isCurrent }) diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-mobile-actions.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-mobile-actions.ts index c61a73eff05..9511f6def01 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-pane-mobile-actions.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-mobile-actions.ts @@ -10,11 +10,11 @@ import { readPrimarySelectionText } from '@/lib/primary-selection' import { getConnectionId } from '@/lib/connection-context' -import { executeTerminalPastePlan, planTerminalPasteWithYield } from './terminal-paste-coordinator' +import { getShortcutPlatform } from '@/lib/shortcut-platform' import { resolveTerminalPasteRuntime } from './terminal-paste-runtime' import { getTerminalPasteSshRemotePlatform } from './terminal-paste-ssh-platform' -import { pasteTerminalText } from './terminal-bracketed-paste' -import { writeTerminalPastePtyInput } from './terminal-pty-paste-writer' +import { isTerminalPanePasteTargetCurrent } from './terminal-paste-target-state' +import { pasteTextIntoTerminalPane } from './terminal-pane-paste-dispatch' import { formatTerminalPasteExecutionError } from './terminal-paste-errors' import { recordTerminalUserInputForLeaf } from './terminal-input-activity' import { splitTerminalPaneWithInheritedCwd } from './terminal-pane-split-with-inherited-cwd' @@ -153,51 +153,30 @@ export function useTerminalPaneMobileActions(controller: TerminalPaneContextCont } const transport = paneTransportsRef.current.get(clickedPane.id) const ptyId = transport?.getPtyId() ?? null - const isMac = navigator.userAgent.includes('Mac') - const shortcutPlatform: NodeJS.Platform = isMac - ? 'darwin' - : navigator.userAgent.includes('Windows') - ? 'win32' - : 'linux' const connectionId = getConnectionId(worktreeId) ?? null - const targetStillMounted = (): boolean => { - const manager = managerRef.current - return Boolean( - manager - ?.getPanes() - .some( - (livePane) => - livePane.id === clickedPane.id && livePane.leafId === clickedPane.leafId - ) && - transport && - paneTransportsRef.current.get(clickedPane.id) === transport && - transport.isConnected() && - transport.getPtyId() === ptyId - ) - } - const plan = await planTerminalPasteWithYield({ - text, - source: 'middle-click', - target: { - kind: 'terminal', + const targetStillMounted = (): boolean => + isTerminalPanePasteTargetCurrent({ + manager: managerRef.current, + paneTransports: paneTransportsRef.current, paneId: clickedPane.id, leafId: clickedPane.leafId, + transport, + ptyId + }) + const execution = await pasteTextIntoTerminalPane({ + pane: clickedPane, + text, + source: 'middle-click', + ptyId, + runtime: resolveTerminalPasteRuntime({ + platform: getShortcutPlatform(), ptyId, - runtime: resolveTerminalPasteRuntime({ - platform: shortcutPlatform, - ptyId, - connectionId, - remotePlatform: getTerminalPasteSshRemotePlatform(connectionId), - transport - }) - }, - terminalBracketedPasteMode: clickedPane.terminal.modes.bracketedPasteMode - }) - const execution = await executeTerminalPastePlan(plan, { - pasteText: (pasteText, pasteOptions) => - pasteTerminalText(clickedPane.terminal, pasteText, pasteOptions), - writePty: (data, signal) => - writeTerminalPastePtyInput(transport, data, 'driving', signal), + connectionId, + remotePlatform: getTerminalPasteSshRemotePlatform(connectionId), + transport + }), + transport, + inputKind: 'driving', isTargetCurrent: targetStillMounted, canContinue: targetStillMounted }) diff --git a/src/renderer/src/lib/worktree-runtime-owner.test.ts b/src/renderer/src/lib/worktree-runtime-owner.test.ts index 557a5c16b1a..caea3fb4ad4 100644 --- a/src/renderer/src/lib/worktree-runtime-owner.test.ts +++ b/src/renderer/src/lib/worktree-runtime-owner.test.ts @@ -644,6 +644,12 @@ describe('getKnownExecutionHostIdForWorktree', () => { it.each([ ['an ownerless repo row', { repos: [{ id: 'r' }] }, 'r::wt', 'local'], ['an SSH repo row', { repos: [{ id: 'r', connectionId: 'box' }] }, 'r::wt', 'ssh:box'], + [ + 'a runtime repo row', + { repos: [{ id: 'r', executionHostId: 'runtime:env-1' }] }, + 'r::wt', + 'runtime:env-1' + ], [ 'a per-worktree host', { worktreesByRepo: { r: [{ id: 'r::wt', repoId: 'r', hostId: 'ssh:box' }] } }, diff --git a/src/renderer/src/runtime/remote-runtime-terminal-snapshot-state.ts b/src/renderer/src/runtime/remote-runtime-terminal-snapshot-state.ts index 852d2e57b78..d47bbff92e4 100644 --- a/src/renderer/src/runtime/remote-runtime-terminal-snapshot-state.ts +++ b/src/renderer/src/runtime/remote-runtime-terminal-snapshot-state.ts @@ -171,10 +171,9 @@ export function isTerminalDriverState( if (!value || typeof value !== 'object' || !('kind' in value)) { return false } - const driver = value as { kind?: unknown; clientId?: unknown } return ( - driver.kind === 'idle' || - driver.kind === 'desktop' || - (driver.kind === 'mobile' && typeof driver.clientId === 'string') + value.kind === 'idle' || + value.kind === 'desktop' || + (value.kind === 'mobile' && 'clientId' in value && typeof value.clientId === 'string') ) } diff --git a/src/renderer/src/runtime/runtime-git-client.ts b/src/renderer/src/runtime/runtime-git-client.ts index 54ceaaba281..32f7b33cb39 100644 --- a/src/renderer/src/runtime/runtime-git-client.ts +++ b/src/renderer/src/runtime/runtime-git-client.ts @@ -1,50 +1,49 @@ -import { getRuntimeGitScope as getRuntimeGitScopeImplementation } from './runtime-git-client-context' -import { - getRuntimeGitBranchCompare as getRuntimeGitBranchCompareImplementation, - getRuntimeGitBranchDiff as getRuntimeGitBranchDiffImplementation, - getRuntimeGitCommitCompare as getRuntimeGitCommitCompareImplementation, - getRuntimeGitCommitDiff as getRuntimeGitCommitDiffImplementation, - getRuntimeGitDiff as getRuntimeGitDiffImplementation +export { getRuntimeGitScope } from './runtime-git-client-context' +export { + getRuntimeGitBranchCompare, + getRuntimeGitBranchDiff, + getRuntimeGitCommitCompare, + getRuntimeGitCommitDiff, + getRuntimeGitDiff } from './runtime-git-diff-client' -import { - cancelRuntimeGenerateCommitMessage as cancelRuntimeGenerateCommitMessageImplementation, - cancelRuntimeGeneratePullRequestFields as cancelRuntimeGeneratePullRequestFieldsImplementation, - discoverRuntimeCommitMessageModels as discoverRuntimeCommitMessageModelsImplementation, - generateRuntimeCommitMessage as generateRuntimeCommitMessageImplementation, - generateRuntimePullRequestFields as generateRuntimePullRequestFieldsImplementation +export { + cancelRuntimeGenerateCommitMessage, + cancelRuntimeGeneratePullRequestFields, + discoverRuntimeCommitMessageModels, + generateRuntimeCommitMessage, + generateRuntimePullRequestFields } from './runtime-git-generation-client' -import { - getRuntimeGitConflictOperation as getRuntimeGitConflictOperationImplementation, - getRuntimeGitHistory as getRuntimeGitHistoryImplementation, - getRuntimeGitIgnoredPaths as getRuntimeGitIgnoredPathsImplementation, - getRuntimeGitStatus as getRuntimeGitStatusImplementation, - getRuntimeGitSubmoduleStatus as getRuntimeGitSubmoduleStatusImplementation, - setRuntimeGitStatusUpstreamRefWatch as setRuntimeGitStatusUpstreamRefWatchImplementation +export { + getRuntimeGitConflictOperation, + getRuntimeGitHistory, + getRuntimeGitIgnoredPaths, + getRuntimeGitStatus, + getRuntimeGitSubmoduleStatus, + setRuntimeGitStatusUpstreamRefWatch } from './runtime-git-status-client' -import { - abortRuntimeGitMerge as abortRuntimeGitMergeImplementation, - abortRuntimeGitRebase as abortRuntimeGitRebaseImplementation, - commitRuntimeGit as commitRuntimeGitImplementation, - fastForwardRuntimeGit as fastForwardRuntimeGitImplementation, - fetchRuntimeGit as fetchRuntimeGitImplementation, - getRuntimeGitUpstreamStatus as getRuntimeGitUpstreamStatusImplementation, - pullRuntimeGit as pullRuntimeGitImplementation, - pushRuntimeGit as pushRuntimeGitImplementation, - rebaseRuntimeGitFromBase as rebaseRuntimeGitFromBaseImplementation, - syncRuntimeGitForkDefaultBranch as syncRuntimeGitForkDefaultBranchImplementation +export { + abortRuntimeGitMerge, + abortRuntimeGitRebase, + commitRuntimeGit, + fastForwardRuntimeGit, + fetchRuntimeGit, + getRuntimeGitUpstreamStatus, + pullRuntimeGit, + pushRuntimeGit, + rebaseRuntimeGitFromBase, + syncRuntimeGitForkDefaultBranch } from './runtime-git-sync-client' -import { - bulkDiscardRuntimeGitPaths as bulkDiscardRuntimeGitPathsImplementation, - bulkStageRuntimeGitPaths as bulkStageRuntimeGitPathsImplementation, - bulkUnstageRuntimeGitPaths as bulkUnstageRuntimeGitPathsImplementation, - discardRuntimeGitPath as discardRuntimeGitPathImplementation, - getRuntimeGitRemoteCommitUrl as getRuntimeGitRemoteCommitUrlImplementation, - getRuntimeGitRemoteFileUrl as getRuntimeGitRemoteFileUrlImplementation, - stageRuntimeGitPath as stageRuntimeGitPathImplementation, - stageRuntimeGitWorktreeScope as stageRuntimeGitWorktreeScopeImplementation, - unstageRuntimeGitPath as unstageRuntimeGitPathImplementation +export { + bulkDiscardRuntimeGitPaths, + bulkStageRuntimeGitPaths, + bulkUnstageRuntimeGitPaths, + discardRuntimeGitPath, + getRuntimeGitRemoteCommitUrl, + getRuntimeGitRemoteFileUrl, + stageRuntimeGitPath, + stageRuntimeGitWorktreeScope, + unstageRuntimeGitPath } from './runtime-git-working-tree-client' - export type { RuntimeGenerateCommitMessageOverrides, RuntimeGenerateCommitMessageResult, @@ -53,41 +52,3 @@ export type { RuntimeGitContext, RuntimePullRequestGenerationInput } from './runtime-git-client-context' - -export const getRuntimeGitScope = getRuntimeGitScopeImplementation -export const getRuntimeGitStatus = getRuntimeGitStatusImplementation -export const setRuntimeGitStatusUpstreamRefWatch = setRuntimeGitStatusUpstreamRefWatchImplementation -export const getRuntimeGitSubmoduleStatus = getRuntimeGitSubmoduleStatusImplementation -export const getRuntimeGitIgnoredPaths = getRuntimeGitIgnoredPathsImplementation -export const getRuntimeGitHistory = getRuntimeGitHistoryImplementation -export const getRuntimeGitConflictOperation = getRuntimeGitConflictOperationImplementation -export const abortRuntimeGitMerge = abortRuntimeGitMergeImplementation -export const abortRuntimeGitRebase = abortRuntimeGitRebaseImplementation -export const getRuntimeGitDiff = getRuntimeGitDiffImplementation -export const getRuntimeGitBranchCompare = getRuntimeGitBranchCompareImplementation -export const getRuntimeGitCommitCompare = getRuntimeGitCommitCompareImplementation -export const getRuntimeGitUpstreamStatus = getRuntimeGitUpstreamStatusImplementation -export const fetchRuntimeGit = fetchRuntimeGitImplementation -export const syncRuntimeGitForkDefaultBranch = syncRuntimeGitForkDefaultBranchImplementation -export const pullRuntimeGit = pullRuntimeGitImplementation -export const fastForwardRuntimeGit = fastForwardRuntimeGitImplementation -export const rebaseRuntimeGitFromBase = rebaseRuntimeGitFromBaseImplementation -export const pushRuntimeGit = pushRuntimeGitImplementation -export const getRuntimeGitBranchDiff = getRuntimeGitBranchDiffImplementation -export const getRuntimeGitCommitDiff = getRuntimeGitCommitDiffImplementation -export const commitRuntimeGit = commitRuntimeGitImplementation -export const generateRuntimeCommitMessage = generateRuntimeCommitMessageImplementation -export const discoverRuntimeCommitMessageModels = discoverRuntimeCommitMessageModelsImplementation -export const cancelRuntimeGenerateCommitMessage = cancelRuntimeGenerateCommitMessageImplementation -export const generateRuntimePullRequestFields = generateRuntimePullRequestFieldsImplementation -export const cancelRuntimeGeneratePullRequestFields = - cancelRuntimeGeneratePullRequestFieldsImplementation -export const stageRuntimeGitPath = stageRuntimeGitPathImplementation -export const bulkStageRuntimeGitPaths = bulkStageRuntimeGitPathsImplementation -export const stageRuntimeGitWorktreeScope = stageRuntimeGitWorktreeScopeImplementation -export const unstageRuntimeGitPath = unstageRuntimeGitPathImplementation -export const bulkUnstageRuntimeGitPaths = bulkUnstageRuntimeGitPathsImplementation -export const bulkDiscardRuntimeGitPaths = bulkDiscardRuntimeGitPathsImplementation -export const discardRuntimeGitPath = discardRuntimeGitPathImplementation -export const getRuntimeGitRemoteFileUrl = getRuntimeGitRemoteFileUrlImplementation -export const getRuntimeGitRemoteCommitUrl = getRuntimeGitRemoteCommitUrlImplementation diff --git a/src/renderer/src/runtime/web-session-tabs-sync/active-session-subscription.ts b/src/renderer/src/runtime/web-session-tabs-sync/active-session-subscription.ts index 94951d9702f..6115493f612 100644 --- a/src/renderer/src/runtime/web-session-tabs-sync/active-session-subscription.ts +++ b/src/renderer/src/runtime/web-session-tabs-sync/active-session-subscription.ts @@ -42,6 +42,7 @@ import { import { dispatchWebRuntimeInitialTerminalBootstrap } from '../web-runtime-initial-terminal-bootstrap-dispatch' import { toRuntimeWorktreeSelector } from '../runtime-worktree-selector' import type { SessionTabsStreamEvent } from './state' +import type { SessionTabsSnapshotHandler } from './visibility-resume-types' import { subscribeRuntimeEnvironment } from '../runtime-environment-pairing-refresh' type Ref = { current: T } @@ -52,30 +53,9 @@ export type ActiveSubscriptionArgs = { activeWorktreeRuntimeConnectionGeneration: number activeWorktreeRuntimePairingRevision: number | undefined workspaceSessionReady: boolean - visibilitySnapshotReceipt: Ref< - ( - environmentId: string, - snapshot: RuntimeMobileSessionTabsResult, - receivedFrame: number, - runtimeId?: string - ) => void - > - visibilitySnapshotApply: Ref< - ( - environmentId: string, - snapshot: RuntimeMobileSessionTabsResult, - receivedFrame: number, - runtimeId?: string - ) => boolean - > - visibilitySnapshotAccepted: Ref< - ( - environmentId: string, - snapshot: RuntimeMobileSessionTabsResult, - receivedFrame: number, - runtimeId?: string - ) => void - > + visibilitySnapshotReceipt: Ref> + visibilitySnapshotApply: Ref> + visibilitySnapshotAccepted: Ref> } /** Install the selected-worktree stream, which resumes immediately on visibility changes. */ diff --git a/src/renderer/src/runtime/web-session-tabs-sync/global-session-subscription.ts b/src/renderer/src/runtime/web-session-tabs-sync/global-session-subscription.ts index 49d179f3336..a0518b46df7 100644 --- a/src/renderer/src/runtime/web-session-tabs-sync/global-session-subscription.ts +++ b/src/renderer/src/runtime/web-session-tabs-sync/global-session-subscription.ts @@ -1,4 +1,3 @@ -import type { RuntimeMobileSessionTabsResult } from '../../../../shared/runtime-types' import { getRuntimeEnvironmentRevision } from '../runtime-environment-revision' import { clearWebSessionCloseIntentsForOwner } from '../web-session-close-intent' import { clearWebSessionFocusIntentsForOwner } from '../web-session-focus-intent' @@ -16,7 +15,10 @@ import { loadInitialWebSessionTabs } from './load-initial' import { handleGlobalSessionEvent } from './global-session-events' import { VisibilityResumeCoordinator } from './visibility-resume-coordinator' import type { VisibilityResumeOmission, SessionTabsStreamEvent } from './state' -import type { MirroredRuntimeEnvironment } from './visibility-resume-types' +import type { + MirroredRuntimeEnvironment, + SessionTabsSnapshotHandler +} from './visibility-resume-types' import { WEB_SESSION_TABS_VISIBILITY_RESUME_STAGGER_MS } from './state' import { subscribeRuntimeEnvironment } from '../runtime-environment-pairing-refresh' @@ -26,30 +28,9 @@ export type GlobalSubscriptionRefs = { activeRuntimeEnvironmentId: Ref activeRuntimeWorktreeKey: Ref visibilityResumeOmissions: Ref> - snapshotReceipt: Ref< - ( - environmentId: string, - snapshot: RuntimeMobileSessionTabsResult, - receivedFrame: number, - runtimeId?: string - ) => void - > - snapshotApply: Ref< - ( - environmentId: string, - snapshot: RuntimeMobileSessionTabsResult, - receivedFrame: number, - runtimeId?: string - ) => boolean - > - snapshotAccepted: Ref< - ( - environmentId: string, - snapshot: RuntimeMobileSessionTabsResult, - receivedFrame: number, - runtimeId?: string - ) => void - > + snapshotReceipt: Ref> + snapshotApply: Ref> + snapshotAccepted: Ref> ownerRevisions: Ref> } diff --git a/src/renderer/src/runtime/web-session-tabs-sync/visibility-resume-types.ts b/src/renderer/src/runtime/web-session-tabs-sync/visibility-resume-types.ts index 11f1aa23eb5..e0d550b0f19 100644 --- a/src/renderer/src/runtime/web-session-tabs-sync/visibility-resume-types.ts +++ b/src/renderer/src/runtime/web-session-tabs-sync/visibility-resume-types.ts @@ -4,6 +4,13 @@ import type { } from '../../../../shared/runtime-types' import type { TrackedWebSessionTabsWorktree } from './state' +export type SessionTabsSnapshotHandler = ( + environmentId: string, + snapshot: RuntimeMobileSessionTabsResult, + receivedFrame: number, + runtimeId?: string +) => R + export type VisibilityResumeEnvironment = { trackedWorktrees: readonly TrackedWebSessionTabsWorktree[] inventoryReceived: boolean