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
- Install wmctrl for X11 (`sudo apt install wmctrl`) or ensure swaymsg/hyprctl are on PATH for sway/hyprland.
- Verify tool availability with `command -v wmctrl swaymsg hyprctl` in the plugin's runtime environment.
- On unsupported Wayland compositors, run an X11/XWayland session or extend the backend — this code path has no other enumeration route.
- Check probeSession's detected session; if it misidentifies X11 as wayland (or vice versa), the wrong branch's tool check fails.
- 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
- Install wmctrl for X11; ensure swaymsg/hyprctl ship with sway/hyprland on PATH.
- Preflight `command -v wmctrl swaymsg hyprctl` and surface install guidance at startup.
- Keep the plugin process inside the desktop session user/PATH context.
- For CI, combine Xvfb with wmctrl so enumeration works.
- Note the unsupported-Wayland-compositor limitation in your integration docs.
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
- list_apps needs wmctrl (X11) or swaymsg/hyprctl (Wayland)
- display enumeration needs xrandr (X11) or swaymsg/hyprctl…
- clipboard read failed
- cursor position needs an X11 session in this build
- linux backend needs " " for — install it and retry
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)