cjpais/Handy · error

failed to create unload model item

Error message

failed to create unload model item

What it means

The unload-model MenuItem is created with enabled = model_loaded (queried from TranscriptionManager). The enabled flag only affects grey-out state, not creation success, so this .expect panics under the same general conditions as the other items: the native menu backend (muda via Tauri) refusing to register the item — overwhelmingly a Linux tray/DBusMenu availability problem or a teardown race.

Solutions

  1. Run the app inside a desktop session with a functional system tray when exercising tray code paths
  2. Make update_tray_menu fallible: on item-creation error, log and abort the rebuild keeping the previous menu
  3. Feature-test tray availability once at startup and skip rebuilds (hide tray, rely on --no-tray mode) when unavailable
  4. Avoid triggering update_tray_menu from model unload paths during shutdown

Example fix

// before
let unload_model_i = MenuItem::with_id(
    app, "unload_model", &strings.unload_model, model_loaded, None::<&str>,
)
.expect("failed to create unload model item");

// after
let unload_model_i = MenuItem::with_id(
    app, "unload_model", &strings.unload_model, model_loaded, None::<&str>,
)
.unwrap_or_else(|e| {
    warn!("Failed to create unload model item: {e}");
    return; // caller aborts rebuild, old tray stays
});
Defensive patterns

Strategy: fallback

Try / catch

let Ok(unload_model_i) = MenuItem::with_id(
    app, "unload_model", &strings.unload_model, model_loaded, None::<&str>,
) else { error!("unload item failed; aborting rebuild"); return; };

Prevention

When it happens

Trigger: Any update_tray_menu rebuild on a system lacking a StatusNotifierWatcher; a rebuild triggered by model load/unload completing while the tray backend is gone; invoking the builder after the app handle's event loop has dropped.

Common situations: GNOME without an AppIndicator extension; sway/niri/Hyprland without a bar providing tray; testing on Wayland sessions where Tauri's tray support is limited.

Related errors


AI-assisted analysis of cjpais/Handy@fbd4e15fa1 (2026-08-17). Data as JSON: /api/errors/a0b5e01906d6a084. Report an issue: GitHub.

Appendix: source

Thrown at src-tauri/src/tray.rs:281

    }

    let desired = compute_desired(app, icon_state);

    // Decode the icon off the main thread, once per path, outside the lock.
    let needs_icon = !state.lock().icons.contains_key(desired.icon_path);
    let loaded_icon = if needs_icon {
        match load_tray_icon(
            app.path()
                .resolve(desired.icon_path, tauri::path::BaseDirectory::Resource),
        ) {
            Ok(image) => Some(image),
            Err(err) => {
                error!("Failed to load tray icon '{}': {err}", desired.icon_path);
                None
            }
        }
    } else {
        None
    };

    let schedule = {
        let mut inner = state.lock();
        if let Some(image) = loaded_icon {
            inner.icons.insert(desired.icon_path, image);
        }
        if seq < inner.desired_seq {
            // A request triggered after this one already stored its snapshot
            // (and scheduled an apply). Ours is stale; drop it.
            trace!(
                "tray sync: request {seq} superseded by {}",
                inner.desired_seq
            );
            return;
        }
        inner.desired = Some(desired);
        inner.desired_seq = seq;

View on GitHub (pinned to fbd4e15fa1)