Hmbown/CodeWhale · error · ExecError

no screenshot taken yet on this computer — call screenshot…

Error message

no screenshot taken yet on this computer — call screenshot first

What it means

zoom crops a region from the most recent screenshot via ffmpeg, using source ?? lastRaster?.file. The backend keeps no raster from a previous screenshot (or none was ever taken), so there is no image to crop and it refuses rather than silently zooming nothing.

Solutions

  1. Call screenshot first, then zoom without a source argument.
  2. Pass an explicit source image path to zoom({ source: '/path/to/shot.png' }).
  3. Ensure the earlier screenshot call actually succeeded and its path was retained.
  4. If a session restart wiped the cache, re-take the screenshot rather than reusing an old path.

Example fix

// before
await backend.zoom({ region: [0, 0, 200, 200] }); // throws: no raster yet
// after
await backend.screenshot({});
await backend.zoom({ region: [0, 0, 200, 200] });
Defensive patterns

Strategy: validation

Validate before calling

if (!source && !lastScreenshotPath) throw new Error('call screenshot() before zoom()');

Type guard

const hasRaster = (s) => typeof s === 'string' && s.length > 0;

Try / catch

try {
  return await backend.zoom({ region });
} catch (e) {
  if (String(e.message).includes('no screenshot taken yet')) {
    await backend.screenshot({});
    return await backend.zoom({ region });
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling zoom before any screenshot call in the session; passing no source while lastRaster was cleared (new session/connection); passing a source that is null/undefined after destructure; using zoom on a fresh backend instance.

Common situations: Scripts that zoom first for detail inspection before taking a baseline screenshot; retry logic that re-runs zoom after the session restarted and lost the last-screenshot cache; agent flows where the screenshot step was skipped or failed earlier.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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

Appendix: source

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

if not ok:
    print(json.dumps({"found": True, "element": None, "reason": "element_stale"}))
    sys.exit(0)
ext = None
try: ext = node.queryComponent().getExtents(pyatspi.DESKTOP_COORDS)
except Exception: pass
print(json.dumps({"found": True, "reason": None, "element": {
    "role": node.getRoleName() or None, "label": node.name or None,
    "position": {"x": ext.x, "y": ext.y} if ext else None,
    "size": {"w": ext.width, "h": ext.height} if ext else None}}))`;
      const r = await run("python3", ["-c", script, name, JSON.stringify(pathArr ?? [])], { timeoutMs: 30_000 });
      const out = tryJson(r.stdout.trim().split("\n").pop() ?? "", null);
      if (!out) throw new ExecError(`AT-SPI resolve failed: ${(r.stderr || r.stdout).slice(0, 250)}`, r);
      return out;
    },
    zoom: async ({ source, region, path: outPath }) => {
      need("ffmpeg", "zoom/crop");
      const src = source ?? lastRaster?.file;
      if (!src) throw new ExecError("no screenshot taken yet on this computer — call screenshot first");
      const out = outputPath(outPath ?? path.join(recordingsDir(), `zoom-${crypto.randomBytes(4).toString("hex")}.png`));
      await runOk("ffmpeg", ["-y", "-loglevel", "error", "-i", src, "-vf", `crop=${Math.round(region[2])}:${Math.round(region[3])}:${Math.round(region[0])}:${Math.round(region[1])}`, out], { timeoutMs: 20_000 });
      return { file: out, bytes: fs.statSync(out).size, region, source: src };
    },
    left_click: ({ target, strategy }) => { assertNum(target.x, "x"); assertNum(target.y, "y"); assertEventStrategy(strategy); return inputChain(target.x, target.y, () => clickButton(1, 1)); },
    double_click: ({ target }) => inputChain(target.x, target.y, () => clickButton(1, 2)),
    triple_click: ({ target }) => inputChain(target.x, target.y, () => clickButton(1, 3)),
    right_click: ({ target }) => inputChain(target.x, target.y, () => clickButton(3, 1)),
    middle_click: ({ target }) => inputChain(target.x, target.y, () => clickButton(2, 1)),
    mouse_move: ({ target }) => inputMove(target.x, target.y),
    left_click_drag: async ({ from_target: from, to }) => {
      requireInputOwner();
      await inputMove(from.x, from.y);
      throwIfAborted();
      mouseHeld = true;
      try {
        if (session === "x11") await xdotool(["mousedown", "1"]);
        else await ydotool(["click", "0x40"]);

View on GitHub (pinned to 73e0f67d83)