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)