源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s

This commit is contained in:
agent-radar committed 2026-09-01 20:17:27 +08:00
commit 2ed3415fb7
8018 files changed
+1936476

No files matched your search

@@ -0,0 +1,78 @@
import { useCallback, useState } from "react";
import type { ReactNode } from "react";
import {
Button,
InspectIcon,
Tooltip,
TooltipContent,
TooltipTrigger,
cn
} from "@tutti-os/ui-system";
import { useActiveBrowserNodeWebview } from "@tutti-os/browser-node/react";
import { useEngineSelector } from "../../shared/engine/useEngineSelector.ts";
import { BrowserElementContextSelectionController } from "./browserElementContextSelectionController.ts";
export interface BrowserElementContextCopy {
cancel: string;
failed: string;
select: string;
}
export function BrowserElementContextAction({
copy,
onAppendMention,
onError,
workspaceId
}: {
copy: BrowserElementContextCopy;
onAppendMention: (mention: string) => void;
onError: (message: string) => void;
workspaceId: string;
}): ReactNode {
const activeWebview = useActiveBrowserNodeWebview();
const [controller] = useState(
() =>
new BrowserElementContextSelectionController({
failedCopy: copy.failed,
onAppendMention,
onError,
workspaceId
})
);
controller.configure({
failedCopy: copy.failed,
onAppendMention,
onError,
workspaceId
});
const state = useEngineSelector(controller, (snapshot) => snapshot.state);
const bindAction = useCallback(
(node: HTMLButtonElement | null) =>
controller.bindAction(node, activeWebview),
[activeWebview, controller]
);
const label = state === "selecting" ? copy.cancel : copy.select;
return (
<Tooltip>
<TooltipTrigger asChild>
<Button
aria-label={label}
aria-pressed={state === "selecting"}
className={cn(
"nodrag shrink-0 rounded-md",
state === "selecting" &&
"bg-[var(--transparency-block)] text-[var(--text-primary)]"
)}
size="icon-sm"
ref={bindAction}
type="button"
variant="chrome"
onClick={() => controller.toggle(activeWebview)}
>
<InspectIcon className="size-[15px]" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">{label}</TooltipContent>
</Tooltip>
);
}
@@ -0,0 +1,249 @@
import type { BrowserNodeWebviewTag } from "@tutti-os/browser-node/react";
import {
normalizeBrowserElementSelectionResult,
serializeBrowserElementSnapshot
} from "./browserElementSnapshot.ts";
import {
browserElementSelectorScript,
cancelBrowserElementSelectorScript
} from "./browserElementSelectorScript.ts";
import {
cancelBrowserElementWebviewSelection,
executeBrowserElementWebviewScript
} from "./browserElementWebview.ts";
import { createBrowserElementMentionMarkdown } from "./browserElementMention.ts";
export interface BrowserElementSelectionControllerInput {
failedCopy: string;
onAppendMention: (mention: string) => void;
onError: (message: string) => void;
workspaceId: string;
}
export interface BrowserElementSelectionSnapshot {
state: "idle" | "selecting";
}
interface BrowserElementSelectionSession {
attempt: number;
navigationPending: boolean;
webview: BrowserNodeWebviewTag | null;
}
export class BrowserElementContextSelectionController {
private activeWebview: BrowserNodeWebviewTag | null = null;
private detachFrame: number | null = null;
private input: BrowserElementSelectionControllerInput;
private readonly listeners = new Set<
(snapshot: BrowserElementSelectionSnapshot) => void
>();
private session: BrowserElementSelectionSession | null = null;
private snapshot: BrowserElementSelectionSnapshot = { state: "idle" };
private subscribedWebview: BrowserNodeWebviewTag | null = null;
constructor(input: BrowserElementSelectionControllerInput) {
this.input = input;
}
readonly getSnapshot = (): BrowserElementSelectionSnapshot => this.snapshot;
readonly subscribe = (
listener: (snapshot: BrowserElementSelectionSnapshot) => void
): (() => void) => {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
};
configure(input: BrowserElementSelectionControllerInput): void {
this.input = input;
}
bindAction(
node: HTMLButtonElement | null,
activeWebview: BrowserNodeWebviewTag | null
): void {
if (node) {
if (this.detachFrame !== null) {
cancelAnimationFrame(this.detachFrame);
this.detachFrame = null;
}
this.moveToActiveWebview(activeWebview);
return;
}
if (this.detachFrame !== null) cancelAnimationFrame(this.detachFrame);
this.detachFrame = requestAnimationFrame(() => {
this.detachFrame = null;
this.dispose();
});
}
toggle(activeWebview: BrowserNodeWebviewTag | null): void {
if (this.session) {
this.endSelection(this.session);
return;
}
if (!activeWebview?.executeJavaScript) {
this.input.onError(this.input.failedCopy);
return;
}
const session: BrowserElementSelectionSession = {
attempt: 0,
navigationPending: false,
webview: null
};
this.session = session;
this.setState("selecting");
this.moveToActiveWebview(activeWebview);
}
private readonly handleStartLoading = (): void => {
if (this.session) this.session.navigationPending = true;
};
private readonly handleDomReady = (): void => {
const session = this.session;
const webview = this.activeWebview;
if (!session || !webview) return;
session.navigationPending = false;
void this.moveSelectionToWebview(session, webview, true);
};
private moveToActiveWebview(webview: BrowserNodeWebviewTag | null): void {
this.activeWebview = webview;
const session = this.session;
if (!session) return;
if (!webview) {
const previousWebview = session.webview;
session.webview = null;
session.attempt += 1;
this.unsubscribeFromWebview();
void cancelBrowserElementWebviewSelection(
previousWebview,
cancelBrowserElementSelectorScript
);
return;
}
this.subscribeToWebview(webview);
void this.moveSelectionToWebview(session, webview);
}
private subscribeToWebview(webview: BrowserNodeWebviewTag): void {
if (this.subscribedWebview === webview) return;
this.unsubscribeFromWebview();
this.subscribedWebview = webview;
webview.addEventListener("did-start-loading", this.handleStartLoading);
webview.addEventListener("dom-ready", this.handleDomReady);
}
private unsubscribeFromWebview(): void {
this.subscribedWebview?.removeEventListener(
"did-start-loading",
this.handleStartLoading
);
this.subscribedWebview?.removeEventListener(
"dom-ready",
this.handleDomReady
);
this.subscribedWebview = null;
}
private async moveSelectionToWebview(
session: BrowserElementSelectionSession,
webview: BrowserNodeWebviewTag,
force = false
): Promise<void> {
if (this.session !== session) return;
if (!force && session.webview === webview && session.attempt > 0) return;
const previousWebview = session.webview;
session.webview = webview;
session.navigationPending = false;
const attempt = ++session.attempt;
await cancelBrowserElementWebviewSelection(
previousWebview && (force || previousWebview !== webview)
? previousWebview
: null,
cancelBrowserElementSelectorScript
);
if (!this.isCurrentSelectionAttempt(session, attempt)) return;
void this.runSelectionAttempt(session, webview, attempt);
}
private async runSelectionAttempt(
session: BrowserElementSelectionSession,
webview: BrowserNodeWebviewTag,
attempt: number
): Promise<void> {
try {
const rawResult = await executeBrowserElementWebviewScript(
webview,
browserElementSelectorScript,
true
);
if (!this.isCurrentSelectionAttempt(session, attempt)) return;
const result = normalizeBrowserElementSelectionResult(rawResult);
if (!result || result.status === "cancelled") {
this.endSelection(session);
return;
}
const content = serializeBrowserElementSnapshot(result.snapshot);
const mention = createBrowserElementMentionMarkdown({
context: content,
id: createBrowserElementReferenceId(),
tagName: result.snapshot.element.tagName,
workspaceId: this.input.workspaceId
});
if (!mention)
throw new Error("Browser element mention could not be created");
this.input.onAppendMention(mention);
this.endSelection(session);
} catch {
if (!this.isCurrentSelectionAttempt(session, attempt)) return;
// A guest navigation destroys the injected Promise. Keep the selection
// session alive until the new document emits dom-ready.
if (session.navigationPending) return;
this.input.onError(this.input.failedCopy);
this.endSelection(session);
}
}
private isCurrentSelectionAttempt(
session: BrowserElementSelectionSession,
attempt: number
): boolean {
return this.session === session && session.attempt === attempt;
}
private endSelection(session: BrowserElementSelectionSession): void {
if (this.session !== session) return;
this.session = null;
session.attempt += 1;
const webview = session.webview;
session.webview = null;
this.unsubscribeFromWebview();
void cancelBrowserElementWebviewSelection(
webview,
cancelBrowserElementSelectorScript
);
this.setState("idle");
}
private setState(state: BrowserElementSelectionSnapshot["state"]): void {
if (this.snapshot.state === state) return;
this.snapshot = { state };
for (const listener of this.listeners) listener(this.snapshot);
}
private dispose(): void {
const session = this.session;
if (session) this.endSelection(session);
this.unsubscribeFromWebview();
this.activeWebview = null;
}
}
function createBrowserElementReferenceId(): string {
return `browser-element:${
globalThis.crypto?.randomUUID?.() ??
`${Date.now()}-${Math.random().toString(36).slice(2)}`
}`;
}
@@ -0,0 +1,51 @@
import { describe, expect, it } from "vitest";
import { parseRichTextMentionHref } from "@tutti-os/ui-rich-text/core";
import {
browserElementMentionLabel,
createBrowserElementMentionMarkdown,
presentBrowserElementMention
} from "./browserElementMention.ts";
describe("browser element mentions", () => {
it("preserve prompt text and show only the DOM tag", () => {
const context = [
"DOM Path: #app > div",
"Position: top=0px",
"HTML Element: <div>Hello</div>"
].join("\n");
const markdown = createBrowserElementMentionMarkdown({
context,
id: "browser-element:1",
tagName: "A",
workspaceId: "workspace-1"
});
const href = markdown.slice(markdown.indexOf("(") + 1, -1);
const mention = parseRichTextMentionHref(href);
expect(markdown).toMatch(/^\[@a\]\(/u);
expect(browserElementMentionLabel(mention?.scope?.tag ?? "")).toBe("<a>");
expect(mention?.providerId).toBe("browser-element");
expect(mention?.scope?.context).toBe(context);
expect(mention?.scope?.workspaceId).toBe("workspace-1");
});
it("reject incomplete references", () => {
expect(
createBrowserElementMentionMarkdown({
context: "",
id: "browser-element:1",
tagName: "div",
workspaceId: "workspace-1"
})
).toBe("");
});
it("keep historical references presentable without inline context", () => {
expect(
presentBrowserElementMention({
label: "a",
scope: { tag: "a", workspaceId: "workspace-1" }
})
).toEqual({ name: "<a>", workspaceId: "workspace-1" });
});
});
@@ -0,0 +1,66 @@
import { createRichTextMentionMarkdown } from "@tutti-os/ui-rich-text/core";
export const browserElementMentionKind = "browser-element";
export interface BrowserElementMentionReference {
context: string;
id: string;
tagName: string;
workspaceId: string;
}
export interface BrowserElementMentionIdentity {
label: string;
scope?: Readonly<Record<string, string>>;
}
export interface BrowserElementMentionPresentation {
name: string;
workspaceId?: string;
}
export function createBrowserElementMentionMarkdown(
reference: BrowserElementMentionReference
): string {
const tagName = normalizeBrowserElementTagName(reference.tagName);
const context = reference.context.trim();
const workspaceId = reference.workspaceId.trim();
if (!tagName || !context || !workspaceId) {
return "";
}
return createRichTextMentionMarkdown({
providerId: browserElementMentionKind,
entityId: reference.id,
label: tagName,
scope: { context, tag: tagName, workspaceId }
});
}
export function browserElementMentionLabel(tagName: string): string {
const normalized = normalizeBrowserElementTagName(tagName);
return normalized ? `<${normalized}>` : "";
}
export function presentBrowserElementMention(
mention: BrowserElementMentionIdentity
): BrowserElementMentionPresentation | null {
const label = browserElementMentionLabel(
mention.scope?.tag?.trim() || mention.label
);
if (!label) {
return null;
}
const workspaceId = mention.scope?.workspaceId?.trim() ?? "";
return {
name: label,
...(workspaceId ? { workspaceId } : {})
};
}
function normalizeBrowserElementTagName(value: string): string {
return value
.trim()
.toLowerCase()
.replace(/[^a-z0-9:-]/gu, "")
.slice(0, 100);
}
@@ -0,0 +1,302 @@
function runTuttiBrowserElementSelector(): Promise<unknown> {
type SelectorGlobal = typeof globalThis & {
__tuttiBrowserElementSelector?: { cancel(): void };
};
const selectorGlobal = globalThis as SelectorGlobal;
selectorGlobal.__tuttiBrowserElementSelector?.cancel();
return new Promise((resolve) => {
const overlay = document.createElement("div");
overlay.setAttribute("data-tutti-browser-element-highlight", "true");
Object.assign(overlay.style, {
background: "rgba(86, 156, 214, 0.12)",
border: "2px solid rgb(86, 156, 214)",
borderRadius: "3px",
boxSizing: "border-box",
display: "none",
left: "0",
pointerEvents: "none",
position: "fixed",
top: "0",
zIndex: "2147483647"
});
document.documentElement.append(overlay);
const previousCursor = document.documentElement.style.cursor;
document.documentElement.style.cursor = "crosshair";
const finish = (value: unknown): void => {
document.removeEventListener("mousemove", handleMouseMove, true);
document.removeEventListener("click", handleClick, true);
document.removeEventListener("keydown", handleKeyDown, true);
document.documentElement.style.cursor = previousCursor;
overlay.remove();
delete selectorGlobal.__tuttiBrowserElementSelector;
resolve(value);
};
const cancel = (): void => finish({ status: "cancelled" });
selectorGlobal.__tuttiBrowserElementSelector = { cancel };
function handleMouseMove(event: MouseEvent): void {
const target = event.target;
if (!(target instanceof Element) || target === overlay) return;
const rect = target.getBoundingClientRect();
Object.assign(overlay.style, {
display: "block",
height: `${Math.max(0, rect.height)}px`,
transform: `translate(${rect.left}px, ${rect.top}px)`,
width: `${Math.max(0, rect.width)}px`
});
}
function handleClick(event: MouseEvent): void {
const target = event.target;
if (!(target instanceof Element) || target === overlay) return;
event.preventDefault();
event.stopImmediatePropagation();
finish({ snapshot: captureSnapshot(target), status: "selected" });
}
function handleKeyDown(event: KeyboardEvent): void {
if (event.key !== "Escape") return;
event.preventDefault();
event.stopImmediatePropagation();
cancel();
}
function captureSnapshot(target: Element): Record<string, unknown> {
const rect = target.getBoundingClientRect();
const rawText = isFormControl(target)
? ""
: (target.textContent ?? "").replace(/\s+/gu, " ").trim();
const clone = target.cloneNode(true) as Element;
clone
.querySelectorAll("script,style,noscript,template")
.forEach((item) => item.remove());
sanitizeElement(clone);
clone.querySelectorAll("*").forEach(sanitizeElement);
const rawHtml = clone.outerHTML;
const computed = getComputedStyle(target);
const styleNames = [
"display",
"position",
"color",
"background-color",
"font-family",
"font-size",
"font-weight",
"line-height",
"margin",
"padding",
"border",
"border-radius",
"width",
"height",
"flex",
"grid-template-columns",
"align-items",
"justify-content",
"overflow",
"opacity",
"visibility"
];
return {
format: "tutti.browser-element.v1",
capturedAt: new Date().toISOString(),
page: {
title: document.title,
url: sanitizeUrl(location.href)
},
element: {
ariaLabel: target.getAttribute("aria-label"),
attributes: readAttributes(target),
bounds: {
height: rect.height,
width: rect.width,
x: rect.x,
y: rect.y
},
classes: [...target.classList].slice(0, 24),
domPath: domPathFor(target),
html: rawHtml.slice(0, 32_000),
htmlTruncated: rawHtml.length > 32_000,
id: target.id || null,
role: target.getAttribute("role"),
selector: selectorFor(target),
styles: Object.fromEntries(
styleNames.map((name) => [name, computed.getPropertyValue(name)])
),
tagName: target.tagName.toLowerCase(),
text: rawText.slice(0, 4_000),
textTruncated: rawText.length > 4_000
},
ancestors: readAncestors(target),
viewport: {
devicePixelRatio: globalThis.devicePixelRatio,
height: globalThis.innerHeight,
scrollX: globalThis.scrollX,
scrollY: globalThis.scrollY,
width: globalThis.innerWidth
}
};
}
function sanitizeElement(element: Element): void {
if (
element instanceof HTMLInputElement ||
element instanceof HTMLTextAreaElement ||
element instanceof HTMLSelectElement ||
element instanceof HTMLOptionElement
) {
element.removeAttribute("value");
if ("value" in element) element.value = "";
if (element instanceof HTMLTextAreaElement) element.textContent = "";
}
for (const attribute of [...element.attributes]) {
const name = attribute.name.toLowerCase();
if (
name.startsWith("on") ||
name === "nonce" ||
name === "integrity" ||
name === "srcdoc" ||
/(?:token|auth|password|secret|session|cookie|api[-_]?key)/iu.test(
name
)
) {
element.setAttribute(attribute.name, "[redacted]");
continue;
}
if (name === "href" || name === "src" || name === "action") {
element.setAttribute(attribute.name, sanitizeUrl(attribute.value));
}
}
}
function isFormControl(element: Element): boolean {
return (
element instanceof HTMLInputElement ||
element instanceof HTMLTextAreaElement ||
element instanceof HTMLSelectElement
);
}
function readAttributes(element: Element): Record<string, string> {
const result: Record<string, string> = {};
for (const attribute of [...element.attributes].slice(0, 40)) {
const name = attribute.name.toLowerCase();
if (name.startsWith("on") || name === "value" || name === "srcdoc") {
continue;
}
result[attribute.name] =
/(?:token|auth|password|secret|session|cookie|api[-_]?key)/iu.test(
name
)
? "[redacted]"
: name === "href" || name === "src" || name === "action"
? sanitizeUrl(attribute.value)
: attribute.value.slice(0, 2_000);
}
return result;
}
function sanitizeUrl(value: string): string {
try {
const url = new URL(value, location.href);
url.username = "";
url.password = "";
url.hash = "";
for (const key of [...url.searchParams.keys()]) {
if (
/(?:token|auth|password|secret|session|cookie|key|signature)/iu.test(
key
)
) {
url.searchParams.set(key, "[redacted]");
}
}
return url.toString();
} catch {
return value.slice(0, 4_000);
}
}
function selectorFor(element: Element): string {
if (element.id) return `#${escapeCss(element.id)}`;
const segments: string[] = [];
let current: Element | null = element;
while (current && segments.length < 8) {
let segment = current.tagName.toLowerCase();
const parentElement: Element | null = current.parentElement;
if (parentElement) {
const currentTagName = current.tagName;
const siblings = [...parentElement.children].filter(
(item) => item.tagName === currentTagName
);
if (siblings.length > 1) {
segment += `:nth-of-type(${siblings.indexOf(current) + 1})`;
}
}
segments.unshift(segment);
current = parentElement;
}
return segments.join(" > ");
}
function domPathFor(element: Element): string {
const appRoot = document.querySelector("#app");
const root =
appRoot?.contains(element) === true
? appRoot
: (document.body ?? document.documentElement);
const segments: string[] = [];
let current: Element | null = element;
while (current) {
segments.unshift(domPathSegment(current, current === root));
if (current === root) break;
current = current.parentElement;
}
return segments.join(" > ").slice(0, 4_000);
}
function domPathSegment(element: Element, isRoot: boolean): string {
if (isRoot && element.id === "app") return "#app";
const tagName = element.tagName.toLowerCase();
const classNames = [...element.classList]
.filter(Boolean)
.slice(0, 6)
.map((name) => `.${name}`)
.join("");
if (classNames) return `${tagName}${classNames}`;
if (element.id) return `${tagName}#${element.id}`;
return tagName;
}
function escapeCss(value: string): string {
return globalThis.CSS?.escape
? globalThis.CSS.escape(value)
: value.replace(/[^a-zA-Z0-9_-]/gu, (character) => `\\${character}`);
}
function readAncestors(element: Element): Array<Record<string, unknown>> {
const result: Array<Record<string, unknown>> = [];
let current = element.parentElement;
while (current && result.length < 8) {
result.push({
id: current.id || null,
role: current.getAttribute("role"),
selector: selectorFor(current),
tagName: current.tagName.toLowerCase()
});
current = current.parentElement;
}
return result;
}
document.addEventListener("mousemove", handleMouseMove, true);
document.addEventListener("click", handleClick, true);
document.addEventListener("keydown", handleKeyDown, true);
});
}
export const browserElementSelectorScript = `(${runTuttiBrowserElementSelector.toString()})()`;
export const cancelBrowserElementSelectorScript =
"globalThis.__tuttiBrowserElementSelector?.cancel();";
@@ -0,0 +1,79 @@
import { describe, expect, it } from "vitest";
import {
browserElementSnapshotFormat,
browserElementSnapshotMaxHtmlChars,
normalizeBrowserElementSelectionResult,
serializeBrowserElementSnapshot
} from "./browserElementSnapshot.ts";
describe("browser element snapshots", () => {
it("bound content and redact secret-looking URL parameters", () => {
const result = normalizeBrowserElementSelectionResult({
status: "selected",
snapshot: {
capturedAt: "2026-07-15T00:00:00.000Z",
page: {
title: "Example",
url: "https://example.com/page?token=secret&tab=main#private"
},
element: {
attributes: {},
bounds: { height: 10, width: 20, x: 1, y: 2 },
classes: ["primary"],
domPath: "#app > main.page > button.primary",
html: "x".repeat(browserElementSnapshotMaxHtmlChars + 100),
selector: "#submit",
styles: {},
tagName: "BUTTON",
text: "Submit"
},
viewport: { height: 800, width: 1200 }
}
});
expect(result?.status).toBe("selected");
if (result?.status !== "selected") return;
expect(result.snapshot.format).toBe(browserElementSnapshotFormat);
expect(result.snapshot.element.html.length).toBe(
browserElementSnapshotMaxHtmlChars
);
expect(result.snapshot.element.tagName).toBe("button");
expect(result.snapshot.page.url).toBe(
"https://example.com/page?token=%5Bredacted%5D&tab=main"
);
});
it("serialize to Cursor's three-field text format", () => {
const result = normalizeBrowserElementSelectionResult({
status: "selected",
snapshot: {
page: { title: "Example", url: "https://example.com" },
element: {
bounds: { height: 40.125, width: 120.5, x: 8, y: -0 },
domPath: "#app > div.page-wrapper-n1Pp9 > a.nav-link",
html: '<a href="https://example.com">\n Example home\n</a>',
tagName: "a"
}
}
});
expect(result?.status).toBe("selected");
if (result?.status !== "selected") return;
expect(serializeBrowserElementSnapshot(result.snapshot)).toBe(
[
"DOM Path: #app > div.page-wrapper-n1Pp9 > a.nav-link",
"Position: top=0px, left=8px, width=120.5px, height=40.13px",
'HTML Element: <a href="https://example.com"> Example home </a>'
].join("\n")
);
});
it("reject selected results without identity", () => {
expect(
normalizeBrowserElementSelectionResult({
status: "selected",
snapshot: { element: {}, page: {}, viewport: {} }
})
).toBeNull();
});
});
@@ -0,0 +1,214 @@
export const browserElementSnapshotFormat = "tutti.browser-element.v1";
export const browserElementSnapshotMaxHtmlChars = 32_000;
export const browserElementSnapshotMaxTextChars = 4_000;
export interface BrowserElementSnapshot {
format: typeof browserElementSnapshotFormat;
capturedAt: string;
page: {
title: string;
url: string;
};
element: {
ariaLabel: string | null;
attributes: Record<string, string>;
bounds: { height: number; width: number; x: number; y: number };
classes: string[];
domPath: string;
html: string;
htmlTruncated: boolean;
id: string | null;
role: string | null;
selector: string;
styles: Record<string, string>;
tagName: string;
text: string;
textTruncated: boolean;
};
ancestors: Array<{
id: string | null;
role: string | null;
selector: string;
tagName: string;
}>;
viewport: {
devicePixelRatio: number;
height: number;
scrollX: number;
scrollY: number;
width: number;
};
}
export type BrowserElementSelectionResult =
| { status: "cancelled" }
| { snapshot: BrowserElementSnapshot; status: "selected" };
export function normalizeBrowserElementSelectionResult(
value: unknown
): BrowserElementSelectionResult | null {
if (!isRecord(value)) return null;
if (value.status === "cancelled") return { status: "cancelled" };
if (value.status !== "selected" || !isRecord(value.snapshot)) return null;
const snapshot = value.snapshot;
const page = isRecord(snapshot.page) ? snapshot.page : {};
const element = isRecord(snapshot.element) ? snapshot.element : {};
const bounds = isRecord(element.bounds) ? element.bounds : {};
const viewport = isRecord(snapshot.viewport) ? snapshot.viewport : {};
const normalized: BrowserElementSnapshot = {
format: browserElementSnapshotFormat,
capturedAt:
boundedString(snapshot.capturedAt, 64) || new Date().toISOString(),
page: {
title: boundedString(page.title, 500),
url: sanitizeSnapshotUrl(boundedString(page.url, 4_000))
},
element: {
ariaLabel: nullableBoundedString(element.ariaLabel, 1_000),
attributes: boundedStringRecord(element.attributes, 40, 200, 2_000),
bounds: {
height: finiteNumber(bounds.height),
width: finiteNumber(bounds.width),
x: finiteNumber(bounds.x),
y: finiteNumber(bounds.y)
},
classes: boundedStringArray(element.classes, 24, 200),
domPath:
boundedString(element.domPath, 4_000) ||
boundedString(element.selector, 2_000),
html: boundedString(element.html, browserElementSnapshotMaxHtmlChars),
htmlTruncated:
element.htmlTruncated === true ||
(typeof element.html === "string" &&
element.html.length > browserElementSnapshotMaxHtmlChars),
id: nullableBoundedString(element.id, 500),
role: nullableBoundedString(element.role, 200),
selector: boundedString(element.selector, 2_000),
styles: boundedStringRecord(element.styles, 32, 100, 500),
tagName: boundedString(element.tagName, 100).toLowerCase(),
text: boundedString(element.text, browserElementSnapshotMaxTextChars),
textTruncated:
element.textTruncated === true ||
(typeof element.text === "string" &&
element.text.length > browserElementSnapshotMaxTextChars)
},
ancestors: Array.isArray(snapshot.ancestors)
? snapshot.ancestors.slice(0, 8).flatMap((item) =>
isRecord(item)
? [
{
id: nullableBoundedString(item.id, 500),
role: nullableBoundedString(item.role, 200),
selector: boundedString(item.selector, 2_000),
tagName: boundedString(item.tagName, 100).toLowerCase()
}
]
: []
)
: [],
viewport: {
devicePixelRatio: finiteNumber(viewport.devicePixelRatio, 1),
height: finiteNumber(viewport.height),
scrollX: finiteNumber(viewport.scrollX),
scrollY: finiteNumber(viewport.scrollY),
width: finiteNumber(viewport.width)
}
};
if (!normalized.element.tagName || !normalized.element.domPath) return null;
return { snapshot: normalized, status: "selected" };
}
export function serializeBrowserElementSnapshot(
snapshot: BrowserElementSnapshot
): string {
const { bounds } = snapshot.element;
return [
`DOM Path: ${snapshot.element.domPath}`,
`Position: top=${formatPixel(bounds.y)}, left=${formatPixel(bounds.x)}, width=${formatPixel(bounds.width)}, height=${formatPixel(bounds.height)}`,
`HTML Element: ${compactHtmlFragment(snapshot.element.html)}`
].join("\n");
}
function formatPixel(value: number): string {
const rounded = Math.round(value * 100) / 100;
return `${Object.is(rounded, -0) ? 0 : rounded}px`;
}
function compactHtmlFragment(value: string): string {
return value
.replace(/\r?\n/gu, " ")
.replace(/\s{2,}/gu, " ")
.trim();
}
function sanitizeSnapshotUrl(value: string): string {
try {
const url = new URL(value);
url.username = "";
url.password = "";
url.hash = "";
for (const key of [...url.searchParams.keys()]) {
if (
/(?:token|auth|password|secret|session|cookie|key|signature)/iu.test(
key
)
) {
url.searchParams.set(key, "[redacted]");
}
}
return url.toString();
} catch {
return boundedString(value, 4_000);
}
}
function boundedString(value: unknown, maxChars: number): string {
return typeof value === "string" ? value.slice(0, maxChars) : "";
}
function nullableBoundedString(
value: unknown,
maxChars: number
): string | null {
const normalized = boundedString(value, maxChars).trim();
return normalized || null;
}
function boundedStringArray(
value: unknown,
maxItems: number,
maxChars: number
): string[] {
return Array.isArray(value)
? value
.slice(0, maxItems)
.filter((item): item is string => typeof item === "string")
.map((item) => item.slice(0, maxChars))
: [];
}
function boundedStringRecord(
value: unknown,
maxItems: number,
maxKeyChars: number,
maxValueChars: number
): Record<string, string> {
if (!isRecord(value)) return {};
return Object.fromEntries(
Object.entries(value)
.slice(0, maxItems)
.flatMap(([key, item]) =>
typeof item === "string"
? [[key.slice(0, maxKeyChars), item.slice(0, maxValueChars)]]
: []
)
);
}
function finiteNumber(value: unknown, fallback = 0): number {
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
@@ -0,0 +1,78 @@
import { describe, expect, it } from "vitest";
import type { BrowserNodeWebviewTag } from "@tutti-os/browser-node/react";
import {
cancelBrowserElementWebviewSelection,
executeBrowserElementWebviewScript,
isBrowserElementWebviewReady,
waitForBrowserElementWebviewReady
} from "./browserElementWebview.ts";
class MockBrowserElementWebview extends EventTarget {
isConnected = true;
ready = false;
executedScripts: string[] = [];
executeJavaScript<T>(script: string): Promise<T> {
this.executedScripts.push(script);
return Promise.resolve("selected" as T);
}
getWebContentsId(): number {
if (!this.ready) {
throw new Error(
"The WebView must be attached to the DOM and the dom-ready event emitted"
);
}
return 42;
}
}
function asBrowserWebview(
webview: MockBrowserElementWebview
): BrowserNodeWebviewTag {
return webview as unknown as BrowserNodeWebviewTag;
}
describe("browser element webview", () => {
it("waits for the active webview dom-ready event before execution", async () => {
const mock = new MockBrowserElementWebview();
const webview = asBrowserWebview(mock);
const execution = executeBrowserElementWebviewScript<string>(
webview,
"select()",
true
);
expect(mock.executedScripts).toEqual([]);
mock.ready = true;
mock.dispatchEvent(new Event("dom-ready"));
expect(await execution).toBe("selected");
expect(mock.executedScripts).toEqual(["select()"]);
});
it("fails detached webview readiness without executing", async () => {
const mock = new MockBrowserElementWebview();
mock.isConnected = false;
const webview = asBrowserWebview(mock);
expect(isBrowserElementWebviewReady(webview)).toBe(false);
expect(await waitForBrowserElementWebviewReady(webview, 1)).toBe(false);
await expect(
executeBrowserElementWebviewScript(webview, "select()")
).rejects.toThrow(/not ready/u);
expect(mock.executedScripts).toEqual([]);
});
it("ignores a webview detached during cancellation cleanup", async () => {
const mock = new MockBrowserElementWebview();
mock.isConnected = false;
await cancelBrowserElementWebviewSelection(
asBrowserWebview(mock),
"cancel()"
);
expect(mock.executedScripts).toEqual([]);
});
});
@@ -0,0 +1,82 @@
import type { BrowserNodeWebviewTag } from "@tutti-os/browser-node/react";
export const browserElementWebviewReadyTimeoutMs = 2_000;
export function isBrowserElementWebviewReady(
webview: BrowserNodeWebviewTag
): boolean {
if (!webview.isConnected) return false;
try {
const webContentsId = webview.getWebContentsId?.();
return typeof webContentsId === "number" && Number.isFinite(webContentsId);
} catch {
return false;
}
}
export async function waitForBrowserElementWebviewReady(
webview: BrowserNodeWebviewTag,
timeoutMs = browserElementWebviewReadyTimeoutMs
): Promise<boolean> {
if (isBrowserElementWebviewReady(webview)) return true;
if (!webview.isConnected) return false;
return new Promise((resolve) => {
let settled = false;
let timeoutSignal: AbortSignal | null = null;
const finish = (ready: boolean): void => {
if (settled) return;
settled = true;
timeoutSignal?.removeEventListener("abort", handleTimeout);
webview.removeEventListener("dom-ready", handleDomReady);
resolve(ready);
};
const handleDomReady = (): void => {
finish(isBrowserElementWebviewReady(webview));
};
timeoutSignal = AbortSignal.timeout(timeoutMs);
const handleTimeout = (): void => finish(false);
timeoutSignal.addEventListener("abort", handleTimeout, { once: true });
webview.addEventListener("dom-ready", handleDomReady, { once: true });
// Covers a dom-ready event emitted between the initial check and listener
// registration without adding a second BrowserNode lifecycle owner.
if (isBrowserElementWebviewReady(webview)) finish(true);
});
}
export async function executeBrowserElementWebviewScript<T>(
webview: BrowserNodeWebviewTag,
script: string,
userGesture = false
): Promise<T> {
if (!webview.executeJavaScript) {
throw new Error("Browser webview JavaScript execution is unavailable");
}
if (!(await waitForBrowserElementWebviewReady(webview))) {
throw new Error("Browser webview is not ready");
}
return webview.executeJavaScript<T>(script, userGesture);
}
export async function cancelBrowserElementWebviewSelection(
webview: BrowserNodeWebviewTag | null,
script: string,
onError?: (error: unknown) => void
): Promise<void> {
if (!webview?.executeJavaScript || !webview.isConnected) return;
try {
await webview.executeJavaScript(script);
} catch (error) {
// Cancellation is best-effort because navigation and unmount detach the
// guest before React cleanup runs.
if (onError) {
onError(error);
} else {
console.warn(
"[agent-gui] browser element selection cleanup failed",
error
);
}
}
}
@@ -0,0 +1,14 @@
export {
BrowserElementContextAction,
type BrowserElementContextCopy
} from "./BrowserElementContextAction";
export {
browserElementMentionKind,
browserElementMentionLabel,
createBrowserElementMentionMarkdown,
presentBrowserElementMention,
type BrowserElementMentionIdentity,
type BrowserElementMentionPresentation,
type BrowserElementMentionReference
} from "./browserElementMention";
export { registerBrowserElementMention } from "./registerBrowserElementMention";
@@ -0,0 +1,29 @@
import { Badge } from "@tutti-os/ui-system";
import { InspectIcon } from "@tutti-os/ui-system/icons";
import { registerAgentCustomMentionKind } from "../../custom-mention";
import {
browserElementMentionKind,
presentBrowserElementMention
} from "./browserElementMention";
/** Registers browser DOM references before the first AgentGUI surface mounts. */
export function registerBrowserElementMention(): void {
registerAgentCustomMentionKind({
kind: browserElementMentionKind,
materializePromptText: (mention) => {
const context = mention.scope?.context?.trim();
return context ? `\n${context}\n` : null;
},
present: presentBrowserElementMention,
renderChip: ({ name }) => (
<Badge
className="h-5 max-w-full gap-1.5 rounded-[6px] px-2 py-0 font-[var(--font-mono)] text-[13px] font-semibold leading-5"
data-agent-browser-element-chip="true"
variant="accent"
>
<InspectIcon aria-hidden="true" className="size-3.5 shrink-0" />
<span className="truncate">{name}</span>
</Badge>
)
});
}