Add paginated browser snapshots and raise dialog z-index

This commit is contained in:
musistudio 2026-07-08 17:02:36 +08:00
parent a8ccdf0403
commit 980a9bae63
6 changed files with 42 additions and 15 deletions

View file

@ -70,6 +70,13 @@ type BrowserSessionRef = {
userId?: string;
};
type SnapshotOptions = {
limit?: number;
maxElements: number;
maxText?: number;
offset?: number;
};
type AttachedSession = {
attachedAt: number;
leaseId: string;
@ -140,6 +147,8 @@ const defaultSnapshotMaxElements = 80;
const defaultSnapshotMaxText = 3_000;
const defaultEventAwaitTimeoutMs = 10_000;
const defaultJavascriptTimeoutMs = 8_000;
const maxSnapshotTextLimit = 20_000;
const maxSnapshotTextOffset = 1_000_000_000;
const maxMcpRequestBytes = 2 * 1024 * 1024;
const maxSubscriptionEvents = 512;
const maxToolResultChars = 60_000;
@ -148,7 +157,7 @@ const maxToolResultStringChars = 2_000;
const maxToolResultObjectKeys = 120;
const maxSnapshotResultElements = 80;
const maxAxSnapshotResultNodes = 80;
const maxBrowserResultTextChars = 3_000;
const maxBrowserResultTextChars = maxSnapshotTextLimit;
const defaultWaitTimeoutMs = 10_000;
const sessionSchema = objectSchema({
@ -552,8 +561,10 @@ function browserLegacyAliasTools(): McpTool[] {
{
description: "Capture page text plus interactable element refs for browser automation. Use returned refs for browser_click, browser_type, browser_select, browser_press_key, or browser_scroll.",
inputSchema: objectSchema({
limit: { description: "Maximum page text characters to include. Overrides maxText when both are provided.", maximum: maxSnapshotTextLimit, minimum: 0, type: "number" },
maxElements: { description: "Maximum interactable elements to include.", maximum: 300, minimum: 1, type: "number" },
maxText: { description: "Maximum page text characters to include.", maximum: 20000, minimum: 0, type: "number" },
maxText: { description: "Legacy alias for limit.", maximum: maxSnapshotTextLimit, minimum: 0, type: "number" },
offset: { description: "Starting character offset into the normalized page text.", maximum: maxSnapshotTextOffset, minimum: 0, type: "number" },
tabId: { description: "Optional tab id. Defaults to the active tab.", type: "string" }
}),
name: "browser_snapshot",
@ -1014,8 +1025,9 @@ class BrowserAutomationMcpService implements BrowserAutomationMcpIntegration {
builtInBrowserService.getAutomationWebContents(readString(args.tabId)),
readString(args.tabId),
{
limit: clampInteger(readNumber(args.limit) ?? readNumber(args.maxText) ?? defaultSnapshotMaxText, 0, maxSnapshotTextLimit),
maxElements: clampInteger(readNumber(args.maxElements) ?? defaultSnapshotMaxElements, 1, 300),
maxText: clampInteger(readNumber(args.maxText) ?? defaultSnapshotMaxText, 0, 20000)
offset: clampInteger(readNumber(args.offset) ?? 0, 0, maxSnapshotTextOffset)
}
);
case "browser_click":
@ -2339,7 +2351,7 @@ async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, message: s
}
}
async function captureSnapshot(webContents: WebContents, options: { maxElements: number; maxText: number }): Promise<unknown> {
async function captureSnapshot(webContents: WebContents, options: SnapshotOptions): Promise<unknown> {
return await executeJavaScriptWithTimeout(
webContents,
`(${snapshotScript})(${JSON.stringify(options)})`,
@ -2351,7 +2363,7 @@ async function captureSnapshot(webContents: WebContents, options: { maxElements:
async function captureSnapshotWithHandoff(
webContents: WebContents,
tabId: string | undefined,
options: { maxElements: number; maxText: number }
options: SnapshotOptions
): Promise<unknown> {
const snapshot = await captureSnapshot(webContents, options);
const handoff = await maybeRequestPageHandoff(webContents, {
@ -2670,9 +2682,11 @@ async function waitForPageCondition(
};
}
const snapshotScript = function(options: { maxElements: number; maxText: number }) {
const snapshotScript = function(options: { limit?: number; maxElements: number; maxText?: number; offset?: number }) {
const maxElements = Math.max(1, Math.min(300, Math.floor(options.maxElements || 80)));
const maxText = Math.max(0, Math.min(20000, Math.floor(options.maxText || 3000)));
const textLimitInput = options.limit ?? options.maxText ?? 3000;
const textLimit = Math.max(0, Math.min(20000, Math.floor(textLimitInput)));
const requestedTextOffset = Math.max(0, Math.floor(options.offset || 0));
const refAttribute = "data-ccr-browser-ref";
const elementSelector = [
"a[href]",
@ -2860,6 +2874,11 @@ const snapshotScript = function(options: { maxElements: number; maxText: number
};
});
const pageText = (document.body?.innerText || "").replace(/\s+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
const textOffset = Math.min(requestedTextOffset, pageText.length);
const textWindow = pageText.slice(textOffset, textOffset + textLimit);
const textNextOffset = textOffset + textWindow.length;
return {
activeElement: document.activeElement ? {
name: nameOf(document.activeElement).slice(0, 240),
@ -2868,7 +2887,15 @@ const snapshotScript = function(options: { maxElements: number; maxText: number
tag: document.activeElement.tagName.toLowerCase()
} : undefined,
elements,
text: (document.body?.innerText || "").replace(/\s+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim().slice(0, maxText),
text: textWindow,
textHasMore: textNextOffset < pageText.length,
textLength: pageText.length,
textLimit,
textNextOffset,
textOffset,
textRemaining: Math.max(0, pageText.length - textNextOffset),
textReturned: textWindow.length,
textRequestedOffset: requestedTextOffset !== textOffset ? requestedTextOffset : undefined,
title: document.title,
url: location.href
};
@ -3437,7 +3464,7 @@ function truncateString(value: string, maxChars: number): string {
function largeResultGuidance(toolName: string): string {
if (toolName === "browser_snapshot") {
return "Result was compacted. Re-run browser_snapshot with lower maxElements/maxText, or use browser_ax_query to fetch targeted elements.";
return "Result was compacted. Re-run browser_snapshot with lower maxElements/limit, page text with offset/limit, or use browser_ax_query to fetch targeted elements.";
}
if (toolName === "browser_ax_snapshot") {
return "Result was compacted. Re-run browser_ax_snapshot with lower limit or scope=outline, or use browser_ax_query with role/name/text filters.";

View file

@ -46,7 +46,7 @@ function Dialog({
return (
<motion.div
animate={{ opacity: 1 }}
className={cn("fixed inset-0 z-50 flex items-center justify-center overflow-hidden bg-black/28 p-3 sm:p-6", className)}
className={cn("fixed inset-0 z-[100] flex items-center justify-center overflow-hidden bg-black/28 p-3 sm:p-6", className)}
exit={{ opacity: 0 }}
initial={{ opacity: 0 }}
onMouseDown={(event) => {

View file

@ -3705,7 +3705,7 @@ function ToolPayloadDialog({
: undefined;
return (
<Dialog className="z-[70] items-start" onOpenChange={(open) => !open && onClose()} open>
<Dialog className="z-[110] items-start" onOpenChange={(open) => !open && onClose()} open>
<DialogContent className="h-[calc(100dvh-1.5rem)] max-w-[1180px] origin-top sm:h-[min(820px,calc(100dvh-3rem))]">
<DialogHeader>
<div className="min-w-0">

View file

@ -2516,7 +2516,7 @@ export function AddProviderDialog({
</Dialog>
{checkConfirmOpen ? (
<Dialog className="z-[60]" onOpenChange={(open) => !open && !checkConfirmBusy && setCheckConfirmOpen(false)}>
<Dialog className="z-[110]" onOpenChange={(open) => !open && !checkConfirmBusy && setCheckConfirmOpen(false)}>
<DialogContent className="max-w-[520px]">
<DialogHeader>
<div className="min-w-0">

View file

@ -636,7 +636,7 @@ function ToolHubMcpServerDialog({
const stdioMessageModeOptions = translateOptions(mcpStdioMessageModeOptions, t);
return (
<Dialog className="z-[80]" onOpenChange={(nextOpen) => !nextOpen && onClose()} open={open}>
<Dialog className="z-[110]" onOpenChange={(nextOpen) => !nextOpen && onClose()} open={open}>
<DialogContent className="max-w-[760px]">
<DialogHeader>
<div className="min-w-0">
@ -755,7 +755,7 @@ function ToolHubMcpJsonDialog({
const t = useAppText();
return (
<Dialog className="z-[80]" onOpenChange={(nextOpen) => !nextOpen && onClose()} open={open}>
<Dialog className="z-[110]" onOpenChange={(nextOpen) => !nextOpen && onClose()} open={open}>
<DialogContent className="max-w-[760px]">
<DialogHeader>
<div className="min-w-0">

View file

@ -605,7 +605,7 @@ function CustomMcpToolDialog({
}
return (
<Dialog className="z-[80]" onOpenChange={(nextOpen) => !nextOpen && onClose()} open={open}>
<Dialog className="z-[110]" onOpenChange={(nextOpen) => !nextOpen && onClose()} open={open}>
<DialogContent className="max-w-[760px]">
<DialogHeader>
<div className="min-w-0">