fix(terminal): make selection guard test-safe

This commit is contained in:
Neil
2026-09-20 16:05:19 -07:00
parent eac939c65e
commit f07f192543
3 changed files with 33 additions and 28 deletions
@@ -23,6 +23,7 @@ import { TERMINAL_FOCUS_IN_SEQUENCE, TERMINAL_FOCUS_OUT_SEQUENCE } from './foreg
import { isRemoteRuntimePtyId } from './paired-parked-terminal-restore'
import { isCodexPaneStale } from './codex-pane-stale'
import { installTerminalSelectionFitGuard } from '../terminal-selection-fit-guard'
import { initializePaneGeometry, readPaneSize } from './read-pane-size'
import type { ConnectPanePtySession } from './connect-pane-pty-session'
@@ -308,12 +309,9 @@ export function installPtyInputForward(session: ConnectPanePtySession): void {
if (reattachCols > 0 && reattachRows > 0) {
session.transport.resize(reattachCols, reattachRows)
}
// Why: POSIX only sends SIGWINCH on an actual dimension change; signal explicitly so restored TUIs repaint at the correct cursor after replay.
if (!isRemoteRuntimePtyId(reattachPtyId)) {
window.api.pty.signal(reattachPtyId, 'SIGWINCH')
}
// Why here: a deferred reveal resolves the fit handle as incomplete, so an awaited
// reassertion at the call site would never run for that path.
if (session.deps.isVisibleRef.current) {
session.ptySizeReassertion.request({ fit: false })
}
@@ -341,8 +339,6 @@ export function installPtyInputForward(session: ConnectPanePtySession): void {
)
}
session.scheduleForegroundGridDriftCheck = (force = false): void => {
// Why: mobile-owned PTYs intentionally keep a non-desktop grid; drift
// healing would refit xterm even if resize forwarding is later suppressed.
if (
session.disposed ||
!session.deps.isVisibleRef.current ||
@@ -371,27 +367,12 @@ export function installPtyInputForward(session: ConnectPanePtySession): void {
) {
return
}
// Why: xterm cell metrics can settle after the DOM box stops resizing, so
// ResizeObserver never fires even though FitAddon now proposes more cols.
requestStablePaneFit(session.pane as ManagedPaneInternal, () =>
session.ptySizeReassertion.request({ fit: false })
)
})
}
// Why: observe the outer pane as the layout signal for both desktop drift
// healing and mobile take-back. Normal desktop panes compare xterm against
// the PTY's applied size; mobile-fit panes only report desktop geometry so
// the parked phone-sized PTY is not resized. See docs/mobile-fit-hold.md.
session.pendingGeometryReportRaf = null
session.lastObservedDesktopGrid = null
session.readPaneSize = (): { width: number; height: number } | null => {
if (typeof session.pane.container.getBoundingClientRect !== 'function') {
return null
}
const rect = session.pane.container.getBoundingClientRect()
return { width: rect.width, height: rect.height }
}
session.lastObservedPaneSize = session.readPaneSize()
session.pendingPaneGeometryChanged = false
session.readPaneSize = () => readPaneSize(session)
initializePaneGeometry(session)
}
@@ -0,0 +1,18 @@
import type { ConnectPanePtySession } from './connect-pane-pty-session'
export function readPaneSize(
session: ConnectPanePtySession
): { width: number; height: number } | null {
if (typeof session.pane.container.getBoundingClientRect !== 'function') {
return null
}
const rect = session.pane.container.getBoundingClientRect()
return { width: rect.width, height: rect.height }
}
export function initializePaneGeometry(session: ConnectPanePtySession): void {
session.pendingGeometryReportRaf = null
session.lastObservedDesktopGrid = null
session.lastObservedPaneSize = readPaneSize(session)
session.pendingPaneGeometryChanged = false
}
@@ -37,17 +37,23 @@ export function installTerminalSelectionFitGuard(
active = false
}
terminalElement?.addEventListener('mousedown', handleMouseDown, true)
ownerDocument?.addEventListener('mouseup', handleMouseUp, true)
ownerWindow?.addEventListener('blur', clear)
if (terminalElement && typeof terminalElement.addEventListener === 'function') {
terminalElement.addEventListener('mousedown', handleMouseDown, true)
}
if (ownerDocument && typeof ownerDocument.addEventListener === 'function') {
ownerDocument.addEventListener('mouseup', handleMouseUp, true)
}
if (ownerWindow && typeof ownerWindow.addEventListener === 'function') {
ownerWindow.addEventListener('blur', clear)
}
return {
isActive: () => active,
dispose: () => {
clear()
terminalElement?.removeEventListener('mousedown', handleMouseDown, true)
ownerDocument?.removeEventListener('mouseup', handleMouseUp, true)
ownerWindow?.removeEventListener('blur', clear)
terminalElement?.removeEventListener?.('mousedown', handleMouseDown, true)
ownerDocument?.removeEventListener?.('mouseup', handleMouseUp, true)
ownerWindow?.removeEventListener?.('blur', clear)
}
}
}