diff --git a/src/renderer/src/components/terminal-pane/pty-connection-agent-session-resume.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-agent-session-resume.test.ts index 422585644d9..c93294ba2f9 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-agent-session-resume.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-agent-session-resume.test.ts @@ -210,7 +210,14 @@ describe('connectPanePty', () => { await flushAsyncTicks(20) await new Promise((resolve) => setTimeout(resolve, 70)) + const reattachArgs = transport.connect.mock.calls.find( + ([args]) => args.sessionId === 'lost-pty' + )?.[0] + const launchToken = (reattachArgs?.env as Record | undefined) + ?.ORCA_AGENT_LAUNCH_TOKEN + expect(transport.sendInput).not.toHaveBeenCalled() + expect(launchToken).toMatch(new RegExp(`^${UUID_RE}$`)) expect(transport.connect).toHaveBeenCalledWith( expect.objectContaining({ sessionId: 'lost-pty', @@ -221,13 +228,13 @@ describe('connectPanePty', () => { ORCA_TAB_ID: 'tab-1', ORCA_WORKTREE_ID: 'wt-1', ORCA_WORKSPACE_ID: 'wt-1', - ORCA_AGENT_LAUNCH_TOKEN: expect.stringMatching(new RegExp(`^${UUID_RE}$`)) + ORCA_AGENT_LAUNCH_TOKEN: launchToken }) }) ) expect(mockStoreState.registerAgentLaunchConfig).toHaveBeenCalledWith(paneKey, launchConfig, { agentType: 'codex', - launchToken: expect.stringMatching(new RegExp(`^${UUID_RE}$`)), + launchToken, tabId: 'tab-1', leafId: LEAF_1 }) 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 a62855f6fa7..48da2e819c6 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 @@ -22,14 +22,17 @@ import { isRemoteRuntimePtyId } from './paired-parked-terminal-restore' * managing input, output, title synchronization, and agent status tracking. */ -import type { ConnectPanePtySession } from './connect-pane-pty-session' import type { ReattachPayloadContext } from './reattach-payload-context' +import type { ReattachPayloadSession } from './reattach-payload-session' -export function bindApplyReattachPayload( - session: ConnectPanePtySession, +export function createReattachPayloadHandlers( + session: ReattachPayloadSession, ctx: ReattachPayloadContext -): void { - session.applyReattachPayload = async (): Promise => { +): { + applyReattachPayload: () => Promise + fitAfterReattachRestore: () => Promise +} { + const applyReattachPayload = async (): Promise => { if (!ctx.isCurrentReattachPayload()) { return } @@ -280,7 +283,7 @@ export function bindApplyReattachPayload( } } - session.fitAfterReattachRestore = async (): Promise => { + const fitAfterReattachRestore = async (): Promise => { if (!ctx.isCurrentReattachPayload()) { return } @@ -312,4 +315,6 @@ export function bindApplyReattachPayload( window.api.pty.signal(reattachPtyId, 'SIGWINCH') } } + + return { applyReattachPayload, fitAfterReattachRestore } } diff --git a/src/renderer/src/components/terminal-pane/pty-connection/cold-restore-resume-startup.ts b/src/renderer/src/components/terminal-pane/pty-connection/cold-restore-resume-startup.ts index e328bed1bc9..14818e2af80 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/cold-restore-resume-startup.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/cold-restore-resume-startup.ts @@ -56,7 +56,7 @@ export function bindBuildColdRestoreAgentResumeStartup(session: ConnectPanePtySe const launchConfig = (useLiveEntry && entry ? state.getAgentLaunchConfigForStatusEntry(entry) : undefined) ?? matchingSleepingLaunchConfig - // Why: the resume line is typed into this session.pane's live shell, so its quoting must + // Why: the resume line is typed into this pane's live shell, so its quoting must // follow the tab's effective Windows shell, not the win32 PowerShell default. const resumeTarget = resolveAgentResumeLaunchTarget({ projectRuntime: session.projectRuntime, 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 76bd8714cb9..713d14719bc 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 @@ -6,9 +6,8 @@ import { isRuntimeOwnedSshTargetId } from '../../../../../shared/execution-host' import { resolveSshPaneConnectGate } from '../ssh-pane-connect-gate' import { - type UserInitiatedSshConnectOutcome, isSshSessionExpiredError, - sshPromptConnectOutcomeForStatus, + waitForUserInitiatedSshConnect, waitForSshConnection } from './ssh-session-connect' import { isRemoteRuntimePtyId } from './paired-parked-terminal-restore' @@ -84,63 +83,7 @@ export function runDeferredSessionAttach(session: ConnectPanePtySession): void { if (!alreadyConnected) { // Wait for the user-driven connect (sidebar card control or terminal reconnect overlay → passphrase → ssh.connect) to complete. // Why: resolve on terminal-failure statuses too ('auth-failed'/'error'/'reconnection-failed') so it can't hang forever if the user cancels or the connect fails. - const outcome = await new Promise((resolve) => { - // Why: 'disconnected' counts as terminal only after a non-disconnected status was seen (a real connect attempt that returned to 'disconnected'). - // Treating the entry-time 'disconnected' as terminal would skip the gate, defeating the passphrase-prompt deferral. - let sawNonDisconnected = - useAppStore.getState().sshConnectionStates.get(session.connectionId)?.status !== - 'disconnected' && - useAppStore.getState().sshConnectionStates.get(session.connectionId)?.status !== - undefined - let resolvedOutcome: UserInitiatedSshConnectOutcome = 'cancelled' - let settled = false - const finish = (nextOutcome: UserInitiatedSshConnectOutcome): void => { - if (settled) { - return - } - resolvedOutcome = nextOutcome - settled = true - unsub() - const idx = session.waitTeardowns.indexOf(teardown) - if (idx !== -1) { - session.waitTeardowns.splice(idx, 1) - } - resolve(resolvedOutcome) - } - const teardown = (): void => finish('cancelled') - // Why: register a teardown so dispose() can unsubscribe+resolve if the session.pane is torn down mid-wait. - // Else the zustand subscriber + async IIFE leak: the callback only checks `session.disposed` when it next fires, which may never happen. - session.waitTeardowns.push(teardown) - const unsub = useAppStore.subscribe((state) => { - if (session.disposed) { - finish('cancelled') - return - } - const status = state.sshConnectionStates.get(session.connectionId)?.status - if (status && status !== 'disconnected') { - sawNonDisconnected = true - } - const nextOutcome = sshPromptConnectOutcomeForStatus(status, sawNonDisconnected) - if (nextOutcome) { - finish(nextOutcome) - } - }) - // Why: re-read state after subscribing to catch a status change that landed between the alreadyConnected check and the subscribe — else we'd wait forever. - if (session.disposed) { - finish('cancelled') - return - } - const currentStatus = useAppStore - .getState() - .sshConnectionStates.get(session.connectionId)?.status - const currentOutcome = sshPromptConnectOutcomeForStatus( - currentStatus, - sawNonDisconnected - ) - if (currentOutcome) { - finish(currentOutcome) - } - }) + const outcome = await waitForUserInitiatedSshConnect(session) if (session.disposed || !session.capturedDirectSshRetryLeaseMatches()) { return } 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 3e03d02f440..2caa6d401e8 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 @@ -32,7 +32,7 @@ export function runDeferredSessionReattachChoice(session: ConnectPanePtySession) )?.ptyId const hasSleepingAgentSession = Boolean(session.getSleepingRecordForPane(storeSnapshot)) - // Why: the tab-level fallback must not steal a PTY a setup sibling already published while the main session.pane waited for split geometry. + // Why: the tab-level fallback must not steal a PTY a setup sibling already published while the main pane waited for split geometry. const tabFallbackPtyId = existingPtyId && !Array.from(session.deps.paneTransportsRef.current.entries()).some( @@ -82,7 +82,7 @@ export function runDeferredSessionReattachChoice(session: ConnectPanePtySession) !isRemoteRuntimePtyId(candidateReattachSessionId) && getEagerPtyBufferHandle(candidateReattachSessionId) ) - // Why: a still-live locally-spawned PTY (e.g. a background automation agent) keeps an eager buffer until a session.pane adopts it. + // Why: a still-live locally-spawned PTY (e.g. a background automation agent) keeps an eager buffer until a pane adopts it. // It must be adopted via attach()+replay — connect({ sessionId }) on its non-session ptyId would spawn a fresh shell and orphan the agent. const eagerLivePtyId = candidateReattachSessionId && @@ -135,7 +135,7 @@ export function runDeferredSessionReattachChoice(session: ConnectPanePtySession) useAppStore.getState().removeDeferredSshSessionId(session.deps.tabId) } } else { - // Why: surface synchronous attach failures via session.reportError so the session.pane shows a diagnostic instead of a blank surface. + // Why: surface synchronous attach failures via session.reportError so the pane shows a diagnostic instead of a blank surface. // On throw, clear the stale ptyId from the tab and fresh-spawn — else the next remount reads the same dead id and loops here. try { session.clearPaneMode2031State() @@ -177,7 +177,7 @@ export function runDeferredSessionReattachChoice(session: ConnectPanePtySession) } if (!spawnedPtyId) { // Why: React StrictMode can mount+spawn then immediately remount; if the first mount produced no PTY id, - // the remounted session.pane must issue its own spawn instead of attaching to a completed-but-empty promise (a dead surface). + // the remounted pane must issue its own spawn instead of attaching to a completed-but-empty promise (a dead surface). if (!isWebTerminalSurfaceTabId(session.deps.tabId)) { console.warn( `Pending PTY spawn for tab ${session.deps.tabId} resolved without a PTY id, retrying fresh spawn` @@ -203,7 +203,7 @@ export function runDeferredSessionReattachChoice(session: ConnectPanePtySession) callbacks: outputCallbacks.callbacks }) const attachedPtyId = session.transport.getPtyId() ?? spawnedPtyId - // Why: this reuses a PTY spawned by an earlier mount, so no later spawn event will bind this remounted session.pane's DOM/container. + // Why: this reuses a PTY spawned by an earlier mount, so no later spawn event will bind this remounted pane's DOM/container. session.bindActivePanePty(attachedPtyId, { updateTabPtyId: 'if-missing', sampleVisibleForegroundAgent: true 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 cd397227bcc..b40e09ec1a4 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 @@ -149,7 +149,7 @@ export function installDirectSshRetryStatus(session: ConnectPanePtySession): voi }) : undefined session.shouldOwnAgentStatusInRenderer = session.runtimeEnvironmentId !== null - // Why: the host also mirrors agent status for this pane through session.tabs. + // Why: the host also mirrors agent status for this pane through tabs. // Claiming here (decided once at transport creation, like the side-effect // authority below) lets the mirror keep this renderer's byte-derived status // instead of overwriting/deleting it on every republication. 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 51e8c564c96..b6f587f6872 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 @@ -66,7 +66,7 @@ export function bindForegroundOutputRefresh(session: ConnectPanePtySession): voi }) // Why: dropped bytes invalidate cross-chunk carry — a partial OSC-9999 prefix spanning the gap would corrupt the next live chunk. session.transport.resetCrossChunkParserState?.() - // Why gated (rc.7.perf loop): on a visible session.pane these markers come from our own restore starving ACKs; re-arming per marker kept the fetch loop alive all flood, so defer to one post-flood repaint. + // Why gated (rc.7.perf loop): on a visible pane these markers come from our own restore starving ACKs; re-arming per marker kept the fetch loop alive all flood, so defer to one post-flood repaint. if (session.isForegroundRestoreBackpressureContext()) { session.noteHiddenOutputRestoreFloodBackpressure() return @@ -82,7 +82,7 @@ export function bindForegroundOutputRefresh(session: ConnectPanePtySession): voi // session.buildMainModelSnapshotReplayWrites, which grounds the pen itself, so // nothing is lost by skipping it here. session.writePtyOutputToXterm(RESET_AFTER_BYTE_GAP, true) - // Why: a marker during an in-flight restore means that snapshot may predate the drop, so a fresh one must follow; capture BEFORE the mark, which starts a restore synchronously on a visible session.pane. + // Why: a marker during an in-flight restore means that snapshot may predate the drop, so a fresh one must follow; capture BEFORE the mark, which starts a restore synchronously on a visible pane. const restoreWasInFlight = session.hiddenOutputRestoreInFlight !== null session.markHiddenOutputRestoreNeeded() if (restoreWasInFlight) { @@ -223,7 +223,7 @@ export function bindForegroundOutputRefresh(session: ConnectPanePtySession): voi session.isLatencySensitiveForegroundOutput = function (data: string): boolean { if (!session.isActiveSplitPane()) { - // Why: many visible split panes each emit tiny TUI frames; a shared budget keeps them live without letting aggregate xterm work starve typing in the active session.pane. + // Why: many visible split panes each emit tiny TUI frames; a shared budget keeps them live without letting aggregate xterm work starve typing in the active pane. if (data.includes('\x1b[')) { return false } diff --git a/src/renderer/src/components/terminal-pane/pty-connection/fresh-spawn-follow-reset.ts b/src/renderer/src/components/terminal-pane/pty-connection/fresh-spawn-follow-reset.ts index 4c8ce6902c7..b1fb8d1d642 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/fresh-spawn-follow-reset.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/fresh-spawn-follow-reset.ts @@ -142,7 +142,7 @@ export function bindFreshSpawnFollowReset(session: ConnectPanePtySession): void isAlternateScreen?: boolean ): string => { // Why a cold restore overrides the agent signal: liveness is read from the - // session.pane's status and title, both of which are persisted, so after a cold + // pane's status and title, both of which are persisted, so after a cold // restore they describe the process that died. Preserving "its" modes arms // mouse, focus and paste reporting against the fresh shell that replaces it, // which then prints the reports as junk at the prompt (#12101). @@ -152,7 +152,7 @@ export function bindFreshSpawnFollowReset(session: ConnectPanePtySession): void if (session.shouldPreserveAgentReattachModes()) { return buildPostReplayLiveAgentReattachReset(payload) } - // Why: an alt-screen session.pane is a live TUI Orca just does not recognise as an agent, and the + // Why: an alt-screen pane is a live TUI Orca just does not recognise as an agent, and the // replay already re-armed its mouse modes — keep them instead of wiping them (#8291). return (isAlternateScreen ?? session.kittyKeyboardModes.isAlternateScreen) ? POST_REPLAY_REATTACH_RESET_KEEP_MOUSE @@ -173,7 +173,7 @@ export function bindFreshSpawnFollowReset(session: ConnectPanePtySession): void return } // Why: fresh Windows ConPTY output paints at screen coordinates, so - // restored session.rows must leave the viewport before the first prompt redraw. + // restored rows must leave the viewport before the first prompt redraw. session.writeFreshShellViewportBlanking() } } 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 2672290c1d0..c722170dc6b 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 @@ -32,7 +32,7 @@ export function bindStartFreshSpawn(session: ConnectPanePtySession): void { if (useAppStore.getState().deleteStateByWorktreeId?.[session.deps.worktreeId]?.isDeleting) { // Why: the worktree is being deleted; its PTYs were just killed for the // filesystem teardown. A fresh shell must not spawn into a directory the - // removal is about to delete (main fences it anyway), and the session.pane is + // removal is about to delete (main fences it anyway), and the pane is // about to unmount — so skip the doomed respawn instead of racing it. return Promise.resolve(null) } @@ -158,7 +158,7 @@ export function bindStartFreshSpawn(session: ConnectPanePtySession): void { typeof spawnedPtyId === 'object' && spawnedPtyId.agentResumeUnavailable ) { - // Why: main dropped the resume argv, so this session.pane is a NEW session — + // Why: main dropped the resume argv, so this pane is a NEW session — // the plain restored banner would claim the old one came back. session.showSessionRestoredBanner('resume-unavailable') } else if (coldRestoreOverride?.hasSleepingRecord) { @@ -171,7 +171,7 @@ export function bindStartFreshSpawn(session: ConnectPanePtySession): void { ) { // Why: delayed draft/follow-up delivery keys off this launch // registry. If spawn produced no PTY, the launch is no longer a - // viable delivery target and must not wait for a future session.pane. + // viable delivery target and must not wait for a future pane. session.clearRegisteredStartupLaunchConfig() } if ( @@ -183,7 +183,7 @@ export function bindStartFreshSpawn(session: ConnectPanePtySession): void { session.transport.getPtyId() === resolvedPtyId ) { // Why: daemon createOrAttach can turn an apparent fresh spawn into - // a reattach; the session.transport skips onPtySpawn there to preserve recency. + // a reattach; the transport skips onPtySpawn there to preserve recency. session.bindActivePanePty(resolvedPtyId, { updateTabPtyId: 'if-missing', sampleVisibleForegroundAgent: true @@ -250,8 +250,8 @@ export function bindStartFreshSpawn(session: ConnectPanePtySession): void { session.settleDirectSshPaneRetryAttempt(session.directSshRetryAttempt, 'failed') }) }) - // Why: split panes in the same tab can spawn concurrently. Key by session.pane - // as well as tab so a remount cannot attach to a sibling setup session.pane's PTY. + // Why: split panes in the same tab can spawn concurrently. Key by pane + // as well as tab so a remount cannot attach to a sibling setup pane's PTY. pendingSpawnByPaneKey.set(session.pendingSpawnKey, trackedPromise) return trackedPromise } diff --git a/src/renderer/src/components/terminal-pane/pty-connection/hidden-output-restore-chunk.ts b/src/renderer/src/components/terminal-pane/pty-connection/hidden-output-restore-chunk.ts index dd1c127dbee..c4ae1a11eb0 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/hidden-output-restore-chunk.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/hidden-output-restore-chunk.ts @@ -136,14 +136,4 @@ export function bindHiddenOutputRestoreChunk(session: ConnectPanePtySession): vo meta: { ...meta, rawLength: sliced.length } } } - - /** - * Apply an authoritative snapshot to the session.pane's kitty mirror in the order: - * unproven reset, replay-semantics scan of the snapshot - * bytes (so screen selection lands first), then the owner's proven flags. - * - * Why the reset only happens when the owner proved something: an old host - * omits the field, and downgrading a mirror that is already tracking live - * output would lose correct state instead of preserving it. - */ } diff --git a/src/renderer/src/components/terminal-pane/pty-connection/hidden-output-restore-request.ts b/src/renderer/src/components/terminal-pane/pty-connection/hidden-output-restore-request.ts index 934e27562b7..890253448e8 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/hidden-output-restore-request.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/hidden-output-restore-request.ts @@ -39,9 +39,9 @@ export function bindHiddenOutputRestoreRequest(session: ConnectPanePtySession): session.requestHiddenOutputRestoreIfNeeded = function (opts?: { bypassScheduler?: boolean }): boolean { - // Why: once the write pipeline is probe-certified dead a restore can never parse; recovery owns the session.pane and the remount gets a fresh xterm + restore. + // Why: once the write pipeline is probe-certified dead a restore can never parse; recovery owns the pane and the remount gets a fresh xterm + restore. if (isTerminalWritePipelineCertifiedDead(session.pane.terminal)) { - // Why the re-kick: certification's recovery request can be budget-declined or cancelled by a sibling remount; without this, a revealed dead session.pane keeps the stale frame forever. + // Why the re-kick: certification's recovery request can be budget-declined or cancelled by a sibling remount; without this, a revealed dead pane keeps the stale frame forever. if (!session.certifiedDeadRestoreRecoveryRequested && !session.disposed) { session.certifiedDeadRestoreRecoveryRequested = true const storePtyId = useAppStore.getState().ptyIdsByTabId?.[session.deps.tabId]?.[0] ?? null @@ -121,7 +121,7 @@ export function bindHiddenOutputRestoreRequest(session: ConnectPanePtySession): if (session.hiddenOutputRestorePtyId === currentPtyId) { session.clearHiddenOutputRestoreState() } - // Remote-only path: the session.transport swapped PTYs mid-restore, which is a + // Remote-only path: the transport swapped PTYs mid-restore, which is a // stream change, not proof the hidden bytes are unrecoverable. if ( !session.rearmRemoteHiddenOutputRestoreInsteadOfWarning( @@ -310,7 +310,7 @@ export function bindHiddenOutputRestoreRequest(session: ConnectPanePtySession): typeof document.removeEventListener === 'function' ) { const onDocumentVisibilityChange = (): void => { - // Why: document hide/show flips the foreground predicate with no session.pane lifecycle event; re-sync the hidden-delivery gate both ways. + // Why: document hide/show flips the foreground predicate with no pane lifecycle event; re-sync the hidden-delivery gate both ways. session.syncHiddenRendererPtyDelivery() if (shouldWritePtyOutputForeground(session.deps.isVisibleRef.current)) { session.requestHiddenOutputRestoreIfNeeded() 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 d74b38319e4..16240a9f605 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 @@ -126,14 +126,14 @@ export function bindHiddenOutputRestoreSnapshot(session: ConnectPanePtySession): snapshot.cols, readProposedTerminalCols(session.pane) ) - // Why: an imageless success is not proof the session.pane is empty; normal replay clears screen and scrollback. + // Why: an imageless success is not proof the pane is empty; normal replay clears screen and scrollback. const snapshotCarriesNoImage = snapshot.alternateScreen !== true && snapshot.data === '' && !snapshot.scrollbackAnsi if (snapshotCarriesNoImage) { // Why still ground: a restore only runs because bytes were dropped, so // the gap's pen outlives a snapshot that cannot repaint over it. The // live-path constant, not the replay baseline — nothing repaints here, - // so the session.pane is handed back to whatever is still running. + // so the pane is handed back to whatever is still running. session.writeReplayData(RESET_AFTER_BYTE_GAP) } else { for (const replayChunk of buildMainModelSnapshotReplayWrites(snapshot, { diff --git a/src/renderer/src/components/terminal-pane/pty-connection/hidden-output-seq-and-skip.ts b/src/renderer/src/components/terminal-pane/pty-connection/hidden-output-seq-and-skip.ts index 8d0bed4d294..fe72aaceb6d 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/hidden-output-seq-and-skip.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/hidden-output-seq-and-skip.ts @@ -129,6 +129,15 @@ export function bindHiddenOutputSeqAndSkip(session: ConnectPanePtySession): void bindHiddenStartupRendererQueryWrite(session) bindHiddenOutputRestoreChunk(session) + /** + * Apply an authoritative snapshot to the pane's kitty mirror in the order: + * unproven reset, replay-semantics scan of the snapshot + * bytes (so screen selection lands first), then the owner's proven flags. + * + * Why the reset only happens when the owner proved something: an old host + * omits the field, and downgrading a mirror that is already tracking live + * output would lose correct state instead of preserving it. + */ session.applySnapshotKittyKeyboardModes = function ( snapshotData: string, snapshot: { kittyKeyboardFlags?: number; snapshotSeq?: number } @@ -152,7 +161,7 @@ export function bindHiddenOutputSeqAndSkip(session: ConnectPanePtySession): void session.kittyKeyboardModes.resetForSnapshot() session.kittyKeyboardModes.scanReplay(snapshotData) session.kittyKeyboardModes.restoreSnapshotFlags(proven) - // Why in the same critical section: without the baseline a quiet session.pane + // Why in the same critical section: without the baseline a quiet pane // could not publish a coherent snapshot until unrelated output arrived. session.recordRendererOrderedSeq({ seq: snapshot.snapshotSeq }) } 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 2cf8604c24d..b8e2750123f 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 @@ -89,7 +89,7 @@ export function bindSerializeHiddenOutputSnapshot(session: ConnectPanePtySession } // ── Hidden-delivery gate sync (Phase 4) ───────────────────────────── - // Why: marks this session.pane's PTY hidden in main while no visible view needs + // Why: marks this pane's PTY hidden in main while no visible view needs // its bytes; main then drops delivery after model ingestion and reveal // restores from the snapshot. The marked id is tracked locally so PTY // changes (reattach/restart) can never leave a stale id gated. @@ -102,8 +102,8 @@ export function bindSerializeHiddenOutputSnapshot(session: ConnectPanePtySession return Date.now() < session.hiddenOutputRestoreFloodSuppressedUntil } - // True when a drop/gap signal on a visible session.pane is attributable to this - // session.pane's OWN restore backpressure (a restore is replaying right now, or + // True when a drop/gap signal on a visible pane is attributable to this + // pane's OWN restore backpressure (a restore is replaying right now, or // one was just cut off for outrunning the stream). Such signals must not // re-arm restores — that is the rc.7.perf feedback loop. session.isForegroundRestoreBackpressureContext = function (): boolean { diff --git a/src/renderer/src/components/terminal-pane/pty-connection/live-data-callback.ts b/src/renderer/src/components/terminal-pane/pty-connection/live-data-callback.ts index a5c26d98d2d..724c7553abf 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/live-data-callback.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/live-data-callback.ts @@ -48,7 +48,7 @@ export function bindLiveDataCallback(session: ConnectPanePtySession): void { session.resetHiddenOutputRestoreIfPtyChanged() session.observeLiveMode2031Chunk(data) if (meta?.droppedOutput === true) { - // Why gated (rc.7.perf loop): a visible session.pane's cap-drop during its own restore is self-caused backpressure; defer to one post-flood repaint instead of re-arming per sentinel. + // Why gated (rc.7.perf loop): a visible pane's cap-drop during its own restore is self-caused backpressure; defer to one post-flood repaint instead of re-arming per sentinel. if (meta?.background !== true && session.isForegroundRestoreBackpressureContext()) { session.noteHiddenOutputRestoreFloodBackpressure() } else { @@ -75,7 +75,7 @@ export function bindLiveDataCallback(session: ConnectPanePtySession): void { if (codexBackfillNotice) { session.reportError(codexBackfillNotice) } - // Why: split panes have visible-but-inactive panes the user watches; throttle only when the session.pane or whole document is hidden. + // Why: split panes have visible-but-inactive panes the user watches; throttle only when the pane or whole document is hidden. const foreground = shouldWritePtyOutputForeground(session.deps.isVisibleRef.current) && meta?.background !== true // Why: latch the hidden-delivery gate from the byte path too, covering a PTY id that arrives after the initial sync (no-op when current). @@ -94,7 +94,7 @@ export function bindLiveDataCallback(session: ConnectPanePtySession): void { session.clearRestoredSnapshotBaseline() // fall through with the ORIGINAL data/meta — post-gap bytes are new } else { - // Why: capture in-flight BEFORE the mark — on a visible session.pane the mark starts the restore synchronously and must not flag itself. + // Why: capture in-flight BEFORE the mark — on a visible pane the mark starts the restore synchronously and must not flag itself. const restoreWasInFlight = session.hiddenOutputRestoreInFlight !== null session.markHiddenOutputRestoreNeeded() if (restoreWasInFlight) { diff --git a/src/renderer/src/components/terminal-pane/pty-connection/pane-serializer-register.ts b/src/renderer/src/components/terminal-pane/pty-connection/pane-serializer-register.ts index 8c9cf157174..81279e9f62d 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/pane-serializer-register.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/pane-serializer-register.ts @@ -58,7 +58,7 @@ export function bindRegisterPaneSerializer(session: ConnectPanePtySession): void }) const orderedSeq = session.rendererOrderedPtyId === ptyId ? session.rendererOrderedSeq : null - // Why snapshotFlags and not `flags`: this session.pane may itself have + // Why snapshotFlags and not `flags`: this pane may itself have // consumed an old-host snapshot that proved nothing, and its // conservative `0` fallback must not be republished downstream as // a host-proven inactive protocol. 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 13510d5cf0d..ee450fadcbd 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 @@ -41,7 +41,7 @@ export function bindSettlePaneSerializer(session: ConnectPanePtySession): void { return } } catch { - // Clear below so a failed parser/replay cannot leave the session.pane generation pending. + // Clear below so a failed parser/replay cannot leave the pane generation pending. } await window.api.pty.clearPendingPaneSerializer(session.cacheKey, generation).catch(() => {}) } @@ -128,12 +128,12 @@ export function bindSettlePaneSerializer(session: ConnectPanePtySession): void { useAppStore.getState(), session.deps.worktreeId ) - // Why: xterm focus reports share this session.transport queue. Bypassing it can + // Why: xterm focus reports share this transport queue. Bypassing it can // race CSI I against the draft on ConPTY and expose a literal `[I` prefix. void sendAgentDraftPasteContent(settings, ptyId, session.startupDraftPrompt, async (data) => { const accepted = await writeTerminalPastePtyInput(session.transport, data) if (accepted && !startupDraftInputRecorded) { - // Why: this session.transport write bypasses xterm's user-input signal; keep + // Why: this transport write bypasses xterm's user-input signal; keep // the composed draft from being discarded by later hibernation. startupDraftInputRecorded = true session.recordTerminalInputForHibernation() @@ -221,7 +221,7 @@ export function bindSettlePaneSerializer(session: ConnectPanePtySession): void { let sessionRestoredBannerShown: SessionRestoredBannerReason | null = null session.showSessionRestoredBanner = (reason: SessionRestoredBannerReason = 'restored'): void => { // Why: a plain 'restored' banner must not latch out the later 'resume-unavailable' - // upgrade — the session.pane would keep claiming a session it never got back. + // upgrade — the pane would keep claiming a session it never got back. if ( sessionRestoredBannerShown === reason || sessionRestoredBannerShown === 'resume-unavailable' diff --git a/src/renderer/src/components/terminal-pane/pty-connection/reattach-payload-context.test.ts b/src/renderer/src/components/terminal-pane/pty-connection/reattach-payload-context.test.ts new file mode 100644 index 00000000000..51041decf04 --- /dev/null +++ b/src/renderer/src/components/terminal-pane/pty-connection/reattach-payload-context.test.ts @@ -0,0 +1,106 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { createPane } from '../pty-connection-test-pane-fixtures' +import { + installTerminalTestGlobals, + restoreTerminalTestGlobals +} from '../pty-connection-test-environment' +import { createReattachPayloadHandlers } from './apply-reattach-payload' +import type { ColdRestoreAgentResumeStartup } from './fresh-spawn-types' +import type { ReattachPayloadContext } from './reattach-payload-context' +import type { ReattachPayloadSession } from './reattach-payload-session' + +function createSession(overrides: Record = {}): ReattachPayloadSession { + return { + pane: createPane(1), + rememberReattachPayloadAgentSignal: vi.fn(), + writeReplayData: vi.fn(), + reattachReplayResetSequence: vi.fn(() => ''), + sendFocusedReattachFocusInAfterReplay: vi.fn(), + kittyKeyboardModes: { + hasProvenBaseline: true, + reset: vi.fn(), + resetForSnapshot: vi.fn(), + scanReplay: vi.fn() + }, + ...overrides + } as unknown as ReattachPayloadSession +} + +function createContext(replay: string, attemptGeneration: number): ReattachPayloadContext { + return { + isCurrentReattachPayload: () => true, + connectResult: { id: 'pty-1', replay }, + ptyId: 'pty-1', + attemptGeneration, + prefetchedParkModelSnapshot: null, + revealFollowsTerminalPark: false, + fetchSshMainModelReattachSnapshot: async () => null, + hasStructuralReplay: true, + coldRestoreStartup: undefined, + reattachPayloadApplied: false + } +} + +describe('reattach payload context', () => { + beforeEach(() => installTerminalTestGlobals()) + + afterEach(async () => restoreTerminalTestGlobals()) + + it('keeps overlapping payload handlers bound to their own attempt', async () => { + const session = createSession() + const attemptA = createReattachPayloadHandlers(session, createContext('PAYLOAD-A', 1)) + const attemptB = createReattachPayloadHandlers(session, createContext('PAYLOAD-B', 2)) + + await attemptB.applyReattachPayload() + await attemptA.applyReattachPayload() + + expect(session.rememberReattachPayloadAgentSignal.mock.calls).toEqual([ + ['PAYLOAD-B', { fullScreenReplay: true }], + ['PAYLOAD-A', { fullScreenReplay: true }] + ]) + expect(session.sendFocusedReattachFocusInAfterReplay.mock.calls).toEqual([ + ['pty-1', 2], + ['pty-1', 1] + ]) + }) + + it('uses the connect startup plan without rebuilding or scheduling paste delivery', async () => { + const startup = { + command: 'codex resume provider-session', + agent: 'codex', + resumeProviderSession: { key: 'session_id', id: 'provider-session' }, + launchConfig: { agentCommand: 'codex', agentArgs: '', agentEnv: {} }, + launchToken: 'transport-launch-token', + useLiveEntry: false, + hasSleepingRecord: false, + sleepingRecordEntry: null + } as ColdRestoreAgentResumeStartup + const buildColdRestoreAgentResumeStartup = vi.fn() + const applyColdRestoreAgentResumeStartup = vi.fn(() => true) + const schedulePendingStartupCommandDelivery = vi.fn() + const session = createSession({ + buildColdRestoreAgentResumeStartup, + applyColdRestoreAgentResumeStartup, + schedulePendingStartupCommandDelivery, + showSessionRestoredBanner: vi.fn(), + clearSleepingRecordAfterColdRestoreSpawn: vi.fn(), + consumeRestoredViewportBlankingMarker: vi.fn(), + writeFreshShellViewportBlanking: vi.fn() + }) + const context: ReattachPayloadContext = { + ...createContext('', 1), + connectResult: { + id: 'pty-1', + coldRestore: { scrollback: 'restored scrollback', cwd: '/workspace' } + }, + coldRestoreStartup: startup + } + + await createReattachPayloadHandlers(session, context).applyReattachPayload() + + expect(applyColdRestoreAgentResumeStartup).toHaveBeenCalledWith(startup) + expect(buildColdRestoreAgentResumeStartup).not.toHaveBeenCalled() + expect(schedulePendingStartupCommandDelivery).not.toHaveBeenCalled() + expect(context.reattachPayloadApplied).toBe(true) + }) +}) diff --git a/src/renderer/src/components/terminal-pane/pty-connection/reattach-payload-context.ts b/src/renderer/src/components/terminal-pane/pty-connection/reattach-payload-context.ts index e7b1579b909..559bde05215 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/reattach-payload-context.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/reattach-payload-context.ts @@ -11,6 +11,6 @@ export type ReattachPayloadContext = { revealFollowsTerminalPark: boolean fetchSshMainModelReattachSnapshot: () => Promise hasStructuralReplay: boolean - coldRestoreStartup?: ColdRestoreAgentResumeStartup | null + coldRestoreStartup: ColdRestoreAgentResumeStartup | null | undefined reattachPayloadApplied: boolean } diff --git a/src/renderer/src/components/terminal-pane/pty-connection/reattach-payload-session.ts b/src/renderer/src/components/terminal-pane/pty-connection/reattach-payload-session.ts new file mode 100644 index 00000000000..6bd4d78886d --- /dev/null +++ b/src/renderer/src/components/terminal-pane/pty-connection/reattach-payload-session.ts @@ -0,0 +1,27 @@ +import type { ConnectPanePtySession } from './connect-pane-pty-session' + +export type ReattachPayloadSession = Pick< + ConnectPanePtySession, + | 'applyColdRestoreAgentResumeStartup' + | 'applySnapshotKittyKeyboardModes' + | 'buildColdRestoreAgentResumeStartup' + | 'clearSleepingRecordAfterColdRestoreSpawn' + | 'consumeRestoredViewportBlankingMarker' + | 'createReattachGridPush' + | 'isPaneOnAlternateScreen' + | 'kittyKeyboardModes' + | 'pane' + | 'pendingReattachFit' + | 'reattachReplayResetSequence' + | 'recordRendererOrderedSeq' + | 'rememberReattachPayloadAgentSignal' + | 'schedulePendingStartupCommandDelivery' + | 'sendFocusedReattachFocusInAfterReplay' + | 'setRestoredSnapshotBaseline' + | 'shouldPreserveAgentReattachModes' + | 'showSessionRestoredBanner' + | 'suppressStructuralReplayPtyResize' + | 'transport' + | 'writeFreshShellViewportBlanking' + | 'writeReplayData' +> 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 edd9ecb61fc..29998677fea 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 @@ -19,9 +19,38 @@ import type { ColdRestoreAgentResumeStartup } from './fresh-spawn-types' import type { ConnectPanePtySession } from './connect-pane-pty-session' import type { ReattachPayloadContext } from './reattach-payload-context' -import { bindApplyReattachPayload } from './apply-reattach-payload' +import { createReattachPayloadHandlers } from './apply-reattach-payload' +import type { ReattachPayloadSession } from './reattach-payload-session' -export function bindHandleReattachResult(session: ConnectPanePtySession): void { +type ReattachResultSession = ReattachPayloadSession & + Pick< + ConnectPanePtySession, + | 'agentCompletionCoordinator' + | 'authoritativeReattachGeneration' + | 'capturedDirectSshRetryPtyAccepted' + | 'deps' + | 'directSshRetryAttempt' + | 'disposed' + | 'getSshMainModelSnapshotProbe' + | 'handleReattachResult' + | 'mountFollowsTerminalPark' + | 'registerEffectiveLaunchConfig' + | 'registerPaneSerializerFor' + | 'registerSideEffectFactConsumerForPty' + | 'rejectObsoleteDirectSshReattach' + | 'reportPanePtyVisibility' + | 'sampleVisiblePaneForegroundAgent' + | 'scheduleReattachIdleAgentCursorReset' + | 'serializeHiddenOutputSnapshot' + | 'setPanePtyFitBinding' + | 'startFreshColdRestoreAgentResume' + | 'structuralReplayCoordinator' + | 'syncHiddenRendererPtyDelivery' + | 'transportStreamGeneration' + > + +export function bindHandleReattachResult(sessionBag: ConnectPanePtySession): void { + const session = sessionBag as unknown as ReattachResultSession session.handleReattachResult = async ( result: PtyConnectResult | string | void, staleSessionId?: string | null, @@ -39,7 +68,7 @@ export function bindHandleReattachResult(session: ConnectPanePtySession): void { result && typeof result === 'object' && 'id' in result ? (result as PtyConnectResult) : null if (connectResult?.exitedBeforeAttach) { - // Why: the session.transport already delivered the dead session's final frame + exit; treat as terminal state, not a failed reattach. + // Why: the transport already delivered the dead session's final frame + exit; treat as terminal state, not a failed reattach. return true } @@ -206,15 +235,18 @@ export function bindHandleReattachResult(session: ConnectPanePtySession): void { coldRestoreStartup, reattachPayloadApplied: !hasStructuralReplay && prefetchedParkModelSnapshot === null } - bindApplyReattachPayload(session, reattachPayload) + const { applyReattachPayload, fitAfterReattachRestore } = createReattachPayloadHandlers( + session, + reattachPayload + ) if (hasStructuralReplay || prefetchedParkModelSnapshot) { - await session.structuralReplayCoordinator.run(session.applyReattachPayload, { + await session.structuralReplayCoordinator.run(applyReattachPayload, { shouldRestore: isCurrentReattachPayload, - afterRestore: session.fitAfterReattachRestore + afterRestore: fitAfterReattachRestore }) } else { - await session.applyReattachPayload() - await session.fitAfterReattachRestore() + await applyReattachPayload() + await fitAfterReattachRestore() } if (!isCurrentReattachPayload() || !reattachPayload.reattachPayloadApplied) { return false diff --git a/src/renderer/src/components/terminal-pane/pty-connection/ssh-session-connect.ts b/src/renderer/src/components/terminal-pane/pty-connection/ssh-session-connect.ts index 4e197725fd5..f1985cedfe5 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/ssh-session-connect.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/ssh-session-connect.ts @@ -1,12 +1,13 @@ import { useAppStore } from '@/store' import { SSH_SESSION_EXPIRED_ERROR } from './pty-connect-limits' +import type { ConnectPanePtySession } from './connect-pane-pty-session' // Why: when multiple panes/tabs need the same deferred SSH connection, // the first one calls ssh.connect() and subsequent ones must wait for it // rather than returning early (which would leave them disconnected). This // helper either connects or waits for an in-flight connect to finish. export type SshConnectResult = { connected: true } | { connected: false; error: string } -export type UserInitiatedSshConnectOutcome = 'connected' | 'cancelled' | 'failed' +type UserInitiatedSshConnectOutcome = 'connected' | 'cancelled' | 'failed' const sshConnectPromises = new Map>() @@ -14,7 +15,7 @@ export function isSshSessionExpiredError(err: unknown): boolean { return (err instanceof Error ? err.message : String(err)).includes(SSH_SESSION_EXPIRED_ERROR) } -export function sshPromptConnectOutcomeForStatus( +function sshPromptConnectOutcomeForStatus( status: string | undefined, sawNonDisconnected: boolean ): UserInitiatedSshConnectOutcome | null { @@ -32,6 +33,60 @@ export function sshPromptConnectOutcomeForStatus( return null } +export function waitForUserInitiatedSshConnect( + session: ConnectPanePtySession +): Promise { + return new Promise((resolve) => { + // Entry-time disconnected means authentication has not started; it only cancels after another status was observed. + let sawNonDisconnected = !['disconnected', undefined].includes( + useAppStore.getState().sshConnectionStates.get(session.connectionId)?.status + ) + let settled = false + const finish = (outcome: UserInitiatedSshConnectOutcome): void => { + if (settled) { + return + } + settled = true + unsubscribe() + const index = session.waitTeardowns.indexOf(teardown) + if (index !== -1) { + session.waitTeardowns.splice(index, 1) + } + resolve(outcome) + } + const teardown = (): void => finish('cancelled') + // Disposal must resolve the wait even if the SSH store never emits again. + session.waitTeardowns.push(teardown) + const readOutcome = (status: string | undefined): UserInitiatedSshConnectOutcome | null => { + if (status && status !== 'disconnected') { + sawNonDisconnected = true + } + return sshPromptConnectOutcomeForStatus(status, sawNonDisconnected) + } + const unsubscribe = useAppStore.subscribe((state) => { + if (session.disposed) { + finish('cancelled') + return + } + const outcome = readOutcome(state.sshConnectionStates.get(session.connectionId)?.status) + if (outcome) { + finish(outcome) + } + }) + if (session.disposed) { + finish('cancelled') + return + } + // Catch a status change that landed between the caller's check and this subscription. + const currentOutcome = readOutcome( + useAppStore.getState().sshConnectionStates.get(session.connectionId)?.status + ) + if (currentOutcome) { + finish(currentOutcome) + } + }) +} + export async function waitForSshConnection(connectionId: string): Promise { const state = useAppStore.getState().sshConnectionStates.get(connectionId) if (state?.status === 'connected') { diff --git a/src/renderer/src/components/terminal-pane/pty-connection/transport-output-callbacks.ts b/src/renderer/src/components/terminal-pane/pty-connection/transport-output-callbacks.ts index 999bd005bfa..0e8e4ec5457 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/transport-output-callbacks.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/transport-output-callbacks.ts @@ -62,7 +62,7 @@ export function bindCaptureTransportOutputCallbacks(session: ConnectPanePtySessi }, onRecoveryStateChange: (state: PtyTransportRecoveryState): void => { if (isCurrent()) { - // Why: cached pixels remain visible while detached; expose session.transport truth for diagnostics and recovery UI. + // Why: cached pixels remain visible while detached; expose transport truth for diagnostics and recovery UI. session.pane.container.dataset.ptyRecoveryState = state.phase session.deps.onPtyRecoveryStateRef?.current?.(session.pane.id, state) } @@ -94,7 +94,7 @@ export function bindCaptureTransportOutputCallbacks(session: ConnectPanePtySessi session.hiddenOutputRestoreRemoteAbandonCycles = 0 session.hiddenOutputSnapshotScrollRestore = null // Why: hidden recovery state belongs to one PTY stream. Reattach/restart - // can reuse the session.pane object for a different session before visibility. + // can reuse the pane object for a different session before visibility. session.hiddenOutputRestorePtyId = null // One recovery re-kick per xterm instance. Generation-aware cooldown and // window-cap retries keep a fresh-but-wedged replacement from fossilizing. diff --git a/src/renderer/src/components/terminal-pane/pty-connection/write-pty-output-to-xterm.ts b/src/renderer/src/components/terminal-pane/pty-connection/write-pty-output-to-xterm.ts index 1fe06737be2..47eca328367 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/write-pty-output-to-xterm.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/write-pty-output-to-xterm.ts @@ -25,7 +25,7 @@ export function bindWritePtyOutputToXterm(session: ConnectPanePtySession): void foreground: boolean, opts?: { hiddenStartupRendererQuery?: boolean } ): void { - // Why: every application byte funnels through here, so it's the one place the kitty keyboard mirror observes the session.pane's protocol negotiation. + // Why: every application byte funnels through here, so it's the one place the kitty keyboard mirror observes the pane's protocol negotiation. session.kittyKeyboardModes.scan(data) if (foreground) { session.resetHiddenOutputRestoreIfPtyChanged()