Bots now announce every click and typing step before it runs, and the web app draws a cursor in the bot's colour that glides to that point over the live computer screen, ripples on clicks and shows typing. - contracts: ComputerPointerPayload on the existing thread.computer event - executor: computer_act and browser_act announce pointer steps (best effort); the page-browser helper reports element screen boxes, which are stripped before results reach the model - web: agentPointerFeed + AgentCursor over the side-panel preview and the full-window screen, mapped through the noVNC letterbox - in-app browser: native WebContentsView on desktop (own session, http(s) only, clamped bounds, back/forward, reload, DevTools, load errors) and a sandboxed iframe on the web; replaces the dead panel, the fake 50%/40% cursors and the unimplemented devServer bridge Claude-Session: https://claude.ai/code/session_01WiGooXtsKrZQmdszepRrNi Co-authored-by: Claude <noreply@anthropic.com>
77 lines
3.0 KiB
TypeScript
77 lines
3.0 KiB
TypeScript
import type { RakazoDesktopBrowserBounds } from "@rakazo/contracts";
|
|
|
|
/** Only plain http(s) pages load in the in-app browser; no files, schemes or URL credentials. */
|
|
export function previewUrl(value: unknown): string | null {
|
|
if (typeof value !== "string" || value.length > 8192) return null;
|
|
try {
|
|
const url = new URL(value);
|
|
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
|
if (url.username || url.password) return null;
|
|
return url.toString();
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Renderer-reported CSS-pixel bounds, converted to window DIPs and clamped to the content area
|
|
* so a compromised renderer cannot place the view outside the panel it reserved.
|
|
*/
|
|
export function previewBounds(
|
|
value: unknown,
|
|
content: { width: number; height: number },
|
|
zoomFactor = 1,
|
|
): RakazoDesktopBrowserBounds | null {
|
|
if (!value || typeof value !== "object") return null;
|
|
const raw = value as Record<string, unknown>;
|
|
const numbers = [raw.x, raw.y, raw.width, raw.height];
|
|
if (!numbers.every((n) => typeof n === "number" && Number.isFinite(n))) return null;
|
|
const scale = Number.isFinite(zoomFactor) && zoomFactor > 0 ? zoomFactor : 1;
|
|
const [x, y, width, height] = (numbers as number[]).map((n) => Math.round(n * scale)) as [
|
|
number,
|
|
number,
|
|
number,
|
|
number,
|
|
];
|
|
const left = Math.min(Math.max(0, x), content.width);
|
|
const top = Math.min(Math.max(0, y), content.height);
|
|
const right = Math.min(Math.max(left, x + width), content.width);
|
|
const bottom = Math.min(Math.max(top, y + height), content.height);
|
|
if (right - left < 1 || bottom - top < 1) return null;
|
|
return { x: left, y: top, width: right - left, height: bottom - top };
|
|
}
|
|
|
|
export type PreviewShortcut = "reload" | "hard-reload" | "back" | "forward" | "devtools";
|
|
|
|
/** Browser keyboard shortcuts while focus is inside the page. */
|
|
export function previewShortcut(
|
|
input: {
|
|
type: string;
|
|
key: string;
|
|
control: boolean;
|
|
meta: boolean;
|
|
alt: boolean;
|
|
shift: boolean;
|
|
},
|
|
platform: string,
|
|
): PreviewShortcut | null {
|
|
if (input.type !== "keyDown") return null;
|
|
const mod = platform === "darwin" ? input.meta : input.control;
|
|
const key = input.key.length === 1 ? input.key.toLowerCase() : input.key;
|
|
if (key === "F5") return input.shift ? "hard-reload" : "reload";
|
|
if (key === "F12") return "devtools";
|
|
if (mod && key === "r") return input.shift ? "hard-reload" : "reload";
|
|
if (mod && input.alt && key === "i") return "devtools";
|
|
if (platform === "darwin" ? input.meta && !input.alt : input.alt && !input.control) {
|
|
if (key === (platform === "darwin" ? "[" : "ArrowLeft")) return "back";
|
|
if (key === (platform === "darwin" ? "]" : "ArrowRight")) return "forward";
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** Load failures worth showing; an aborted load (-3) is a superseded navigation, not an error. */
|
|
export function previewLoadError(errorCode: number, description: string): string | null {
|
|
if (errorCode === -3 || errorCode === 0) return null;
|
|
return description || `Load failed (${errorCode})`;
|
|
}
|