| author | |
| committer | |
| log | 12e8997e58dbca8f1bd8c63b67d662eb69cf0e0d |
| tree | 6182307e53a8d98364046ea754ba70bb33ac7003 |
| parent | c726c30a148c2add6a3ec7f31f54ae48dc5d2f0c |
| signature | Signed by PGP key B5690EEEBB952194 |
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); |
| 26 | 28 | ||
| 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]); |
| 73 | 77 | ||
| 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]); |
| 78 | 82 | ||
| 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 | }); |
| 164 | 166 | ||
| 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 { |
| 10 | export interface BrowserSurfacePresentation { | 10 | export 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 | } |
| 46 | 48 | ||
| 47 | export interface BrowserSurfaceLease { | 49 | export 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 | } |
| 51 | 58 | ||
| ... | @@ -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); |
| 207 | 217 | ||
| 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 | }); |
| 44 | 46 |
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 | }); |
| 95 | 96 | ||
| 97 | describe("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 | |||
| 96 | describe("proactive panels", () => { | 118 | describe("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 | } |
| 88 | 88 | ||
| 89 | export 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 | |||
| 89 | export function shouldOpenProactivePullRequest( | 102 | export 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 | ]); | ||
| 1879 | 1874 | ||
| 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> |
| 7921 | 7916 | ||
| 7922 | {activeThreadRef && activePreviewMiniPlayer ? ( | 7917 | {activeThreadRef && activePreviewMiniPlayer && previewMiniPlayerVisible ? ( |
| 7923 | <ThreadPreviewMiniPlayer | 7918 | <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 | <RightPanelSheet | 8035 | <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 | <RightPanelTabs | 8041 | <RightPanelTabs |
apps/web/src/components/RightPanelSheet.tsx+11-1| ... | @@ -1,12 +1,16 @@ | ... | @@ -1,12 +1,16 @@ |
| 1 | import { type ReactNode } from "react"; | 1 | import { type ReactNode } from "react"; |
| 2 | 2 | ||
| 3 | import { RIGHT_PANEL_SHEET_CLASS_NAME } from "../rightPanelLayout"; | 3 | import { |
| 4 | RIGHT_PANEL_SHEET_CLASS_NAME, | ||
| 5 | RIGHT_PANEL_SHEET_LAYER_CLASS_NAME, | ||
| 6 | } from "../rightPanelLayout"; | ||
| 4 | import { Sheet, SheetPopup } from "./ui/sheet"; | 7 | import { Sheet, SheetPopup } from "./ui/sheet"; |
| 5 | 8 | ||
| 6 | export function RightPanelSheet(props: { | 9 | export 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 | keepMounted | 29 | 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"; |
| 22 | import { resolvePreviewViewport } from "@t3tools/shared/previewViewport"; | 22 | import { resolvePreviewViewport } from "@t3tools/shared/previewViewport"; |
| 23 | import { useCallback, useContext, useEffect, useMemo, useState } from "react"; | 23 | import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; |
| 24 | import { Atom } from "effect/unstable/reactivity"; | 24 | import { Atom } from "effect/unstable/reactivity"; |
| 25 | 25 | ||
| 26 | import { | 26 | import { |
| ... | @@ -29,7 +29,7 @@ import { | ... | @@ -29,7 +29,7 @@ import { |
| 29 | reconcilePreviewServerSessions, | 29 | reconcilePreviewServerSessions, |
| 30 | updatePreviewServerSnapshot, | 30 | updatePreviewServerSnapshot, |
| 31 | } from "~/previewStateStore"; | 31 | } from "~/previewStateStore"; |
| 32 | import { usePreviewMiniPlayerStore } from "~/previewMiniPlayerStore"; | 32 | import { selectThreadPreviewMiniPlayer, usePreviewMiniPlayerStore } from "~/previewMiniPlayerStore"; |
| 33 | import { resolveBrowserNavigationTarget } from "~/browser/browserTargetResolver"; | 33 | import { resolveBrowserNavigationTarget } from "~/browser/browserTargetResolver"; |
| 34 | import { | 34 | import { |
| 35 | readActiveBrowserRecordingTargets, | 35 | readActiveBrowserRecordingTargets, |
| ... | @@ -59,8 +59,10 @@ import { | ... | @@ -59,8 +59,10 @@ import { |
| 59 | PreviewAutomationViewportTimeoutError, | 59 | PreviewAutomationViewportTimeoutError, |
| 60 | } from "./previewAutomationErrors"; | 60 | } from "./previewAutomationErrors"; |
| 61 | import { | 61 | import { |
| 62 | explicitlySuppressesPreviewMiniPlayer, | ||
| 62 | previewAutomationDefaultViewport, | 63 | previewAutomationDefaultViewport, |
| 63 | previewAutomationOpenNeedsOverlay, | 64 | previewAutomationOpenNeedsOverlay, |
| 65 | shouldAutoShowPreviewForAutomationUse, | ||
| 64 | shouldOpenPreviewMiniPlayer, | 66 | shouldOpenPreviewMiniPlayer, |
| 65 | } from "./previewAutomationOpenReadiness"; | 67 | } from "./previewAutomationOpenReadiness"; |
| 66 | import { | 68 | import { |
| ... | @@ -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>>()); | ||
| 314 | 317 | ||
| 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"; |
| 23 | 24 | ||
| 24 | interface DragState { | 25 | interface 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 | <div | 248 | <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> |
| 317 | 318 | ||
| 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 | <BrowserSurfaceSlot | 321 | <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 | fitSourceContent | 326 | fitSourceContent |
| 325 | layoutVersion={ | 327 | layoutVersion={ |
| 326 | position | 328 | 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 | <button | 340 | <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"; |
| 3 | 3 | ||
| 4 | import { | 4 | import { |
| 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"; |
| 10 | 12 | ||
| ... | @@ -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 | |||
| 104 | describe("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 @@ |
| 1 | import { | 1 | import { |
| 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 | } |
| 31 | 32 | ||
| 33 | export 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 | |||
| 43 | export function explicitlySuppressesPreviewMiniPlayer(input: PreviewAutomationOpenInput): boolean { | ||
| 44 | return (input.open ?? input.show) === false; | ||
| 45 | } | ||
| 46 | |||
| 32 | export function previewAutomationOpenNeedsOverlay( | 47 | export 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 @@ |
| 1 | import type { PreviewMiniPlayerPosition, PreviewMiniPlayerSize } from "~/previewMiniPlayerStore"; | 1 | import type { PreviewMiniPlayerPosition, PreviewMiniPlayerSize } from "~/previewMiniPlayerStore"; |
| 2 | 2 | ||
| 3 | export const PREVIEW_MINI_PLAYER_EDGE_GAP = 12; | 3 | export const PREVIEW_MINI_PLAYER_EDGE_GAP = 12; |
| 4 | // The mini-player shell straddles this webview at 47 and 49; dialogs begin at 50. | ||
| 5 | export const PREVIEW_MINI_PLAYER_WEBVIEW_Z_INDEX = 48; | ||
| 4 | export const PREVIEW_MINI_PLAYER_DEFAULT_SIZE = { width: 320, height: 200 } as const; | 6 | export const PREVIEW_MINI_PLAYER_DEFAULT_SIZE = { width: 320, height: 200 } as const; |
| 5 | export const PREVIEW_MINI_PLAYER_MIN_SIZE = { width: 240, height: 150 } as const; | 7 | export const PREVIEW_MINI_PLAYER_MIN_SIZE = { width: 240, height: 150 } as const; |
| 6 | 8 |
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 | <SettingsRow | 561 | <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 | <SettingResetButton | 566 | <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 | <SheetBackdrop | 90 | <SheetBackdrop |
| 87 | className={ | 91 | className={cn( |
| 88 | instant | 92 | 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 | : undefined | 94 | 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.Popup | 99 | <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 @@ |
| 1 | export const RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY = "(max-width: 980px)"; | 1 | export const RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY = "(max-width: 980px)"; |
| 2 | // Applied only while a floating preview overlaps the compact sheet. | ||
| 3 | export const RIGHT_PANEL_SHEET_LAYER_CLASS_NAME = "z-[35]"; | ||
| 2 | export const RIGHT_PANEL_SHEET_CLASS_NAME = | 4 | export 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 into | 229 | * 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 explicit | 230 | * 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 agent | 231 | * `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. |