Skip to content

feat: model-load progress indicator (tray/overlay) for launch and switch preload #17

Description

@BumpyClock

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.rsCoreEvent (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

  1. 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.

  2. 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).

  3. 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.

  4. 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

  • CoreEvent::ModelLoadStarted { model_id } is emitted at the start of every spawn_warm_up call that actually enters warm_up (not when already resident / no-op path).
  • CoreEvent::ModelLoadFinished { model_id } is emitted after warm_up returns Ok(()).
  • Tray icon or tooltip reflects "loading model…" while load is in progress and reverts on ModelLoadFinished.
  • Overlay shows a "loading" state (badge or label) during the load window and clears on finish.
  • No regression: existing session-phase transitions (Idle → Listening → Transcribing → Idle) are unaffected.
  • No regression: warm_up idempotency — calling spawn_warm_up when the engine is already resident does not emit a visible loading state (event pair emits and resolves instantly, UI transition imperceptible).
  • Serialization tests pass for ModelLoadStarted and ModelLoadFinished (snake_case type tag, model_id field).
  • Existing tests warm_up_loads_model_without_transcribing and warm_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_upcrates/core/src/controller.rs:594
  • spawn_model_preload callsite in AppController::newcrates/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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions