Problem
spawn_warm_up fires on every app launch (when a model is selected) and on every model switch or accelerator change, but emits no UI signal. The call chain is:
crates/core/src/controller.rs
// line 165-167 (AppController::new)
if has_selected_model {
controller.spawn_model_preload();
}
// lines 585-587 (spawn_model_preload)
fn spawn_model_preload(&self) {
spawn_warm_up(&self.runtime, self.transcription.clone());
}
// lines 594-600 (spawn_warm_up — the actual producer gap)
fn spawn_warm_up(runtime: &tokio::runtime::Handle, transcription: Arc<TranscriptionEngineState>) {
let _preload_task = runtime.spawn_blocking(move || {
if let Err(err) = transcription.warm_up() {
log::warn!("Failed to warm up STT model: {err}");
}
});
}
spawn_warm_up is also called from start_listening (line 338), update_settings (line 256 — on model/accelerator change), and activate_downloaded_model (line 275 in controller/models.rs).
TranscriptionEngineState::warm_up (crates/core/src/transcription/engine.rs:547) does the actual ONNX load under the engine cell lock; it returns only after the model is in memory. No CoreEvent is emitted before or after.
crates/shared/src/events.rs — CoreEvent (lines 22–50) has no ModelLoad* variants. The overlay's session-phase loop in crates/app/src/app_state.rs:subscribe_to_events (lines 251–363) only watches SessionPhase and mic frames; it has no path to render a "loading model" state.
Result: from the user's perspective the app is completely silent during a multi-second Whisper-large warm-up; the tray icon and overlay sit idle with no feedback.
Why it matters
- Perceived freeze on first launch. Parakeet (~1–2 s cold) is tolerable; Whisper-large can exceed 5 s on CPU. No tray icon change, no overlay badge — the app looks broken.
- Model-switch confusion. When the user changes models in settings,
spawn_warm_up fires immediately (line 256 in update_settings). The next dictation attempt silently blocks inside warm_up until the new engine loads. No feedback during that window.
- Runs on every launch for any user with a selected model (line 165–167 in
AppController::new), so the regression surface is broad.
Proposed change
-
crates/shared/src/events.rs — add two variants to CoreEvent:
/// STT model has started loading (warm-up began).
ModelLoadStarted { model_id: ModelId },
/// STT model finished loading and is ready.
ModelLoadFinished { model_id: ModelId },
Add serialization tests mirroring the existing model_download_progress pattern.
-
crates/core/src/controller.rs — thread EventBus into spawn_warm_up (or wrap it at the spawn_model_preload callsite) and emit the pair around transcription.warm_up():
event_bus.emit(CoreEvent::ModelLoadStarted { model_id });
transcription.warm_up()?;
event_bus.emit(CoreEvent::ModelLoadFinished { model_id });
spawn_warm_up already receives the TranscriptionEngineState arc; add EventBus and ModelId (readable from transcription.current_model_id() or passed in from the controller). The warm_up call is idempotent when the engine is already resident, so emitting Started/Finished in that case is a no-op from the UI's perspective (instant round-trip, invisible).
-
crates/app/src/app_state.rs — handle ModelLoadStarted / ModelLoadFinished in subscribe_to_events (or a new dedicated subscription). The simplest surface is the tray icon tooltip / menu item label; the overlay could show a small "loading…" badge in place of the session-phase arc. Both are additive — no existing state machine changes required.
-
Tests to add or guard:
- Unit test in
crates/core/src/controller.rs that a mock EventBus receives ModelLoadStarted then ModelLoadFinished around a warm-up (can be done with a broadcast::channel spy, no real model needed — mock warm_up to return Ok(())).
- Serialization roundtrip tests for the two new
CoreEvent variants in crates/shared/src/events.rs.
- Existing
warm_up_loads_model_without_transcribing and related tests in crates/core/src/transcription/engine.rs must remain green; they do not touch EventBus so no changes expected.
Recommendation / triage
DO LATER — small, additive, no risk to the model-residency feature that already shipped. Wire one event pair from spawn_warm_up and a minimal tray/overlay state. Good first follow-up once the residency feature soaks in production for a release cycle.
Acceptance criteria
Traceability
Planning follow-up from the model-residency session — no clawpatch finding ID. Not tied to a separate open issue at time of filing.
Key symbols verified against current code (2026-06-18):
spawn_warm_up — crates/core/src/controller.rs:594
spawn_model_preload callsite in AppController::new — crates/core/src/controller.rs:166
CoreEvent enum — crates/shared/src/events.rs:22 (no ModelLoad* variants present)
subscribe_to_events overlay loop — crates/app/src/app_state.rs:251
Problem
spawn_warm_upfires on every app launch (when a model is selected) and on every model switch or accelerator change, but emits no UI signal. The call chain is:crates/core/src/controller.rsspawn_warm_upis also called fromstart_listening(line 338),update_settings(line 256 — on model/accelerator change), andactivate_downloaded_model(line 275 incontroller/models.rs).TranscriptionEngineState::warm_up(crates/core/src/transcription/engine.rs:547) does the actual ONNX load under the engine cell lock; it returns only after the model is in memory. NoCoreEventis emitted before or after.crates/shared/src/events.rs—CoreEvent(lines 22–50) has noModelLoad*variants. The overlay's session-phase loop incrates/app/src/app_state.rs:subscribe_to_events(lines 251–363) only watchesSessionPhaseand mic frames; it has no path to render a "loading model" state.Result: from the user's perspective the app is completely silent during a multi-second Whisper-large warm-up; the tray icon and overlay sit idle with no feedback.
Why it matters
spawn_warm_upfires immediately (line 256 inupdate_settings). The next dictation attempt silently blocks insidewarm_upuntil the new engine loads. No feedback during that window.AppController::new), so the regression surface is broad.Proposed change
crates/shared/src/events.rs— add two variants toCoreEvent:Add serialization tests mirroring the existing
model_download_progresspattern.crates/core/src/controller.rs— threadEventBusintospawn_warm_up(or wrap it at thespawn_model_preloadcallsite) and emit the pair aroundtranscription.warm_up():spawn_warm_upalready receives theTranscriptionEngineStatearc; addEventBusandModelId(readable fromtranscription.current_model_id()or passed in from the controller). Thewarm_upcall is idempotent when the engine is already resident, so emittingStarted/Finishedin that case is a no-op from the UI's perspective (instant round-trip, invisible).crates/app/src/app_state.rs— handleModelLoadStarted/ModelLoadFinishedinsubscribe_to_events(or a new dedicated subscription). The simplest surface is the tray icon tooltip / menu item label; the overlay could show a small "loading…" badge in place of the session-phase arc. Both are additive — no existing state machine changes required.Tests to add or guard:
crates/core/src/controller.rsthat a mockEventBusreceivesModelLoadStartedthenModelLoadFinishedaround a warm-up (can be done with abroadcast::channelspy, no real model needed — mockwarm_upto returnOk(())).CoreEventvariants incrates/shared/src/events.rs.warm_up_loads_model_without_transcribingand related tests incrates/core/src/transcription/engine.rsmust remain green; they do not touchEventBusso no changes expected.Recommendation / triage
DO LATER — small, additive, no risk to the model-residency feature that already shipped. Wire one event pair from
spawn_warm_upand a minimal tray/overlay state. Good first follow-up once the residency feature soaks in production for a release cycle.Acceptance criteria
CoreEvent::ModelLoadStarted { model_id }is emitted at the start of everyspawn_warm_upcall that actually enterswarm_up(not when already resident / no-op path).CoreEvent::ModelLoadFinished { model_id }is emitted afterwarm_upreturnsOk(()).ModelLoadFinished.Idle → Listening → Transcribing → Idle) are unaffected.warm_upidempotency — callingspawn_warm_upwhen the engine is already resident does not emit a visible loading state (event pair emits and resolves instantly, UI transition imperceptible).ModelLoadStartedandModelLoadFinished(snake_casetypetag,model_idfield).warm_up_loads_model_without_transcribingandwarm_up_after_model_switch_during_inflight_loads_fresh_engine(engine.rs) remain green.Traceability
Planning follow-up from the model-residency session — no clawpatch finding ID. Not tied to a separate open issue at time of filing.
Key symbols verified against current code (2026-06-18):
spawn_warm_up—crates/core/src/controller.rs:594spawn_model_preloadcallsite inAppController::new—crates/core/src/controller.rs:166CoreEventenum —crates/shared/src/events.rs:22(noModelLoad*variants present)subscribe_to_eventsoverlay loop —crates/app/src/app_state.rs:251