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

234 lines
6.9 KiB
TypeScript

import {
lazy,
Suspense,
useCallback,
useMemo,
useState,
type HTMLAttributes,
type ReactNode
} from "react";
import {
activateBrowserNodePageByUrl,
closeBrowserNodeTab,
createBrowserNodeFeature,
isBrowserNodeSurfaceEvent,
type BrowserNodeChromeImportPromptAdapter,
type BrowserNodeAutomationTargetMetadata,
type BrowserNodeFeature,
type BrowserNodeHostApi,
type BrowserNodeSessionMode
} from "@tutti-os/browser-node";
import type { BrowserNodeErrorRenderContext } from "@tutti-os/browser-node/react";
import type { I18nRuntime } from "@tutti-os/ui-i18n-runtime";
import { createAgentToolBrowserPage } from "./agentToolBrowserPage.ts";
const LazyBrowserNode = lazy(() =>
import("@tutti-os/browser-node/react").then(({ BrowserNode }) => ({
default: BrowserNode
}))
);
const LazyBrowserNodeWorkbenchHeader = lazy(() =>
import("@tutti-os/browser-node/react").then(
({ BrowserNodeWorkbenchHeader }) => ({
default: BrowserNodeWorkbenchHeader
})
)
);
export const agentToolBrowserDefaultUrl = "https://www.google.com/";
export interface AgentToolBrowserPanelProps {
automationTarget?: Omit<
BrowserNodeAutomationTargetMetadata,
"focused" | "selected" | "surfaceId" | "tabId"
> | null;
browserApi: BrowserNodeHostApi;
chromeCookieImportPrompt?: BrowserNodeChromeImportPromptAdapter;
defaultUrl?: string;
defaultActions?: ReactNode;
dragHandleProps?: HTMLAttributes<HTMLElement>;
hidden: boolean;
i18n: I18nRuntime<string>;
loadingFallback?: ReactNode;
navigationActions?: ReactNode;
nodeIdPrefix?: string;
onCloseRequest?: () => void;
onControllerReady?: (controller: AgentToolBrowserController | null) => void;
profileId?: string | null;
renderError?: (context: BrowserNodeErrorRenderContext) => ReactNode;
sessionMode?: BrowserNodeSessionMode;
sessionPartition?: string | null;
}
export interface AgentToolBrowserController {
activatePageByUrl(url: string): string | null;
closePage(nodeId: string): "closed" | "last-page" | "not-found";
createPage(url?: string | null): string;
ownsPage(nodeId: string): boolean;
selectPage(nodeId: string): boolean;
surfaceNodeId: string;
}
export function AgentToolBrowserPanel({
automationTarget = null,
browserApi,
chromeCookieImportPrompt,
defaultUrl = agentToolBrowserDefaultUrl,
defaultActions,
dragHandleProps,
hidden,
i18n,
loadingFallback = null,
navigationActions,
nodeIdPrefix = "browser:agent-tool",
onCloseRequest,
onControllerReady,
profileId = null,
renderError,
sessionMode = "shared",
sessionPartition = null
}: AgentToolBrowserPanelProps): ReactNode {
const [nodeId] = useState(() => createAgentToolBrowserNodeId(nodeIdPrefix));
const feature = useMemo(
() =>
createAgentToolBrowserFeature({
browserApi,
...(chromeCookieImportPrompt ? { chromeCookieImportPrompt } : {}),
i18n,
nodeId
}),
[browserApi, chromeCookieImportPrompt, i18n, nodeId]
);
const controller = useMemo<AgentToolBrowserController>(() => {
const getPage = (pageNodeId: string) => {
const state = feature.tabsStore.getSurfaceState(nodeId);
const tab = state?.tabs.find(
(candidate) => candidate.nodeId === pageNodeId
);
return state && tab ? { state, tab } : null;
};
return {
activatePageByUrl(url) {
return (
activateBrowserNodePageByUrl(feature, nodeId, url)?.nodeId ?? null
);
},
closePage(pageNodeId) {
const page = getPage(pageNodeId);
if (!page) return "not-found";
if (page.state.tabs.length === 1) return "last-page";
closeBrowserNodeTab(feature, nodeId, page.tab.id);
return "closed";
},
createPage(url) {
return createAgentToolBrowserPage(feature, nodeId, defaultUrl, url);
},
ownsPage: (pageNodeId) => getPage(pageNodeId) !== null,
selectPage(pageNodeId) {
const page = getPage(pageNodeId);
if (!page) return false;
feature.tabsStore.selectTab(nodeId, page.tab.id);
return true;
},
surfaceNodeId: nodeId
};
}, [defaultUrl, feature, nodeId]);
const bindController = useCallback(
(node: HTMLDivElement | null) =>
onControllerReady?.(node ? controller : null),
[controller, onControllerReady]
);
return (
<div
className="relative flex h-full min-h-0 flex-col overflow-hidden"
data-agent-tool-browser-surface="true"
data-agent-tool-browser-surface-id={nodeId}
ref={bindController}
>
<Suspense fallback={loadingFallback}>
<LazyBrowserNodeWorkbenchHeader
defaultUrl={defaultUrl}
feature={feature}
defaultActions={defaultActions}
dragHandleProps={dragHandleProps}
navigationActions={navigationActions}
nodeId={nodeId}
onCloseRequest={onCloseRequest}
/>
<div className="min-h-0 flex-1">
<LazyBrowserNode
automationTarget={
automationTarget
? { ...automationTarget, focused: !hidden }
: null
}
defaultUrl={defaultUrl}
feature={feature}
hidden={hidden}
nodeId={nodeId}
renderError={renderError}
onCloseRequest={onCloseRequest}
profileId={profileId}
sessionMode={sessionMode}
sessionPartition={sessionPartition}
showHeader={false}
syncDefaultUrl
tabs
/>
</div>
</Suspense>
</div>
);
}
export function createAgentToolBrowserFeature(input: {
browserApi: BrowserNodeHostApi;
chromeCookieImportPrompt?: BrowserNodeChromeImportPromptAdapter;
i18n: I18nRuntime<string>;
nodeId: string;
}): BrowserNodeFeature {
return createBrowserNodeFeature({
...(input.chromeCookieImportPrompt
? { chromeCookieImportPrompt: input.chromeCookieImportPrompt }
: {}),
hostApi: createScopedAgentToolBrowserHostApi(
input.browserApi,
input.nodeId
),
i18n: input.i18n,
resolveSearchUrl: resolveAgentToolBrowserSearchUrl
});
}
function createScopedAgentToolBrowserHostApi(
browserApi: BrowserNodeHostApi,
nodeId: string
): BrowserNodeHostApi {
return {
...browserApi,
onEvent(listener) {
return browserApi.onEvent((event) => {
if (isBrowserNodeSurfaceEvent(nodeId, event)) {
listener(event);
}
});
}
};
}
function resolveAgentToolBrowserSearchUrl(query: string): string {
const searchUrl = new URL("https://www.google.com/search");
searchUrl.searchParams.set("q", query);
return searchUrl.toString();
}
function createAgentToolBrowserNodeId(prefix: string): string {
const instanceId =
typeof globalThis.crypto?.randomUUID === "function"
? globalThis.crypto.randomUUID()
: `${Date.now()}-${Math.random().toString(36).slice(2)}`;
return `${prefix}:${instanceId}`;
}