Hmbown/CodeWhale · error · ExecError
low-level press/release is not exposed by uitest uiInput…
Error message
low-level press/release is not exposed by uitest uiInput; use left_click_drag
What it means
The harmony backend exposes drags only through uitest's `swipe` sub-command (left_click_drag). There is no press-and-hold primitive, so `left_mouse_down` throws this ExecError directing callers to left_click_drag. It is an intentional fail-fast for the press half of the low-level mouse API.
Solutions
- Rewrite the gesture as a single left_click_drag({ from_target, to }) — the backend implements it as uitest swipe and is the fix the message names.
- For press-and-hold semantics (not movement), use perform_action({ target, action: "longClick" }) or hold_key({ text: "click" }).
- Buffer intermediate move points and collapse the down/move/up sequence into one drag call before dispatching to a harmony backend.
- If fine-grained path control is required, chain multiple left_click_drag calls along the path, or add a press/release primitive to harmonyos.mjs only if uitest grows one.
Example fix
// before
await backend.left_mouse_down({ target: from });
await backend.mouse_move({ target: to });
await backend.left_mouse_up({});
// after
await backend.left_click_drag({ from_target: from, to }); Defensive patterns
Strategy: fallback
Validate before calling
const supportsLowLevelMouse = (backend) => backend.kind !== "harmonyos";
if (!supportsLowLevelMouse(backend)) {
throw new Error("use left_click_drag on harmony; no press primitive");
} Type guard
const hasPressPrimitive = (backend) => typeof backend.left_click_drag === "function" && backend.kind !== "harmonyos";
Try / catch
try {
await backend.left_mouse_down({ target });
} catch (e) {
if (/low-level press\/release is not exposed/.test(e.message)) {
// redo the gesture as an atomic drag instead
await backend.left_click_drag({ from_target: from, to });
} else {
throw e;
}
} Prevention
- Never build drags from down/move/up primitives when targeting HarmonyOS; emit left_click_drag directly.
- Keep a capability flag (e.g. lowLevelMouse: false) alongside each backend and branch on it.
- Detect the error once and remember the degraded capability for the session instead of retrying.
- Test drag interactions on every backend your scripts support.
When it happens
Trigger: Calling the left_mouse_down action ({ action: "left_mouse_down" }) on the harmony backend from harmonyos.mjs; custom drag loops that sequence left_mouse_down → mouse_move → left_mouse_up.
Common situations: Porting a desktop drag implementation that builds drags from raw down/move/up events; agents emitting granular mouse primitives; interaction frameworks that assume a full low-level mouse API on every backend.
Understand the failure class
Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.
Related errors
- middle click is not exposed by uitest uiInput
- uitest uiInput has no right-click; use longClick semantics…
- zoom is not supported on the harmony backend yet —…
- aa start failed
- app_denied
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/f2bc3087e8cfc415.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/plugins/computer-use/src/backends/harmonyos.mjs:229
const buf = fs.readFileSync(file);
displayPixels = jpegSize(buf) ?? displayPixels;
return { file, bytes: buf.length, pixels: jpegSize(buf), scale: 1, points: jpegSize(buf) };
},
zoom: async ({ region, path: outPath }) => {
throw new ExecError("zoom is not supported on the harmony backend yet — screenshot + region on the host is the workaround");
},
left_click: ({ target, strategy }) => { assertEventStrategy(strategy); return uiInput(["click", String(Math.round(target.x)), String(Math.round(target.y))]); },
double_click: ({ target }) => uiInput(["doubleClick", String(Math.round(target.x)), String(Math.round(target.y))]),
triple_click: async ({ target }) => {
await uiInput(["doubleClick", String(Math.round(target.x)), String(Math.round(target.y))]);
return uiInput(["click", String(Math.round(target.x)), String(Math.round(target.y))]);
},
right_click: async () => { throw new ExecError("uitest uiInput has no right-click; use longClick semantics via hold or left_click"); },
middle_click: async () => { throw new ExecError("middle click is not exposed by uitest uiInput"); },
mouse_move: async () => ({ action_sent: false, note: "hover without press is not exposed by uitest uiInput" }),
left_click_drag: ({ from_target: from, to }) =>
uiInput(["swipe", String(Math.round(from.x)), String(Math.round(from.y)), String(Math.round(to.x)), String(Math.round(to.y)), "200"], { timeoutMs: 30_000 }),
left_mouse_down: async () => { throw new ExecError("low-level press/release is not exposed by uitest uiInput; use left_click_drag"); },
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);View on GitHub (pinned to 73e0f67d83)