{"record":{"id":"97f993d722eb9fd9","repo":"cjpais/Handy","slug":"failed-to-create-cancel-item","errorCode":null,"errorMessage":"failed to create cancel item","messagePattern":"failed to create cancel item","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src-tauri/src/tray.rs","lineNumber":286,"sourceCode":"    let needs_icon = !state.lock().icons.contains_key(desired.icon_path);\n    let loaded_icon = if needs_icon {\n        match load_tray_icon(\n            app.path()\n                .resolve(desired.icon_path, tauri::path::BaseDirectory::Resource),\n        ) {\n            Ok(image) => Some(image),\n            Err(err) => {\n                error!(\"Failed to load tray icon '{}': {err}\", desired.icon_path);\n                None\n            }\n        }\n    } else {\n        None\n    };\n\n    let schedule = {\n        let mut inner = state.lock();\n        if let Some(image) = loaded_icon {\n            inner.icons.insert(desired.icon_path, image);\n        }\n        if seq < inner.desired_seq {\n            // A request triggered after this one already stored its snapshot\n            // (and scheduled an apply). Ours is stale; drop it.\n            trace!(\n                \"tray sync: request {seq} superseded by {}\",\n                inner.desired_seq\n            );\n            return;\n        }\n        inner.desired = Some(desired);\n        inner.desired_seq = seq;\n        // If an apply is already pending it will read the snapshot we just\n        // stored; otherwise schedule one.\n        !std::mem::replace(&mut inner.pending, true)\n    };\n","sourceCodeStart":268,"sourceCodeEnd":304,"githubUrl":"https://github.com/cjpais/Handy/blob/fbd4e15fa14a721c66c57006ae110428b9e255b3/src-tauri/src/tray.rs#L268-L304","documentation":"When the tray state is Recording or Transcribing, the rebuild first creates a cancel item before assembling the menu. This .expect panics if item creation fails. Notably this code path executes in the middle of an active recording/transcription session (the state change itself triggers the rebuild), so a panic here does not just crash the UI — it aborts a session where the user is actively dictating, losing the buffer unless cancellation cleanup runs.","triggerScenarios":"Starting or finishing a recording on a Linux system whose DBusMenu registrar is missing/flaky; the state transition to Recording firing update_tray_menu at the exact moment the session tray restarts; rapid toggle of the global shortcut causing back-to-back rebuilds.","commonSituations":"Minimal window managers; CI smoke tests that toggle transcription without a desktop; users whose tray applet crashed mid-session.","solutions":["Reproduce with a working tray present to confirm the environment is the cause","Convert the Recording/Transcribing branch to construct the cancel item with graceful fallback (skip the item or reuse the idle layout) instead of .expect","Debounce/route all update_tray_menu calls through a single serialized task so overlapping rebuilds cannot race","On Linux, detect StatusNotifierWatcher availability at startup and degrade to no-tray mode instead of panicking mid-recording"],"exampleFix":"// before\nlet cancel_i = MenuItem::with_id(app, \"cancel\", &strings.cancel, true, None::<&str>)\n    .expect(\"failed to create cancel item\");\n\n// after\nlet Ok(cancel_i) = MenuItem::with_id(app, \"cancel\", &strings.cancel, true, None::<&str>) else {\n    error!(\"Failed to create cancel item; keeping previous tray menu\");\n    return;\n};","handlingStrategy":"fallback","validationCode":null,"typeGuard":null,"tryCatchPattern":"let Ok(cancel_i) = MenuItem::with_id(app, \"cancel\", &strings.cancel, true, None::<&str>) else {\n    error!(\"cancel item failed; keeping previous menu mid-recording\");\n    return; // never panic while the user is dictating\n};","preventionTips":["Treat recording-state code paths as crash-critical: any panic there loses the user's in-flight audio","Build the cancel-bearing menu lazily and only when the recording state is stable, not on transient states","Add integration tests that toggle recording on a tray-less Linux session to prove graceful degradation"],"tags":["tauri","tray","recording","muda","rust","panic"],"backgroundTag":"tauri-tray-menu-creation-failed","analyzedSha":"fbd4e15fa14a721c66c57006ae110428b9e255b3","analyzedAt":"2026-08-17T10:29:55.597Z","contentChangedAt":"2026-08-17T10:29:55.597Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}