Hmbown/CodeWhale · error · ExecError
perform_action failed
Error message
perform_action failed: ${out.code} What it means
perform_action invokes an AT-SPI action on a resolved element via an embedded python script; this error is thrown when the script returns ok=false with a code, most commonly action_not_found listing the element's available action names. The element resolved, but it does not expose the requested action.
Solutions
- Read out.code: action_not_found includes the available names — pick one of those exact names.
- Use the element's exposed action name verbatim (case-insensitive match is applied; spelling must match).
- Re-resolve a fresh path via get_app_state in case the element moved or changed.
- For clicks, omit the action (defaults to 'click') or pass a name present in the listed actions; otherwise drive input via left_click coordinates instead.
Example fix
// before
await backend.perform_action({ target, action: 'press' }); // not exposed
// after
const state = await backend.get_app_state({ app_ref: { name: 'Firefox' } });
const available = state.actions; // e.g. ['click','showContextMenu']
await backend.perform_action({ target, action: available.includes('click') ? 'click' : available[0] }); Defensive patterns
Strategy: try-catch
Validate before calling
const state = await backend.get_app_state({ app_ref });
const el = findElement(state, targetPath);
if (!el.actions?.map(a => a.toLowerCase()).includes(action.toLowerCase())) throw new Error(`action '${action}' not exposed; available: ${el.actions}`); Try / catch
try {
return await backend.perform_action({ target, action });
} catch (e) {
if (String(e.message).startsWith('perform_action failed:')) {
const available = String(e.message).match(/action_not_found: (.*)$/)?.[1]?.split(',');
return backend.perform_action({ target, action: available?.[0] ?? 'click' });
}
throw e;
} Prevention
- Read available action names from app state before performing an action
- Use 'click' as the safe default action for buttons
- Refresh element paths from a recent snapshot; stale paths resolve to wrong nodes
When it happens
Trigger: Requesting an action name that does not exist on the element (e.g. 'press' when the element only exposes 'click'); case/spacing mismatch in the action name; element resolves but is actionless (static text, containers); stale element path resolving to a different node that lacks the action.
Common situations: Hard-coding macOS-style action names against Linux a11y trees; assuming buttons expose 'press' when GTK/Qt expose 'click' or 'activate'; automating non-interactive nodes; app state changed since get_app_state so the path now points elsewhere.
Related errors
- application not found or name is ambiguous in the AT-SPI…
- AT-SPI action failed
- AT-SPI resolve failed
- AT-SPI walk failed — is python3-pyatspi installed and the…
- element_disabled
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/7ab7353b76797270.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/plugins/computer-use/src/backends/linux.mjs:738
print(json.dumps({"ok": True, "after": after}))`, String(value));
if (!out.ok) throw new ExecError(`set_value failed: ${out.code}`);
return { action_sent: true, strategy: "a11y", verified: true, after: out.after };
},
select_text: async () => { throw new ExecError("select_text is not implemented on the linux backend — fail-closed"); },
perform_action: async ({ target, action }) => {
const body = ` a = found.queryAction()
names = [a.getName(i) for i in range(a.nActions)]
want = (extra or "click").lower()
match = next((n for n in names if n.lower() == want), None)
if match is None and want == "click":
match = next((n for n in names if n.lower() in ("click", "press", "activate")), None)
if match is None:
print(json.dumps({"ok": False, "code": "action_not_found: " + ",".join(names)}))
else:
a.doAction(names.index(match))
print(json.dumps({"ok": True, "sent": True}))`;
const out = await atspiResolve(target, body, String(action));
if (!out.ok) throw new ExecError(`perform_action failed: ${out.code}`);
return { action_sent: true, strategy: "a11y", action };
},
read_clipboard: async () => {
await probeSession();
const cmd = session === "x11"
? (tools.xclip ? ["xclip", "-selection", "clipboard", "-o"] : ["xsel", "--clipboard", "--output"])
: ["wl-paste"];
need(cmd[0], "clipboard read");
const r = await run(cmd[0], cmd.slice(1), { timeoutMs: 10_000 });
if (r.code !== 0) throw new ExecError("clipboard read failed", r);
return { text: r.stdout, encoding: "utf8" };
},
write_clipboard: async ({ text }) => {
await probeSession();
const cmd = session === "x11"
? (tools.xclip ? ["xclip", "-selection", "clipboard"] : ["xsel", "--clipboard", "--input"])
: ["wl-copy"];
need(cmd[0], "clipboard write");View on GitHub (pinned to 73e0f67d83)