// Bridge to the Tauri overlay shell. The overlay window is click-through by // default; Rust polls the global cursor and re-enables events only while it's // inside one of these rects. The frontend's job: keep Rust's rect list fresh. // Everything no-ops outside Tauri (sim in a browser, the Space). interface HitRect { x: number; y: number; w: number; h: number; } const HIT_SELECTORS = [ ".puck", ".bubble", ".toasts", ".comp", ".drop", ".settings", ".interrupt-card", ".bloom-card", // dev-only: vite's error overlay must be clickable or it's undismissable // (anything interactive but unlisted = clicks sail through to apps below) "vite-error-overlay", ].join(", "); const PAD = 8; // forgiving edges — a near-miss poke should still land export function inTauri(): boolean { return "__TAURI_INTERNALS__" in window; } // Best-effort event-source → macOS app name. Imperfect (Claude Code lives in a // terminal, "browser" could be any of several) but right for the common cases; // easy to refine once we can read the real frontmost app per event. const SOURCE_APP: Record = { Claude: "Claude", Build: "Terminal", Git: "Terminal", Mail: "Mail", Discord: "Discord", Calendar: "Calendar", Browser: "Safari", }; export interface Region { x: number; y: number; w: number; h: number; } /** What a peek yields: the line Puck says + how the patch made him feel (drives the * sprite gesture). `emotion` is a PeekEmotion string; the engine coerces it. */ export interface PeekResult { quip: string; emotion: string; } /** Companion loop: Puck peeks at a small region (given in LOGICAL/CSS px — we * scale to physical here) and Rust returns the daemon's `{quip, emotion}`. Image * stays native-side; only the short result crosses the IPC. Null outside Tauri or * on failure (Screen Recording not granted, vision down). */ export async function peekNative( region: Region, fairyState: { mischief: number; mood: string }, ): Promise { if (!inTauri()) return null; const dpr = window.devicePixelRatio || 1; try { const { invoke } = await import("@tauri-apps/api/core"); const json = await invoke("peek", { x: Math.round(region.x * dpr), y: Math.round(region.y * dpr), w: Math.round(region.w * dpr), h: Math.round(region.h * dpr), fairyState, }); const r = JSON.parse(json) as { quip?: string; emotion?: string }; return r.quip ? { quip: r.quip, emotion: r.emotion ?? "curious" } : null; } catch (e) { console.error("puck: native peek failed (grant Screen Recording?)", e); return null; } } /** Make the overlay the key window so you can type into it (chat). The borderless * always-on-top overlay never becomes key on its own. No-op outside Tauri. */ export async function focusWindow(): Promise { if (!inTauri()) return; try { const { invoke } = await import("@tauri-apps/api/core"); await invoke("focus_window"); } catch (e) { console.error("puck: focus_window failed", e); } } /** Bring the app a notification is about to the front (the "tap to go there"). */ export async function focusApp(source: string): Promise { if (!inTauri()) return; const app = SOURCE_APP[source]; if (!app) return; try { const { invoke } = await import("@tauri-apps/api/core"); await invoke("focus_app", { app }); } catch (e) { console.error("puck: focus_app failed", e); } } /** Native "look around": Rust captures the real screen and posts it to the daemon * in the background, returning at once (the ~20s vision inference must not block * — perceived events arrive via the normal poll). Resolves true once the capture * is dispatched, false on failure (e.g. Screen Recording not granted). The image * never crosses the IPC bridge (multi-MB through it crashed WKWebView). */ export async function lookAroundNative(fairyState: { mischief: number; mood: string }): Promise { if (!inTauri()) return false; try { const { invoke } = await import("@tauri-apps/api/core"); await invoke("look_around", { fairyState }); return true; } catch (e) { console.error("puck: native look-around failed (grant Screen Recording?)", e); return false; } } /** Poll interactive element rects and report them to the Rust cursor poller. */ export function startHitRectReporter(): () => void { if (!inTauri()) return () => {}; let last = ""; const iv = setInterval(async () => { const rects: HitRect[] = [...document.querySelectorAll(HIT_SELECTORS)].map((el) => { const r = el.getBoundingClientRect(); return { x: Math.round(r.x - PAD), y: Math.round(r.y - PAD), w: Math.round(r.width + PAD * 2), h: Math.round(r.height + PAD * 2), }; }); const key = JSON.stringify(rects); if (key === last) return; // don't spam IPC when nothing moved last = key; try { const { invoke } = await import("@tauri-apps/api/core"); await invoke("set_hit_rects", { rects }); } catch (e) { console.error("puck: hit-rect report failed", e); } }, 150); return () => clearInterval(iv); }