Hmbown/CodeWhale · error · ExecError

select_text is not exposed by uitest dumpLayout/uiInput on…

Error message

select_text is not exposed by uitest dumpLayout/uiInput on the harmony backend

What it means

select_text is intentionally unimplemented on the harmony backend: the hdc uitest surface used here (dumpLayout + uiInput: click, inputText, longClick, keyEvent) exposes no text-selection primitives. The backend throws unconditionally rather than silently failing to select.

Solutions

  1. Avoid select_text on HarmonyOS; restructure the workflow to use set_value to replace the whole field content
  2. Simulate selection with UI gestures (longClick then dpad/keys) if the app supports it, accepting it is heuristic
  3. Contribute a backend implementation if a newer hdc uitest API exposes selection

Example fix

// before
await backend.select_text({ target: "#field", start: 0, end: 5 });
// after
await backend.set_value({ target: "#field", value: "replacement text" });
Defensive patterns

Strategy: fallback

Validate before calling

const SUPPORTS_SELECT = { harmonyos: false }; // check per backend before calling
if (SUPPORTS_SELECT[backend.name] === false) throw new Error("select_text unsupported on " + backend.name);

Try / catch

try {
  await backend.select_text({ target, start, end });
} catch (e) {
  if (String(e.message).includes("select_text is not exposed")) await backend.set_value({ target, value: replacement });
  else throw e;
}

Prevention

When it happens

Trigger: Calling select_text({target, start, end}) (any arguments) on the harmonyos backend.

Common situations: Scripts copied from Android/desktop backends that rely on programmatic selection for copy/delete of text ranges; tests exercising the full cross-backend action matrix.

Related errors


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

Appendix: source

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

    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);
      if (!/^[A-Za-z0-9_]+$/.test(k)) throw new ExecError(`unsupported key "${text}" on harmony backend`);
      return uiInput(["keyEvent", k]);
    },
    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 () => {

View on GitHub (pinned to 73e0f67d83)