Hmbown/CodeWhale · error · ExecError
multiple non-modifier keys in
Error message
multiple non-modifier keys in "${text}" What it means
A chord may contain any number of modifiers plus exactly one non-modifier key. parseChord throws when it encounters a second non-modifier key name, because a physical key event can press only one main key at a time.
Solutions
- Send each key press as a separate call: press('a') then press('b')
- Keep at most one non-modifier key per chord: 'cmd+c', not 'cmd+c+v'
- Normalize/split multi-key input at the call site before invoking the backend
Example fix
// before
await press("cmd+c+v");
// after
await press("cmd+c");
await press("cmd+v"); Defensive patterns
Strategy: validation
Validate before calling
const MODS = new Set(["cmd", "ctrl", "alt", "shift", "fn"]);
const nonMods = key.split("+").map(s => s.trim().toLowerCase()).filter(p => p && !MODS.has(p));
if (nonMods.length > 1) throw new Error(`at most one non-modifier key, got: ${nonMods.join(", ")}`); Try / catch
try {
await press(text);
} catch (e) {
if (e instanceof ExecError && e.message.startsWith("multiple non-modifier keys")) {
for (const k of text.split("+")) await press(k.trim());
} else throw e;
} Prevention
- Treat '+' as modifier chord syntax only, never as sequencing
- Issue one press call per key in sequence
- Normalize input in one shared helper before the backend call
When it happens
Trigger: Passing text like 'cmd+a+b', 'a+b', or a space-separated pair that still splits on '+' into two KEY_CODES names.
Common situations: Caller intends 'press a, then b' but formats it as a chord; LLM-generated key strings joining two keys; misunderstanding that '+' sequences mean sequential presses.
Understand the failure class
Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.
Related errors
- no non-modifier key in
- empty key text
- unknown key combination
- unknown key " " (supported: + modifiers…
- 1
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/88c1557d70bed307.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/plugins/computer-use/src/backends/darwin.mjs:492
const lease = await nativeLease("key_event", { code, flags, down: true });
try {
await action();
// The acknowledgement carries the yield_ms the helper waited for a
// hardware-input gap before posting the press.
return lease.receipt;
} finally {
await withSignal(null, () => lease.release());
}
}
function parseChord(text) {
const parts = String(text).split("+").map((s) => s.trim().toLowerCase()).filter(Boolean);
if (!parts.length) throw new ExecError("empty key text");
let flags = 0;
let key = null;
for (const p of parts) {
if (MODIFIERS[p] != null) flags |= MODIFIERS[p];
else if (KEY_CODES[p] != null) { if (key) throw new ExecError(`multiple non-modifier keys in "${text}"`); key = p; }
else throw new ExecError(`unknown key "${p}" (supported: ${Object.keys(KEY_CODES).join(", ")} + modifiers cmd/ctrl/alt/shift/fn)`);
}
if (key == null) throw new ExecError(`no non-modifier key in "${text}"`);
return { flags, code: KEY_CODES[key], key };
}
// ---------- displays ----------
async function displayInfo() { return native("displays"); }
// ---------- screenshots ----------
function recordingsDir() {
return process.env.CODEWHALE_CU_RECORDINGS_DIR || path.join(os.homedir(), ".codewhale-cu", "recordings");
}
async function screenshot({ display, region, app_ref, window_id, path: outPath } = {}) {
// Once an app is selected, ordinary observations follow it behind the
// user's work. An explicit display/region remains a deliberate desktop capture.
if (app_ref === undefined && display === undefined && region === undefined) app_ref = state.inputApp ?? undefined;View on GitHub (pinned to 73e0f67d83)