Hmbown/CodeWhale · error · ExecError

no non-modifier key in

Error message

no non-modifier key in "${text}"

What it means

After parsing all '+'-separated tokens, parseChord requires at least one non-modifier key. A chord consisting only of modifiers (e.g. 'cmd+shift') names no key to press, so it is rejected instead of sending a modifier-only event.

Solutions

  1. Append the actual key to press: 'cmd+c', not 'cmd'
  2. If the goal is holding modifiers, use dedicated press/release or modifier-hold APIs if the backend exposes them
  3. Fix the chord-building code that failed to include the main key

Example fix

// before
await press("cmd+shift");
// after
await press("cmd+shift+p");
Defensive patterns

Strategy: validation

Validate before calling

const MODS = new Set(["cmd", "ctrl", "alt", "shift", "fn"]);
const parts = key.split("+").map(s => s.trim().toLowerCase()).filter(Boolean);
if (parts.every(p => MODS.has(p))) throw new Error("chord must include a non-modifier key");

Type guard

const hasMainKey = (k) => k.split("+").some(p => !["cmd","ctrl","alt","shift","fn"].includes(p.trim().toLowerCase()));

Try / catch

try {
  await press(key);
} catch (e) {
  if (e instanceof ExecError && e.message.startsWith("no non-modifier key")) {
    // reject or ask the caller for the key to combine with the modifiers
  } else throw e;
}

Prevention

When it happens

Trigger: Passing key='cmd', 'shift+alt', 'cmd+ctrl' — modifier tokens only, no KEY_CODES entry.

Common situations: Caller wants to 'hold' modifiers (unsupported via this API) or mistakenly believes modifiers alone trigger an action; a chord builder that appends modifiers but drops the main key.

Understand the failure class

Background: "missing required argument" and "the following required arguments were not provided": what required-argument errors mean and how to fix them — this error's family across 20 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/2b98deb2a7993584. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/plugins/computer-use/src/backends/darwin.mjs:495

      // 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;
    const dir = recordingsDir();
    fs.mkdirSync(dir, { recursive: true });
    // JPEG, not PNG. A screen is photographic content — gradients, wallpaper,

View on GitHub (pinned to 73e0f67d83)