feat(desktop): subscribe preview bridge events into browser store

Routes preview://event (navigated/ready) from Rust into the browser store via a new subscribePreviewEvents subscriber; adds title field and setNavigated/setReady reducers to BrowserSessionState; BrowserSurface subscribes on mount to replace M1 optimistic nav-state.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
程序员阿江(Relakkes) 2026-05-29 13:08:36 +08:00
parent 36a3ffff34
commit 5d02f4c4e7
6 changed files with 61 additions and 1 deletions

View File

@ -10,6 +10,7 @@ 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 }))
vi.mock('@tauri-apps/api/event', () => ({ listen: () => Promise.resolve(() => {}) }))
import { BrowserSurface } from './BrowserSurface'
import { useBrowserPanelStore } from '../../stores/browserPanelStore'

View File

@ -2,6 +2,7 @@ import { useEffect, useLayoutEffect, useRef } from 'react'
import { BrowserAddressBar } from './BrowserAddressBar'
import { computeWebviewBounds } from './computeWebviewBounds'
import { previewBridge } from '../../lib/previewBridge'
import { subscribePreviewEvents } from '../../lib/previewEvents'
import { useBrowserPanelStore } from '../../stores/browserPanelStore'
export function BrowserSurface({ sessionId }: { sessionId: string }) {
@ -33,6 +34,13 @@ export function BrowserSurface({ sessionId }: { sessionId: string }) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sessionId])
useEffect(() => {
let unsub: (() => void) | undefined
void subscribePreviewEvents(sessionId).then((u) => { unsub = u })
return () => { unsub?.() }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sessionId])
useEffect(() => () => { previewBridge.setVisible(false) }, [])
if (!session) return null

View File

@ -0,0 +1,18 @@
import { describe, expect, it, vi } from 'vitest'
const listeners: Record<string, (e: { payload: string }) => void> = {}
vi.mock('@tauri-apps/api/event', () => ({
listen: (name: string, cb: (e: { payload: string }) => void) => { listeners[name] = cb; return Promise.resolve(() => {}) },
}))
import { subscribePreviewEvents } from './previewEvents'
import { useBrowserPanelStore } from '../stores/browserPanelStore'
describe('subscribePreviewEvents', () => {
it('routes navigated event to the store', async () => {
useBrowserPanelStore.getState().open('s1', 'http://x/a')
await subscribePreviewEvents('s1')
listeners['preview://event']!({ payload: JSON.stringify({ v: 1, type: 'navigated', url: 'http://x/c', title: 'C' }) })
expect(useBrowserPanelStore.getState().bySession['s1']!.url).toBe('http://x/c')
})
})

View File

@ -0,0 +1,13 @@
import { listen } from '@tauri-apps/api/event'
import { useBrowserPanelStore } from '../stores/browserPanelStore'
export async function subscribePreviewEvents(sessionId: string): Promise<() => void> {
return listen<string>('preview://event', (e) => {
let msg: { type?: string; url?: string; title?: string }
try { msg = JSON.parse(e.payload) } catch { return }
const store = useBrowserPanelStore.getState()
if (msg.type === 'navigated' && msg.url) store.setNavigated(sessionId, msg.url, msg.title ?? '')
else if (msg.type === 'ready') store.setReady(sessionId)
// selection / screenshot 在 M4/M5 接入
})
}

View File

@ -48,4 +48,13 @@ describe('browserPanelStore', () => {
st.setPicker('s1', true)
expect(useBrowserPanelStore.getState().bySession['s1']!.pickerActive).toBe(true)
})
it('setNavigated updates url/title without growing history', () => {
const st = useBrowserPanelStore.getState()
st.open('s1', 'http://x/a')
st.setNavigated('s1', 'http://x/b', 'B')
const s = useBrowserPanelStore.getState().bySession['s1']!
expect(s.url).toBe('http://x/b')
expect(s.title).toBe('B')
})
})

View File

@ -3,6 +3,7 @@ import { create } from 'zustand'
export type BrowserSessionState = {
isOpen: boolean
url: string
title: string
history: string[]
historyIndex: number
loading: boolean
@ -20,10 +21,12 @@ type BrowserPanelState = {
setLoading: (sessionId: string, loading: boolean) => void
setPicker: (sessionId: string, active: boolean) => void
close: (sessionId: string) => void
setNavigated: (sessionId: string, url: string, title: string) => void
setReady: (sessionId: string) => void
}
const empty = (url: string): BrowserSessionState => ({
isOpen: true, url, history: [url], historyIndex: 0,
isOpen: true, url, title: '', history: [url], historyIndex: 0,
loading: false, pickerActive: false, canGoBack: false, canGoForward: false,
})
@ -64,4 +67,12 @@ export const useBrowserPanelStore = create<BrowserPanelState>((set) => ({
const cur = st.bySession[sessionId]; if (!cur) return st
return { bySession: { ...st.bySession, [sessionId]: { ...cur, isOpen: false, pickerActive: false } } }
}),
setNavigated: (sessionId, url, title) => set((st) => {
const cur = st.bySession[sessionId]; if (!cur) return st
return { bySession: { ...st.bySession, [sessionId]: { ...cur, url, title, loading: false } } }
}),
setReady: (sessionId) => set((st) => {
const cur = st.bySession[sessionId]; if (!cur) return st
return { bySession: { ...st.bySession, [sessionId]: { ...cur, loading: false } } }
}),
}))