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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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