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

  1. Call get_app_state on the bound app and copy the exact menu titles it reports (an ellipsis is part of the title).
  2. Match case and punctuation exactly; try the trimmed title as displayed.
  3. 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.
  4. 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

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


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)