refactor(renderer-terminal-runtime): clean up Phase 3-3.8 code (#26900)

- One helper plans and runs a paste into a terminal pane (five copies before)
- Reuse getShortcutPlatform instead of three private copies
- runtime-git-client becomes plain re-exports
- TerminalErrorToast builds each token's test/replace regex from one helper
- Reuse PreconnectInputWriter for the recovery input hold
- Name the session-tabs snapshot callback type once
- Narrow isTerminalDriverState with 'in' instead of a cast
- Inline the single-use terminal-pane-client-host wrapper

Co-authored-by: m4air <m4air@Mac.localdomain>
This commit is contained in:
OrcaWin
2026-10-09 12:09:56 -07:00
committed by GitHub
co-authored by m4air
parent 150bf49cb2
commit cce31b4b4d
18 changed files with 265 additions and 422 deletions
@@ -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<boolean>
}): 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)
@@ -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 (
<>
@@ -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
@@ -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<boolean>
continuesAfterFailedWrite?: () => boolean
}
// Why: release() always supplies an accepted-write path, so the hold requires what the buffer only accepts.
type HeldInputWriter = PreconnectInputWriter &
Required<Pick<PreconnectInputWriter, 'sendInputAccepted'>>
export type RemoteRuntimeRecoveryInputHold = {
isHolding: () => boolean
@@ -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)
})
})
@@ -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
}
@@ -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
@@ -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<typeof pasteTerminalText>[0] }
text: string
source: TerminalPasteSource
ptyId: string | null
runtime: TerminalPasteRuntime
transport: Parameters<typeof writeTerminalPastePtyInput>[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<TerminalPasteExecutionResult> {
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
})
}
@@ -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))
@@ -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()
})
}
@@ -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<TerminalPasteExecutionResult> {
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
})
@@ -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
})
@@ -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' }] } },
@@ -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')
)
}
+41 -80
View File
@@ -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
@@ -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<T> = { 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<SessionTabsSnapshotHandler<void>>
visibilitySnapshotApply: Ref<SessionTabsSnapshotHandler<boolean>>
visibilitySnapshotAccepted: Ref<SessionTabsSnapshotHandler<void>>
}
/** Install the selected-worktree stream, which resumes immediately on visibility changes. */
@@ -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<string | null>
activeRuntimeWorktreeKey: Ref<string | null>
visibilityResumeOmissions: Ref<Map<string, VisibilityResumeOmission>>
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<SessionTabsSnapshotHandler<void>>
snapshotApply: Ref<SessionTabsSnapshotHandler<boolean>>
snapshotAccepted: Ref<SessionTabsSnapshotHandler<void>>
ownerRevisions: Ref<Map<string, number | undefined>>
}
@@ -4,6 +4,13 @@ import type {
} from '../../../../shared/runtime-types'
import type { TrackedWebSessionTabsWorktree } from './state'
export type SessionTabsSnapshotHandler<R> = (
environmentId: string,
snapshot: RuntimeMobileSessionTabsResult,
receivedFrame: number,
runtimeId?: string
) => R
export type VisibilityResumeEnvironment = {
trackedWorktrees: readonly TrackedWebSessionTabsWorktree[]
inventoryReceived: boolean