Skip to content

Give Timeline's loading overlay a row per history stage - #181

Merged
thalida merged 3 commits into
mainfrom
feat/issue-179-timeline-loading-steps
Aug 14, 2026
Merged

Give Timeline's loading overlay a row per history stage#181
thalida merged 3 commits into
mainfrom
feat/issue-179-timeline-loading-steps

Conversation

@thalida

@thalida thalida commented Aug 14, 2026

Copy link
Copy Markdown
Owner

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

TimelineLoading splits into TimelineFetch / TimelineHistory / TimelineBlobs, each driven by its stage and carrying its own tail:

✓ Fetching history   100%
✓ Walking commits    6,266 commits
▸ Resolving files    9,056/9,056 files
  Building city

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 on steps[0].

The row that sat at 100%

Testing it on preactjs/preact turned up a second gap: the overlay reached "Resolving files 9056/9056" and stopped there for a long while. Two causes, both server-side:

  • the blobs done tick fired before blob_sizes_batch, a whole batch git call over every sha
  • union assembly, the bundle's trip down the wire, and the client pack had no stage at all

The done tick now lands after the size batch, and a new assemble stage 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.ts is regenerated: stage gains "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. Now TimelineStage (a StrEnum, like ScanEvent) on the server, and a matching TS enum that TimelineProgress substitutes for the wire union, so stage: 'blobs' no longer typechecks.

The field keeps its inline enum in the schema (WithJsonSchema, the _CLONE_STAGES idiom), so the generated union is unchanged; pydantic emits a TimelineStage component beside it, which manifest.contract.ts now 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:

Live Timeline (before) Timeline (now)
Refresh / Fresh scan full overlay dot only full overlay, holding the scrub, cancellable
Exclude edit dot only dot only dot + live stage ("rebuilding… 12,000 commits")

The stage tail is deliberately not in the accessible name: ScanMenu announces 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:

when the trees pass starts overlay active row
skeleton up, metadata pending visible skeleton — the scan reaction overwrites the step in the same batch
metadata landed, history streaming hides at that instant not seen
after the stream ended hides at that instant not seen

It was never the active row in either mode, so the row, its label and its REBUILD_STATUS bridge effect go. RebuildStatus.Decorating stays: the freshness readout reports it, and that one works.

Not in here

  • "Sketching layout" for Timeline. No partial on the wire: /api/timeline emits 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.py uses Optional without importing it (2× F821). Harmless at runtime under from __future__ import annotations, and ruff check isn't in the gate, which is why it has survived. Untouched here.
  • just gen-types output 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

thalida and others added 2 commits August 13, 2026 21:28
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>
@thalida thalida linked an issue Aug 14, 2026 that may be closed by this pull request
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>
@thalida
thalida merged commit 70a0487 into main Aug 14, 2026
1 check passed
@thalida
thalida deleted the feat/issue-179-timeline-loading-steps branch August 14, 2026 01:50
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.

Timeline's loading overlay hides the detail it already has

1 participant