Give Timeline's loading overlay a row per history stage - #181
Merged
Conversation
Entering Timeline showed two rows against Live's six, with all three server stages flattened into one row's tail: "downloading 40%", then "12,000 commits", then "812/1200 files", and nothing to say those were three different things happening rather than one stalled one. Split TimelineLoading into TimelineFetch, TimelineHistory and TimelineBlobs, each driven by its own stage and carrying its own tail. A local source hides the fetch row the way it hides Live's resolve and clone, which is now one rule (stepRuns / firstStepFor) rather than a hardcoded pair, so a list opens on the first row that actually runs. The overlay also stopped at "Resolving files 9056/9056" and sat there: the blobs done tick fired before a whole blob_sizes_batch, and union assembly, the wire trip and the pack had no stage at all. The done tick now lands after the size batch, and a new `assemble` stage covers the rest, landing on the build row rather than leaving a finished-looking row to explain a minutes-long wait. An in-place refetch had no progress surface at all. It now follows Live: a refresh you asked for gets the same stepped overlay, holding the scrub position and cancellable, while the exclude-edit refetch stays quiet but carries its stage beside the freshness dot instead of a bare "rebuilding…". Drops the "Adding decorations" row. Measured against every path a decoration pass can start on: with the skeleton still up the scan reaction overwrites the step in the same batch, and on both later paths the overlay hides at that instant. It was never the active row in either mode, so it and its REBUILD_STATUS bridge go. RebuildStatus.Decorating stays: the freshness readout reports it, and that one works. Refs #179 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The pre-push gate lints changed app files with house/comment-length on, so touching a file brings its older comments under the 2-line cap too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The stage discriminant was a bare string at every site: the emitters in scan/timeline.py, the router's comparisons, and the client's tail formatter and step map. Now TimelineStage (StrEnum, like ScanEvent) in api/models/events.py, and a matching TS enum that TimelineProgress substitutes for the wire union, so a loose string no longer typechecks. The schema keeps the inline enum on the field (WithJsonSchema, like _CLONE_STAGES), so the generated union is unchanged; pydantic adds a TimelineStage component beside it, which manifest.contract.ts asserts against in both directions. Verified by breaking it each way. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 14, 2026
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.
Closes #179
Timeline entry showed two rows against Live's six, with all three server stages flattened into one row's tail: "downloading 40%", then "12,000 commits", then "812/1200 files", and nothing to say those were three different things happening rather than one stalled one.
The three stages get their own rows
TimelineLoadingsplits intoTimelineFetch/TimelineHistory/TimelineBlobs, each driven by its stage and carrying its own tail:A local source hides the fetch row the way it hides Live's resolve and clone. That rule is now one definition (
stepRuns/firstStepFor) instead of a hardcoded pair, so any list opens on the first row that actually runs rather than onsteps[0].The row that sat at 100%
Testing it on
preactjs/preactturned up a second gap: the overlay reached "Resolving files 9056/9056" and stopped there for a long while. Two causes, both server-side:blobsdone tick fired beforeblob_sizes_batch, a whole batch git call over every shaThe done tick now lands after the size batch, and a new
assemblestage covers the rest. It maps onto the existing Building city row rather than adding a label: from the outside, the server assembling the union, the wire trip, and the pack are one wait you can't tell apart. The wait is the same length, but it no longer sits under a row that says it finished.Wire change, so
manifest.generated.tsis regenerated:stagegains"assemble".The stage is an enum now
It was a bare string at every site — the emitters in
scan/timeline.py, the router's comparisons, the client's tail formatter and step map. NowTimelineStage(aStrEnum, likeScanEvent) on the server, and a matching TS enum thatTimelineProgresssubstitutes for the wire union, sostage: 'blobs'no longer typechecks.The field keeps its inline enum in the schema (
WithJsonSchema, the_CLONE_STAGESidiom), so the generated union is unchanged; pydantic emits aTimelineStagecomponent beside it, whichmanifest.contract.tsnow asserts against in both directions — a stage added on either side fails typecheck until it is added on the other. Checked by breaking it each way.In-place refetch
Was minutes of work behind the freshness dot, with no steps, no tail and no cancel. It now follows Live's split:
The stage tail is deliberately not in the accessible name:
ScanMenuannounces that from a live region, and a count ticking every few hundred ms would talk over everything else."Adding decorations" is gone
The issue asked whether that row belonged on Timeline's list too. It doesn't belong on either. Measured against every path a decoration pass can start on:
skeleton— the scan reaction overwrites the step in the same batchIt was never the active row in either mode, so the row, its label and its
REBUILD_STATUSbridge effect go.RebuildStatus.Decoratingstays: the freshness readout reports it, and that one works.Not in here
/api/timelineemits progress then one complete bundle, so a skeleton row could never light up. It's feasible (the union tree is known after the walk, before blob resolution) but it needs a server event, a partial-apply path and packer work.api/manifest_types.pyusesOptionalwithout importing it (2× F821). Harmless at runtime underfrom __future__ import annotations, andruff checkisn't in the gate, which is why it has survived. Untouched here.just gen-typesoutput doesn't match the committed file's formatting (4-space/double-quote vs the committed 2-space/single-quote), and the file is in.prettierignore, so a plain regeneration produces ~1,100 lines of churn. I force-formatted to match what's committed; the recipe and the ignore entry disagree about who owns that file.Verification
just-equivalent gate, all green: 452 python tests, 1976 frontend tests, ruff format, prettier, eslint (including the comment cap over changed files), typecheck. The pre-push gate ran the lot in containers.The Decorating finding came from a throwaway probe against the real reactions rather than from reading the code; the guard tests were checked by breaking the code and watching them go red.
🤖 Generated with Claude Code