Hmbown/CodeWhale · error · ExecError

background preview capture failed

Error message

background preview capture failed: ${r.stderr}

What it means

updatePreview captures a window-scoped screenshot of the bound app's window with macOS `screencapture -x -o -l <window_id>`, writing to a temp PNG that is renamed into the preview path. If screencapture exits non-zero, this error surfaces its stderr. Capture is a preview-only step; its failure means no background screenshot was produced.

Solutions

  1. Grant Screen Recording permission (System Settings → Privacy & Security → Screen Recording) to the app running the backend, then restart it.
  2. Re-run open_application/observe so window_info returns a fresh, live window_id before capturing.
  3. Test the capture manually: `screencapture -x -o -l <window_id> /tmp/t.png` to see the raw error.
  4. If stderr shows a stale window id after app restart, rebind the application (pid/window changed).

Example fix

// before
await updatePreview(); // background preview capture failed: could not create image from window id

// after: rebind to refresh the window id, ensure screen-recording permission
await open_application(app.ref);
await observe(); // triggers a fresh capture
Defensive patterns

Strategy: try-catch

Validate before calling

const win = await native('window_info', { app_ref: appRef });
if (!win?.window_id) throw new Error('no live window to capture — re-observe first');

Try / catch

try {
  await updatePreview();
} catch (e) {
  if (String(e.message).startsWith('background preview capture failed')) {
    console.error('Check Screen Recording permission and that the window is still open:', e.message);
    await open_application(appRef); // refresh window_id, then retry once
  } else throw e;
}

Prevention

When it happens

Trigger: Calling the background preview/observe flow where `screencapture` fails: invalid or closed window_id from native("window_info"), missing Screen Recording permission for the host process, or a screencapture timeout (8s) / tool error reported via stderr.

Common situations: First-run macOS privacy prompt for Screen Recording denied or never granted; target window closed between window_info and the capture; running in an SSH/headless session without GUI access; macOS update resetting TCC permissions for the terminal/app.

Related errors


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

Appendix: source

Thrown at crates/tui/plugins/computer-use/src/backends/darwin.mjs:333

    if (tool === "pointer_sequence") requireSharedPointer();
    if (!exec.runInputLease) throw new ExecError("This executor cannot safely own held input; update Computer Use");
    if ((await native("input_capabilities"))?.input_lease !== 1) throw new ExecError("The native helper needs an update for disconnect-safe held input");
    const helper = await nativeHelper();
    try {
      return await exec.runInputLease(helper, [JSON.stringify({ tool, args: { ...args, ...yieldArgs, input_app_ref: state.inputApp, foreground_input: state.foregroundInput, owner_pipe: true, input_lease: true } })]);
    } catch (error) {
      error.code = nativeErrorCode(error.message) ?? error.code;
      throw error;
    }
  }

  async function updatePreview(show = false) {
    const win = await native("window_info", { app_ref: state.inputApp });
    const dir = path.join(stateDir(), "preview");
    fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
    const temp = path.join(dir, "next.png"), file = path.join(dir, "latest.png");
    const r = await runL("screencapture", ["-x", "-o", "-l", String(win.window_id), "-t", "png", temp], { timeoutMs: 8000 });
    if (r.code !== 0) throw new ExecError(`background preview capture failed: ${r.stderr}`);
    fs.renameSync(temp, file);
    const p = state.pointer;
    // The user's own hardware cursor goes on the preview too, so the panel
    // shows both pointers in the same window-relative space.
    let userCursor = null;
    try { userCursor = await native("cursor_position"); } catch {}
    await native("preview_notify", { enabled: true, show, title: `Codewhale · ${win.name} · ${state.foregroundInput ? "Shared desktop control" : "Background app control"}`, x: p ? (p.x-win.points.x)/win.points.w : -1, y: p ? (p.y-win.points.y)/win.points.h : -1,
      user_x: userCursor && Number.isFinite(userCursor.x) ? (userCursor.x-win.points.x)/win.points.w : -1,
      user_y: userCursor && Number.isFinite(userCursor.y) ? (userCursor.y-win.points.y)/win.points.h : -1 });
    // Any successful capture (bind, explicit preview, action refresh) starts
    // the live refresh; the tick itself re-enters this function as a no-op.
    if (state.previewEnabled && state.inputApp) startPreviewLoop();
    return { enabled: true, file, app: state.inputApp, pointer: p };
  }

  // ---------- pointer input ----------
  // Our qualified raw pointer path uses the shared event tap, which moves
  // the user's real cursor. Process/window-directed mouse delivery has not

View on GitHub (pinned to 73e0f67d83)