Skip to content

fix(ui): suppress initial loading flash and drop stale file-list responses#117

Merged
umputun merged 2 commits intomasterfrom
fix-initial-loading-flash
Apr 17, 2026
Merged

fix(ui): suppress initial loading flash and drop stale file-list responses#117
umputun merged 2 commits intomasterfrom
fix-initial-loading-flash

Conversation

@umputun
Copy link
Copy Markdown
Owner

@umputun umputun commented Apr 17, 2026

Problem

Opening revdiff on a repo with changes briefly shows an empty two-pane layout with "no file selected" before the real content appears. On small repos it's unnoticeable, on larger ones it's a 100-500 ms flash.

Cause: Model.Init() returns loadFiles() as an async tea.Cmd. Bubble Tea delivers the first WindowSizeMsg well before filesLoadedMsg, so for the gap between ready=true and the goroutine returning, View() paints the two-pane layout against a nil-entry tree and empty m.file.name.

Fix

Two related changes to the file-list loading state machine.

1. Suppress the empty frame. Add a filesLoaded flag on Model. View() returns "loading..." while !ready, then "loading files..." while ready && !filesLoaded, then the populated layout. handleFilesLoaded flips the flag first thing, including on the error path, so the loading screen always exits.

2. Drop stale file-list responses. Tag filesLoadedMsg with a seq uint64 captured at load time (same pattern fileLoadedMsg.seq already uses). handleFilesLoaded drops messages whose seq doesn't match m.filesLoadSeq. toggleUntracked bumps filesLoadSeq before issuing a new load, so if the user hits u while an older load is in flight, the late response can't overwrite the tree with stale data that contradicts m.modes.showUntracked.

Tests

  • TestModel_InitialLoadingState_NoEmptyFlash drives the real ready=false → WindowSizeMsg → filesLoadedMsg sequence and asserts the intermediate View() is "loading files...", not the empty two-pane layout.
  • TestModel_FilesLoaded_DropsStaleResponses verifies a stale seq response is dropped (doesn't flip filesLoaded, doesn't populate the tree) while a fresh seq response is accepted.
  • TestModel_ToggleUntrackedBumpsFilesLoadSeq locks in the bump invariant.
  • TestModel_FilesLoadedError now asserts filesLoaded=true on the error path, so a future refactor can't leave the loading screen pinned forever.
  • testModel() helper and TestModel_PlainStyles updated to also set filesLoaded = true so they exercise the render path rather than the loading string.

docs/ARCHITECTURE.md File Loading section updated with the two-stage View gating.

…onses

Bubble Tea delivered the first WindowSizeMsg before filesLoadedMsg, so between
ready=true and the async ChangedFiles returning, View() painted the populated
two-pane layout with an empty tree and "no file selected" — a 100-500 ms flash
on larger repos.

Gate View() on a new filesLoaded flag: show "loading..." while !ready, then
"loading files..." while ready && !filesLoaded. handleFilesLoaded flips the flag
first thing, including on error, so the loading screen always exits.

Also tag filesLoadedMsg with a seq captured at load time and drop mismatched
messages in handleFilesLoaded. toggleUntracked (and any future reload site) bumps
filesLoadSeq so an older in-flight ChangedFiles can't overwrite the tree after
the user toggles state.
Copilot AI review requested due to automatic review settings April 17, 2026 17:12
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adjusts the UI’s file-list loading state machine to prevent an initial “empty two-pane” flash during startup and to ensure late/stale file-list responses can’t overwrite newer state (e.g., after toggling untracked visibility).

Changes:

  • Add a filesLoaded view gate so View() renders a loading string until the first accepted filesLoadedMsg is handled.
  • Add filesLoadSeq / filesLoadedMsg.seq to drop stale file-list responses and bump the seq on toggleUntracked.
  • Update tests (and architecture docs) to lock in the new loading/seq behavior.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
docs/ARCHITECTURE.md Documents the two-stage View() gating and the file-load lifecycle.
app/ui/view.go Adds a filesLoaded gate to suppress rendering the empty two-pane layout before file data arrives.
app/ui/model.go Introduces filesLoaded and filesLoadSeq, and tags filesLoadedMsg with seq.
app/ui/loaders.go Captures filesLoadSeq in loadFiles() and drops stale filesLoadedMsg in handleFilesLoaded.
app/ui/model_test.go Adds regression test for the startup flash and updates testModel() helper for the new state.
app/ui/loaders_test.go Adds tests for stale-response dropping, seq bumping, and ensures error path exits loading state.
app/ui/diffview_test.go Updates setup to bypass the new loading gate and exercise the real render path.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread docs/ARCHITECTURE.md Outdated
Comment on lines +261 to +263
`m.ready && !m.filesLoaded` (after resize, before `filesLoadedMsg`). `filesLoaded`
flips to true on every `filesLoadedMsg`, including the error path, so the loading
screen always exits.
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This paragraph says filesLoaded “flips to true on every filesLoadedMsg”, but stale responses are intentionally dropped when msg.seq != m.filesLoadSeq, so not every filesLoadedMsg flips the flag. Suggest rewording to “on every accepted/latest filesLoadedMsg” (still including the error path) to reflect the seq-based drop logic.

Suggested change
`m.ready && !m.filesLoaded` (after resize, before `filesLoadedMsg`). `filesLoaded`
flips to true on every `filesLoadedMsg`, including the error path, so the loading
screen always exits.
`m.ready && !m.filesLoaded` (after resize, before an accepted/latest
`filesLoadedMsg`). `filesLoaded` flips to true on every accepted/latest
`filesLoadedMsg`, including the error path, so the loading screen always exits.

Copilot uses AI. Check for mistakes.
Comment thread docs/ARCHITECTURE.md Outdated
Model.Init() → loadFiles cmd
→ Renderer.ChangedFiles() → filesLoadedMsg
→ tree.Rebuild(entries)
→ tree.Rebuild(entries) + m.filesLoaded = true
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ARCH diagram line implies m.filesLoaded is set as part of tree.Rebuild(entries), but in code it’s set at the start of handleFilesLoaded (and also on the error / early-return paths). Consider splitting this into its own step (e.g., handleFilesLoaded: m.filesLoaded = true) so the diagram matches the actual state machine.

Suggested change
tree.Rebuild(entries) + m.filesLoaded = true
handleFilesLoaded: m.filesLoaded = true
→ tree.Rebuild(entries)

Copilot uses AI. Check for mistakes.
Address Copilot review feedback on PR #117. The previous wording said filesLoaded
flips "on every filesLoadedMsg", which is inaccurate after the seq-check was
added — stale messages are dropped and do not flip the flag. The diagram also
coupled tree.Rebuild with the flag flip, but the flag is set before the error
check while Rebuild only runs on non-error accepted messages.
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages bot commented Apr 17, 2026

Deploying revdiff with  Cloudflare Pages  Cloudflare Pages

Latest commit: ee25d27
Status: ✅  Deploy successful!
Preview URL: https://bf66ef16.revdiff.pages.dev
Branch Preview URL: https://fix-initial-loading-flash.revdiff.pages.dev

View logs

@umputun umputun merged commit c33494d into master Apr 17, 2026
5 checks passed
@umputun umputun deleted the fix-initial-loading-flash branch April 17, 2026 17:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants