diff --git a/desktop/src/components/browser/BrowserAddressBar.test.tsx b/desktop/src/components/browser/BrowserAddressBar.test.tsx
index 7db901b0..8c1faf1e 100644
--- a/desktop/src/components/browser/BrowserAddressBar.test.tsx
+++ b/desktop/src/components/browser/BrowserAddressBar.test.tsx
@@ -48,6 +48,20 @@ describe('BrowserAddressBar', () => {
expect(onNavigate).toHaveBeenNthCalledWith(2, 'https://example.com')
})
+ it('preserves typed local html paths for the browser surface to route', () => {
+ const onNavigate = vi.fn()
+ render()
+ const input = screen.getByRole('textbox')
+
+ fireEvent.change(input, { target: { value: '/private/tmp/report.html' } })
+ fireEvent.submit(input.closest('form')!)
+ fireEvent.change(input, { target: { value: 'out/index.html' } })
+ fireEvent.submit(input.closest('form')!)
+
+ expect(onNavigate).toHaveBeenNthCalledWith(1, '/private/tmp/report.html')
+ expect(onNavigate).toHaveBeenNthCalledWith(2, 'out/index.html')
+ })
+
it('preserves explicit URL schemes', () => {
const onNavigate = vi.fn()
render()
diff --git a/desktop/src/components/browser/BrowserAddressBar.tsx b/desktop/src/components/browser/BrowserAddressBar.tsx
index f2a1e75c..38f0effe 100644
--- a/desktop/src/components/browser/BrowserAddressBar.tsx
+++ b/desktop/src/components/browser/BrowserAddressBar.tsx
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react'
import type { ReactNode } from 'react'
import { ArrowLeft, ArrowRight, Loader2, RotateCw } from 'lucide-react'
+import { isHtmlFilePath } from '../../lib/htmlPreviewPolicy'
type Props = {
url: string
@@ -58,6 +59,7 @@ export function normalizeBrowserAddress(input: string): string {
const value = input.trim()
if (!value) return ''
if (/^[a-z][a-z\d+\-.]*:\/\//i.test(value) || /^(about|data|file):/i.test(value)) return value
+ if (isHtmlFilePath(value)) return value
if (/^(localhost|127(?:\.\d{1,3}){3}|\[::1\]|::1)(?::\d+)?(?:[/?#].*)?$/i.test(value)) {
return `http://${value}`
}
diff --git a/desktop/src/components/browser/BrowserSurface.test.tsx b/desktop/src/components/browser/BrowserSurface.test.tsx
index bc4feb4d..666272ee 100644
--- a/desktop/src/components/browser/BrowserSurface.test.tsx
+++ b/desktop/src/components/browser/BrowserSurface.test.tsx
@@ -16,6 +16,7 @@ vi.mock('../../lib/previewBridge', () => ({ previewBridge: bridge }))
vi.mock('@tauri-apps/api/event', () => ({ listen: () => Promise.resolve(() => {}) }))
import { BrowserSurface } from './BrowserSurface'
+import { getDefaultBaseUrl, setBaseUrl } from '../../api/client'
import { useBrowserPanelStore } from '../../stores/browserPanelStore'
import { useWorkspacePanelStore } from '../../stores/workspacePanelStore'
import { useOverlayStore } from '../../stores/overlayStore'
@@ -28,6 +29,7 @@ afterEach(() => {
// browserPanelStore.open() now also opens the unified workbench; keep it isolated.
useWorkspacePanelStore.setState(useWorkspacePanelStore.getInitialState(), true)
useOverlayStore.setState(useOverlayStore.getInitialState(), true)
+ setBaseUrl(getDefaultBaseUrl())
})
describe('BrowserSurface', () => {
@@ -76,6 +78,27 @@ describe('BrowserSurface', () => {
expect(bridge.navigate).not.toHaveBeenCalled()
})
+ it('opens a typed file URL for local html through the local-file preview route', async () => {
+ vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(null, { status: 200 }))
+ setBaseUrl('http://127.0.0.1:8787')
+ useBrowserPanelStore.getState().ensureBlank('s1')
+ render()
+
+ const input = screen.getByRole('textbox')
+ fireEvent.change(input, { target: { value: 'file:///private/tmp/report.html' } })
+ fireEvent.submit(input.closest('form')!)
+
+ await waitFor(() => {
+ expect(bridge.open).toHaveBeenCalledWith(
+ 'http://127.0.0.1:8787/local-file/private/tmp/report.html',
+ expect.objectContaining({ width: expect.any(Number) }),
+ )
+ })
+ expect(useBrowserPanelStore.getState().bySession['s1']!.url).toBe(
+ 'http://127.0.0.1:8787/local-file/private/tmp/report.html',
+ )
+ })
+
it('navigating via address bar calls store + bridge', () => {
useBrowserPanelStore.getState().open('s1', 'http://localhost:5173/')
render()
diff --git a/desktop/src/components/browser/BrowserSurface.tsx b/desktop/src/components/browser/BrowserSurface.tsx
index f3586613..0f465f13 100644
--- a/desktop/src/components/browser/BrowserSurface.tsx
+++ b/desktop/src/components/browser/BrowserSurface.tsx
@@ -2,7 +2,9 @@ import { useEffect, useLayoutEffect, useRef } from 'react'
import { Camera, Loader2, MousePointer2 } from 'lucide-react'
import { BrowserAddressBar } from './BrowserAddressBar'
import { computeWebviewBounds } from './computeWebviewBounds'
-import { isLoopbackHostname } from '../../lib/desktopRuntime'
+import { getServerBaseUrl, isLoopbackHostname } from '../../lib/desktopRuntime'
+import { classifyPreviewLink } from '../../lib/previewLinkRouter'
+import { isAbsoluteLocalPath, localFileUrl, previewFsUrl } from '../../lib/handlePreviewLink'
import { previewBridge } from '../../lib/previewBridge'
import { subscribePreviewEvents } from '../../lib/previewEvents'
import { useBrowserPanelStore } from '../../stores/browserPanelStore'
@@ -38,6 +40,21 @@ async function waitForLocalPreview(url: string): Promise {
}
}
+function resolveBrowserNavigationUrl(input: string, sessionId: string): string {
+ const value = input.trim()
+ if (!value) return ''
+
+ const classified = classifyPreviewLink(value)
+ if (classified.kind === 'browser-file' && classified.path) {
+ const serverBaseUrl = getServerBaseUrl()
+ return isAbsoluteLocalPath(classified.path)
+ ? localFileUrl(serverBaseUrl, classified.path)
+ : previewFsUrl(serverBaseUrl, sessionId, classified.path)
+ }
+
+ return value
+}
+
export function BrowserSurface({ sessionId }: { sessionId: string }) {
const hostRef = useRef(null)
const loadSeqRef = useRef(0)
@@ -126,7 +143,8 @@ export function BrowserSurface({ sessionId }: { sessionId: string }) {
if (!session) return null
- const openOrNavigate = (url: string) => {
+ const openOrNavigate = (inputUrl: string) => {
+ const url = resolveBrowserNavigationUrl(inputUrl, sessionId)
if (!url) return
const current = useBrowserPanelStore.getState().bySession[sessionId]
store.navigate(sessionId, url)