fix(dev-fleet): report slow actions as indeterminate, not as a fake percentage - #2439
Conversation
Design Review (Fable 5) — ✅ PASSAdvisory design-level review of The change is small, well-scoped, and deletes more model than it adds. The Design-Verdict: PASS Deleting the fabricated percentage model instead of tuning it is the right call; indeterminate + server step label matches what the backend can actually assert. [DESIGN-REVIEWED] 32dccba |
Opus 4.8 Review — ✅ no blocking findingsReviewed Review detailsThe diff removes the determinate progress bar and replaces it with an indeterminate spinner. Verified: no dangling references to removed functions ( No findings. [OPUS-REVIEWED] 32dccba Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
UX Review (Fable 5) — ✅ PASSAdvisory UX-level review of UX-Verdict: PASS Replacing a stalling fake percentage with spinner + server step label + elapsed time is honest, verifiable in the committed screenshots, and ARIA-correct. Suggestions
[UX-REVIEWED] 32dccba |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
Disposition — UX Review suggestion on
|
…ercentage Two Dev Fleet progress affordances read as a hung page. Prune merged: the merged-scan (`prune-candidates`) walks worktrees SERIALLY, running several `git` calls plus a `gh` PR lookup each, so on a large fleet the click is followed by seconds of silence. The button was only `disabled` for that window, which is indistinguishable from a wedged page — so users click again or reload mid-scan. It now swaps its trash glyph for a spinner and sets `aria-busy`. Pull+Build: the sync row rendered a percentage derived from the step index over a hardcoded `[0, 5, 8, 25, 55, 100]` curve, plus a time-based "creep" toward the next band. The five steps differ in duration by more than an order of magnitude and shift with network and cache state, so the bar sat at ~8% through the whole pip install and then jumped — precision the backend does not have, and a stall to anyone watching. Progress is now indeterminate: a spinner whose `role="progressbar"` carries no `aria-valuenow` (the ARIA form for "in progress, amount unknown"), beside the server's own step label and elapsed time. The step-index model behind the percentage (`SYNC_STEP_CUM`, `syncPercent`, `syncPhaseFromLines`, `SyncRun.phase`/`phaseAt`) is deleted with it; the honest signal, `step_label`, comes from the run worker and is unchanged. Three `LoaderCircle` icons on the page (pod busy, provisioning, sync) also carried no spin class at all — static "loading" glyphs. They spin now. No new catalog keys: the spinner reuses the existing `sync_progress` string as its accessible name, so no locale churn and no dead key.
3b437f3 to
32dccba
Compare
Disposition — the two red Windows shards on
|
Dispositions — UX Review's three suggestions on
|
…ercentage (kirodotdev#2439) Two Dev Fleet progress affordances read as a hung page. Prune merged: the merged-scan (`prune-candidates`) walks worktrees SERIALLY, running several `git` calls plus a `gh` PR lookup each, so on a large fleet the click is followed by seconds of silence. The button was only `disabled` for that window, which is indistinguishable from a wedged page — so users click again or reload mid-scan. It now swaps its trash glyph for a spinner and sets `aria-busy`. Pull+Build: the sync row rendered a percentage derived from the step index over a hardcoded `[0, 5, 8, 25, 55, 100]` curve, plus a time-based "creep" toward the next band. The five steps differ in duration by more than an order of magnitude and shift with network and cache state, so the bar sat at ~8% through the whole pip install and then jumped — precision the backend does not have, and a stall to anyone watching. Progress is now indeterminate: a spinner whose `role="progressbar"` carries no `aria-valuenow` (the ARIA form for "in progress, amount unknown"), beside the server's own step label and elapsed time. The step-index model behind the percentage (`SYNC_STEP_CUM`, `syncPercent`, `syncPhaseFromLines`, `SyncRun.phase`/`phaseAt`) is deleted with it; the honest signal, `step_label`, comes from the run worker and is unchanged. Three `LoaderCircle` icons on the page (pod busy, provisioning, sync) also carried no spin class at all — static "loading" glyphs. They spin now. No new catalog keys: the spinner reuses the existing `sync_progress` string as its accessible name, so no locale churn and no dead key.
Problem
Two Dev Fleet progress affordances read as a hung page.
disabledfor that window, which is visually indistinguishable from a wedged page, so the user clicks again or reloads mid-scan.pip install, then jumped.Why it matters
Both are the slowest actions on the page, which is exactly where a user needs to know the difference between "working" and "dead". A wrong percentage is worse than no percentage: it invites the user to wait for a number that isn't tracking anything, and then to give up when it stalls. And an unresponsive-looking Prune button gets re-clicked or reloaded mid-scan.
Fix (symptoms → root cause → change)
Prune merged: silence during a real scan
prune-candidateswalks worktrees serially, running severalgitcalls plus aghPR lookup per worktree (_prune_candidates→_prunable→_pr_status_cached/_fetch_pr_head_oid). The delay is real work, not a hang — but the UI had no way to say so, becausedisabledis the only state the button had.The trash glyph now becomes a spinner in place and the button sets
aria-busyfor the duration of the scan.The serial loop itself is the cause of the wait, and parallelizing it (as
_prune_runalready does with a semaphore) would shorten it — deliberately left to a separate PR, since that is a different problem.Pull+Build: a percentage the backend cannot support
syncPercent()mapped the step index onto a hardcodedSYNC_STEP_CUM = [0, 5, 8, 25, 55, 100]curve, plus a time-based "creep" that crawled toward the next band at one point per 4s. The five steps (fetch / merge / pip / npm ci / build) differ in duration by more than an order of magnitude and shift with network and cache state, so the bar parks in one band for most of the run and then jumps — precision the backend does not have, and a stall to anyone watching.Progress is now indeterminate: a spinner whose
role="progressbar"carries noaria-valuenow(the ARIA form for "in progress, amount unknown"), beside the server's own step label and the elapsed timer.step_labelcomes from the run worker and is the honest signal — it survives the 60-line output tail window and names the step actually in flight.The whole step-index model behind the percentage is deleted with it:
SYNC_STEP_CUM,SYNC_TOTAL_STEPS,syncPhaseFromLines(),syncPercent(), andSyncRun.phase/phaseAtacross all fivesetSyncRuncall sites.STEP_MARKER_REstays — it still strips markers from the log panel and picks the last real output line.Static spinners
Three
LoaderCircleicons on this page (pod busy, provisioning, sync) carried no spin class at all — static "loading" glyphs. They spin now, and the restart overlay's inlineanimation: 'spin 1s linear infinite'moves to the sameanimate-spinutility.No new catalog keys
The spinner reuses the existing
pages.devFleetPage.sync_progressstring as its accessible name, so there is no locale churn and no dead key. The change also deletes a hardcoded, unlocalized'~' + pct + '%'render, so the i18n surface gets strictly smaller.Screenshots
Captured from an isolated pod running this branch's built bundle (
kirocrew pod up fleet-progress), driving a real Pull+Build against themainworktree. Nothing is injected or stubbed — the step labels and elapsed times below are a genuine sync in flight, which is the only way this row is reachable.Mid-sync, early — spinner + the server's
pip installstep label + elapsed. No percentage, no bar:Later in the same run — the step label has moved to
npm build + stageat 0:22, with the log panel open. This stretch is exactly where the old bar sat parked in its[8, 25]band and looked stalled; the label plus a ticking timer says the same thing honestly:At rest, and the terminal state
The fleet at rest —
Prune mergedin its normal trash-glyph state, for comparison with the spinner it swaps to while scanning:The same run reaching its terminal state — the indeterminate row is replaced by the existing green
Syncedsummary, unchanged by this PR:The prune-scan spinner has no capture. On a two-worktree fleet the scan finishes in well under a second, so the in-flight window is not reliably photographable, and the only way to hold it open would be to stub the backend response — which produces a frame that shows a state the server was forced into. The two vitest cases below assert that state instead; that is the honest artifact for it.
Tests
website/src/test/DevFleetPage.test.tsx:reports a running sync as indeterminate — a spinner, never a percentage(replaces the old test, which asserted the percentage band). Locks in:aria-valuenowandaria-valuemaxare absent (not zeroed), the progressbar carriesanimate-spin, no%text renders anywhere, and the server'sstep_labelis still displayed. The mock feeds both a::step::0::marker andstep: 3so a reintroduced percentage of any flavour fails it.spins the Prune merged button while the merged-scan is in flight(new). Gates theprune-candidatesresponse on a manually-resolved promise so the in-flight window is observable, then asserts: no spinner before the click →aria-busy="true"+ spinner after → spinner gone once the dialog opens.Manual verification
The screenshots above are the manual verification for the sync half, taken against a real in-flight Pull+Build in an isolated pod. The prune half is covered by unit assertions for the reason given above.
Local gates, all green in the worktree:
npx tsc -bnpx vitest runnpx eslint src/ --max-warnings 1116npx jscpd .npm run i18n:check./scripts/docs-lint.sh./scripts/scrub-lint.sh --no-historycheck_brand_name.pyBackend untouched, so no pytest run.
Docs
docs/system-specs/modules/dev-fleet.mdgains the scan-feedback rationale under the prune section, and the sync section now states that progress is indeterminate by design and why the step index is a poor basis for a percentage. The stale "beside the 'Syncing' progress bar" sentence is corrected.