Hmbown/CodeWhale · error · ExecError
zoom is not supported on the harmony backend yet —…
Error message
zoom is not supported on the harmony backend yet — screenshot + region on the host is the workaround
What it means
The harmony (HarmonyOS) backend of the computer-use plugin does not implement the zoom action. `create()` returns a capabilities object whose `zoom` method unconditionally throws this ExecError. It is a deliberate fail-fast stub: the backend drives the device over hdc/uitest and offers no zoomed-screenshot primitive, so the library throws instead of silently returning a scaled image.
Solutions
- Take a normal screenshot with the screenshot action, then crop the saved JPEG to the region on the host (e.g. with sharp, jimp, or ffmpeg) — this is exactly the workaround the error message names.
- If higher effective resolution is needed, compute the crop coordinates in the screenshot's pixel space (the returned `pixels`/`scale` fields) rather than zooming.
- Guard the call: check backend capabilities (or skip zoom for harmonyos) before dispatching the action, and fall back to the screenshot+crop path.
- If zoom support is genuinely needed, implement it in the harmony backend as screenshot + host-side crop following the same stub signature.
Example fix
// before
await backend.zoom({ region: { x: 100, y: 100, width: 200, height: 200 }, path: "zoom.jpeg" });
// after
const shot = await backend.screenshot({ path: "full.jpeg" });
// crop `region` out of full.jpeg on the host (sharp example)
await sharp("full.jpeg")
.extract({ left: 100, top: 100, width: 200, height: 200 })
.toFile("zoom.jpeg"); Defensive patterns
Strategy: fallback
Validate before calling
const ZOOM_SUPPORTED = (backend) => backend.kind !== "harmonyos";
if (ZOOM_SUPPORTED(backend)) {
await backend.zoom({ region, path });
} else {
const shot = await backend.screenshot({});
await cropOnHost(shot.file, region, path);
} Type guard
const supportsZoom = (backend) => typeof backend.zoom === "function" && backend.capabilities?.zoom !== false;
Try / catch
try {
await backend.zoom({ region, path });
} catch (e) {
if (/zoom is not supported on the harmony backend/.test(e.message)) {
const shot = await backend.screenshot({});
await cropOnHost(shot.file, region, path);
} else {
throw e;
}
} Prevention
- Maintain a per-backend capability table and consult it before dispatching any action.
- Write computer-use plans against a touch-safe action subset (screenshot/click/longClick/swipe) when targeting HarmonyOS.
- Test cross-backend scripts with each backend before release; unsupported actions throw, not no-op.
- Centralize host-side image cropping as a shared helper so screenshot+crop is a one-line fallback.
When it happens
Trigger: Calling the zoom action (e.g. { action: "zoom", region, path }) against a backend created by create() in crates/tui/plugins/computer-use/src/backends/harmonyos.mjs when the connected device is HarmonyOS. The throw is unconditional — no region or path value avoids it.
Common situations: An automation flow that works against the desktop backend is pointed at a HarmonyOS device and issues zoom to read small text or fine UI detail; a generic agent tool-loop enumerates all actions including zoom on a touch device; porting a computer-use script from macOS/Windows hosts to a HarmonyOS phone.
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
- cursor position does not exist on touch devices
- harmony backend exposes display 1 only
- low-level press/release is not exposed by uitest uiInput…
- middle click is not exposed by uitest uiInput
- no screenshot taken yet on this computer — call screenshot…
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/c36b2086d91d7cbe.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/plugins/computer-use/src/backends/harmonyos.mjs:216
get_app_state: async (args = {}) => {
rejectAppSelectors(args);
const tree = await dumpLayout();
const els = flatten(tree);
return { bundle_id: tree.attributes?.bundleName ?? null, elements: els, truncated: els.length >= 600 };
},
screenshot: async (args = {}) => {
rejectAppSelectors(args);
const { path: outPath } = args;
const dir = process.env.CODEWHALE_CU_RECORDINGS_DIR || path.join(os.homedir(), ".codewhale-cu", "recordings");
fs.mkdirSync(dir, { recursive: true });
const file = outPath || path.join(dir, `shot-${new Date().toISOString().replace(/[:.]/g, "-")}-${crypto.randomBytes(3).toString("hex")}.jpeg`);
await snapshot(file);
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;View on GitHub (pinned to 73e0f67d83)