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: {...@@ -8,6 +8,7 @@ export function BrowserSurfaceSlot(props: {
8 readonly tabId: string;8 readonly tabId: string;
9 readonly visible: boolean;9 readonly visible: boolean;
10 readonly cornerRadius?: number;10 readonly cornerRadius?: number;
11 readonly zIndex?: number;
11 readonly layoutVersion?: string | number;12 readonly layoutVersion?: string | number;
12 readonly className?: string;13 readonly className?: string;
13 readonly fitSourceContent?: boolean;14 readonly fitSourceContent?: boolean;
...@@ -16,12 +17,13 @@ export function BrowserSurfaceSlot(props: {...@@ -16,12 +17,13 @@ export function BrowserSurfaceSlot(props: {
16 tabId,17 tabId,
17 visible,18 visible,
18 cornerRadius = 0,19 cornerRadius = 0,
20 zIndex = 30,
19 layoutVersion,21 layoutVersion,
20 className,22 className,
21 fitSourceContent = false,23 fitSourceContent = false,
22 } = props;24 } = props;
23 const elementRef = useRef<HTMLDivElement | null>(null);25 const elementRef = useRef<HTMLDivElement | null>(null);
24 const presentationRef = useRef({ visible, cornerRadius });26 const presentationRef = useRef({ visible, cornerRadius, zIndex });
25 const updateRef = useRef<(() => void) | null>(null);27 const updateRef = useRef<(() => void) | null>(null);
2628
27 useLayoutEffect(() => {29 useLayoutEffect(() => {
...@@ -40,6 +42,7 @@ export function BrowserSurfaceSlot(props: {...@@ -40,6 +42,7 @@ export function BrowserSurfaceSlot(props: {
40 },42 },
41 presentation.visible && rect.width > 0 && rect.height > 0,43 presentation.visible && rect.width > 0 && rect.height > 0,
42 presentation.cornerRadius,44 presentation.cornerRadius,
45 presentation.zIndex,
43 );46 );
44 if (presentation.visible && !presented) {47 if (presentation.visible && !presented) {
45 lease.release();48 lease.release();
...@@ -53,6 +56,7 @@ export function BrowserSurfaceSlot(props: {...@@ -53,6 +56,7 @@ export function BrowserSurfaceSlot(props: {
53 },56 },
54 rect.width > 0 && rect.height > 0,57 rect.width > 0 && rect.height > 0,
55 presentation.cornerRadius,58 presentation.cornerRadius,
59 presentation.zIndex,
56 );60 );
57 }61 }
58 };62 };
...@@ -72,9 +76,9 @@ export function BrowserSurfaceSlot(props: {...@@ -72,9 +76,9 @@ export function BrowserSurfaceSlot(props: {
72 }, [fitSourceContent, tabId]);76 }, [fitSourceContent, tabId]);
7377
74 useLayoutEffect(() => {78 useLayoutEffect(() => {
75 presentationRef.current = { visible, cornerRadius };79 presentationRef.current = { visible, cornerRadius, zIndex };
76 updateRef.current?.();80 updateRef.current?.();
77 }, [cornerRadius, layoutVersion, visible]);81 }, [cornerRadius, layoutVersion, visible, zIndex]);
7882
79 return <div ref={elementRef} className={className} data-browser-surface-slot={tabId} />;83 return <div ref={elementRef} className={className} data-browser-surface-slot={tabId} />;
80}84}
apps/web/src/browser/HostedBrowserWebview.tsx+2
...@@ -83,6 +83,7 @@ export function HostedBrowserWebview(props: {...@@ -83,6 +83,7 @@ export function HostedBrowserWebview(props: {
83 fittedSourceContent: current?.fittedSourceContent ?? null,83 fittedSourceContent: current?.fittedSourceContent ?? null,
84 rect: resolveBrowserSurfacePanelRect(state.byTabId, runtimeTabId),84 rect: resolveBrowserSurfacePanelRect(state.byTabId, runtimeTabId),
85 visible: current?.visible ?? false,85 visible: current?.visible ?? false,
86 zIndex: current?.zIndex ?? 30,
86 };87 };
87 }),88 }),
88 );89 );
...@@ -259,6 +260,7 @@ export function HostedBrowserWebview(props: {...@@ -259,6 +260,7 @@ export function HostedBrowserWebview(props: {
259 // suspend them, and automation continues to see the macOS guests as inactive.260 // suspend them, and automation continues to see the macOS guests as inactive.
260 keepPaintableWhenInactive: isMacPlatform(navigator.platform),261 keepPaintableWhenInactive: isMacPlatform(navigator.platform),
261 cornerRadius: presentation.cornerRadius,262 cornerRadius: presentation.cornerRadius,
263 zIndex: presentation.zIndex,
262 rect: lastRect,264 rect: lastRect,
263 hiddenSize,265 hiddenSize,
264 });266 });
apps/web/src/browser/browserSurfaceStore.test.ts+13
...@@ -107,6 +107,7 @@ describe("browserSurfaceStore", () => {...@@ -107,6 +107,7 @@ describe("browserSurfaceStore", () => {
107 hidden: {107 hidden: {
108 rect: staleRect,108 rect: staleRect,
109 visible: false,109 visible: false,
110 zIndex: 30,
110 content: null,111 content: null,
111 fittedSourceContent: null,112 fittedSourceContent: null,
112 fitSourceContent: false,113 fitSourceContent: false,
...@@ -117,6 +118,7 @@ describe("browserSurfaceStore", () => {...@@ -117,6 +118,7 @@ describe("browserSurfaceStore", () => {
117 active: {118 active: {
118 rect: liveRect,119 rect: liveRect,
119 visible: true,120 visible: true,
121 zIndex: 30,
120 content: null,122 content: null,
121 fittedSourceContent: null,123 fittedSourceContent: null,
122 fitSourceContent: false,124 fitSourceContent: false,
...@@ -162,6 +164,17 @@ describe("browserSurfaceStore", () => {...@@ -162,6 +164,17 @@ describe("browserSurfaceStore", () => {
162 });164 });
163 });165 });
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
165 it("clears fitted presentation state when its lease is released", () => {178 it("clears fitted presentation state when its lease is released", () => {
166 const tabId = "released-fitted-browser-surface";179 const tabId = "released-fitted-browser-surface";
167 const fittedLease = acquireBrowserSurface(tabId, true);180 const fittedLease = acquireBrowserSurface(tabId, true);
apps/web/src/browser/browserSurfaceStore.ts+15-5
...@@ -10,6 +10,7 @@ export interface BrowserSurfaceRect {...@@ -10,6 +10,7 @@ export interface BrowserSurfaceRect {
10export interface BrowserSurfacePresentation {10export interface BrowserSurfacePresentation {
11 readonly rect: BrowserSurfaceRect | null;11 readonly rect: BrowserSurfaceRect | null;
12 readonly visible: boolean;12 readonly visible: boolean;
13 readonly zIndex: number;
13 readonly content: BrowserSurfaceContentPresentation | null;14 readonly content: BrowserSurfaceContentPresentation | null;
14 readonly fittedSourceContent: BrowserSurfaceContentPresentation | null;15 readonly fittedSourceContent: BrowserSurfaceContentPresentation | null;
15 readonly fitSourceContent: boolean;16 readonly fitSourceContent: boolean;
...@@ -39,13 +40,19 @@ interface BrowserSurfaceStoreState {...@@ -39,13 +40,19 @@ interface BrowserSurfaceStoreState {
39 rect: BrowserSurfaceRect,40 rect: BrowserSurfaceRect,
40 visible: boolean,41 visible: boolean,
41 cornerRadius: number,42 cornerRadius: number,
43 zIndex: number,
42 ) => void;44 ) => void;
43 readonly presentContent: (tabId: string, content: BrowserSurfaceContentPresentation) => void;45 readonly presentContent: (tabId: string, content: BrowserSurfaceContentPresentation) => void;
44 readonly release: (tabId: string, owner: symbol) => void;46 readonly release: (tabId: string, owner: symbol) => void;
45}47}
4648
47export interface BrowserSurfaceLease {49export 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;
49 readonly release: () => void;56 readonly release: () => void;
50}57}
5158
...@@ -97,6 +104,7 @@ export const useBrowserSurfaceStore = create<BrowserSurfaceStoreState>()((set) =...@@ -97,6 +104,7 @@ export const useBrowserSurfaceStore = create<BrowserSurfaceStoreState>()((set) =
97 [tabId]: {104 [tabId]: {
98 rect: current?.rect ?? null,105 rect: current?.rect ?? null,
99 visible: false,106 visible: false,
107 zIndex: current?.zIndex ?? 30,
100 content: current?.content ?? null,108 content: current?.content ?? null,
101 fittedSourceContent: fitSourceContent ? (current?.content ?? null) : null,109 fittedSourceContent: fitSourceContent ? (current?.content ?? null) : null,
102 fitSourceContent,110 fitSourceContent,
...@@ -107,7 +115,7 @@ export const useBrowserSurfaceStore = create<BrowserSurfaceStoreState>()((set) =...@@ -107,7 +115,7 @@ export const useBrowserSurfaceStore = create<BrowserSurfaceStoreState>()((set) =
107 },115 },
108 };116 };
109 }),117 }),
110 present: (tabId, owner, rect, visible, cornerRadius) =>118 present: (tabId, owner, rect, visible, cornerRadius, zIndex) =>
111 set((state) => {119 set((state) => {
112 const current = state.byTabId[tabId];120 const current = state.byTabId[tabId];
113 if (current?.owner !== owner) return state;121 if (current?.owner !== owner) return state;
...@@ -115,6 +123,7 @@ export const useBrowserSurfaceStore = create<BrowserSurfaceStoreState>()((set) =...@@ -115,6 +123,7 @@ export const useBrowserSurfaceStore = create<BrowserSurfaceStoreState>()((set) =
115 current &&123 current &&
116 current.visible === visible &&124 current.visible === visible &&
117 current.cornerRadius === cornerRadius &&125 current.cornerRadius === cornerRadius &&
126 current.zIndex === zIndex &&
118 rectEquals(current.rect, rect)127 rectEquals(current.rect, rect)
119 ) {128 ) {
120 return state;129 return state;
...@@ -122,7 +131,7 @@ export const useBrowserSurfaceStore = create<BrowserSurfaceStoreState>()((set) =...@@ -122,7 +131,7 @@ export const useBrowserSurfaceStore = create<BrowserSurfaceStoreState>()((set) =
122 return {131 return {
123 byTabId: {132 byTabId: {
124 ...state.byTabId,133 ...state.byTabId,
125 [tabId]: { ...current, rect, visible, cornerRadius, updatedAt: Date.now() },134 [tabId]: { ...current, rect, visible, cornerRadius, zIndex, updatedAt: Date.now() },
126 },135 },
127 };136 };
128 }),137 }),
...@@ -136,6 +145,7 @@ export const useBrowserSurfaceStore = create<BrowserSurfaceStoreState>()((set) =...@@ -136,6 +145,7 @@ export const useBrowserSurfaceStore = create<BrowserSurfaceStoreState>()((set) =
136 [tabId]: {145 [tabId]: {
137 rect: null,146 rect: null,
138 visible: false,147 visible: false,
148 zIndex: 30,
139 content,149 content,
140 fittedSourceContent: null,150 fittedSourceContent: null,
141 fitSourceContent: false,151 fitSourceContent: false,
...@@ -206,10 +216,10 @@ export function acquireBrowserSurface(...@@ -206,10 +216,10 @@ export function acquireBrowserSurface(
206 useBrowserSurfaceStore.getState().claim(tabId, owner, fitSourceContent);216 useBrowserSurfaceStore.getState().claim(tabId, owner, fitSourceContent);
207217
208 return {218 return {
209 present: (rect, visible, cornerRadius = 0) => {219 present: (rect, visible, cornerRadius = 0, zIndex = 30) => {
210 if (released) return false;220 if (released) return false;
211 if (useBrowserSurfaceStore.getState().byTabId[tabId]?.owner !== owner) return false;221 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);
213 return true;223 return true;
214 },224 },
215 release: () => {225 release: () => {
apps/web/src/browser/hostedBrowserWebviewStyle.test.ts+2
...@@ -30,6 +30,7 @@ describe("resolveHostedBrowserWebviewWrapperStyle", () => {...@@ -30,6 +30,7 @@ describe("resolveHostedBrowserWebviewWrapperStyle", () => {
30 active: true,30 active: true,
31 renderingActive: true,31 renderingActive: true,
32 cornerRadius: 12,32 cornerRadius: 12,
33 zIndex: 48,
33 rect: { x: 12, y: 34, width: 360, height: 203 },34 rect: { x: 12, y: 34, width: 360, height: 203 },
34 hiddenSize: { width: 1280, height: 800 },35 hiddenSize: { width: 1280, height: 800 },
35 }),36 }),
...@@ -39,6 +40,7 @@ describe("resolveHostedBrowserWebviewWrapperStyle", () => {...@@ -39,6 +40,7 @@ describe("resolveHostedBrowserWebviewWrapperStyle", () => {
39 width: 360,40 width: 360,
40 height: 203,41 height: 203,
41 borderRadius: 12,42 borderRadius: 12,
43 zIndex: 48,
42 });44 });
43 });45 });
4446
apps/web/src/browser/hostedBrowserWebviewStyle.ts+3-1
...@@ -23,6 +23,7 @@ export function resolveHostedBrowserWebviewWrapperStyle(input: {...@@ -23,6 +23,7 @@ export function resolveHostedBrowserWebviewWrapperStyle(input: {
23 readonly renderingActive: boolean;23 readonly renderingActive: boolean;
24 readonly keepPaintableWhenInactive?: boolean;24 readonly keepPaintableWhenInactive?: boolean;
25 readonly cornerRadius?: number;25 readonly cornerRadius?: number;
26 readonly zIndex?: number;
26 readonly rect: BrowserSurfaceRect | null;27 readonly rect: BrowserSurfaceRect | null;
27 readonly hiddenSize: HostedBrowserWebviewSize;28 readonly hiddenSize: HostedBrowserWebviewSize;
28}): HostedBrowserWebviewWrapperStyle {29}): HostedBrowserWebviewWrapperStyle {
...@@ -33,6 +34,7 @@ export function resolveHostedBrowserWebviewWrapperStyle(input: {...@@ -33,6 +34,7 @@ export function resolveHostedBrowserWebviewWrapperStyle(input: {
33 keepPaintableWhenInactive = false,34 keepPaintableWhenInactive = false,
34 rect,35 rect,
35 renderingActive,36 renderingActive,
37 zIndex = 30,
36 } = input;38 } = input;
37 if (active && rect) {39 if (active && rect) {
38 return {40 return {
...@@ -40,7 +42,7 @@ export function resolveHostedBrowserWebviewWrapperStyle(input: {...@@ -40,7 +42,7 @@ export function resolveHostedBrowserWebviewWrapperStyle(input: {
40 top: rect.y,42 top: rect.y,
41 width: rect.width,43 width: rect.width,
42 height: rect.height,44 height: rect.height,
43 zIndex: 30,45 zIndex,
44 pointerEvents: "auto",46 pointerEvents: "auto",
45 ...(cornerRadius > 0 ? { borderRadius: cornerRadius } : {}),47 ...(cornerRadius > 0 ? { borderRadius: cornerRadius } : {}),
46 };48 };
apps/web/src/components/ChatView.logic.test.ts+22
...@@ -50,6 +50,7 @@ import {...@@ -50,6 +50,7 @@ import {
50 shouldReleaseTimelineAnchorForToolActivity,50 shouldReleaseTimelineAnchorForToolActivity,
51 shouldOpenProactivePullRequest,51 shouldOpenProactivePullRequest,
52 shouldOpenProactiveTurnDiff,52 shouldOpenProactiveTurnDiff,
53 shouldRenderPreviewMiniPlayer,
53 shouldShowBranchMismatchBanner,54 shouldShowBranchMismatchBanner,
54 shouldShowPlanFollowUpPrompt,55 shouldShowPlanFollowUpPrompt,
55 shouldWriteThreadErrorToCurrentServerThread,56 shouldWriteThreadErrorToCurrentServerThread,
...@@ -93,6 +94,27 @@ describe("agent browser close confirmation", () => {...@@ -93,6 +94,27 @@ describe("agent browser close confirmation", () => {
93 });94 });
94});95});
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
96describe("proactive panels", () => {118describe("proactive panels", () => {
97 it("opens a pull request only after a newly observed link appears", () => {119 it("opens a pull request only after a newly observed link appears", () => {
98 expect(shouldOpenProactivePullRequest(undefined, "project:repo:42")).toBe(false);120 expect(shouldOpenProactivePullRequest(undefined, "project:repo:42")).toBe(false);
apps/web/src/components/ChatView.logic.ts+13
...@@ -86,6 +86,19 @@ export function agentControlledBrowserCloseConfirmation(...@@ -86,6 +86,19 @@ export function agentControlledBrowserCloseConfirmation(
86 ].join("\n");86 ].join("\n");
87}87}
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
89export function shouldOpenProactivePullRequest(102export function shouldOpenProactivePullRequest(
90 previousTargetKey: string | null | undefined,103 previousTargetKey: string | null | undefined,
91 targetKey: string | null,104 targetKey: string | null,
apps/web/src/components/ChatView.tsx+9-13
...@@ -366,6 +366,7 @@ import {...@@ -366,6 +366,7 @@ import {
366 shouldShowPlanFollowUpPrompt,366 shouldShowPlanFollowUpPrompt,
367 shouldOpenProactivePullRequest,367 shouldOpenProactivePullRequest,
368 shouldOpenProactiveTurnDiff,368 shouldOpenProactiveTurnDiff,
369 shouldRenderPreviewMiniPlayer,
369 getStartedThreadModelChangeBlockReason,370 getStartedThreadModelChangeBlockReason,
370 LAST_INVOKED_SCRIPT_BY_PROJECT_KEY,371 LAST_INVOKED_SCRIPT_BY_PROJECT_KEY,
371 LastInvokedScriptByProjectSchema,372 LastInvokedScriptByProjectSchema,
...@@ -1847,6 +1848,10 @@ function ChatViewContent(props: ChatViewProps) {...@@ -1847,6 +1848,10 @@ function ChatViewContent(props: ChatViewProps) {
1847 rightPanelPresent && (!shouldUseRightPanelSheet || rightPanelOpen);1848 rightPanelPresent && (!shouldUseRightPanelSheet || rightPanelOpen);
1848 const renderedRightPanelSurface = rightPanelPresence.value?.activeSurface ?? null;1849 const renderedRightPanelSurface = rightPanelPresence.value?.activeSurface ?? null;
1849 const renderedRightPanelSurfaces = rightPanelPresence.value?.surfaces ?? [];1850 const renderedRightPanelSurfaces = rightPanelPresence.value?.surfaces ?? [];
1851 const previewMiniPlayerVisible = shouldRenderPreviewMiniPlayer(
1852 activePreviewMiniPlayer?.tabId ?? null,
1853 renderedRightPanelSurface,
1854 );
1850 const canMaximizeRightPanel = rightPanelOpen && !shouldUseRightPanelSheet;1855 const canMaximizeRightPanel = rightPanelOpen && !shouldUseRightPanelSheet;
1851 const rightPanelMaximized =1856 const rightPanelMaximized =
1852 canMaximizeRightPanel && maximizedRightPanelThreadKey === routeThreadKey;1857 canMaximizeRightPanel && maximizedRightPanelThreadKey === routeThreadKey;
...@@ -1862,20 +1867,10 @@ function ChatViewContent(props: ChatViewProps) {...@@ -1862,20 +1867,10 @@ function ChatViewContent(props: ChatViewProps) {
1862 useEffect(() => {1867 useEffect(() => {
1863 if (!activeThreadRef || !activePreviewMiniPlayer) return;1868 if (!activeThreadRef || !activePreviewMiniPlayer) return;
1864 const miniTabStillExists = Boolean(activePreviewState.sessions[activePreviewMiniPlayer.tabId]);1869 const miniTabStillExists = Boolean(activePreviewState.sessions[activePreviewMiniPlayer.tabId]);
1865 const sameTabOpenInPanel =1870 if (!miniTabStillExists) {
1866 previewPanelOpen &&
1867 activeRightPanelSurface?.kind === "preview" &&
1868 activeRightPanelSurface.resourceId === activePreviewMiniPlayer.tabId;
1869 if (!miniTabStillExists || sameTabOpenInPanel) {
1870 usePreviewMiniPlayerStore.getState().close(activeThreadRef);1871 usePreviewMiniPlayerStore.getState().close(activeThreadRef);
1871 }1872 }
1872 }, [1873 }, [activePreviewMiniPlayer, activePreviewState.sessions, activeThreadRef]);
1873 activePreviewMiniPlayer,
1874 activePreviewState.sessions,
1875 activeRightPanelSurface,
1876 activeThreadRef,
1877 previewPanelOpen,
1878 ]);
18791874
1880 const existingOpenTerminalThreadKeys = useMemo(() => {1875 const existingOpenTerminalThreadKeys = useMemo(() => {
1881 const existingThreadKeys = new Set<string>([...serverThreadKeys, ...draftThreadKeys]);1876 const existingThreadKeys = new Set<string>([...serverThreadKeys, ...draftThreadKeys]);
...@@ -7919,7 +7914,7 @@ function ChatViewContent(props: ChatViewProps) {...@@ -7919,7 +7914,7 @@ function ChatViewContent(props: ChatViewProps) {
7919 </div>7914 </div>
7920 </div>7915 </div>
79217916
7922 {activeThreadRef && activePreviewMiniPlayer ? (7917 {activeThreadRef && activePreviewMiniPlayer && previewMiniPlayerVisible ? (
7923 <ThreadPreviewMiniPlayer7918 <ThreadPreviewMiniPlayer
7924 key={`${activeThreadKey}:${activePreviewMiniPlayer.tabId}`}7919 key={`${activeThreadKey}:${activePreviewMiniPlayer.tabId}`}
7925 threadRef={activeThreadRef}7920 threadRef={activeThreadRef}
...@@ -8040,6 +8035,7 @@ function ChatViewContent(props: ChatViewProps) {...@@ -8040,6 +8035,7 @@ function ChatViewContent(props: ChatViewProps) {
8040 <RightPanelSheet8035 <RightPanelSheet
8041 animationDurationMs={panelAnimationsActive ? panelAnimationDurationMs : 0}8036 animationDurationMs={panelAnimationsActive ? panelAnimationDurationMs : 0}
8042 open={rightPanelOpen}8037 open={rightPanelOpen}
8038 underFloatingPreview={previewMiniPlayerVisible}
8043 onClose={closePreviewPanel}8039 onClose={closePreviewPanel}
8044 >8040 >
8045 <RightPanelTabs8041 <RightPanelTabs
apps/web/src/components/RightPanelSheet.tsx+11-1
...@@ -1,12 +1,16 @@...@@ -1,12 +1,16 @@
1import { type ReactNode } from "react";1import { 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";
4import { Sheet, SheetPopup } from "./ui/sheet";7import { Sheet, SheetPopup } from "./ui/sheet";
58
6export function RightPanelSheet(props: {9export function RightPanelSheet(props: {
7 animationDurationMs: number;10 animationDurationMs: number;
8 children: ReactNode;11 children: ReactNode;
9 open: boolean;12 open: boolean;
13 underFloatingPreview?: boolean;
10 onClose: () => void;14 onClose: () => void;
11}) {15}) {
12 return (16 return (
...@@ -23,6 +27,12 @@ export function RightPanelSheet(props: {...@@ -23,6 +27,12 @@ export function RightPanelSheet(props: {
23 side="right"27 side="right"
24 showCloseButton={false}28 showCloseButton={false}
25 keepMounted29 keepMounted
30 {...(props.underFloatingPreview
31 ? {
32 backdropClassName: RIGHT_PANEL_SHEET_LAYER_CLASS_NAME,
33 viewportClassName: RIGHT_PANEL_SHEET_LAYER_CLASS_NAME,
34 }
35 : {})}
26 className={RIGHT_PANEL_SHEET_CLASS_NAME}36 className={RIGHT_PANEL_SHEET_CLASS_NAME}
27 >37 >
28 {props.children}38 {props.children}
apps/web/src/components/preview/PreviewAutomationHosts.tsx+46-2
...@@ -20,7 +20,7 @@ import {...@@ -20,7 +20,7 @@ import {
20 type ScopedThreadRef,20 type ScopedThreadRef,
21} from "@t3tools/contracts";21} from "@t3tools/contracts";
22import { resolvePreviewViewport } from "@t3tools/shared/previewViewport";22import { resolvePreviewViewport } from "@t3tools/shared/previewViewport";
23import { useCallback, useContext, useEffect, useMemo, useState } from "react";23import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
24import { Atom } from "effect/unstable/reactivity";24import { Atom } from "effect/unstable/reactivity";
2525
26import {26import {
...@@ -29,7 +29,7 @@ import {...@@ -29,7 +29,7 @@ import {
29 reconcilePreviewServerSessions,29 reconcilePreviewServerSessions,
30 updatePreviewServerSnapshot,30 updatePreviewServerSnapshot,
31} from "~/previewStateStore";31} from "~/previewStateStore";
32import { usePreviewMiniPlayerStore } from "~/previewMiniPlayerStore";32import { selectThreadPreviewMiniPlayer, usePreviewMiniPlayerStore } from "~/previewMiniPlayerStore";
33import { resolveBrowserNavigationTarget } from "~/browser/browserTargetResolver";33import { resolveBrowserNavigationTarget } from "~/browser/browserTargetResolver";
34import {34import {
35 readActiveBrowserRecordingTargets,35 readActiveBrowserRecordingTargets,
...@@ -59,8 +59,10 @@ import {...@@ -59,8 +59,10 @@ import {
59 PreviewAutomationViewportTimeoutError,59 PreviewAutomationViewportTimeoutError,
60} from "./previewAutomationErrors";60} from "./previewAutomationErrors";
61import {61import {
62 explicitlySuppressesPreviewMiniPlayer,
62 previewAutomationDefaultViewport,63 previewAutomationDefaultViewport,
63 previewAutomationOpenNeedsOverlay,64 previewAutomationOpenNeedsOverlay,
65 shouldAutoShowPreviewForAutomationUse,
64 shouldOpenPreviewMiniPlayer,66 shouldOpenPreviewMiniPlayer,
65} from "./previewAutomationOpenReadiness";67} from "./previewAutomationOpenReadiness";
66import {68import {
...@@ -311,6 +313,7 @@ function PreviewAutomationHost(props: { readonly environmentId: EnvironmentId })...@@ -311,6 +313,7 @@ function PreviewAutomationHost(props: { readonly environmentId: EnvironmentId })
311 );313 );
312 const [automationConnectionAtom] = useState(() => Atom.make<string | null>(null));314 const [automationConnectionAtom] = useState(() => Atom.make<string | null>(null));
313 const automationConnectionId = useAtomValue(automationConnectionAtom);315 const automationConnectionId = useAtomValue(automationConnectionAtom);
316 const presentationSuppressedRuntimeTabsRef = useRef(new Map<string, Set<string>>());
314317
315 const handleRequest = useCallback(318 const handleRequest = useCallback(
316 async (request: PreviewAutomationRequest): Promise<unknown> => {319 async (request: PreviewAutomationRequest): Promise<unknown> => {
...@@ -353,6 +356,21 @@ function PreviewAutomationHost(props: { readonly environmentId: EnvironmentId })...@@ -353,6 +356,21 @@ function PreviewAutomationHost(props: { readonly environmentId: EnvironmentId })
353 }356 }
354 const readyState = readThreadPreviewState(threadRef);357 const readyState = readThreadPreviewState(threadRef);
355 const runtimeTabId = previewRuntimeTabId(threadRef, readyState.serverEpoch, readyTabId);358 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 }
356 browserActivity.release ??= acquireBrowserSurfaceActivity(runtimeTabId);374 browserActivity.release ??= acquireBrowserSurfaceActivity(runtimeTabId);
357 await waitForDesktopOverlay(375 await waitForDesktopOverlay(
358 threadRef,376 threadRef,
...@@ -450,6 +468,32 @@ function PreviewAutomationHost(props: { readonly environmentId: EnvironmentId })...@@ -450,6 +468,32 @@ function PreviewAutomationHost(props: { readonly environmentId: EnvironmentId })
450 input,468 input,
451 (await resolveBrowserDefaults()).autoShowFloatingPreview,469 (await resolveBrowserDefaults()).autoShowFloatingPreview,
452 );470 );
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 }
453 if (shouldPresentPreview) {497 if (shouldPresentPreview) {
454 usePreviewMiniPlayerStore.getState().open(threadRef, activeTabId);498 usePreviewMiniPlayerStore.getState().open(threadRef, activeTabId);
455 }499 }
apps/web/src/components/preview/ThreadPreviewMiniPlayer.tsx+7-5
...@@ -19,6 +19,7 @@ import {...@@ -19,6 +19,7 @@ import {
19 clampPreviewMiniPlayerSize,19 clampPreviewMiniPlayerSize,
20 PREVIEW_MINI_PLAYER_DEFAULT_SIZE,20 PREVIEW_MINI_PLAYER_DEFAULT_SIZE,
21 PREVIEW_MINI_PLAYER_EDGE_GAP,21 PREVIEW_MINI_PLAYER_EDGE_GAP,
22 PREVIEW_MINI_PLAYER_WEBVIEW_Z_INDEX,
22} from "./previewMiniPlayerLayout";23} from "./previewMiniPlayerLayout";
2324
24interface DragState {25interface DragState {
...@@ -243,7 +244,7 @@ export function ThreadPreviewMiniPlayer({ threadRef, tabId, bottomInset }: Props...@@ -243,7 +244,7 @@ export function ThreadPreviewMiniPlayer({ threadRef, tabId, bottomInset }: Props
243 }244 }
244 }245 }
245 >246 >
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">
247 <div248 <div
248 aria-hidden="true"249 aria-hidden="true"
249 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"250 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...@@ -316,11 +317,12 @@ export function ThreadPreviewMiniPlayer({ threadRef, tabId, bottomInset }: Props
316 </div>317 </div>
317318
318 <div className="relative h-full min-h-0">319 <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" />
320 <BrowserSurfaceSlot321 <BrowserSurfaceSlot
321 tabId={runtimeTabId}322 tabId={runtimeTabId}
322 visible={Boolean(desktopOverlay?.hasWebContents)}323 visible={Boolean(desktopOverlay?.hasWebContents)}
323 cornerRadius={12}324 cornerRadius={12}
325 zIndex={PREVIEW_MINI_PLAYER_WEBVIEW_Z_INDEX}
324 fitSourceContent326 fitSourceContent
325 layoutVersion={327 layoutVersion={
326 position328 position
...@@ -329,16 +331,16 @@ export function ThreadPreviewMiniPlayer({ threadRef, tabId, bottomInset }: Props...@@ -329,16 +331,16 @@ export function ThreadPreviewMiniPlayer({ threadRef, tabId, bottomInset }: Props
329 }331 }
330 className="absolute inset-0"332 className="absolute inset-0"
331 />333 />
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" />
333 {!desktopOverlay?.hasWebContents ? (335 {!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">
335 Reconnecting preview…337 Reconnecting preview…
336 </div>338 </div>
337 ) : null}339 ) : null}
338 <button340 <button
339 type="button"341 type="button"
340 aria-label="Resize floating preview"342 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"
342 onPointerDown={handleResizePointerDown}344 onPointerDown={handleResizePointerDown}
343 onPointerMove={handleResizePointerMove}345 onPointerMove={handleResizePointerMove}
344 onPointerUp={endResize}346 onPointerUp={endResize}
apps/web/src/components/preview/previewAutomationOpenReadiness.test.ts+47
...@@ -3,8 +3,10 @@ import { describe, expect, it } from "vite-plus/test";...@@ -3,8 +3,10 @@ import { describe, expect, it } from "vite-plus/test";
33
4import {4import {
5 DEFAULT_PREVIEW_AUTOMATION_VIEWPORT,5 DEFAULT_PREVIEW_AUTOMATION_VIEWPORT,
6 explicitlySuppressesPreviewMiniPlayer,
6 previewAutomationDefaultViewport,7 previewAutomationDefaultViewport,
7 previewAutomationOpenNeedsOverlay,8 previewAutomationOpenNeedsOverlay,
9 shouldAutoShowPreviewForAutomationUse,
8 shouldOpenPreviewMiniPlayer,10 shouldOpenPreviewMiniPlayer,
9} from "./previewAutomationOpenReadiness";11} from "./previewAutomationOpenReadiness";
1012
...@@ -91,4 +93,49 @@ describe("shouldOpenPreviewMiniPlayer with the floating-preview preference", ()...@@ -91,4 +93,49 @@ describe("shouldOpenPreviewMiniPlayer with the floating-preview preference", ()
91 expect(shouldOpenPreviewMiniPlayer({ open: false }, true)).toBe(false);93 expect(shouldOpenPreviewMiniPlayer({ open: false }, true)).toBe(false);
92 expect(shouldOpenPreviewMiniPlayer({ show: true }, false)).toBe(true);94 expect(shouldOpenPreviewMiniPlayer({ show: true }, false)).toBe(true);
93 });95 });
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 });
94});141});
apps/web/src/components/preview/previewAutomationOpenReadiness.ts+15
...@@ -1,5 +1,6 @@...@@ -1,5 +1,6 @@
1import {1import {
2 FILL_PREVIEW_VIEWPORT,2 FILL_PREVIEW_VIEWPORT,
3 type PreviewAutomationOperation,
3 type PreviewAutomationOpenInput,4 type PreviewAutomationOpenInput,
4 type PreviewSessionSnapshot,5 type PreviewSessionSnapshot,
5 type PreviewViewportSetting,6 type PreviewViewportSetting,
...@@ -29,6 +30,20 @@ export function shouldOpenPreviewMiniPlayer(...@@ -29,6 +30,20 @@ export function shouldOpenPreviewMiniPlayer(
29 return input.open ?? input.show ?? autoShowFloatingPreview;30 return input.open ?? input.show ?? autoShowFloatingPreview;
30}31}
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
32export function previewAutomationOpenNeedsOverlay(47export function previewAutomationOpenNeedsOverlay(
33 input: PreviewAutomationOpenInput,48 input: PreviewAutomationOpenInput,
34 snapshot: PreviewSessionSnapshot,49 snapshot: PreviewSessionSnapshot,
apps/web/src/components/preview/previewMiniPlayerLayout.ts+2
...@@ -1,6 +1,8 @@...@@ -1,6 +1,8 @@
1import type { PreviewMiniPlayerPosition, PreviewMiniPlayerSize } from "~/previewMiniPlayerStore";1import type { PreviewMiniPlayerPosition, PreviewMiniPlayerSize } from "~/previewMiniPlayerStore";
22
3export const PREVIEW_MINI_PLAYER_EDGE_GAP = 12;3export 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;
4export const PREVIEW_MINI_PLAYER_DEFAULT_SIZE = { width: 320, height: 200 } as const;6export const PREVIEW_MINI_PLAYER_DEFAULT_SIZE = { width: 320, height: 200 } as const;
5export const PREVIEW_MINI_PLAYER_MIN_SIZE = { width: 240, height: 150 } as const;7export 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...@@ -560,7 +560,7 @@ function BrowserAutoShowFloatingPreviewSetting({ disabled }: { readonly disabled
560 return (560 return (
561 <SettingsRow561 <SettingsRow
562 {...searchableSetting("browser-auto-show-floating-preview")}562 {...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."
564 resetAction={564 resetAction={
565 !disabled && autoShow !== DEFAULT_BROWSER_AUTO_SHOW_FLOATING_PREVIEW ? (565 !disabled && autoShow !== DEFAULT_BROWSER_AUTO_SHOW_FLOATING_PREVIEW ? (
566 <SettingResetButton566 <SettingResetButton
apps/web/src/components/ui/sheet.tsx+10-6
...@@ -64,6 +64,8 @@ function SheetPopup({...@@ -64,6 +64,8 @@ function SheetPopup({
64 showCloseButton = true,64 showCloseButton = true,
65 keepMounted = false,65 keepMounted = false,
66 transitionDurationMs,66 transitionDurationMs,
67 backdropClassName,
68 viewportClassName,
67 side = "right",69 side = "right",
68 variant = "default",70 variant = "default",
69 style,71 style,
...@@ -72,6 +74,8 @@ function SheetPopup({...@@ -72,6 +74,8 @@ function SheetPopup({
72 showCloseButton?: boolean;74 showCloseButton?: boolean;
73 keepMounted?: boolean;75 keepMounted?: boolean;
74 transitionDurationMs?: number;76 transitionDurationMs?: number;
77 backdropClassName?: string;
78 viewportClassName?: string;
75 side?: "right" | "left" | "top" | "bottom";79 side?: "right" | "left" | "top" | "bottom";
76 variant?: "default" | "inset";80 variant?: "default" | "inset";
77}) {81}) {
...@@ -84,14 +88,14 @@ function SheetPopup({...@@ -84,14 +88,14 @@ function SheetPopup({
84 return (88 return (
85 <SheetPortal keepMounted={keepMounted}>89 <SheetPortal keepMounted={keepMounted}>
86 <SheetBackdrop90 <SheetBackdrop
87 className={91 className={cn(
88 instant92 instant &&
89 ? "transition-none! data-ending-style:opacity-100! data-starting-style:opacity-100!"93 "transition-none! data-ending-style:opacity-100! data-starting-style:opacity-100!",
90 : undefined94 backdropClassName,
91 }95 )}
92 style={transitionStyle}96 style={transitionStyle}
93 />97 />
94 <SheetViewport side={side} variant={variant}>98 <SheetViewport className={viewportClassName} side={side} variant={variant}>
95 <SheetPrimitive.Popup99 <SheetPrimitive.Popup
96 className={cn(100 className={cn(
97 "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%)]",101 "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 @@...@@ -1,3 +1,5 @@
1export const RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY = "(max-width: 980px)";1export 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]";
2export const RIGHT_PANEL_SHEET_CLASS_NAME =4export const RIGHT_PANEL_SHEET_CLASS_NAME =
3 "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))]";5 "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({...@@ -226,7 +226,7 @@ export const ClientSettingsSchema = Schema.Struct({
226 Schema.withDecodingDefault(Effect.succeed(DEFAULT_BROWSER_LINK_TARGET)),226 Schema.withDecodingDefault(Effect.succeed(DEFAULT_BROWSER_LINK_TARGET)),
227 ),227 ),
228 /**228 /**
229 * Whether an agent opening a preview pops the floating mini player into229 * Whether an agent using a preview pops the floating mini player into
230 * view. Only applies when the agent didn't ask either way — an explicit230 * view. Only applies when the agent didn't ask either way — an explicit
231 * `open`/`show` on `preview_open` still wins, since that is the agent231 * `open`/`show` on `preview_open` still wins, since that is the agent
232 * deliberately showing or hiding its work.232 * deliberately showing or hiding its work.