Hmbown/CodeWhale · error · ExecError
menu_item_not_found
menu_item_not_found
Error message
menu item "${titles[level]}" not found ${pressed.length ? `under ${pressed.join(" ▸ ")}` : "on the menu bar"} — menus expose items only while open; check the exact title with get_app_state (an ellipsis is part of the title) What it means
During invokeMenu's level-by-level traversal, each title is looked up via findMenuItem against the bound app's menu bar; menus only expose their items while open, so the library presses each level and polls the next. When a title cannot be found at its level, it throws this error carrying code "menu_item_not_found" and reports how far the traversal got.
Solutions
- Call get_app_state on the bound app and copy the exact menu titles it reports (an ellipsis is part of the title).
- Match case and punctuation exactly; try the trimmed title as displayed.
- Split deeper navigation: if the item is beyond 3 levels or in a context submenu, navigate in steps or use an element action on the control instead.
- Ensure the app is bound via open_application and its menu bar is live before invoking.
Example fix
// before await backend.invokeMenu(["File", "Save As"]); // after await backend.invokeMenu(["File", "Save As…"]); // ellipsis is part of the title
Defensive patterns
Strategy: try-catch
Validate before calling
null
Try / catch
try {
await backend.invokeMenu(path);
} catch (e) {
if (e.code === 'menu_item_not_found') {
const st = await backend.getAppState({});
// re-derive exact titles from st and retry or surface to the user
} else throw e;
} Prevention
- Read menu titles from get_app_state instead of hardcoding them.
- Include ellipses (…) exactly as the title displays them.
- Expect localized menu bars — do not reuse titles across locales.
- Prefer element actions on window controls when a menu path is fragile.
When it happens
Trigger: A misspelled or case-mismatched menu title; omitting an ellipsis that is part of the title (e.g. "Save As…"); a localized menu bar; an item that only exists in a submenu reachable by a different path; the target app's menu bar not yet populated.
Common situations: Automating apps whose menus change by context; localizing scripts written against an English menu bar; agents guessing titles instead of reading get_app_state output.
Understand the failure class
Background: 'Could not be found', 'does not exist', 'not found in database': the resource-not-found family when an ID, slug, key, or URI lookup comes back empty — this error's family across 20 libraries.
Related errors
- application not found — call list_apps for exact names/pids
- element does not belong to the bound application —…
- element has no resolved accessibility identity
- element press was not acknowledged
- native accessibility helper needs a built app or Xcode…
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/7cf77be0b1fc1959.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/plugins/computer-use/src/backends/darwin.mjs:834
}
/**
* Menu items by title path, through accessibility only: no key events, no
* focus lease. Menus expose items only while open, so each level is pressed
* and the next is polled for. Exact titles; an ellipsis is part of the title.
*/
async function invokeMenu(menuPath) {
if (!state.inputApp) throw new ExecError("open_application first — invoke_menu acts on the bound application");
if (!Array.isArray(menuPath) || menuPath.length < 1 || menuPath.length > 3 || menuPath.some((s) => typeof s !== "string" || !s.trim())) {
throw new ExecError('invoke_menu needs path: 1..3 non-empty menu titles, e.g. ["File","New"]');
}
const titles = menuPath.map((s) => s.trim());
const app_ref = state.inputApp;
const pressed = [];
for (let level = 0; level < titles.length; level++) {
const found = await findMenuItem(app_ref, titles[level], level === 0);
if (!found) {
throw Object.assign(new ExecError(`menu item "${titles[level]}" not found ${pressed.length ? `under ${pressed.join(" ▸ ")}` : "on the menu bar"} — menus expose items only while open; check the exact title with get_app_state (an ellipsis is part of the title)`), { code: "menu_item_not_found" });
}
if (found.enabled === false) {
throw Object.assign(new ExecError(`menu item "${titles[level]}" is present but disabled right now — the app validates it against its current state (in background mode that is often a missing key window for window-targeted commands like Close). Use an element action on the window's own control instead of pressing a disabled item.`), { code: "menu_item_disabled" });
}
const target = { app_ref, windowIndex: found.windowIndex ?? 0, path: found.path, role: found.role, label: found.label };
assertBoundElement(target);
const action = found.role === "AXMenuItem" && (found.actions ?? []).includes("AXPick") ? "AXPick" : "AXPress";
await native("perform_action", { target, action });
pressed.push(titles[level]);
if (level < titles.length - 1) await wait(140);
}
return { action_sent: true, strategy: "a11y", route: "accessibility", delivery: "background", menu: pressed, front_lease: false,
note: "Menu activation used accessibility only — no key events or focus lease. Verify the app effect (list_windows / get_app_state) before reporting success." };
}
/** Poll for the exact menu element; opens and submenu population are async. */
async function findMenuItem(app_ref, label, menuBar) {
const deadline = Date.now() + 4_000;View on GitHub (pinned to 73e0f67d83)