Files
agent-radar 2ed3415fb7
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
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

83 lines
2.6 KiB
TypeScript

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
);
}
}
}