Hmbown/CodeWhale · error · ExecError

list_windows needs wmctrl (X11), swaymsg (sway) or hyprctl…

Error message

list_windows needs wmctrl (X11), swaymsg (sway) or hyprctl (hyprland)

What it means

Thrown by list_windows when, after probing the session, none of the three supported enumeration routes is available: wmctrl for X11, swaymsg for sway, or hyprctl for hyprland. It is the terminal fallback at the end of the list_windows implementation, signaling the required window-manager tooling is absent for the detected session.

Solutions

  1. Install wmctrl for X11 (`sudo apt install wmctrl`) or ensure swaymsg/hyprctl are on PATH for sway/hyprland.
  2. Verify tool availability with `command -v wmctrl swaymsg hyprctl` in the plugin's runtime environment.
  3. On unsupported Wayland compositors, run an X11/XWayland session or extend the backend — this code path has no other enumeration route.
  4. Check probeSession's detected session; if it misidentifies X11 as wayland (or vice versa), the wrong branch's tool check fails.
  5. For CI, pair Xvfb with wmctrl so list_windows has a usable source.

Example fix

// before (sway, CLI missing from PATH)
# swaymsg: command not found
// after
export PATH="$PATH:/usr/bin"  # or reinstall sway providing swaymsg
swaymsg -t get_tree
Defensive patterns

Strategy: fallback

Validate before calling

import { execFileSync } from 'child_process';
const has = (cmd) => { try { execFileSync('sh', ['-c', `command -v ${cmd}`], { stdio: 'ignore' }); return true; } catch { return false; } };
function windowsAvailable(session) {
  return session === 'x11' ? has('wmctrl') : (has('swaymsg') || has('hyprctl'));
}

Try / catch

try {
  const { windows } = await listWindows();
} catch (e) {
  if (e instanceof ExecError && e.message.startsWith('list_windows needs')) {
    return { windows: [], note: 'no supported window enumeration tool for this session' };
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling list_windows when the X11 branch lacks tools.wmctrl, and the Wayland branches lack tools.swaymsg / tools.hyprctl; or the session is a Wayland compositor unsupported by all three branches.

Common situations: Minimal X11 installs without wmctrl; GNOME/KDE Wayland sessions where none of the three tools exist; sway/hyprland installed headlessly without their CLI tools on PATH; running the plugin under a different user/PATH than the desktop session.

Understand the failure class

Background: "not installed", "pip install", "required for": how missing-dependency errors surface across open-source libraries — this error's family across 34 libraries.

Related errors


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

Appendix: source

Thrown at crates/tui/plugins/computer-use/src/backends/linux.mjs:460

        return { windows };
      }
      if (session === "wayland" && tools.swaymsg) {
        const r = await run("swaymsg", ["-t", "get_tree", "-r"], { timeoutMs: 15_000 });
        const windows = [];
        const walk = (n) => {
          if (n.type === "con" && n.name) windows.push({ id: String(n.id), title: n.name, wm_class: n.app_id ?? null, position: { x: n.rect?.x, y: n.rect?.y }, size: { w: n.rect?.width, h: n.rect?.height }, focused: !!n.focused });
          (n.nodes ?? []).forEach(walk);
          (n.floating_nodes ?? []).forEach(walk);
        };
        walk(tryJson(r.stdout, {}));
        return { windows };
      }
      if (session === "wayland" && tools.hyprctl) {
        const r = await run("hyprctl", ["-j", "clients"], { timeoutMs: 15_000 });
        const clients = tryJson(r.stdout, []);
        return { windows: clients.map((c) => ({ id: String(c.address), title: c.title, wm_class: c.class, position: { x: c.at?.[0], y: c.at?.[1] }, size: { w: c.size?.[0], h: c.size?.[1] }, focused: !!c.focused })) };
      }
      throw new ExecError("list_windows needs wmctrl (X11), swaymsg (sway) or hyprctl (hyprland)");
    },
    open_application: async ({ name, bundle_id: bid, url: urlArg, activate } = {}) => {
      const target = name ?? bid;
      if (!target || !/^[A-Za-z0-9][A-Za-z0-9 ._-]*$/.test(target)) throw new ExecError("open_application needs a plain executable/desktop name");
      // activate defaults to background: on X11 a new window grabs focus, so
      // remember the active window and hand focus back after the launch.
      let prevWindow = null;
      if (activate !== true) {
        try {
          await probeSession();
          if (session === "x11" && tools.xdotool) {
            const active = await run("xdotool", ["getactivewindow"], { timeoutMs: 3_000 });
            if (active.code === 0 && /^\d+$/.test(active.stdout.trim())) prevWindow = active.stdout.trim();
          }
        } catch { /* no session/tools — the launch itself is still fine */ }
      }
      spawnDetached(target, urlArg ? [urlArg] : [], "", true);
      await new Promise((r) => setTimeout(r, 500));

View on GitHub (pinned to 73e0f67d83)