Hmbown/CodeWhale · error · ExecError

unsupported key " " on harmony backend

Error message

unsupported key "${text}" on harmony backend

What it means

The harmonyos backend's `key` action maps a small set of key names (enter, esc, back, dpad directions, power, menu) to uitest keyEvent codes via KEYMAP. If the mapped key still contains characters other than [A-Za-z0-9_], the backend refuses to forward it, because hdc uitest keyEvent only accepts identifier-shaped key codes. This guards against sending arbitrary text through the key channel.

Solutions

  1. Use one of the mapped key names: enter, return, escape, esc, back, home, backspace, delete, tab, left, right, up, down, power, menu
  2. For character input use the text/input action (inputText) instead of key — the field must be focused first
  3. For modifier combinations, perform separate key calls since the backend does not support chords

Example fix

// before
await backend.key({ text: "ctrl+c" });
// after
await backend.key({ text: "menu" }); // or use text input for characters
Defensive patterns

Strategy: validation

Validate before calling

const ALLOWED = new Set(["enter","return","escape","esc","back","home","backspace","delete","tab","left","right","up","down","power","menu"]);
const k = String(text).toLowerCase();
if (!ALLOWED.has(k) && !/^[A-Za-z0-9_]+$/.test(k)) throw new Error(`key "${text}" not supported on harmony backend`);

Type guard

const isHarmonyKey = (t) => typeof t === "string" && (/^[A-Za-z0-9_]+$/.test(t) || ["enter","return","escape","esc","back","home","backspace","delete","tab","left","right","up","down","power","menu"].includes(t.toLowerCase()));

Prevention

When it happens

Trigger: Calling the key action with a text value not in KEYMAP that contains spaces, punctuation or symbols, e.g. key({text:"ctrl+c"}), key({text:"a b"}), key({text:"\""}) — anything where KEYMAP lookup misses and the raw string fails /^[A-Za-z0-9_]+$/.

Common situations: Porting Android automation scripts that use modifier combos (ctrl+c, meta+space) to HarmonyOS; typos like "space bar"; assuming all keyboard keys are supported when only a fixed KEYMAP subset is.

Understand the failure class

Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.

Related errors


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

Appendix: source

Thrown at crates/tui/plugins/computer-use/src/backends/harmonyos.mjs:248

    left_mouse_up: async () => { throw new ExecError("low-level press/release is not exposed by uitest uiInput; use left_click_drag"); },
    scroll: ({ target, direction = "down", amount = 300 }) => {
      const dist = Math.max(60, Math.min(1200, amount * 24));
      const dx = direction === "left" ? dist : direction === "right" ? -dist : 0;
      const dy = direction === "up" ? dist : direction === "down" ? -dist : 0;
      return uiInput(["swipe", String(Math.round(target.x)), String(Math.round(target.y)), String(Math.round(target.x + dx)), String(Math.round(target.y + dy)), "400"]);
    },
    type: async ({ text }) => {
      if (!text) return { action_sent: false, note: "empty text" };
      await uiInput(["inputText", "300", "300", escDeviceText(text)]).catch(async (e) => {
        // Some builds require coordinates of the focused field; retry with a click-first pattern.
        throw e;
      });
      return { action_sent: true, chars: text.length, note: "inputText at 300,300 — click the field first for focused input" };
    },
    key: ({ text }) => {
      const KEYMAP = { enter: "Enter", return: "Enter", escape: "Esc", esc: "Esc", back: "Back", home: "Home", backspace: "Back", delete: "Del", tab: "Tab", left: "DPAD_LEFT", right: "DPAD_RIGHT", up: "DPAD_UP", down: "DPAD_DOWN", power: "Power", menu: "Menu" };
      const k = KEYMAP[String(text).toLowerCase()] ?? String(text);
      if (!/^[A-Za-z0-9_]+$/.test(k)) throw new ExecError(`unsupported key "${text}" on harmony backend`);
      return uiInput(["keyEvent", k]);
    },
    hold_key: async ({ text, duration }) => {
      if (String(text).toLowerCase() !== "click") throw new ExecError('hold_key on harmony supports only hold_key({"text":"click"}) = longClick');
      const d = Math.max(1, Math.min(5, Number(duration) || 1));
      return uiInput(["longClick", "300", "300"]);
    },
    set_value: async ({ target, value }) => {
      const b = await centerOf(target);
      await uiInput(["click", String(b.cx), String(b.cy)]);
      await new Promise((r) => setTimeout(r, 300));
      await uiInput(["inputText", String(b.cx), String(b.cy), escDeviceText(String(value))]);
      return { action_sent: true, strategy: "uitest-element" };
    },
    select_text: async () => { throw new ExecError("select_text is not exposed by uitest dumpLayout/uiInput on the harmony backend"); },
    perform_action: async ({ target, action }) => {
      const b = await centerOf(target);
      if (action === "longClick") return uiInput(["longClick", String(b.cx), String(b.cy)]);

View on GitHub (pinned to 73e0f67d83)