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
- Use one of the mapped key names: enter, return, escape, esc, back, home, backspace, delete, tab, left, right, up, down, power, menu
- For character input use the text/input action (inputText) instead of key — the field must be focused first
- 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
- Check the backend's KEYMAP before sending key names
- Use the text/input action for characters, key only for mapped control keys
- Avoid modifier-combo strings ("ctrl+c") — they never pass the identifier check
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
- invalid_target
- open_application needs a valid Harmony ability identifier
- open_application needs a valid Harmony bundle identifier
- 1
- A pinned task provider requires an explicit model
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)