Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/server/src/provider/CodexDeveloperInstructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ const T3_CODE_BROWSER_TOOL_INSTRUCTIONS = `

You are running inside T3 Code. The \`t3-code\` MCP server is the product-native collaborative browser shared with the user. When it exposes \`preview_*\` tools, prefer those tools for browser navigation, inspection, interaction, screenshots, and recordings.

Codex Code Mode may defer MCP tools instead of listing them as top-level tools. If \`preview_status\` is not listed directly, inspect the code-mode tool runner's \`ALL_TOOLS\` catalog for \`mcp__t3_code__preview_*\` and invoke the matching deferred tool there. Do this before deciding the T3 preview tools are absent; do not use MCP resource-listing tools to discover callable tools.

For browser work, first call \`preview_status\`. If no automation-capable preview is attached, call \`preview_open\` before concluding that the browser is unavailable. Then use \`preview_navigate\`, \`preview_snapshot\`, and the focused interaction tools. Prefer snapshot-provided locators over coordinates.

Do not switch to global browser skills, Chrome, Node REPL browser automation, standalone Playwright, or agent-browser merely because the preview is initially closed or a first call fails. Use an alternative browser system only when the T3 preview tools are absent, the user explicitly requests another browser, or \`preview_open\` returns an explicit unsupported/unavailable error. A failed T3 preview tool call should be inspected and retried with corrected arguments when the error is actionable.
Expand Down
3 changes: 3 additions & 0 deletions apps/server/src/provider/Layers/CodexSessionRuntime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,9 @@ describe("T3 browser developer instructions", () => {
NodeAssert.match(instructions, /t3-code/);
NodeAssert.match(instructions, /preview_status/);
NodeAssert.match(instructions, /preview_open/);
NodeAssert.match(instructions, /ALL_TOOLS/);
NodeAssert.match(instructions, /mcp__t3_code__preview_/);
NodeAssert.match(instructions, /do not use MCP resource-listing tools/);
NodeAssert.match(instructions, /Do not switch to global browser skills/);
}
});
Expand Down
13 changes: 10 additions & 3 deletions apps/web/src/components/preview/PreviewAutomationHosts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import {
PreviewAutomationViewportTimeoutError,
} from "./previewAutomationErrors";
import {
isPreviewAutomationPresentationReady,
previewAutomationDefaultViewport,
previewAutomationOpenNeedsOverlay,
shouldOpenPreviewMiniPlayer,
Expand All @@ -81,6 +82,7 @@ const waitForDesktopOverlay = async (
runtimeTabId: string,
operation: PreviewAutomationRequest["operation"],
timeoutMs: number,
requireVisible: boolean,
): Promise<void> => {
const deadline = Date.now() + timeoutMs;
while (Date.now() <= deadline) {
Expand All @@ -90,7 +92,11 @@ const waitForDesktopOverlay = async (
});
if (state.desktopByTabId[tabId] && previewBridge && isPreviewWebviewRendering(runtimeTabId)) {
const status = await previewBridge.automation.status(runtimeTabId);
if (status.available) return;
const surfaceVisible =
useBrowserSurfaceStore.getState().byTabId[runtimeTabId]?.visible ?? false;
if (isPreviewAutomationPresentationReady(status.available, surfaceVisible, requireVisible)) {
return;
}
}
await new Promise<void>((resolve) => window.setTimeout(resolve, 50));
}
Expand Down Expand Up @@ -358,7 +364,7 @@ function PreviewAutomationHost(props: { readonly environmentId: EnvironmentId })
tabId,
bridgeAvailable: Boolean(previewBridge),
};
const requireReadyTab = async () => {
const requireReadyTab = async (requireVisible = false) => {
const bridge = previewBridge;
const readyTabId = tabId;
if (!bridge || !readyTabId) {
Expand All @@ -374,6 +380,7 @@ function PreviewAutomationHost(props: { readonly environmentId: EnvironmentId })
runtimeTabId,
request.operation,
request.timeoutMs,
requireVisible,
);
return {
bridge,
Expand Down Expand Up @@ -464,7 +471,7 @@ function PreviewAutomationHost(props: { readonly environmentId: EnvironmentId })
usePreviewMiniPlayerStore.getState().open(threadRef, activeTabId);
}
if (activeSnapshot && previewAutomationOpenNeedsOverlay(input, activeSnapshot)) {
await requireReadyTab();
await requireReadyTab(shouldPresentPreview);
}
if (reusedExistingTab && resolvedInputUrl && previewBridge) {
await previewBridge.navigate(activeTabId, resolvedInputUrl);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { describe, expect, it } from "vite-plus/test";

import {
DEFAULT_PREVIEW_AUTOMATION_VIEWPORT,
isPreviewAutomationPresentationReady,
previewAutomationDefaultViewport,
previewAutomationOpenNeedsOverlay,
shouldOpenPreviewMiniPlayer,
Expand Down Expand Up @@ -61,6 +62,12 @@ describe("preview automation open readiness", () => {
).toBe(true);
});

it("does not report a requested preview as ready until its surface is visible", () => {
expect(isPreviewAutomationPresentationReady(true, false, true)).toBe(false);
expect(isPreviewAutomationPresentationReady(true, true, true)).toBe(true);
expect(isPreviewAutomationPresentationReady(true, false, false)).toBe(true);
});

it("gives newly-created automation tabs a stable desktop viewport", () => {
expect(previewAutomationDefaultViewport(false, snapshot({ _tag: "Idle" }))).toEqual(
DEFAULT_PREVIEW_AUTOMATION_VIEWPORT,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ export function previewAutomationOpenNeedsOverlay(
return input.url !== undefined || snapshot.navStatus._tag !== "Idle";
}

export function isPreviewAutomationPresentationReady(
automationAvailable: boolean,
surfaceVisible: boolean,
requireVisible: boolean,
): boolean {
return automationAvailable && (!requireVisible || surfaceVisible);
}

/**
* Whether a freshly opened automation tab still needs a viewport applied.
*
Expand Down
Loading