fix(diffs): bound the selection-restore retry window

Adversarial review (Opus) showed the wall-clock deadline re-armed on Pierre's
'update' phase, which fires on every render, so a row receiving periodic renders
could push it forward indefinitely -- the 2s bound was not a bound.

Keep the extension (convergence genuinely needs a later Pierre render; gating to
'mount' alone regresses restore) but add a hard ceiling from mount, past which no
further extension is granted. A row that renders forever without converging can
no longer spin forever.

Also moves the measured-height discard below the stale-payload skip: a rejected
payload was resizing the row the user is typing in.
This commit is contained in:
Neil
2026-09-10 18:46:25 -07:00
parent c2ab75f5e4
commit ca249fa480
2 changed files with 22 additions and 7 deletions
@@ -151,10 +151,6 @@ export function useCombinedDiffSectionLoader({
) {
return
}
if (wasShowingContent) {
// Why: content really changed, so the old Monaco height no longer describes this row.
setSectionHeights((prev) => removeDiffSectionMeasuredHeight(prev, index))
}
// `dirty` flips back to false once a save is acknowledged, so it cannot tell a stale payload
// from a fresh one. If the draft moved while this fetch was in flight, only commit when the
// payload actually agrees with that draft — otherwise this reverts the user's saved text on
@@ -163,6 +159,11 @@ export function useCombinedDiffSectionLoader({
if (liveDraft !== draftAtFetchStart && storedContent.modifiedContent !== liveDraft) {
return
}
if (wasShowingContent) {
// Why: content really changed, so the old measured height no longer describes this row.
// Must follow the skip above, or a rejected payload resizes the row being typed in.
setSectionHeights((prev) => removeDiffSectionMeasuredHeight(prev, index))
}
setSections((prev) => {
return prev.map((s, i) =>
i === index && !s.dirty
@@ -14,7 +14,12 @@ import {
// The editor reasserts its own selection on attach, which can land well after ours. Budget by
// wall clock, not frames: under load a frame count expires long before the editor settles.
// User interaction still cancels the pending restore, so this only bounds the quiet case.
// The editor reasserts its own selection well after ours, and convergence needs a later Pierre
// render, so the window extends on each render while a restore is still pending. RESTORE_CEILING
// is the hard bound: past it no further extension is granted, so a row that renders forever
// without converging cannot spin forever. User interaction cancels the restore sooner still.
const RESTORE_DEADLINE_MS = 2_000
const RESTORE_CEILING_MS = 15_000
export function usePierreDiffNativeView(
key: string | undefined,
@@ -34,6 +39,7 @@ export function usePierreDiffNativeView(
const frame = useRef<number | null>(null)
// Armed on attach; 0 until then so a stale ref can never keep a restore alive.
const deadline = useRef(0)
const ceiling = useRef(0)
const lastSnapshot = useRef<PierreNativeViewState | undefined>(undefined)
const schedule = useCallback(() => {
if (frame.current !== null || !pending.current || Date.now() > deadline.current) {
@@ -103,7 +109,9 @@ export function usePierreDiffNativeView(
})
}, [editorRef])
useLayoutEffect(() => {
deadline.current = Date.now() + RESTORE_DEADLINE_MS
const now = Date.now()
ceiling.current = now + RESTORE_CEILING_MS
deadline.current = now + RESTORE_DEADLINE_MS
schedule()
}, [activeGroupId, schedule])
useLayoutEffect(() => {
@@ -174,8 +182,14 @@ export function usePierreDiffNativeView(
(host: HTMLElement, phase: PostRenderPhase, instance: PierreDiffInstance) => {
if (phase !== 'unmount') {
view.current = { host, instance }
// Re-arm on attach: the remount is the point a restore becomes possible.
deadline.current = Date.now() + RESTORE_DEADLINE_MS
const now = Date.now()
if (phase === 'mount') {
ceiling.current = now + RESTORE_CEILING_MS
}
// Extend while a restore is still pending, but never past the ceiling.
if (pending.current && now < ceiling.current) {
deadline.current = Math.min(now + RESTORE_DEADLINE_MS, ceiling.current)
}
schedule()
}
},