diff --git a/desktop/src/components/browser/BrowserSurface.test.tsx b/desktop/src/components/browser/BrowserSurface.test.tsx
new file mode 100644
index 00000000..6e44289f
--- /dev/null
+++ b/desktop/src/components/browser/BrowserSurface.test.tsx
@@ -0,0 +1,45 @@
+import '@testing-library/jest-dom'
+import { fireEvent, render, screen } from '@testing-library/react'
+import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'
+
+beforeAll(() => {
+ globalThis.ResizeObserver = class { observe() {} unobserve() {} disconnect() {} } as unknown as typeof ResizeObserver
+})
+
+const { bridge } = vi.hoisted(() => ({
+ bridge: { open: vi.fn(), navigate: vi.fn(), setBounds: vi.fn(), setVisible: vi.fn(), close: vi.fn() },
+}))
+vi.mock('../../lib/previewBridge', () => ({ previewBridge: bridge }))
+
+import { BrowserSurface } from './BrowserSurface'
+import { useBrowserPanelStore } from '../../stores/browserPanelStore'
+
+afterEach(() => {
+ Object.values(bridge).forEach((f) => f.mockReset())
+ useBrowserPanelStore.setState(useBrowserPanelStore.getInitialState(), true)
+})
+
+describe('BrowserSurface', () => {
+ it('opens the preview at the session url on mount when surface is open', () => {
+ useBrowserPanelStore.getState().open('s1', 'http://localhost:5173/')
+ render()
+ expect(bridge.open).toHaveBeenCalledWith('http://localhost:5173/', expect.objectContaining({ width: expect.any(Number) }))
+ })
+
+ it('navigating via address bar calls store + bridge', () => {
+ useBrowserPanelStore.getState().open('s1', 'http://localhost:5173/')
+ render()
+ const input = screen.getByRole('textbox')
+ fireEvent.change(input, { target: { value: 'http://localhost:3000/' } })
+ fireEvent.submit(input.closest('form')!)
+ expect(bridge.navigate).toHaveBeenCalledWith('http://localhost:3000/')
+ expect(useBrowserPanelStore.getState().bySession['s1']!.url).toBe('http://localhost:3000/')
+ })
+
+ it('hides the native webview on unmount', () => {
+ useBrowserPanelStore.getState().open('s1', 'http://localhost:5173/')
+ const { unmount } = render()
+ unmount()
+ expect(bridge.setVisible).toHaveBeenLastCalledWith(false)
+ })
+})
diff --git a/desktop/src/components/browser/BrowserSurface.tsx b/desktop/src/components/browser/BrowserSurface.tsx
new file mode 100644
index 00000000..f51d39aa
--- /dev/null
+++ b/desktop/src/components/browser/BrowserSurface.tsx
@@ -0,0 +1,54 @@
+import { useEffect, useLayoutEffect, useRef } from 'react'
+import { BrowserAddressBar } from './BrowserAddressBar'
+import { computeWebviewBounds } from './computeWebviewBounds'
+import { previewBridge } from '../../lib/previewBridge'
+import { useBrowserPanelStore } from '../../stores/browserPanelStore'
+
+export function BrowserSurface({ sessionId }: { sessionId: string }) {
+ const hostRef = useRef(null)
+ const session = useBrowserPanelStore((s) => s.bySession[sessionId])
+ const store = useBrowserPanelStore.getState()
+
+ const reportBounds = () => {
+ const el = hostRef.current
+ if (!el) return
+ previewBridge.setBounds(computeWebviewBounds(el.getBoundingClientRect()))
+ }
+
+ useLayoutEffect(() => {
+ const el = hostRef.current
+ if (!el || !session) return
+ previewBridge.open(session.url, computeWebviewBounds(el.getBoundingClientRect()))
+ previewBridge.setVisible(true)
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [sessionId])
+
+ useEffect(() => {
+ const el = hostRef.current
+ if (!el) return
+ const ro = new ResizeObserver(() => reportBounds())
+ ro.observe(el)
+ window.addEventListener('resize', reportBounds)
+ return () => { ro.disconnect(); window.removeEventListener('resize', reportBounds) }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [sessionId])
+
+ useEffect(() => () => { previewBridge.setVisible(false) }, [])
+
+ if (!session) return null
+
+ return (
+
+
{ store.navigate(sessionId, url); previewBridge.navigate(url) }}
+ onBack={() => { store.goBack(sessionId); previewBridge.navigate(useBrowserPanelStore.getState().bySession[sessionId]!.url) }}
+ onForward={() => { store.goForward(sessionId); previewBridge.navigate(useBrowserPanelStore.getState().bySession[sessionId]!.url) }}
+ onReload={() => previewBridge.navigate(session.url)}
+ />
+
+
+ )
+}