Hmbown/CodeWhale · error · ExecError

clipboard read is not exposed by hdc on current HarmonyOS…

Error message

clipboard read is not exposed by hdc on current HarmonyOS builds

What it means

Reading the device clipboard is not possible through hdc's uitest interface on current HarmonyOS builds — there is no dumpLayout/uiInput command that returns clipboard contents. The backend throws this ExecError from read_clipboard to make the platform gap explicit.

Solutions

  1. Use an on-device side channel: have the app under test expose the value via UI (then dumpLayout/set_value round-trip) or via a test hook
  2. Use hdc file transfer (send/receive a file the app writes) instead of the clipboard
  3. Skip clipboard assertions on HarmonyOS and gate those steps by backend type

Example fix

// before
const clip = await backend.read_clipboard();
// after
if (backend.name === "harmonyos") skip("clipboard read unsupported on harmony");
const clip = await backend.read_clipboard();
Defensive patterns

Strategy: fallback

Validate before calling

if (backend.name === "harmonyos") {
  // clipboard read unsupported: use app UI or hdc file transfer instead
}

Try / catch

try {
  clip = await backend.read_clipboard();
} catch (e) {
  if (String(e.message).includes("clipboard read is not exposed by hdc")) clip = await readViaUiDump(backend);
  else throw e;
}

Prevention

When it happens

Trigger: Calling read_clipboard() on the harmonyos backend, on any HarmonyOS version whose hdc build lacks a clipboard command.

Common situations: Workflows that copy data in an app and read it back for verification; cross-backend test suites that assume clipboard parity; attempting clipboard-based data transfer between apps in automation.

Understand the failure class

Background: "unsupported platform" / "not supported on this platform" errors: what they mean and how to fix them — this error's family across 47 libraries.

Related errors


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

Appendix: source

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

    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)]);
      return uiInput(["click", String(b.cx), String(b.cy)]);
    },
    read_clipboard: async () => { throw new ExecError("clipboard read is not exposed by hdc on current HarmonyOS builds"); },
    write_clipboard: async () => { throw new ExecError("clipboard write is not exposed by hdc on current HarmonyOS builds"); },
    cursor_position: async () => { throw new ExecError("cursor position does not exist on touch devices"); },
    recordingStart: async ({ intervalMs = 400 } = {}) => {
      if (recording) throw new ExecError(`recording ${recording.id} already running`);
      if (!(await have("ffmpeg"))) throw new ExecError("ffmpeg is required on the host to mux harmony snapshot-series recordings");
      const id = crypto.randomBytes(4).toString("hex");
      const dir = fs.mkdtempSync(path.join(os.tmpdir(), `cu-rec-${id}-`));
      const startedAt = new Date().toISOString();
      const rec = recording = { id, dir, startedAt, intervalMs, seq: 0, controller: new AbortController() };
      const tick = () => {
        if (rec.stopped || rec.pending) return rec.pending;
        rec.pending = (async () => {
          const opts = { timeoutMs: 15_000, signal: rec.controller.signal };
          const remote = `${DEVICE_TMP}-rec-${id}-${String(rec.seq).padStart(5, "0")}.jpeg`;
          try {
            await deviceOut(["snapshot_display", "-f", remote], opts);
            await exec.pullFile(remote, path.join(dir, `f${String(rec.seq).padStart(5, "0")}.jpeg`), opts);
            rec.seq++;

View on GitHub (pinned to 73e0f67d83)