From 5d02f4c4e71dccc611337bab7492d7ea5e710983 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Fri, 29 May 2026 13:08:36 +0800 Subject: [PATCH] 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 --- .../components/browser/BrowserSurface.test.tsx | 1 + .../src/components/browser/BrowserSurface.tsx | 8 ++++++++ desktop/src/lib/previewEvents.test.ts | 18 ++++++++++++++++++ desktop/src/lib/previewEvents.ts | 13 +++++++++++++ desktop/src/stores/browserPanelStore.test.ts | 9 +++++++++ desktop/src/stores/browserPanelStore.ts | 13 ++++++++++++- 6 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 desktop/src/lib/previewEvents.test.ts create mode 100644 desktop/src/lib/previewEvents.ts diff --git a/desktop/src/components/browser/BrowserSurface.test.tsx b/desktop/src/components/browser/BrowserSurface.test.tsx index 6e44289f..bb4e732a 100644 --- a/desktop/src/components/browser/BrowserSurface.test.tsx +++ b/desktop/src/components/browser/BrowserSurface.test.tsx @@ -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' diff --git a/desktop/src/components/browser/BrowserSurface.tsx b/desktop/src/components/browser/BrowserSurface.tsx index f51d39aa..ef6f23ee 100644 --- a/desktop/src/components/browser/BrowserSurface.tsx +++ b/desktop/src/components/browser/BrowserSurface.tsx @@ -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 diff --git a/desktop/src/lib/previewEvents.test.ts b/desktop/src/lib/previewEvents.test.ts new file mode 100644 index 00000000..a33d749e --- /dev/null +++ b/desktop/src/lib/previewEvents.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it, vi } from 'vitest' + +const listeners: Record 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') + }) +}) diff --git a/desktop/src/lib/previewEvents.ts b/desktop/src/lib/previewEvents.ts new file mode 100644 index 00000000..344076bf --- /dev/null +++ b/desktop/src/lib/previewEvents.ts @@ -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('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 接入 + }) +} diff --git a/desktop/src/stores/browserPanelStore.test.ts b/desktop/src/stores/browserPanelStore.test.ts index fb702ab9..25782a86 100644 --- a/desktop/src/stores/browserPanelStore.test.ts +++ b/desktop/src/stores/browserPanelStore.test.ts @@ -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') + }) }) diff --git a/desktop/src/stores/browserPanelStore.ts b/desktop/src/stores/browserPanelStore.ts index 2f4a059b..f17f0a03 100644 --- a/desktop/src/stores/browserPanelStore.ts +++ b/desktop/src/stores/browserPanelStore.ts @@ -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((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 } } } + }), }))