feat(desktop): add browserPanelStore with history/loading/picker state

Per-session in-app mini-browser state: current URL, back/forward
history stack (historyIndex with forward-truncation on navigate),
loading flag, and element-picker toggle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
程序员阿江(Relakkes) 2026-05-29 11:41:41 +08:00
parent 53724a6652
commit 633ab63f36
2 changed files with 118 additions and 0 deletions

View File

@ -0,0 +1,51 @@
import { beforeEach, describe, expect, it } from 'vitest'
import { useBrowserPanelStore } from './browserPanelStore'
const reset = () => useBrowserPanelStore.setState(useBrowserPanelStore.getInitialState(), true)
describe('browserPanelStore', () => {
beforeEach(reset)
it('opens a session at a url and records history', () => {
useBrowserPanelStore.getState().open('s1', 'http://localhost:5173/')
const s = useBrowserPanelStore.getState().bySession['s1']!
expect(s.url).toBe('http://localhost:5173/')
expect(s.isOpen).toBe(true)
expect(s.history).toEqual(['http://localhost:5173/'])
expect(s.historyIndex).toBe(0)
expect(s.canGoBack).toBe(false)
})
it('navigate pushes history and truncates forward entries', () => {
const st = useBrowserPanelStore.getState()
st.open('s1', 'http://localhost/a')
st.navigate('s1', 'http://localhost/b')
st.navigate('s1', 'http://localhost/c')
st.goBack('s1')
st.navigate('s1', 'http://localhost/d') // 截断 c
const s = useBrowserPanelStore.getState().bySession['s1']!
expect(s.history).toEqual(['http://localhost/a', 'http://localhost/b', 'http://localhost/d'])
expect(s.url).toBe('http://localhost/d')
expect(s.canGoForward).toBe(false)
})
it('goBack/goForward move within history without mutating it', () => {
const st = useBrowserPanelStore.getState()
st.open('s1', 'http://localhost/a')
st.navigate('s1', 'http://localhost/b')
st.goBack('s1')
expect(useBrowserPanelStore.getState().bySession['s1']!.url).toBe('http://localhost/a')
expect(useBrowserPanelStore.getState().bySession['s1']!.canGoForward).toBe(true)
st.goForward('s1')
expect(useBrowserPanelStore.getState().bySession['s1']!.url).toBe('http://localhost/b')
})
it('tracks loading and picker per session', () => {
const st = useBrowserPanelStore.getState()
st.open('s1', 'http://localhost/a')
st.setLoading('s1', true)
expect(useBrowserPanelStore.getState().bySession['s1']!.loading).toBe(true)
st.setPicker('s1', true)
expect(useBrowserPanelStore.getState().bySession['s1']!.pickerActive).toBe(true)
})
})

View File

@ -0,0 +1,67 @@
import { create } from 'zustand'
export type BrowserSessionState = {
isOpen: boolean
url: string
history: string[]
historyIndex: number
loading: boolean
pickerActive: boolean
canGoBack: boolean
canGoForward: boolean
}
type BrowserPanelState = {
bySession: Record<string, BrowserSessionState>
open: (sessionId: string, url: string) => void
navigate: (sessionId: string, url: string) => void
goBack: (sessionId: string) => void
goForward: (sessionId: string) => void
setLoading: (sessionId: string, loading: boolean) => void
setPicker: (sessionId: string, active: boolean) => void
close: (sessionId: string) => void
}
const empty = (url: string): BrowserSessionState => ({
isOpen: true, url, history: [url], historyIndex: 0,
loading: false, pickerActive: false, canGoBack: false, canGoForward: false,
})
const withNav = (s: BrowserSessionState): BrowserSessionState => ({
...s,
url: s.history[s.historyIndex]!,
canGoBack: s.historyIndex > 0,
canGoForward: s.historyIndex < s.history.length - 1,
})
export const useBrowserPanelStore = create<BrowserPanelState>((set) => ({
bySession: {},
open: (sessionId, url) => set((st) => ({
bySession: { ...st.bySession, [sessionId]: empty(url) },
})),
navigate: (sessionId, url) => set((st) => {
const cur = st.bySession[sessionId] ?? empty(url)
const history = [...cur.history.slice(0, cur.historyIndex + 1), url]
return { bySession: { ...st.bySession, [sessionId]: withNav({ ...cur, isOpen: true, history, historyIndex: history.length - 1 }) } }
}),
goBack: (sessionId) => set((st) => {
const cur = st.bySession[sessionId]; if (!cur || cur.historyIndex <= 0) return st
return { bySession: { ...st.bySession, [sessionId]: withNav({ ...cur, historyIndex: cur.historyIndex - 1 }) } }
}),
goForward: (sessionId) => set((st) => {
const cur = st.bySession[sessionId]; if (!cur || cur.historyIndex >= cur.history.length - 1) return st
return { bySession: { ...st.bySession, [sessionId]: withNav({ ...cur, historyIndex: cur.historyIndex + 1 }) } }
}),
setLoading: (sessionId, loading) => set((st) => {
const cur = st.bySession[sessionId]; if (!cur) return st
return { bySession: { ...st.bySession, [sessionId]: { ...cur, loading } } }
}),
setPicker: (sessionId, active) => set((st) => {
const cur = st.bySession[sessionId]; if (!cur) return st
return { bySession: { ...st.bySession, [sessionId]: { ...cur, pickerActive: active } } }
}),
close: (sessionId) => set((st) => {
const cur = st.bySession[sessionId]; if (!cur) return st
return { bySession: { ...st.bySession, [sessionId]: { ...cur, isOpen: false, pickerActive: false } } }
}),
}))