Hmbown/CodeWhale · error · ExecError

clipboard write is not exposed by hdc on current HarmonyOS…

Error message

clipboard write is not exposed by hdc on current HarmonyOS builds

What it means

Writing the device clipboard is not possible through hdc's uitest interface on current HarmonyOS builds, symmetric with read_clipboard. The backend throws this ExecError unconditionally instead of attempting a non-existent command.

Solutions

  1. Deliver text directly into the field with set_value or the text/input action instead of paste
  2. If paste flow must be tested, type the clipboard content into another field of the app and use the app's own copy UI
  3. Gate clipboard-write steps by backend and skip them on harmonyos

Example fix

// before
await backend.write_clipboard("hello");
await backend.key({ text: "enter" }); // paste flow
// after
await backend.set_value({ target: "#field", value: "hello" });
Defensive patterns

Strategy: fallback

Validate before calling

if (backend.name === "harmonyos") {
  // clipboard write unsupported: use set_value / inputText instead
}

Try / catch

try {
  await backend.write_clipboard(text);
} catch (e) {
  if (String(e.message).includes("clipboard write is not exposed by hdc")) await backend.set_value({ target, value: text });
  else throw e;
}

Prevention

When it happens

Trigger: Calling write_clipboard("text") on the harmonyos backend, typically to stage data before a paste action in the app under test.

Common situations: Setup steps that seed the clipboard for paste-into-field tests; scripts ported from Android backends where clipboard write exists; cross-platform automation frameworks sharing one clipboard-based helper.

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/078107904c48abd0. Report an issue: GitHub.

Appendix: source

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

      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++;
          } finally { await shell(["rm", "-f", remote], opts).catch(() => {}); }

View on GitHub (pinned to 73e0f67d83)