Hmbown/CodeWhale · warning · ExecError
invoke_menu needs path: 1..3 non-empty menu titles, e.g…
Error message
invoke_menu needs path: 1..3 non-empty menu titles, e.g. ["File","New"]
What it means
invokeMenu validates the menu path argument: it must be an array of 1 to 3 non-empty strings (menu titles, trimmed). Anything else — wrong type, zero items, more than 3 levels, or whitespace-only/non-string entries — throws this error before any accessibility lookup happens.
Solutions
- Wrap the path in an array: ["File", "New"], not "File".
- Keep the path to 1–3 titles and ensure each is a non-empty, non-whitespace string.
- Trim or filter empty segments before calling.
- Verify exact titles (including ellipsis '…' if present) with get_app_state.
Example fix
// before
await backend.invokeMenu("File > New");
// after
await backend.invokeMenu(["File", "New"]); Defensive patterns
Strategy: type-guard
Validate before calling
const validMenuPath = p => Array.isArray(p) && p.length >= 1 && p.length <= 3 && p.every(s => typeof s === 'string' && s.trim().length > 0);
if (!validMenuPath(menuPath)) throw new Error('menu path must be 1..3 non-empty strings'); Type guard
function isMenuPath(p) {
return Array.isArray(p) && p.length >= 1 && p.length <= 3 && p.every(s => typeof s === 'string' && s.trim() !== '');
} Try / catch
if (!isMenuPath(menuPath)) {
throw new Error(`bad menu path: ${JSON.stringify(menuPath)}`);
}
await backend.invokeMenu(menuPath); Prevention
- Normalize the path: array of trimmed non-empty strings before calling.
- Never pass a joined string like 'File > New' — always an array.
- Cap paths at 3 levels; navigate deeper structures in element-action steps.
When it happens
Trigger: Passing a plain string like "File" instead of ["File"], an empty array, a 4-level path, or an array containing null/empty/whitespace-only titles.
Common situations: Model/agent output shaping the path as a single joined string; building the path programmatically and including an empty segment from a split; macOS menu nesting deeper than three levels being attempted.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- 1
- A pinned task provider requires an explicit model
- A positive pull request number is required
- A provider and page loader are required.
- accepted_requests must equal requests_attempted; sender…
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/e64f021728bf8f62.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/plugins/computer-use/src/backends/darwin.mjs:826
// Recording) must never block the bind itself. The first successful
// capture also starts the refresh loop so the panel stays live while bound.
if (state.previewEnabled) {
previewBusy = true;
updatePreview(true).catch(() => {}).finally(() => { previewBusy = false; });
}
return { launched, activate, keyboard_delivery: activate ? "foreground-guarded" : "process", input_scope: activate ? "shared-desktop" : "application", shared_pointer: !!activate, isolated_desktop: false, url: urlArg ?? null, resolved: p?.found ? { name: p.name, pid: p.pid, bundle_id: p.bundle_id, frontmost: p.frontmost } : null,
...(Number.isFinite(p?.yield_ms) && p.yield_ms > 0 ? { yield_ms: p.yield_ms } : {}) };
}
/**
* 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);View on GitHub (pinned to 73e0f67d83)