cjpais/Handy · error

failed to create model submenu

Error message

failed to create model submenu

What it means

The model submenu is created with Submenu::with_id(app, "model_submenu", ...) and its creation is unwrapped with .expect. A submenu is a nested native menu, so on top of the usual menu-manager failures it can also fail when the backend cannot attach a child menu to the (not yet attached) parent, or when the label (the active model's display name) trips platform-specific menu limits. This builder runs on every tray rebuild, i.e. every state change, model load/unload, and language switch.

Solutions

  1. Verify a tray registrar is present (Linux) and retry the rebuild once after a short delay before giving up
  2. Return Result from update_tray_menu and skip the model submenu (append a disabled placeholder item) when Submenu::with_id fails, instead of panicking
  3. Sanitize/truncate submenu_label (the active model name) to a sane length before passing it to the native backend
  4. Debounce update_tray_menu calls triggered by rapid state changes so partial backends are not hammered

Example fix

// before
let submenu = Submenu::with_id(app, "model_submenu", &submenu_label, true)
    .expect("failed to create model submenu");

// after
let submenu = match Submenu::with_id(app, "model_submenu", &submenu_label, true) {
    Ok(s) => s,
    Err(e) => {
        warn!("Model submenu unavailable, using plain label item: {e}");
        MenuItem::with_id(app, "model_submenu", &submenu_label, false, None::<&str>)
            .unwrap_or_else(|_| return None) // caller keeps old menu
    }
};
Defensive patterns

Strategy: fallback

Try / catch

match Submenu::with_id(app, "model_submenu", &submenu_label, true) {
    Ok(s) => s,
    Err(e) => { warn!("model submenu unavailable: {e}"); return; }
}

Prevention

When it happens

Trigger: Rebuilding the tray while no DBus menu registrar is available on Linux; a model name with problematic content in a native-menu backend; creating the submenu during app teardown; very large label strings from a corrupt model metadata entry.

Common situations: Minimal Wayland compositors without tray support; unloading/loading a model which triggers update_tray_menu while the session tray is being restarted; development loops that rapidly toggle states.

Related errors


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

Appendix: source

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

    sync_tray_with(app, |_| {});
}

fn sync_tray_with(app: &AppHandle, update: impl FnOnce(&mut TrayInner)) {
    let Some(state) = app.try_state::<TrayState>() else {
        return;
    };

    // Record intent and claim a sequence number in one critical section, so
    // sequence order == the order in which state changes were requested.
    let (seq, icon_state) = {
        let mut inner = state.lock();
        update(&mut inner);
        inner.next_seq += 1;
        (inner.next_seq, inner.icon_state)
    };

    // Tray not built yet (early secure-input monitor callbacks). The intent
    // is kept and picked up by the first sync after the tray exists.
    if app.try_state::<TrayIcon>().is_none() {
        return;
    }

    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
            }

View on GitHub (pinned to fbd4e15fa1)