{"record":{"id":"e64f021728bf8f62","repo":"Hmbown/CodeWhale","slug":"invoke-menu-needs-path-1-3-non-empty-menu-titles-e-g-file","errorCode":null,"errorMessage":"invoke_menu needs path: 1..3 non-empty menu titles, e.g. [\"File\",\"New\"]","messagePattern":"invoke_menu needs path: 1\\.\\.3 non-empty menu titles, e\\.g\\. \\[\"File\",\"New\"\\]","errorType":"exception","errorClass":"ExecError","httpStatus":null,"severity":"warning","filePath":"crates/tui/plugins/computer-use/src/backends/darwin.mjs","lineNumber":826,"sourceCode":"    // Recording) must never block the bind itself. The first successful\n    // capture also starts the refresh loop so the panel stays live while bound.\n    if (state.previewEnabled) {\n      previewBusy = true;\n      updatePreview(true).catch(() => {}).finally(() => { previewBusy = false; });\n    }\n    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,\n      ...(Number.isFinite(p?.yield_ms) && p.yield_ms > 0 ? { yield_ms: p.yield_ms } : {}) };\n  }\n\n  /**\n   * Menu items by title path, through accessibility only: no key events, no\n   * focus lease. Menus expose items only while open, so each level is pressed\n   * and the next is polled for. Exact titles; an ellipsis is part of the title.\n   */\n  async function invokeMenu(menuPath) {\n    if (!state.inputApp) throw new ExecError(\"open_application first — invoke_menu acts on the bound application\");\n    if (!Array.isArray(menuPath) || menuPath.length < 1 || menuPath.length > 3 || menuPath.some((s) => typeof s !== \"string\" || !s.trim())) {\n      throw new ExecError('invoke_menu needs path: 1..3 non-empty menu titles, e.g. [\"File\",\"New\"]');\n    }\n    const titles = menuPath.map((s) => s.trim());\n    const app_ref = state.inputApp;\n    const pressed = [];\n    for (let level = 0; level < titles.length; level++) {\n      const found = await findMenuItem(app_ref, titles[level], level === 0);\n      if (!found) {\n        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\" });\n      }\n      if (found.enabled === false) {\n        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\" });\n      }\n      const target = { app_ref, windowIndex: found.windowIndex ?? 0, path: found.path, role: found.role, label: found.label };\n      assertBoundElement(target);\n      const action = found.role === \"AXMenuItem\" && (found.actions ?? []).includes(\"AXPick\") ? \"AXPick\" : \"AXPress\";\n      await native(\"perform_action\", { target, action });\n      pressed.push(titles[level]);\n      if (level < titles.length - 1) await wait(140);","sourceCodeStart":808,"sourceCodeEnd":844,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/plugins/computer-use/src/backends/darwin.mjs#L808-L844","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nawait backend.invokeMenu(\"File > New\");\n// after\nawait backend.invokeMenu([\"File\", \"New\"]);","handlingStrategy":"type-guard","validationCode":"const validMenuPath = p => Array.isArray(p) && p.length >= 1 && p.length <= 3 && p.every(s => typeof s === 'string' && s.trim().length > 0);\nif (!validMenuPath(menuPath)) throw new Error('menu path must be 1..3 non-empty strings');","typeGuard":"function isMenuPath(p) {\n  return Array.isArray(p) && p.length >= 1 && p.length <= 3 && p.every(s => typeof s === 'string' && s.trim() !== '');\n}","tryCatchPattern":"if (!isMenuPath(menuPath)) {\n  throw new Error(`bad menu path: ${JSON.stringify(menuPath)}`);\n}\nawait backend.invokeMenu(menuPath);","preventionTips":["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."],"tags":["validation","argument-format","menu"],"backgroundTag":"invalid-argument-value","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}