diff --git a/desktop/src/components/browser/BrowserSurface.test.tsx b/desktop/src/components/browser/BrowserSurface.test.tsx
index f20294e9..ab25e286 100644
--- a/desktop/src/components/browser/BrowserSurface.test.tsx
+++ b/desktop/src/components/browser/BrowserSurface.test.tsx
@@ -28,6 +28,7 @@ import { getDefaultBaseUrl, setBaseUrl } from '../../api/client'
import { useBrowserPanelStore } from '../../stores/browserPanelStore'
import { useWorkspacePanelStore } from '../../stores/workspacePanelStore'
import { useOverlayStore } from '../../stores/overlayStore'
+import { useSettingsStore } from '../../stores/settingsStore'
afterEach(() => {
cleanup()
@@ -37,6 +38,7 @@ afterEach(() => {
// browserPanelStore.open() now also opens the unified workbench; keep it isolated.
useWorkspacePanelStore.setState(useWorkspacePanelStore.getInitialState(), true)
useOverlayStore.setState(useOverlayStore.getInitialState(), true)
+ useSettingsStore.setState({ uiZoom: 1 })
setBaseUrl(getDefaultBaseUrl())
})
@@ -49,6 +51,40 @@ describe('BrowserSurface', () => {
})
})
+ it('rescales native preview bounds when app zoom changes', async () => {
+ vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockReturnValue({
+ left: 180,
+ top: 150,
+ width: 420,
+ height: 300,
+ } as DOMRect)
+ useSettingsStore.setState({ uiZoom: 1.25 })
+ useBrowserPanelStore.getState().open('s1', 'http://localhost:5173/')
+ render()
+
+ await waitFor(() => {
+ expect(bridge.open).toHaveBeenCalledWith('http://localhost:5173/', {
+ x: 225,
+ y: 187.5,
+ width: 525,
+ height: 375,
+ })
+ })
+
+ act(() => {
+ useSettingsStore.setState({ uiZoom: 1.5 })
+ })
+
+ await waitFor(() => {
+ expect(bridge.setBounds).toHaveBeenLastCalledWith({
+ x: 270,
+ y: 225,
+ width: 630,
+ height: 450,
+ })
+ })
+ })
+
it('waits for local preview URLs before opening the native preview', async () => {
const url = 'http://127.0.0.1:59028/preview-fs/s1/66estmutl_files/index.html'
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(null, { status: 200 }))
diff --git a/desktop/src/components/browser/BrowserSurface.tsx b/desktop/src/components/browser/BrowserSurface.tsx
index 1dbffaf7..dca5fc34 100644
--- a/desktop/src/components/browser/BrowserSurface.tsx
+++ b/desktop/src/components/browser/BrowserSurface.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useLayoutEffect, useRef } from 'react'
+import { useCallback, useEffect, useLayoutEffect, useRef } from 'react'
import { Camera, Loader2, Minus, MousePointer2, Plus, RotateCcw } from 'lucide-react'
import { BrowserAddressBar } from './BrowserAddressBar'
import { computeWebviewBounds } from './computeWebviewBounds'
@@ -16,6 +16,7 @@ import {
useBrowserPanelStore,
} from '../../stores/browserPanelStore'
import { useOverlayStore } from '../../stores/overlayStore'
+import { useSettingsStore } from '../../stores/settingsStore'
const LOCAL_PREVIEW_PATH_PREFIXES = ['/preview-fs/', '/local-file/']
const LOCAL_PREVIEW_READY_TIMEOUT_MS = 2500
@@ -68,6 +69,7 @@ export function BrowserSurface({ sessionId }: { sessionId: string }) {
const requestedUrlRef = useRef(null)
const hasNativePreviewRef = useRef(false)
const session = useBrowserPanelStore((s) => s.bySession[sessionId])
+ const appZoom = useSettingsStore((s) => s.uiZoom)
const store = useBrowserPanelStore.getState()
const overlayCount = useOverlayStore((s) => s.count)
const previewZoom = session?.zoom ?? DEFAULT_BROWSER_ZOOM
@@ -75,11 +77,11 @@ export function BrowserSurface({ sessionId }: { sessionId: string }) {
const canZoomOut = previewZoom > MIN_BROWSER_ZOOM
const canZoomIn = previewZoom < MAX_BROWSER_ZOOM
- const reportBounds = () => {
+ const reportBounds = useCallback(() => {
const el = hostRef.current
if (!el) return
- previewBridge.setBounds(computeWebviewBounds(el.getBoundingClientRect()))
- }
+ previewBridge.setBounds(computeWebviewBounds(el.getBoundingClientRect(), appZoom))
+ }, [appZoom])
const loadNativePreview = (
url: string,
@@ -118,7 +120,7 @@ export function BrowserSurface({ sessionId }: { sessionId: string }) {
const el = hostRef.current
hasNativePreviewRef.current = true
if (el) {
- await previewBridge.open(url, computeWebviewBounds(el.getBoundingClientRect()))
+ await previewBridge.open(url, computeWebviewBounds(el.getBoundingClientRect(), appZoom))
} else {
await previewBridge.navigate(url)
}
@@ -168,8 +170,11 @@ export function BrowserSurface({ sessionId }: { sessionId: string }) {
ro.observe(el)
window.addEventListener('resize', reportBounds)
return () => { ro.disconnect(); window.removeEventListener('resize', reportBounds) }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [sessionId])
+ }, [reportBounds, sessionId])
+
+ useLayoutEffect(() => {
+ reportBounds()
+ }, [reportBounds, sessionId])
useEffect(() => {
let unsub: (() => void) | undefined
diff --git a/desktop/src/components/browser/computeWebviewBounds.test.ts b/desktop/src/components/browser/computeWebviewBounds.test.ts
index 31e324eb..71762095 100644
--- a/desktop/src/components/browser/computeWebviewBounds.test.ts
+++ b/desktop/src/components/browser/computeWebviewBounds.test.ts
@@ -7,6 +7,11 @@ describe('computeWebviewBounds', () => {
expect(computeWebviewBounds(rect)).toEqual({ x: 100.4, y: 50.6, width: 800.2, height: 600.9 })
})
+ it('maps zoomed CSS coordinates back to the native parent coordinate space', () => {
+ const rect = { left: 180, top: 150, width: 420, height: 300 } as DOMRect
+ expect(computeWebviewBounds(rect, 1.25)).toEqual({ x: 225, y: 187.5, width: 525, height: 375 })
+ })
+
it('clamps negative/zero sizes to 0', () => {
const rect = { left: -5, top: -5, width: -10, height: 0 } as DOMRect
expect(computeWebviewBounds(rect)).toEqual({ x: -5, y: -5, width: 0, height: 0 })
diff --git a/desktop/src/components/browser/computeWebviewBounds.ts b/desktop/src/components/browser/computeWebviewBounds.ts
index 2c6564b4..bb0f10a4 100644
--- a/desktop/src/components/browser/computeWebviewBounds.ts
+++ b/desktop/src/components/browser/computeWebviewBounds.ts
@@ -1,10 +1,13 @@
export type WebviewBounds = { x: number; y: number; width: number; height: number }
-export function computeWebviewBounds(rect: Pick): WebviewBounds {
+export function computeWebviewBounds(
+ rect: Pick,
+ appZoom = 1,
+): WebviewBounds {
return {
- x: rect.left,
- y: rect.top,
- width: Math.max(0, rect.width),
- height: Math.max(0, rect.height),
+ x: rect.left * appZoom,
+ y: rect.top * appZoom,
+ width: Math.max(0, rect.width * appZoom),
+ height: Math.max(0, rect.height * appZoom),
}
}