Hmbown/CodeWhale · error · ExecError
list_apps needs wmctrl (X11) or swaymsg/hyprctl (Wayland)
Error message
list_apps needs wmctrl (X11) or swaymsg/hyprctl (Wayland)
What it means
Thrown by list_apps when no supported application-enumeration tool exists for the session: wmctrl on X11, or swaymsg/hyprctl on Wayland. Unlike the AT-SPI path, the code here requires one of these window-manager tools; without them it throws this directive message instead of returning results.
Solutions
- On X11 install wmctrl (`sudo apt install wmctrl`) and verify with `wmctrl -lx`.
- On sway/hyprland ensure swaymsg/hyprctl are in PATH (they ship with the compositor).
- On other Wayland compositors, this backend cannot list apps via this path — switch to the AT-SPI-backed flow or run an X11/XWayland session with wmctrl.
- Check that probeSession detected the expected session; a wrong session guess makes it look for the wrong tool.
- Confirm the binary is on PATH for the same user running the plugin.
Example fix
// before (X11) # wmctrl: command not found // after sudo apt install wmctrl
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 appsAvailable(session) {
return session === 'x11' ? has('wmctrl') : (has('swaymsg') || has('hyprctl'));
} Try / catch
try {
const { apps } = await listApps();
} catch (e) {
if (e instanceof ExecError && e.message.startsWith('list_apps needs')) {
// fallback: empty app list with a capability note, or route to AT-SPI flow
return { apps: [], note: 'install wmctrl/swaymsg/hyprctl for app enumeration' };
}
throw e;
} Prevention
- Install wmctrl on X11 images (`sudo apt install wmctrl`).
- Verify swaymsg/hyprctl are present and on PATH under sway/hyprland.
- Run the plugin with the desktop session's PATH/user so tools are found.
- Document the GNOME/KDE-Wayland limitation and route those users to the AT-SPI flow.
- Add a preflight capability check before exposing list_apps to callers.
When it happens
Trigger: Calling list_apps when: X11 session without wmctrl installed; Wayland session where probeSession succeeded but neither swaymsg nor hyprctl is available (or the sway/hypr branches were skipped because the tools are missing).
Common situations: Fresh desktop installs without wmctrl; Wayland compositors other than sway/hyprland (GNOME, KDE) where none of the three tools apply; containerized sessions with no window-manager tooling.
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_windows needs wmctrl (X11), swaymsg (sway) or hyprctl…
- display enumeration needs xrandr (X11) or swaymsg/hyprctl…
- application not found or name is ambiguous in the AT-SPI…
- application window not found in UIA tree — pass…
- AT-SPI action failed
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/fa1d152aabe26858.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/plugins/computer-use/src/backends/linux.mjs:430
const r = await runOk("wmctrl", ["-lx"], { timeoutMs: 15_000 });
const seen = new Map();
for (const line of r.stdout.split("\n")) {
const parts = line.split(/\s+/);
const wmClass = parts[2];
if (wmClass) seen.set(wmClass, { name: wmClass.split(".")[0], wm_class: wmClass });
}
return { apps: [...seen.values()] };
}
if (session === "wayland" && (tools.swaymsg || tools.hyprctl)) {
const w = await this.list_windows();
const seen = new Map();
for (const win of w.windows) {
const cls = win.wm_class || win.app_id;
if (cls) seen.set(cls, { name: cls, wm_class: cls });
}
return { apps: [...seen.values()] };
}
throw new ExecError("list_apps needs wmctrl (X11) or swaymsg/hyprctl (Wayland)");
},
list_windows: async (args = {}) => {
rejectWindowSelectors(args);
await probeSession();
if (session === "x11" && tools.wmctrl) {
const r = await runOk("wmctrl", ["-lGx"], { timeoutMs: 15_000 });
const windows = [];
for (const line of r.stdout.split("\n")) {
const m = /^(\S+)\s+(-?\d+)\s+(-?\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(.*)$/.exec(line);
if (m) windows.push({ id: m[1], desktop: m[2], position: { x: Number(m[3]), y: Number(m[4]) }, size: { w: Number(m[5]), h: Number(m[6]) }, wm_class: m[7], title: m[8] });
}
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 });View on GitHub (pinned to 73e0f67d83)