diff --git a/CHANGELOG.md b/CHANGELOG.md
index 21bd003e..f4791798 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -91,6 +91,7 @@ the frozen-backend fallback mirror it for their toolchains.
- Fast macOS process exits no longer turn a completed shutdown into a permission error (#1809)
+- The bootstrap splash no longer shows fabricated first-run install steps on a warm start or repair sync — a step now renders done only once it was actually observed (#1894)
## [0.5.2] — 2026-09-02
diff --git a/frontend/src/components/BootstrapSplash.jsx b/frontend/src/components/BootstrapSplash.jsx
index 391b132b..9d983e9d 100644
--- a/frontend/src/components/BootstrapSplash.jsx
+++ b/frontend/src/components/BootstrapSplash.jsx
@@ -120,6 +120,13 @@ const STEPS = [
'starting_backend',
];
+// Stages that only occur when there is actual first-run/repair work to do.
+// `awaiting_setup` renders its own screen (FirstRunSetup) rather than the
+// step list below, but it still counts as "install work observed" (#1894):
+// reaching it means Rust found no venv and is about to do real work, so the
+// journey chrome should already be armed by the time the step list appears.
+const INSTALL_STAGES = ['downloading_uv', 'creating_venv', 'installing_deps', 'awaiting_setup'];
+
const MAX_LOG_LINES = 200;
/** Scan logs + error message for known failure patterns and return i18n keys
@@ -445,6 +452,10 @@ export function BootstrapSplash({ stage, message }) {
const [progress, setProgress] = useState(null);
const [region, setRegionState] = useState('auto');
const [retrying, setRetrying] = useState(false);
+ // Stages actually seen this session (sticky/monotonic — see the tracking
+ // effect below). Drives "done" ticks and journey visibility off observed
+ // reality instead of list position (#1894).
+ const [observedStages, setObservedStages] = useState(() => new Set([stage]));
const logRef = useRef(null);
const prevProgRef = useRef(null); // {bytes, t} — last progress event
const rateRef = useRef(0); // EMA bytes/sec across events
@@ -454,6 +465,12 @@ export function BootstrapSplash({ stage, message }) {
const isFailed = stage === 'failed';
// Retrying an Intel-Mac install can never succeed — don't offer the dead end.
const isUnrecoverable = isFailed && isUnrecoverableFailure(message, logs);
+ // True once any genuine install stage has been observed this session. On a
+ // warm start the Rust stage jumps straight from `checking` to
+ // `starting_backend` — nothing here ever fires — so the first-run install
+ // chrome (journey rail, "Installing" heading, step list) stays suppressed
+ // instead of fabricating completed work (#1894).
+ const installWorkSeen = INSTALL_STAGES.some((s) => observedStages.has(s));
const handleRetry = async () => {
if (retrying) return;
@@ -484,6 +501,15 @@ export function BootstrapSplash({ stage, message }) {
}
};
+ // Record `stage` as observed the moment it's seen. Sticky/monotonic: the
+ // functional updater bails out (same Set reference) once a stage is
+ // already recorded, so this never un-observes anything and never loops.
+ // `bootstrap_status` is polled ~1/s by useBootstrapStage, so a stage that
+ // actually ran is guaranteed to land here at least once (#1894).
+ useEffect(() => {
+ setObservedStages((prev) => (prev.has(stage) ? prev : new Set(prev).add(stage)));
+ }, [stage]);
+
// Load persisted region on mount.
useEffect(() => {
if (typeof window === 'undefined' || !('__TAURI_INTERNALS__' in window)) return;
@@ -638,7 +664,10 @@ export function BootstrapSplash({ stage, message }) {
data-tauri-drag-region
>
- {t( - 'firstrun.resume_note', - 'Interrupted downloads resume automatically — closing the app is safe.', - )} -
+ {activeStep && stageProgress && ( + + {formatBytes(stageProgress.bytes_done)} + {stageProgress.bytes_total > 0 + ? ` / ${formatBytes(stageProgress.bytes_total)}` + : ''} + {pctFromBytes != null ? ` (${pctFromBytes}%)` : ''} + {stageProgress.bytes_total > 0 && + rateRef.current > 0 && + stageProgress.bytes_done < stageProgress.bytes_total && + ` · ${t('firstrun.eta_left', { + eta: formatEta( + (stageProgress.bytes_total - stageProgress.bytes_done) / + rateRef.current, + ), + defaultValue: '~{{eta}} left', + })}`} + + )} + + ); + })} + + )} + {installWorkSeen && ( ++ {t( + 'firstrun.resume_note', + 'Interrupted downloads resume automatically — closing the app is safe.', + )} +
+ )}