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

  1. 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.
  2. If higher effective resolution is needed, compute the crop coordinates in the screenshot's pixel space (the returned `pixels`/`scale` fields) rather than zooming.
  3. Guard the call: check backend capabilities (or skip zoom for harmonyos) before dispatching the action, and fall back to the screenshot+crop path.
  4. 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

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


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)