authorgravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-09-03 18:50:04-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-03 18:50:04-04:00
log12e8997e58dbca8f1bd8c63b67d662eb69cf0e0d
tree6182307e53a8d98364046ea754ba70bb33ac7003
parentc726c30a148c2add6a3ec7f31f54ae48dc5d2f0c
signature Signed by PGP key B5690EEEBB952194

fix(web): keep agent browser preview visible (#9484)

Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com>

19 files changed, 228 insertions(+), 38 deletions(-)

apps/web/src/browser/BrowserSurfaceSlot.tsx+7-3
......@@ -8,6 +8,7 @@ export function BrowserSurfaceSlot(props: {
88 readonly tabId: string;
99 readonly visible: boolean;
1010 readonly cornerRadius?: number;
11 readonly zIndex?: number;
1112 readonly layoutVersion?: string | number;
1213 readonly className?: string;
1314 readonly fitSourceContent?: boolean;
......@@ -16,12 +17,13 @@ export function BrowserSurfaceSlot(props: {
1617 tabId,
1718 visible,
1819 cornerRadius = 0,
20 zIndex = 30,
1921 layoutVersion,
2022 className,
2123 fitSourceContent = false,
2224 } = props;
2325 const elementRef = useRef<HTMLDivElement | null>(null);
24 const presentationRef = useRef({ visible, cornerRadius });
26 const presentationRef = useRef({ visible, cornerRadius, zIndex });
2527 const updateRef = useRef<(() => void) | null>(null);
2628
2729 useLayoutEffect(() => {
......@@ -40,6 +42,7 @@ export function BrowserSurfaceSlot(props: {
4042 },
4143 presentation.visible && rect.width > 0 && rect.height > 0,
4244 presentation.cornerRadius,
45 presentation.zIndex,
4346 );
4447 if (presentation.visible && !presented) {
4548 lease.release();
......@@ -53,6 +56,7 @@ export function BrowserSurfaceSlot(props: {
5356 },
5457 rect.width > 0 && rect.height > 0,
5558 presentation.cornerRadius,
59 presentation.zIndex,
5660 );
5761 }
5862 };
......@@ -72,9 +76,9 @@ export function BrowserSurfaceSlot(props: {
7276 }, [fitSourceContent, tabId]);
7377
7478 useLayoutEffect(() => {
75 presentationRef.current = { visible, cornerRadius };
79 presentationRef.current = { visible, cornerRadius, zIndex };
7680 updateRef.current?.();
77 }, [cornerRadius, layoutVersion, visible]);
81 }, [cornerRadius, layoutVersion, visible, zIndex]);
7882
7983 return <div ref={elementRef} className={className} data-browser-surface-slot={tabId} />;
8084}
apps/web/src/browser/HostedBrowserWebview.tsx+2
......@@ -83,6 +83,7 @@ export function HostedBrowserWebview(props: {
8383 fittedSourceContent: current?.fittedSourceContent ?? null,
8484 rect: resolveBrowserSurfacePanelRect(state.byTabId, runtimeTabId),
8585 visible: current?.visible ?? false,
86 zIndex: current?.zIndex ?? 30,
8687 };
8788 }),
8889 );
......@@ -259,6 +260,7 @@ export function HostedBrowserWebview(props: {
259260 // suspend them, and automation continues to see the macOS guests as inactive.
260261 keepPaintableWhenInactive: isMacPlatform(navigator.platform),
261262 cornerRadius: presentation.cornerRadius,
263 zIndex: presentation.zIndex,
262264 rect: lastRect,
263265 hiddenSize,
264266 });
apps/web/src/browser/browserSurfaceStore.test.ts+13
......@@ -107,6 +107,7 @@ describe("browserSurfaceStore", () => {
107107 hidden: {
108108 rect: staleRect,
109109 visible: false,
110 zIndex: 30,
110111 content: null,
111112 fittedSourceContent: null,
112113 fitSourceContent: false,
......@@ -117,6 +118,7 @@ describe("browserSurfaceStore", () => {
117118 active: {
118119 rect: liveRect,
119120 visible: true,
121 zIndex: 30,
120122 content: null,
121123 fittedSourceContent: null,
122124 fitSourceContent: false,
......@@ -162,6 +164,17 @@ describe("browserSurfaceStore", () => {
162164 });
163165 });
164166
167 it("keeps the requested layer with the active surface lease", () => {
168 const tabId = "layered-browser-surface";
169 const lease = acquireBrowserSurface(tabId);
170 lease.present({ x: 10, y: 20, width: 320, height: 200 }, true, 12, 48);
171
172 expect(useBrowserSurfaceStore.getState().byTabId[tabId]).toMatchObject({
173 visible: true,
174 zIndex: 48,
175 });
176 });
177
165178 it("clears fitted presentation state when its lease is released", () => {
166179 const tabId = "released-fitted-browser-surface";
167180 const fittedLease = acquireBrowserSurface(tabId, true);
apps/web/src/browser/browserSurfaceStore.ts+15-5
......@@ -10,6 +10,7 @@ export interface BrowserSurfaceRect {
1010export interface BrowserSurfacePresentation {
1111 readonly rect: BrowserSurfaceRect | null;
1212 readonly visible: boolean;
13 readonly zIndex: number;
1314 readonly content: BrowserSurfaceContentPresentation | null;
1415 readonly fittedSourceContent: BrowserSurfaceContentPresentation | null;
1516 readonly fitSourceContent: boolean;
......@@ -39,13 +40,19 @@ interface BrowserSurfaceStoreState {
3940 rect: BrowserSurfaceRect,
4041 visible: boolean,
4142 cornerRadius: number,
43 zIndex: number,
4244 ) => void;
4345 readonly presentContent: (tabId: string, content: BrowserSurfaceContentPresentation) => void;
4446 readonly release: (tabId: string, owner: symbol) => void;
4547}
4648
4749export interface BrowserSurfaceLease {
48 readonly present: (rect: BrowserSurfaceRect, visible: boolean, cornerRadius?: number) => boolean;
50 readonly present: (
51 rect: BrowserSurfaceRect,
52 visible: boolean,
53 cornerRadius?: number,
54 zIndex?: number,
55 ) => boolean;
4956 readonly release: () => void;
5057}
5158
......@@ -97,6 +104,7 @@ export const useBrowserSurfaceStore = create<BrowserSurfaceStoreState>()((set) =
97104 [tabId]: {
98105 rect: current?.rect ?? null,
99106 visible: false,
107 zIndex: current?.zIndex ?? 30,
100108 content: current?.content ?? null,
101109 fittedSourceContent: fitSourceContent ? (current?.content ?? null) : null,
102110 fitSourceContent,
......@@ -107,7 +115,7 @@ export const useBrowserSurfaceStore = create<BrowserSurfaceStoreState>()((set) =
107115 },
108116 };
109117 }),
110 present: (tabId, owner, rect, visible, cornerRadius) =>
118 present: (tabId, owner, rect, visible, cornerRadius, zIndex) =>
111119 set((state) => {
112120 const current = state.byTabId[tabId];
113121 if (current?.owner !== owner) return state;
......@@ -115,6 +123,7 @@ export const useBrowserSurfaceStore = create<BrowserSurfaceStoreState>()((set) =
115123 current &&
116124 current.visible === visible &&
117125 current.cornerRadius === cornerRadius &&
126 current.zIndex === zIndex &&
118127 rectEquals(current.rect, rect)
119128 ) {
120129 return state;
......@@ -122,7 +131,7 @@ export const useBrowserSurfaceStore = create<BrowserSurfaceStoreState>()((set) =
122131 return {
123132 byTabId: {
124133 ...state.byTabId,
125 [tabId]: { ...current, rect, visible, cornerRadius, updatedAt: Date.now() },
134 [tabId]: { ...current, rect, visible, cornerRadius, zIndex, updatedAt: Date.now() },
126135 },
127136 };
128137 }),
......@@ -136,6 +145,7 @@ export const useBrowserSurfaceStore = create<BrowserSurfaceStoreState>()((set) =
136145 [tabId]: {
137146 rect: null,
138147 visible: false,
148 zIndex: 30,
139149 content,
140150 fittedSourceContent: null,
141151 fitSourceContent: false,
......@@ -206,10 +216,10 @@ export function acquireBrowserSurface(
206216 useBrowserSurfaceStore.getState().claim(tabId, owner, fitSourceContent);
207217
208218 return {
209 present: (rect, visible, cornerRadius = 0) => {
219 present: (rect, visible, cornerRadius = 0, zIndex = 30) => {
210220 if (released) return false;
211221 if (useBrowserSurfaceStore.getState().byTabId[tabId]?.owner !== owner) return false;
212 useBrowserSurfaceStore.getState().present(tabId, owner, rect, visible, cornerRadius);
222 useBrowserSurfaceStore.getState().present(tabId, owner, rect, visible, cornerRadius, zIndex);
213223 return true;
214224 },
215225 release: () => {
apps/web/src/browser/hostedBrowserWebviewStyle.test.ts+2
......@@ -30,6 +30,7 @@ describe("resolveHostedBrowserWebviewWrapperStyle", () => {
3030 active: true,
3131 renderingActive: true,
3232 cornerRadius: 12,
33 zIndex: 48,
3334 rect: { x: 12, y: 34, width: 360, height: 203 },
3435 hiddenSize: { width: 1280, height: 800 },
3536 }),
......@@ -39,6 +40,7 @@ describe("resolveHostedBrowserWebviewWrapperStyle", () => {
3940 width: 360,
4041 height: 203,
4142 borderRadius: 12,
43 zIndex: 48,
4244 });
4345 });
4446
apps/web/src/browser/hostedBrowserWebviewStyle.ts+3-1
......@@ -23,6 +23,7 @@ export function resolveHostedBrowserWebviewWrapperStyle(input: {
2323 readonly renderingActive: boolean;
2424 readonly keepPaintableWhenInactive?: boolean;
2525 readonly cornerRadius?: number;
26 readonly zIndex?: number;
2627 readonly rect: BrowserSurfaceRect | null;
2728 readonly hiddenSize: HostedBrowserWebviewSize;
2829}): HostedBrowserWebviewWrapperStyle {
......@@ -33,6 +34,7 @@ export function resolveHostedBrowserWebviewWrapperStyle(input: {
3334 keepPaintableWhenInactive = false,
3435 rect,
3536 renderingActive,
37 zIndex = 30,
3638 } = input;
3739 if (active && rect) {
3840 return {
......@@ -40,7 +42,7 @@ export function resolveHostedBrowserWebviewWrapperStyle(input: {
4042 top: rect.y,
4143 width: rect.width,
4244 height: rect.height,
43 zIndex: 30,
45 zIndex,
4446 pointerEvents: "auto",
4547 ...(cornerRadius > 0 ? { borderRadius: cornerRadius } : {}),
4648 };
apps/web/src/components/ChatView.logic.test.ts+22
......@@ -50,6 +50,7 @@ import {
5050 shouldReleaseTimelineAnchorForToolActivity,
5151 shouldOpenProactivePullRequest,
5252 shouldOpenProactiveTurnDiff,
53 shouldRenderPreviewMiniPlayer,
5354 shouldShowBranchMismatchBanner,
5455 shouldShowPlanFollowUpPrompt,
5556 shouldWriteThreadErrorToCurrentServerThread,
......@@ -93,6 +94,27 @@ describe("agent browser close confirmation", () => {
9394 });
9495});
9596
97describe("floating browser preview", () => {
98 it("only hides the duplicate while the same browser is rendered in the panel", () => {
99 expect(shouldRenderPreviewMiniPlayer(null, null)).toBe(false);
100 expect(
101 shouldRenderPreviewMiniPlayer("tab-1", {
102 id: "browser:one",
103 kind: "preview",
104 resourceId: "tab-1",
105 }),
106 ).toBe(false);
107 expect(
108 shouldRenderPreviewMiniPlayer("tab-1", {
109 id: "browser:two",
110 kind: "preview",
111 resourceId: "tab-2",
112 }),
113 ).toBe(true);
114 expect(shouldRenderPreviewMiniPlayer("tab-1", { id: "diff", kind: "diff" })).toBe(true);
115 });
116});
117
96118describe("proactive panels", () => {
97119 it("opens a pull request only after a newly observed link appears", () => {
98120 expect(shouldOpenProactivePullRequest(undefined, "project:repo:42")).toBe(false);
apps/web/src/components/ChatView.logic.ts+13
......@@ -86,6 +86,19 @@ export function agentControlledBrowserCloseConfirmation(
8686 ].join("\n");
8787}
8888
89export function shouldRenderPreviewMiniPlayer(
90 miniPlayerTabId: string | null,
91 renderedRightPanelSurface: RightPanelSurface | null,
92): boolean {
93 return (
94 miniPlayerTabId !== null &&
95 !(
96 renderedRightPanelSurface?.kind === "preview" &&
97 renderedRightPanelSurface.resourceId === miniPlayerTabId
98 )
99 );
100}
101
89102export function shouldOpenProactivePullRequest(
90103 previousTargetKey: string | null | undefined,
91104 targetKey: string | null,
apps/web/src/components/ChatView.tsx+9-13
......@@ -366,6 +366,7 @@ import {
366366 shouldShowPlanFollowUpPrompt,
367367 shouldOpenProactivePullRequest,
368368 shouldOpenProactiveTurnDiff,
369 shouldRenderPreviewMiniPlayer,
369370 getStartedThreadModelChangeBlockReason,
370371 LAST_INVOKED_SCRIPT_BY_PROJECT_KEY,
371372 LastInvokedScriptByProjectSchema,
......@@ -1847,6 +1848,10 @@ function ChatViewContent(props: ChatViewProps) {
18471848 rightPanelPresent && (!shouldUseRightPanelSheet || rightPanelOpen);
18481849 const renderedRightPanelSurface = rightPanelPresence.value?.activeSurface ?? null;
18491850 const renderedRightPanelSurfaces = rightPanelPresence.value?.surfaces ?? [];
1851 const previewMiniPlayerVisible = shouldRenderPreviewMiniPlayer(
1852 activePreviewMiniPlayer?.tabId ?? null,
1853 renderedRightPanelSurface,
1854 );
18501855 const canMaximizeRightPanel = rightPanelOpen && !shouldUseRightPanelSheet;
18511856 const rightPanelMaximized =
18521857 canMaximizeRightPanel && maximizedRightPanelThreadKey === routeThreadKey;
......@@ -1862,20 +1867,10 @@ function ChatViewContent(props: ChatViewProps) {
18621867 useEffect(() => {
18631868 if (!activeThreadRef || !activePreviewMiniPlayer) return;
18641869 const miniTabStillExists = Boolean(activePreviewState.sessions[activePreviewMiniPlayer.tabId]);
1865 const sameTabOpenInPanel =
1866 previewPanelOpen &&
1867 activeRightPanelSurface?.kind === "preview" &&
1868 activeRightPanelSurface.resourceId === activePreviewMiniPlayer.tabId;
1869 if (!miniTabStillExists || sameTabOpenInPanel) {
1870 if (!miniTabStillExists) {
18701871 usePreviewMiniPlayerStore.getState().close(activeThreadRef);
18711872 }
1872 }, [
1873 activePreviewMiniPlayer,
1874 activePreviewState.sessions,
1875 activeRightPanelSurface,
1876 activeThreadRef,
1877 previewPanelOpen,
1878 ]);
1873 }, [activePreviewMiniPlayer, activePreviewState.sessions, activeThreadRef]);
18791874
18801875 const existingOpenTerminalThreadKeys = useMemo(() => {
18811876 const existingThreadKeys = new Set<string>([...serverThreadKeys, ...draftThreadKeys]);
......@@ -7919,7 +7914,7 @@ function ChatViewContent(props: ChatViewProps) {
79197914 </div>
79207915 </div>
79217916
7922 {activeThreadRef && activePreviewMiniPlayer ? (
7917 {activeThreadRef && activePreviewMiniPlayer && previewMiniPlayerVisible ? (
79237918 <ThreadPreviewMiniPlayer
79247919 key={`${activeThreadKey}:${activePreviewMiniPlayer.tabId}`}
79257920 threadRef={activeThreadRef}
......@@ -8040,6 +8035,7 @@ function ChatViewContent(props: ChatViewProps) {
80408035 <RightPanelSheet
80418036 animationDurationMs={panelAnimationsActive ? panelAnimationDurationMs : 0}
80428037 open={rightPanelOpen}
8038 underFloatingPreview={previewMiniPlayerVisible}
80438039 onClose={closePreviewPanel}
80448040 >
80458041 <RightPanelTabs
apps/web/src/components/RightPanelSheet.tsx+11-1
......@@ -1,12 +1,16 @@
11import { type ReactNode } from "react";
22
3import { RIGHT_PANEL_SHEET_CLASS_NAME } from "../rightPanelLayout";
3import {
4 RIGHT_PANEL_SHEET_CLASS_NAME,
5 RIGHT_PANEL_SHEET_LAYER_CLASS_NAME,
6} from "../rightPanelLayout";
47import { Sheet, SheetPopup } from "./ui/sheet";
58
69export function RightPanelSheet(props: {
710 animationDurationMs: number;
811 children: ReactNode;
912 open: boolean;
13 underFloatingPreview?: boolean;
1014 onClose: () => void;
1115}) {
1216 return (
......@@ -23,6 +27,12 @@ export function RightPanelSheet(props: {
2327 side="right"
2428 showCloseButton={false}
2529 keepMounted
30 {...(props.underFloatingPreview
31 ? {
32 backdropClassName: RIGHT_PANEL_SHEET_LAYER_CLASS_NAME,
33 viewportClassName: RIGHT_PANEL_SHEET_LAYER_CLASS_NAME,
34 }
35 : {})}
2636 className={RIGHT_PANEL_SHEET_CLASS_NAME}
2737 >
2838 {props.children}
apps/web/src/components/preview/PreviewAutomationHosts.tsx+46-2
......@@ -20,7 +20,7 @@ import {
2020 type ScopedThreadRef,
2121} from "@t3tools/contracts";
2222import { resolvePreviewViewport } from "@t3tools/shared/previewViewport";
23import { useCallback, useContext, useEffect, useMemo, useState } from "react";
23import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
2424import { Atom } from "effect/unstable/reactivity";
2525
2626import {
......@@ -29,7 +29,7 @@ import {
2929 reconcilePreviewServerSessions,
3030 updatePreviewServerSnapshot,
3131} from "~/previewStateStore";
32import { usePreviewMiniPlayerStore } from "~/previewMiniPlayerStore";
32import { selectThreadPreviewMiniPlayer, usePreviewMiniPlayerStore } from "~/previewMiniPlayerStore";
3333import { resolveBrowserNavigationTarget } from "~/browser/browserTargetResolver";
3434import {
3535 readActiveBrowserRecordingTargets,
......@@ -59,8 +59,10 @@ import {
5959 PreviewAutomationViewportTimeoutError,
6060} from "./previewAutomationErrors";
6161import {
62 explicitlySuppressesPreviewMiniPlayer,
6263 previewAutomationDefaultViewport,
6364 previewAutomationOpenNeedsOverlay,
65 shouldAutoShowPreviewForAutomationUse,
6466 shouldOpenPreviewMiniPlayer,
6567} from "./previewAutomationOpenReadiness";
6668import {
......@@ -311,6 +313,7 @@ function PreviewAutomationHost(props: { readonly environmentId: EnvironmentId })
311313 );
312314 const [automationConnectionAtom] = useState(() => Atom.make<string | null>(null));
313315 const automationConnectionId = useAtomValue(automationConnectionAtom);
316 const presentationSuppressedRuntimeTabsRef = useRef(new Map<string, Set<string>>());
314317
315318 const handleRequest = useCallback(
316319 async (request: PreviewAutomationRequest): Promise<unknown> => {
......@@ -353,6 +356,21 @@ function PreviewAutomationHost(props: { readonly environmentId: EnvironmentId })
353356 }
354357 const readyState = readThreadPreviewState(threadRef);
355358 const runtimeTabId = previewRuntimeTabId(threadRef, readyState.serverEpoch, readyTabId);
359 if (request.operation !== "open") {
360 const { autoShowFloatingPreview } = await resolveBrowserDefaults();
361 if (
362 shouldAutoShowPreviewForAutomationUse({
363 operation: request.operation,
364 autoShowFloatingPreview,
365 presentationSuppressed:
366 presentationSuppressedRuntimeTabsRef.current
367 .get(request.threadId)
368 ?.has(runtimeTabId) ?? false,
369 })
370 ) {
371 usePreviewMiniPlayerStore.getState().open(threadRef, readyTabId);
372 }
373 }
356374 browserActivity.release ??= acquireBrowserSurfaceActivity(runtimeTabId);
357375 await waitForDesktopOverlay(
358376 threadRef,
......@@ -450,6 +468,32 @@ function PreviewAutomationHost(props: { readonly environmentId: EnvironmentId })
450468 input,
451469 (await resolveBrowserDefaults()).autoShowFloatingPreview,
452470 );
471 const explicitlySuppressed = explicitlySuppressesPreviewMiniPlayer(input);
472 const suppressedTabs = presentationSuppressedRuntimeTabsRef.current.get(
473 request.threadId,
474 );
475 if (explicitlySuppressed) {
476 if (suppressedTabs) {
477 suppressedTabs.add(activeRuntimeTabId);
478 } else {
479 presentationSuppressedRuntimeTabsRef.current.set(
480 request.threadId,
481 new Set([activeRuntimeTabId]),
482 );
483 }
484 const miniPlayer = selectThreadPreviewMiniPlayer(
485 usePreviewMiniPlayerStore.getState().byThreadKey,
486 threadRef,
487 );
488 if (miniPlayer?.tabId === activeTabId) {
489 usePreviewMiniPlayerStore.getState().close(threadRef);
490 }
491 } else if (shouldPresentPreview) {
492 suppressedTabs?.delete(activeRuntimeTabId);
493 if (suppressedTabs?.size === 0) {
494 presentationSuppressedRuntimeTabsRef.current.delete(request.threadId);
495 }
496 }
453497 if (shouldPresentPreview) {
454498 usePreviewMiniPlayerStore.getState().open(threadRef, activeTabId);
455499 }
apps/web/src/components/preview/ThreadPreviewMiniPlayer.tsx+7-5
......@@ -19,6 +19,7 @@ import {
1919 clampPreviewMiniPlayerSize,
2020 PREVIEW_MINI_PLAYER_DEFAULT_SIZE,
2121 PREVIEW_MINI_PLAYER_EDGE_GAP,
22 PREVIEW_MINI_PLAYER_WEBVIEW_Z_INDEX,
2223} from "./previewMiniPlayerLayout";
2324
2425interface DragState {
......@@ -243,7 +244,7 @@ export function ThreadPreviewMiniPlayer({ threadRef, tabId, bottomInset }: Props
243244 }
244245 }
245246 >
246 <div className="group pointer-events-auto absolute right-2 top-2 z-[34] size-3">
247 <div className="group pointer-events-auto absolute right-2 top-2 z-[49] size-3">
247248 <div
248249 aria-hidden="true"
249250 className="absolute right-0 top-0 size-2 rounded-full bg-foreground/25 shadow-sm ring-1 ring-background/70 transition-opacity group-hover:opacity-0 group-focus-within:opacity-0"
......@@ -316,11 +317,12 @@ export function ThreadPreviewMiniPlayer({ threadRef, tabId, bottomInset }: Props
316317 </div>
317318
318319 <div className="relative h-full min-h-0">
319 <div className="absolute inset-0 z-[29] rounded-xl bg-muted shadow-2xl/35" />
320 <div className="absolute inset-0 z-[47] rounded-xl bg-muted shadow-2xl/35" />
320321 <BrowserSurfaceSlot
321322 tabId={runtimeTabId}
322323 visible={Boolean(desktopOverlay?.hasWebContents)}
323324 cornerRadius={12}
325 zIndex={PREVIEW_MINI_PLAYER_WEBVIEW_Z_INDEX}
324326 fitSourceContent
325327 layoutVersion={
326328 position
......@@ -329,16 +331,16 @@ export function ThreadPreviewMiniPlayer({ threadRef, tabId, bottomInset }: Props
329331 }
330332 className="absolute inset-0"
331333 />
332 <div className="pointer-events-none absolute inset-0 z-[31] rounded-xl ring-1 ring-inset ring-border/80" />
334 <div className="pointer-events-none absolute inset-0 z-[49] rounded-xl ring-1 ring-inset ring-border/80" />
333335 {!desktopOverlay?.hasWebContents ? (
334 <div className="pointer-events-none absolute inset-0 z-[32] flex items-center justify-center rounded-xl bg-muted text-xs text-muted-foreground">
336 <div className="pointer-events-none absolute inset-0 z-[49] flex items-center justify-center rounded-xl bg-muted text-xs text-muted-foreground">
335337 Reconnecting preview…
336338 </div>
337339 ) : null}
338340 <button
339341 type="button"
340342 aria-label="Resize floating preview"
341 className="pointer-events-auto absolute bottom-0 right-0 z-[33] size-5 cursor-nwse-resize rounded-br-xl after:absolute after:bottom-1 after:right-1 after:size-2 after:border-b after:border-r after:border-foreground/45"
343 className="pointer-events-auto absolute bottom-0 right-0 z-[49] size-5 cursor-nwse-resize rounded-br-xl after:absolute after:bottom-1 after:right-1 after:size-2 after:border-b after:border-r after:border-foreground/45"
342344 onPointerDown={handleResizePointerDown}
343345 onPointerMove={handleResizePointerMove}
344346 onPointerUp={endResize}
apps/web/src/components/preview/previewAutomationOpenReadiness.test.ts+47
......@@ -3,8 +3,10 @@ import { describe, expect, it } from "vite-plus/test";
33
44import {
55 DEFAULT_PREVIEW_AUTOMATION_VIEWPORT,
6 explicitlySuppressesPreviewMiniPlayer,
67 previewAutomationDefaultViewport,
78 previewAutomationOpenNeedsOverlay,
9 shouldAutoShowPreviewForAutomationUse,
810 shouldOpenPreviewMiniPlayer,
911} from "./previewAutomationOpenReadiness";
1012
......@@ -91,4 +93,49 @@ describe("shouldOpenPreviewMiniPlayer with the floating-preview preference", ()
9193 expect(shouldOpenPreviewMiniPlayer({ open: false }, true)).toBe(false);
9294 expect(shouldOpenPreviewMiniPlayer({ show: true }, false)).toBe(true);
9395 });
96
97 it("distinguishes an explicit background request from a disabled preference", () => {
98 expect(explicitlySuppressesPreviewMiniPlayer({ open: false })).toBe(true);
99 expect(explicitlySuppressesPreviewMiniPlayer({ show: false })).toBe(true);
100 expect(explicitlySuppressesPreviewMiniPlayer({})).toBe(false);
101 });
102});
103
104describe("auto-show for existing automation tabs", () => {
105 it("records floating-preview intent whenever an agent uses the tab", () => {
106 expect(
107 shouldAutoShowPreviewForAutomationUse({
108 operation: "navigate",
109 autoShowFloatingPreview: true,
110 presentationSuppressed: false,
111 }),
112 ).toBe(true);
113 });
114
115 it("leaves explicit opens and background-only tabs alone", () => {
116 expect(
117 shouldAutoShowPreviewForAutomationUse({
118 operation: "open",
119 autoShowFloatingPreview: true,
120 presentationSuppressed: false,
121 }),
122 ).toBe(false);
123 expect(
124 shouldAutoShowPreviewForAutomationUse({
125 operation: "click",
126 autoShowFloatingPreview: true,
127 presentationSuppressed: true,
128 }),
129 ).toBe(false);
130 });
131
132 it("honours the auto-show preference for reused tabs", () => {
133 expect(
134 shouldAutoShowPreviewForAutomationUse({
135 operation: "snapshot",
136 autoShowFloatingPreview: false,
137 presentationSuppressed: false,
138 }),
139 ).toBe(false);
140 });
94141});
apps/web/src/components/preview/previewAutomationOpenReadiness.ts+15
......@@ -1,5 +1,6 @@
11import {
22 FILL_PREVIEW_VIEWPORT,
3 type PreviewAutomationOperation,
34 type PreviewAutomationOpenInput,
45 type PreviewSessionSnapshot,
56 type PreviewViewportSetting,
......@@ -29,6 +30,20 @@ export function shouldOpenPreviewMiniPlayer(
2930 return input.open ?? input.show ?? autoShowFloatingPreview;
3031}
3132
33export function shouldAutoShowPreviewForAutomationUse(input: {
34 readonly operation: PreviewAutomationOperation;
35 readonly autoShowFloatingPreview: boolean;
36 readonly presentationSuppressed: boolean;
37}): boolean {
38 return (
39 input.operation !== "open" && input.autoShowFloatingPreview && !input.presentationSuppressed
40 );
41}
42
43export function explicitlySuppressesPreviewMiniPlayer(input: PreviewAutomationOpenInput): boolean {
44 return (input.open ?? input.show) === false;
45}
46
3247export function previewAutomationOpenNeedsOverlay(
3348 input: PreviewAutomationOpenInput,
3449 snapshot: PreviewSessionSnapshot,
apps/web/src/components/preview/previewMiniPlayerLayout.ts+2
......@@ -1,6 +1,8 @@
11import type { PreviewMiniPlayerPosition, PreviewMiniPlayerSize } from "~/previewMiniPlayerStore";
22
33export const PREVIEW_MINI_PLAYER_EDGE_GAP = 12;
4// The mini-player shell straddles this webview at 47 and 49; dialogs begin at 50.
5export const PREVIEW_MINI_PLAYER_WEBVIEW_Z_INDEX = 48;
46export const PREVIEW_MINI_PLAYER_DEFAULT_SIZE = { width: 320, height: 200 } as const;
57export const PREVIEW_MINI_PLAYER_MIN_SIZE = { width: 240, height: 150 } as const;
68
apps/web/src/components/settings/IntegrationsSettings.tsx+1-1
......@@ -560,7 +560,7 @@ function BrowserAutoShowFloatingPreviewSetting({ disabled }: { readonly disabled
560560 return (
561561 <SettingsRow
562562 {...searchableSetting("browser-auto-show-floating-preview")}
563 description="Pop the floating preview into view when an agent opens a browser. An agent that explicitly asks to show or hide its preview still gets what it asked for."
563 description="Pop the floating preview into view when an agent uses a browser. An agent that explicitly asks to show or hide its preview still gets what it asked for."
564564 resetAction={
565565 !disabled && autoShow !== DEFAULT_BROWSER_AUTO_SHOW_FLOATING_PREVIEW ? (
566566 <SettingResetButton
apps/web/src/components/ui/sheet.tsx+10-6
......@@ -64,6 +64,8 @@ function SheetPopup({
6464 showCloseButton = true,
6565 keepMounted = false,
6666 transitionDurationMs,
67 backdropClassName,
68 viewportClassName,
6769 side = "right",
6870 variant = "default",
6971 style,
......@@ -72,6 +74,8 @@ function SheetPopup({
7274 showCloseButton?: boolean;
7375 keepMounted?: boolean;
7476 transitionDurationMs?: number;
77 backdropClassName?: string;
78 viewportClassName?: string;
7579 side?: "right" | "left" | "top" | "bottom";
7680 variant?: "default" | "inset";
7781}) {
......@@ -84,14 +88,14 @@ function SheetPopup({
8488 return (
8589 <SheetPortal keepMounted={keepMounted}>
8690 <SheetBackdrop
87 className={
88 instant
89 ? "transition-none! data-ending-style:opacity-100! data-starting-style:opacity-100!"
90 : undefined
91 }
91 className={cn(
92 instant &&
93 "transition-none! data-ending-style:opacity-100! data-starting-style:opacity-100!",
94 backdropClassName,
95 )}
9296 style={transitionStyle}
9397 />
94 <SheetViewport side={side} variant={variant}>
98 <SheetViewport className={viewportClassName} side={side} variant={variant}>
9599 <SheetPrimitive.Popup
96100 className={cn(
97101 "relative flex max-h-full min-h-0 w-full min-w-0 flex-col bg-popover not-dark:bg-clip-padding text-popover-foreground shadow-lg/5 transition-[opacity,translate] duration-200 ease-in-out will-change-transform before:pointer-events-none before:absolute before:inset-0 before:shadow-[0_1px_--theme(--color-black/4%)] data-ending-style:opacity-0 data-starting-style:opacity-0 max-sm:before:hidden dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
apps/web/src/rightPanelLayout.ts+2
......@@ -1,3 +1,5 @@
11export const RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY = "(max-width: 980px)";
2// Applied only while a floating preview overlaps the compact sheet.
3export const RIGHT_PANEL_SHEET_LAYER_CLASS_NAME = "z-[35]";
24export const RIGHT_PANEL_SHEET_CLASS_NAME =
35 "w-[min(42vw,28rem)] min-w-80 max-w-[28rem] p-0 max-[760px]:w-[min(88vw,24rem)] max-[760px]:min-w-0 wco:mt-[env(titlebar-area-height)] wco:h-[calc(100%-env(titlebar-area-height))] wco:max-h-[calc(100%-env(titlebar-area-height))]";
packages/contracts/src/settings.ts+1-1
......@@ -226,7 +226,7 @@ export const ClientSettingsSchema = Schema.Struct({
226226 Schema.withDecodingDefault(Effect.succeed(DEFAULT_BROWSER_LINK_TARGET)),
227227 ),
228228 /**
229 * Whether an agent opening a preview pops the floating mini player into
229 * Whether an agent using a preview pops the floating mini player into
230230 * view. Only applies when the agent didn't ask either way — an explicit
231231 * `open`/`show` on `preview_open` still wins, since that is the agent
232232 * deliberately showing or hiding its work.