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
- Verify a tray registrar is present (Linux) and retry the rebuild once after a short delay before giving up
- Return Result from update_tray_menu and skip the model submenu (append a disabled placeholder item) when Submenu::with_id fails, instead of panicking
- Sanitize/truncate submenu_label (the active model name) to a sane length before passing it to the native backend
- 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
- Sanitize/truncate model names used as native menu labels
- Debounce update_tray_menu calls triggered by rapid model load/unload or language changes
- On Linux CI, set HANDY_NO_TRAY-style behavior or run under a desktop with a tray before exercising tray paths
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
- failed to create cancel item
- failed to create menu
- failed to create model item
- failed to create quit item
- failed to create separator
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)