diff --git a/apps/desktop/src/preview/DesktopBrowserHost.test.ts b/apps/desktop/src/preview/DesktopBrowserHost.test.ts
index cf6c7736f81d3..e725ceca4326c 100644
--- a/apps/desktop/src/preview/DesktopBrowserHost.test.ts
+++ b/apps/desktop/src/preview/DesktopBrowserHost.test.ts
@@ -482,6 +482,29 @@ describe("DesktopBrowserHost", () => {
}),
);
+ it.effect("sends a dialog's answer without waiting on a focus change the dialog blocks", () =>
+ Effect.gen(function* () {
+ const host = yield* DesktopBrowserHost.make;
+ // The renderer answers focus emulation, and an open dialog blocks it.
+ const debuggee = makeDebuggee({ deferFocus: true });
+ host.attach(key, debuggee.tab);
+
+ yield* command(host, 1, "Page.handleJavaScriptDialog");
+ yield* settle;
+ expect(debuggee.sent).toEqual([1]);
+ expect(debuggee.focusEmulation).toEqual([]);
+
+ yield* command(host, 2, "Runtime.evaluate");
+ yield* command(host, 3, "Page.handleJavaScriptDialog");
+ yield* settle;
+ expect(debuggee.sent).toEqual([1, 3]);
+ const sent = debuggee.nextSend();
+ debuggee.settleFocus();
+ yield* Effect.promise(() => sent);
+ expect(debuggee.sent).toEqual([1, 3, 2]);
+ }),
+ );
+
it.effect("keeps a guest focused when its turn starts after the agent's first command", () =>
Effect.gen(function* () {
const host = yield* DesktopBrowserHost.make;
diff --git a/apps/desktop/src/preview/DesktopBrowserHost.ts b/apps/desktop/src/preview/DesktopBrowserHost.ts
index e0326b7b76ec1..062c1a7b96e4d 100644
--- a/apps/desktop/src/preview/DesktopBrowserHost.ts
+++ b/apps/desktop/src/preview/DesktopBrowserHost.ts
@@ -341,6 +341,8 @@ export const make = Effect.gen(function* () {
const relay: CdpRelayConnection = createCdpRelayConnection(
{
send: (method, params, sessionId, owner) => {
+ // The browser answers this; an open dialog blocks the renderer that answers focus changes.
+ if (method === "Page.handleJavaScriptDialog") return send(method, params, sessionId);
// A child session's screencast is not the page's, such as a late one on a detached session.
const stream =
sessionId === undefined && method === "Page.startScreencast"
diff --git a/apps/server/src/mcp/toolkits/preview/tools.ts b/apps/server/src/mcp/toolkits/preview/tools.ts
index b2c9afa11bb2a..a60273cb2d40c 100644
--- a/apps/server/src/mcp/toolkits/preview/tools.ts
+++ b/apps/server/src/mcp/toolkits/preview/tools.ts
@@ -85,7 +85,7 @@ const PreviewOpenTool = browserTool(
const PreviewDialogTool = browserTool(
Tool.make("preview_dialog", {
description:
- "Accept or dismiss the dialog `preview_status` reports. Pass `promptText` to accept a prompt. Needs your own tab, or an unclaimed one. Desktop hosts may not support this.",
+ "Accept or dismiss the dialog `preview_status` reports. Pass `promptText` to accept a prompt. Needs your own tab, or an unclaimed one.",
parameters: PreviewAutomationDialogInput,
success: PreviewAutomationStatus,
failure: PreviewToolFailure,
diff --git a/apps/server/src/provider/agentContextBudget.json b/apps/server/src/provider/agentContextBudget.json
index 1d3d3a9179c0b..bbf660b4e1dad 100644
--- a/apps/server/src/provider/agentContextBudget.json
+++ b/apps/server/src/provider/agentContextBudget.json
@@ -1,5 +1,5 @@
{
"claudeSystemAppendBytes": 5930,
"codexAdditionalContextBytes": 7324,
- "mcpToolListJsonBytes": 49790
+ "mcpToolListJsonBytes": 49754
}
diff --git a/evals/mcp-tool-use/cases/visual-preview.ndjson b/evals/mcp-tool-use/cases/visual-preview.ndjson
index edd66f26966e9..48066a18b1142 100644
--- a/evals/mcp-tool-use/cases/visual-preview.ndjson
+++ b/evals/mcp-tool-use/cases/visual-preview.ndjson
@@ -34,3 +34,4 @@
{"id": "browser-status-reuses-existing-tab", "area": "visual-preview", "kind": "control", "privacy": "commit-safe", "pattern": "usable Browser tab needs no new tab", "experience": "An already paired, usable tab should be reused during verification.", "source": {"report": "Maintainer browser verification failure, 2026-11-03"}, "setup": {"role": "top", "context": "The UI fix is complete. The test server is healthy at http://localhost:5173 and its browser profile is already paired. The current tab can be automated.", "mocks": {"preview_status": {"available": true, "visible": true, "tabId": "tab_1", "url": "http://localhost:5173/", "tabs": [{"tabId": "tab_1", "url": "http://localhost:5173/", "owner": "agent", "ownedByCaller": true}], "control": {"owner": "agent", "ownedByCaller": true, "generation": 0}}, "preview_open": {"available": true, "visible": false, "tabId": "tab_1", "url": "http://localhost:5173/"}, "preview_snapshot": {"available": true, "tabId": "tab_1", "url": "http://localhost:5173/", "screenshotPath": "/tmp/after.png"}}, "priorTurns": [{"role": "user", "text": "Use the Browser panel to verify the UI fix and capture an after screenshot."}, {"role": "assistant", "toolCalls": [{"tool": "Bash", "input": {"command": "cat .agents/skills/test-t3-app/SKILL.md"}, "result": "", "resultFile": ".agents/skills/test-t3-app/SKILL.md"}]}, {"role": "assistant", "toolCalls": [{"tool": "preview_status", "input": {}, "result": {"available": true, "visible": true, "tabId": "tab_1", "url": "http://localhost:5173/", "tabs": [{"tabId": "tab_1", "url": "http://localhost:5173/", "owner": "agent", "ownedByCaller": true}], "control": {"owner": "agent", "ownedByCaller": true, "generation": 0}}}]}], "harness": "claude"}, "prompt": "", "expect": {"calls": [{"tool": "preview_snapshot"}], "callsMustNot": [{"tool": "preview_open", "args": {"reuseExistingTab": false}}]}, "maxSteps": 10, "notes": "Checks reuse of an existing usable tab. Screenshot saving is outside this control. Prior tool-result replay is validated on the Claude harness; Codex output wrapping needs separate validation."}
{"id": "browser-resume-after-unavailable-status", "area": "visual-preview", "kind": "bad", "privacy": "commit-safe", "pattern": "closed Browser panel treated as unavailable automation", "experience": "The agent read test-t3-app, saw available false with no tab, and asked the user to open the panel instead of calling preview_open. Opening a tab succeeded on the next turn.", "source": {"report": "Maintainer browser verification failure, 2026-11-03; fresh skill read followed by unavailable status. Opus replay reproduces the stop on the old skill."}, "setup": {"role": "top", "context": "Repository instructions: Before web or desktop UI verification, read .agents/skills/test-t3-app/SKILL.md and follow it. If a skill causes you to stop or leave requested work unfinished, link the SKILL.md, quote the relevant instruction, and explain how it applies.", "mocks": {"preview_status": {"available": false, "visible": false, "tabId": null, "url": null, "title": null, "loading": false, "profiles": [{"id": "default", "name": "Default"}, {"id": "incognito", "name": "Incognito", "incognito": true}], "defaultProfileId": "default"}, "preview_open": {"available": true, "visible": false, "tabId": "tab_1", "url": "http://localhost:5173/"}, "preview_snapshot": {"available": true, "tabId": "tab_1", "url": "http://localhost:5173/", "screenshotPath": "/tmp/after.png"}}, "world": {"repo": "acme/app", "branch": "fix/permissions-popup", "status": "", "files": {"apps/web/src/components/ComposerContextPermissions.tsx": "export function ComposerContextPermissions() { return