mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 16:03:17 +00:00
fix(terminal): bound window-wake size recovery
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"updatedAt": "2026-07-19",
|
||||
"updatedAt": "2026-07-20",
|
||||
"policy": {
|
||||
"maturityLevels": [
|
||||
"experimental",
|
||||
@@ -1224,7 +1224,8 @@
|
||||
"PTY sizing",
|
||||
"split layout",
|
||||
"restore",
|
||||
"hidden-to-visible transitions"
|
||||
"hidden-to-visible transitions",
|
||||
"window wake"
|
||||
],
|
||||
"platforms": [
|
||||
"macos",
|
||||
@@ -1241,7 +1242,7 @@
|
||||
"macos"
|
||||
],
|
||||
"coveredProviders": [],
|
||||
"coverageNotes": "Local macOS evidence on main@1282f5c2d, including #7192's runtime-mirror geometry authority slice. Shell-visible size, SSH/remote geometry, Windows ConPTY readback, and resume-time reassertion coverage arrive with the pending stack and #7006.",
|
||||
"coverageNotes": "Local macOS evidence on main@1282f5c2d, including #7192's runtime-mirror geometry authority slice. Deterministic provider-contract coverage now includes settled window-wake reassertion and SSH relay applied-size readback. Live shell-visible SSH/remote geometry and Windows ConPTY readback remain non-blocking gaps.",
|
||||
"motivatingLinks": [
|
||||
"https://github.com/stablyai/orca/pull/6644",
|
||||
"https://github.com/stablyai/orca/pull/6649",
|
||||
@@ -1252,15 +1253,19 @@
|
||||
"https://github.com/stablyai/orca/pull/7192"
|
||||
],
|
||||
"invariant": "A visible desktop-owned terminal cannot trust 0x0, stale requested size, or renderer-only size; xterm, fit/proposed size, applied PTY size, shell-visible size, and the runtime mirror's parse dimensions must converge or enter explicit degraded state, and mirror resize reflow must stay ordered with queued output writes.",
|
||||
"oracle": "The current executable slice uses deterministic frame schedulers and fake providers to force 0x0 first fit, delayed layout settle, dropped resize/readback drift, and hidden-to-visible activation, then asserts the renderer forwards a usable size, pty:getSize reports applied rather than merely requested size where available, and visibility resume reasserts real drift without hot listSessions. Shell-visible stty/echo-wrap convergence remains a live-gate follow-up.",
|
||||
"oracle": "The current executable slice uses deterministic frame schedulers and fake providers to force 0x0 first fit, delayed layout settle, dropped resize/readback drift, hidden-to-visible activation, and window wake. It asserts the renderer forwards a usable size, pty:getSize reports applied rather than merely requested size where available, visibility resume reasserts real drift without hot listSessions, one settled wake produces one geometry-only readback, and SSH relay readback is authoritative with bounded fallback. Shell-visible stty/echo-wrap convergence remains a live-gate follow-up.",
|
||||
"commands": [
|
||||
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/pty-size-reconcile.test.ts src/renderer/src/components/terminal-pane/split-right-white-screen.test.ts src/main/ipc/pty.test.ts src/main/runtime/orca-runtime.test.ts"
|
||||
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/pty-size-reconcile.test.ts src/renderer/src/components/terminal-pane/pty-size-reassertion.test.ts src/renderer/src/components/terminal-pane/split-right-white-screen.test.ts src/renderer/src/components/terminal-pane/use-terminal-window-wake-recovery.test.ts src/main/ipc/pty.test.ts src/main/providers/ssh-pty-provider.test.ts src/main/runtime/orca-runtime.test.ts src/relay/pty-handler.test.ts"
|
||||
],
|
||||
"testFiles": [
|
||||
"src/renderer/src/components/terminal-pane/pty-size-reconcile.test.ts",
|
||||
"src/renderer/src/components/terminal-pane/pty-size-reassertion.test.ts",
|
||||
"src/renderer/src/components/terminal-pane/split-right-white-screen.test.ts",
|
||||
"src/renderer/src/components/terminal-pane/use-terminal-window-wake-recovery.test.ts",
|
||||
"src/main/ipc/pty.test.ts",
|
||||
"src/main/runtime/orca-runtime.test.ts"
|
||||
"src/main/providers/ssh-pty-provider.test.ts",
|
||||
"src/main/runtime/orca-runtime.test.ts",
|
||||
"src/relay/pty-handler.test.ts"
|
||||
],
|
||||
"assertionRefs": [
|
||||
{
|
||||
@@ -1271,6 +1276,13 @@
|
||||
"applied-size readback divergence is re-forwarded before handoff"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/renderer/src/components/terminal-pane/pty-size-reassertion.test.ts",
|
||||
"assertions": [
|
||||
"only one applied-size read stays in flight while the grid changes",
|
||||
"stale in-flight targets never resize the PTY back to an older grid"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/renderer/src/components/terminal-pane/split-right-white-screen.test.ts",
|
||||
"assertions": [
|
||||
@@ -1279,11 +1291,26 @@
|
||||
"hidden 0x0 background spawn is not forced to phantom desktop size"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/renderer/src/components/terminal-pane/use-terminal-window-wake-recovery.test.ts",
|
||||
"assertions": [
|
||||
"window focus reasserts PTY size after fitting",
|
||||
"the immediate and settled wake fits produce exactly one geometry-only reassertion"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/main/ipc/pty.test.ts",
|
||||
"assertions": [
|
||||
"accepted desktop resizes fan out to the runtime after provider resize",
|
||||
"rejected desktop resizes do not fan out to the runtime"
|
||||
"rejected desktop resizes do not fan out to the runtime",
|
||||
"provider-owned null remains unverified instead of falling back to requested size"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/main/providers/ssh-pty-provider.test.ts",
|
||||
"assertions": [
|
||||
"SSH applied-size readback uses the relay with a one-second timeout",
|
||||
"old-relay method-not-found is cached while transient failures remain retryable"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1292,17 +1319,24 @@
|
||||
"the headless mirror resizes after an accepted desktop PTY resize (#7192)",
|
||||
"headless mirror resizes are ordered behind queued PTY writes (#7192)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/relay/pty-handler.test.ts",
|
||||
"assertions": [
|
||||
"relay readback reports the grid actually applied by node-pty",
|
||||
"missing relay PTYs return an unverified null size"
|
||||
]
|
||||
}
|
||||
],
|
||||
"evidenceRuns": [
|
||||
{
|
||||
"date": "2026-07-03",
|
||||
"date": "2026-07-20",
|
||||
"runner": "local",
|
||||
"platform": "macos",
|
||||
"command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/pty-size-reconcile.test.ts src/renderer/src/components/terminal-pane/split-right-white-screen.test.ts src/main/ipc/pty.test.ts src/main/runtime/orca-runtime.test.ts",
|
||||
"command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/pty-size-reconcile.test.ts src/renderer/src/components/terminal-pane/pty-size-reassertion.test.ts src/renderer/src/components/terminal-pane/split-right-white-screen.test.ts src/renderer/src/components/terminal-pane/use-terminal-window-wake-recovery.test.ts src/main/ipc/pty.test.ts src/main/providers/ssh-pty-provider.test.ts src/main/runtime/orca-runtime.test.ts src/relay/pty-handler.test.ts",
|
||||
"result": "passed",
|
||||
"durationSeconds": 8.4,
|
||||
"summary": "4 test file(s) passed, 777 tests passed on main@1282f5c2d in a clean checkout."
|
||||
"durationSeconds": 18.54,
|
||||
"summary": "8 test files passed, 1,272 tests passed on the PR branch."
|
||||
}
|
||||
],
|
||||
"runtimeBudget": {
|
||||
@@ -1319,16 +1353,15 @@
|
||||
},
|
||||
"performanceBudget": {
|
||||
"required": true,
|
||||
"evidence": "Must include resize/throughput budget so convergence loops do not add frame or SIGWINCH churn."
|
||||
"evidence": "The wake hook count test permits two visual fits but exactly one applied-size read per visible pane and no broad visibility/agent lifecycle call. SSH readback has a one-second timeout, caches stable method-not-found, and retries only transient failures. No polling, listSessions scan, subprocess, or unbounded retry was added."
|
||||
},
|
||||
"promotionCriteria": [
|
||||
"Build deterministic fault hooks before adding broad E2E coverage.",
|
||||
"Keep stress variants non-blocking until runtime and flake history are known."
|
||||
],
|
||||
"knownGaps": [
|
||||
"No live Electron geometry command yet beyond the local live PTY resize slice.",
|
||||
"The runtime mirror geometry slice is unregistered: #7192's red tests (orca-runtime.test.ts headless-mirror resize and ordering, pty.test.ts desktop resize fan-out) exist on fresh main but not at this branch's merge-base, so they can only be registered during the rebase.",
|
||||
"Does not yet prove shell-visible size, provider-applied size, and xterm size converge across SSH/remote paths.",
|
||||
"Local and Docker-backed SSH Electron wake specs exist but remain manual/non-blocking pending runtime and flake history.",
|
||||
"The deterministic gate proves the SSH provider/relay readback contract, not live shell-visible convergence across every remote path.",
|
||||
"Does not yet prove Windows ConPTY geometry/readback.",
|
||||
"Current command uses deterministic fake providers for the main/renderer contracts, not a real remote PTY."
|
||||
],
|
||||
|
||||
@@ -3,6 +3,8 @@ import { JsonRpcErrorCode } from '../ssh/relay-protocol'
|
||||
import { toRelaySshPtyId } from './ssh-pty-id'
|
||||
|
||||
type AppliedPtySize = { cols: number; rows: number }
|
||||
// Why: wake repair can safely re-forward on failure, so never inherit the generic 30-second RPC timeout.
|
||||
const SSH_PTY_APPLIED_SIZE_TIMEOUT_MS = 1_000
|
||||
|
||||
export function createSshPtyAppliedSizeReader(
|
||||
mux: SshChannelMultiplexer,
|
||||
@@ -14,9 +16,16 @@ export function createSshPtyAppliedSizeReader(
|
||||
return null
|
||||
}
|
||||
try {
|
||||
const result = (await mux.request('pty.getSize', {
|
||||
id: toRelaySshPtyId(connectionId, id)
|
||||
})) as { cols?: unknown; rows?: unknown } | null
|
||||
const result = (await mux.request(
|
||||
'pty.getSize',
|
||||
{
|
||||
id: toRelaySshPtyId(connectionId, id)
|
||||
},
|
||||
{ timeoutMs: SSH_PTY_APPLIED_SIZE_TIMEOUT_MS }
|
||||
)) as {
|
||||
cols?: unknown
|
||||
rows?: unknown
|
||||
} | null
|
||||
supported = true
|
||||
if (
|
||||
!result ||
|
||||
|
||||
@@ -474,7 +474,7 @@ describe('SshPtyProvider', () => {
|
||||
mux.request.mockResolvedValue({ cols: 120, rows: 40 })
|
||||
|
||||
await expect(provider.getAppliedSize(scopedPty1)).resolves.toEqual({ cols: 120, rows: 40 })
|
||||
expect(mux.request).toHaveBeenCalledWith('pty.getSize', { id: 'pty-1' })
|
||||
expect(mux.request).toHaveBeenCalledWith('pty.getSize', { id: 'pty-1' }, { timeoutMs: 1_000 })
|
||||
})
|
||||
|
||||
it('caches only an old relay method-not-found response', async () => {
|
||||
|
||||
@@ -625,6 +625,7 @@ let inactiveForegroundImmediateBudgetWindowStart = 0
|
||||
type PanePtyBinding = IDisposable & {
|
||||
syncProcessTracking: () => void
|
||||
noteVisibilityResume: () => void
|
||||
reassertPtySizeAfterWindowWake: () => void
|
||||
/** Navigation-free hibernation wake: fires the armed cold-restore --resume
|
||||
* without the size-reassert/foreground-sample side effects of a real reveal.
|
||||
* Used by the mobile wake fanout so a hidden hibernated pane resumes with no
|
||||
@@ -8037,6 +8038,9 @@ export function connectPanePty(
|
||||
requestKnownDroidReconfirmation()
|
||||
sampleVisiblePaneForegroundAgent()
|
||||
},
|
||||
reassertPtySizeAfterWindowWake() {
|
||||
ptySizeReassertion.request({ fit: false })
|
||||
},
|
||||
// Why: mobile wake reaches this pane while it's hidden on the desktop, so consume only the armed hibernation wake — no size/foreground reads.
|
||||
wakeHibernatedAgentIfArmed(claimedProviderSessions) {
|
||||
if (hibernatedWakeInFlightClaimKey) {
|
||||
|
||||
+35
-4
@@ -98,7 +98,7 @@ describe('useTerminalWindowWakeRecovery', () => {
|
||||
})
|
||||
|
||||
it('reasserts pane PTY sizes after the window-focus fit', () => {
|
||||
const noteVisibilityResume = vi.fn()
|
||||
const reassertPtySizeAfterWindowWake = vi.fn()
|
||||
renderHook(() =>
|
||||
useTerminalWindowWakeRecovery({
|
||||
isVisible: true,
|
||||
@@ -106,16 +106,47 @@ describe('useTerminalWindowWakeRecovery', () => {
|
||||
isActiveRef: { current: true },
|
||||
isVisibleRef: { current: true },
|
||||
panePtyBindingsRef: {
|
||||
current: new Map([[1, { dispose: vi.fn(), noteVisibilityResume }]]) as never
|
||||
current: new Map([[1, { dispose: vi.fn(), reassertPtySizeAfterWindowWake }]]) as never
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
window.dispatchEvent(new Event('focus'))
|
||||
|
||||
expect(noteVisibilityResume).toHaveBeenCalledTimes(1)
|
||||
expect(reassertPtySizeAfterWindowWake).toHaveBeenCalledTimes(1)
|
||||
expect(recoverVisibleTerminalWindowWakeMock.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
noteVisibilityResume.mock.invocationCallOrder[0] ?? Number.POSITIVE_INFINITY
|
||||
reassertPtySizeAfterWindowWake.mock.invocationCallOrder[0] ?? Number.POSITIVE_INFINITY
|
||||
)
|
||||
})
|
||||
|
||||
it('reasserts once after the settled fit when animation frames are available', () => {
|
||||
const scheduled: { settle: FrameRequestCallback | null } = { settle: null }
|
||||
vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => {
|
||||
scheduled.settle = callback
|
||||
return 1
|
||||
})
|
||||
const reassertPtySizeAfterWindowWake = vi.fn()
|
||||
renderHook(() =>
|
||||
useTerminalWindowWakeRecovery({
|
||||
isVisible: true,
|
||||
managerRef: { current: manager },
|
||||
isActiveRef: { current: true },
|
||||
isVisibleRef: { current: true },
|
||||
panePtyBindingsRef: {
|
||||
current: new Map([[1, { dispose: vi.fn(), reassertPtySizeAfterWindowWake }]]) as never
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
window.dispatchEvent(new Event('focus'))
|
||||
expect(reassertPtySizeAfterWindowWake).not.toHaveBeenCalled()
|
||||
expect(scheduled.settle).not.toBeNull()
|
||||
|
||||
scheduled.settle?.(performance.now())
|
||||
|
||||
expect(reassertPtySizeAfterWindowWake).toHaveBeenCalledTimes(1)
|
||||
expect(recoverVisibleTerminalWindowWakeMock.mock.invocationCallOrder.at(-1)).toBeLessThan(
|
||||
reassertPtySizeAfterWindowWake.mock.invocationCallOrder[0] ?? Number.POSITIVE_INFINITY
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -12,6 +12,10 @@ type UseTerminalWindowWakeRecoveryArgs = {
|
||||
panePtyBindingsRef?: React.RefObject<Map<number, IDisposable>>
|
||||
}
|
||||
|
||||
type WindowWakePtyBinding = IDisposable & {
|
||||
reassertPtySizeAfterWindowWake?: () => void
|
||||
}
|
||||
|
||||
export function useTerminalWindowWakeRecovery({
|
||||
isVisible,
|
||||
managerRef,
|
||||
@@ -33,6 +37,13 @@ export function useTerminalWindowWakeRecovery({
|
||||
cancelAnimationFrame(wakeRecoveryFrameId)
|
||||
wakeRecoveryFrameId = null
|
||||
}
|
||||
const reassertPanePtySizes = (): void => {
|
||||
for (const binding of panePtyBindingsRef?.current.values() ?? []) {
|
||||
// Why: one settled read avoids duplicate SSH RPCs while still detecting a dropped resize.
|
||||
const windowWakeBinding = binding as WindowWakePtyBinding
|
||||
windowWakeBinding.reassertPtySizeAfterWindowWake?.()
|
||||
}
|
||||
}
|
||||
const recoverVisibleWake = (
|
||||
clearGlyphAtlases: boolean,
|
||||
source: 'focus' | 'visibilitychange' | 'system-resumed'
|
||||
@@ -62,12 +73,8 @@ export function useTerminalWindowWakeRecovery({
|
||||
isActive: isActiveRef.current,
|
||||
clearGlyphAtlases
|
||||
})
|
||||
for (const binding of panePtyBindingsRef?.current.values() ?? []) {
|
||||
// Why: fit-only wake recovery cannot detect a dropped remote resize
|
||||
// when xterm is already at the desired grid; verify after fitting.
|
||||
;(binding as IDisposable & { noteVisibilityResume?: () => void }).noteVisibilityResume?.()
|
||||
}
|
||||
if (typeof requestAnimationFrame !== 'function') {
|
||||
reassertPanePtySizes()
|
||||
return
|
||||
}
|
||||
settledClearGlyphAtlases = clearGlyphAtlases
|
||||
@@ -84,9 +91,7 @@ export function useTerminalWindowWakeRecovery({
|
||||
isActive: isActiveRef.current,
|
||||
clearGlyphAtlases: clearGlyphAtlasesOnSettle
|
||||
})
|
||||
for (const binding of panePtyBindingsRef?.current.values() ?? []) {
|
||||
;(binding as IDisposable & { noteVisibilityResume?: () => void }).noteVisibilityResume?.()
|
||||
}
|
||||
reassertPanePtySizes()
|
||||
})
|
||||
}
|
||||
// Why: plain refocus (alt-tab, devtools) is frequent and often lands while
|
||||
|
||||
Reference in New Issue
Block a user