From 466039280111f76dd2c555b67afa3a90530511a2 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Thu, 8 Oct 2026 12:48:23 -0700 Subject: [PATCH] Fall back to frame's native capture for Safari puppet shots The puppet launch agent has no Screen Recording grant, so screencapture -l fails under launchd. Locally, bring the tab forward and capture its page rectangle through SlabMenubar, which holds the grant. Co-Authored-By: Claude Opus 5.5 --- slab/lib/safari-puppet.mjs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/slab/lib/safari-puppet.mjs b/slab/lib/safari-puppet.mjs index 610f2731e9..44bde4a66f 100644 --- a/slab/lib/safari-puppet.mjs +++ b/slab/lib/safari-puppet.mjs @@ -21,6 +21,9 @@ import { randomUUID } from "node:crypto"; import { withMachineLease } from "./computer-use-lease.mjs"; import { clickPointAsync, sendKeysAsync, shAsync } from "../bin/macos.mjs"; import { evalCall, pageCall, pendingCall } from "./safari-page.mjs"; +import { localFrame } from "./frame-local.mjs"; +import { homedir } from "node:os"; +import { join } from "node:path"; const ID_RE = /^safari:(\d+):(\d+)$/; const sleep = ms => new Promise(r => setTimeout(r, ms)); @@ -159,11 +162,30 @@ const p = pick(); p.win.currentTab = p.tab; p.id;`); pw=$(sips -g pixelWidth "$f" | awk '/pixelWidth/{print $2}'); ph=$(sips -g pixelHeight "$f" | awk '/pixelHeight/{print $2}') t=$(( pw * ${top} / ${ow} )); l=$(( pw * ${left} / ${ow} )) sips -c $(( ph - t )) $(( pw - l )) --cropOffset $t $l "$f" >/dev/null 2>&1 -base64 -i "$f"; rm -f "$f"`); +base64 -i "$f"; rm -f "$f"`).catch(error => { + // The puppet launch agent usually has no Screen Recording grant of its + // own. Locally, fall back to frame's native capture (SlabMenubar holds + // that grant): bring the tab forward, then capture its page rectangle. + if (spec.local && /could not create image/.test(error.message)) return null; + throw error; + }); + if (b64 === null) return frameShot(spec, id, g); if (!b64) throw new Error("screencapture returned nothing — grant Screen Recording to the process running puppet"); return b64.replace(/\s+/g, ""); } +async function frameShot(spec, id) { + return withMachineLease(spec, async () => { + await focus(spec, id, { exact: true }); + const g = await page(spec, id, "P.geometry()", { exact: true }); + const [x, y] = toScreen(g, 0, 0); + const crop = [x, y, g.iw, g.ih].map(Math.round).join(","); + const frame = await localFrame(join(homedir(), ".local", "share", "slab", "state"), `window noocr novisual fast crop=${crop}`); + if (!frame?.jpg?.length) throw new Error("frame capture returned no image"); + return Buffer.from(frame.jpg).toString("base64"); + }); +} + // Native pointer stream through CSS-pixel points. One point is a click. async function strokeScreen(spec, pts) { if (pts.length === 1) return clickPointAsync(spec, pts[0][0], pts[0][1]); -- 2.51.2