fix(preview): keep the last denoise frame through completion, reconnect and tab hiding - #214
Merged
Conversation
…ct and tab hiding The generation preview lost its frame at exactly the moments it mattered: - On the terminal socket event the coordinator released the followed slot at once, but the finished image is two HTTP round trips away (queue item read, then per-image read). Preview dropped out of live-follow and rendered the previous selection until the new image landed. - On any socket drop the coordinator wiped the frame and target, and nothing re-requested them. A run that finished while the tab was hidden left a blank card until the next event, or for good. - Between two items of a batch the next slot became live before it produced a frame, so the panel rendered an empty card. - One manual gallery click permanently disabled auto-select of new results, so later completions never reached the preview. Client-side preview state machine (Phase 1 of the preview restructuring): - Completed slots move to a "settling" phase and stay followed until result routing lands; the last frame is held for a bridge to the batch's next slot and for a swap that is painted over the finished image until it has decoded (bound to the image names routing delivered, consumed on first decode, 10 s TTL). Running slots win over settling ones so multi-GPU streams are never hidden, and the tile grid counts running slots only. - Frames and targets survive a socket drop; a `visibilitychange` listener runs the reconcile sweep on the visibility edge, and sweep requests coalesce instead of being dropped while one is in flight. - Progress frames carry an optional `revision`; a frame at or below the last accepted revision for the same item+session is dropped (ready for the server snapshot in Phase 2; no-op for today's ordered socket stream). - A deliberate gallery selection stamps `liveFollowPausedAt`; submitting new work resumes live-follow, and a result is auto-selected only when its generation was submitted after the pick. An explicit toggle lifts the stamp. - Result routing returns a per-route promise instead of the shared flush, so a settling slot is released when its own route lands. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DoHBJ9QPWfq1qMTfzdzuke
lstein
requested review from
JPPhoto,
Pfannkuchensack and
blessedcoolant
as code owners
September 6, 2026 16:32
7 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 1 (client only) of the generation-preview restructuring discussed on Discord. The server-side part (retained last frame,
revision, throttling, reconnect snapshot) follows as a separate additive PR that stacks on this one.Two reported problems: (1) progress updates missed while the tab is inactive leave the preview blank or on the previous image; (2) on completion the preview often shows black or an older gallery image instead of the just-finished one. Both turned out to be mostly client-side lifecycle bugs, verified in the code:
coordinator.tsreleased the followed slot synchronously on the terminal event, while the finished image only arrives after two HTTP round trips. Preview fell out of live-follow and showed the previous selection in that window.handleConnectionChangewiped the frame and target on every non-connected status, and nothing re-requested them. A run that finished while hidden left a blank card.nulland the panel rendered an empty card.showProgressImagesInViewer=falsefor good, so later completions never auto-selected.What changes
Preview state machine
activeProgressTargetStoregains a settling phase. A completed slot stays followed until its result routing lands; running slots win over settling ones; the tile grid counts running slots only (so a single-GPU batch never flashes into a two-tile grid).progressImageStoreholds the last frame per queue item on completion: a bridge frame shown while the batch's next slot has none of its own, and a swap frame painted over the finished image until the browser has decoded it. The swap is bound to the image names routing delivered (never item 3's frame over item 1), consumed on first decode, and expires after 10 s.PreviewFramegetsholdSource/onSourceLoaded: while a hold is up the real<img>keeps the geometry withvisibility:hiddenand the held frame paints over it. No detached preload: in multiuser mode the image route isno-store, so the swap has to happen on the same element.Reconnect / hidden tab
visibilitychangelistener runs the reconcile sweep when the tab becomes visible; sweep requests coalesce instead of being dropped while one is in flight.invocation_progressgets an optionalrevision(additive; the backend does not send it yet). Frames at or below the last accepted revision for the same item + session are dropped.Auto-select policy (confirmed with @lstein)
liveFollowPausedAt. Submitting new work resumes live-follow. A completed result is auto-selected only if its generation was submitted after the pick, so the batch that was running when the user clicked stays out of the way. An explicit toggle of the setting lifts the stamp; an explicit opt-out is never re-armed by Invoke.Routing
scheduleResultRoutereturns a per-route promise instead of the shared flush, so a settling slot is released when its route lands rather than when every pending route drains.Known behavioural notes
main.Test plan
pnpm lint(format, oxlint, tsc, architecture)pnpm test— 523 files / 7595 testspnpm test:browser— full suite, plus new cases: hold-until-decoded swap (PreviewFrameHold.browser.test.tsx), following a settling slot, running-over-settling, batch bridge (PreviewNavigation.browser.test.tsx)pnpm run test:performance:buildandtest:performance:browserpass in check mode (no baseline re-record needed)🤖 Generated with Claude Code
https://claude.ai/code/session_01DoHBJ9QPWfq1qMTfzdzuke