import { create } from 'zustand' import { useWorkspacePanelStore } from './workspacePanelStore' export const MIN_BROWSER_ZOOM = 0.5 export const MAX_BROWSER_ZOOM = 1.5 export const DEFAULT_BROWSER_ZOOM = 1 export const BROWSER_ZOOM_STEP = 0.1 export function normalizeBrowserZoom(value: unknown): number { const numeric = typeof value === 'number' ? value : Number(value) if (!Number.isFinite(numeric)) return DEFAULT_BROWSER_ZOOM const clamped = Math.min(Math.max(numeric, MIN_BROWSER_ZOOM), MAX_BROWSER_ZOOM) return Math.round(clamped * 10) / 10 } export type BrowserSessionState = { isOpen: boolean url: string title: string history: string[] historyIndex: number loading: boolean pickerActive: boolean canGoBack: boolean canGoForward: boolean zoom: number } type BrowserPanelState = { bySession: Record open: (sessionId: string, url: string) => void ensureBlank: (sessionId: 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 setZoom: (sessionId: string, zoom: number) => void close: (sessionId: string) => void setNavigated: (sessionId: string, url: string, title: string) => void setReady: (sessionId: string) => void } const empty = (url = ''): BrowserSessionState => ({ isOpen: true, url, title: '', history: url ? [url] : [], historyIndex: url ? 0 : -1, loading: false, pickerActive: false, canGoBack: false, canGoForward: false, zoom: DEFAULT_BROWSER_ZOOM, }) const withNav = (s: BrowserSessionState): BrowserSessionState => ({ ...s, url: s.history[s.historyIndex] ?? s.url, canGoBack: s.historyIndex > 0, canGoForward: s.historyIndex < s.history.length - 1, }) export const useBrowserPanelStore = create((set) => ({ bySession: {}, open: (sessionId, url) => { set((st) => { const zoom = st.bySession[sessionId]?.zoom ?? DEFAULT_BROWSER_ZOOM return { bySession: { ...st.bySession, [sessionId]: { ...empty(url), zoom, loading: true } }, } }) // The browser is one mode of the unified workbench: opening a previewable // link / localhost url surfaces the workbench in BROWSER mode. Import is // one-directional (workspacePanelStore never imports this store), so no cycle. const workspacePanel = useWorkspacePanelStore.getState() workspacePanel.openPanel(sessionId) workspacePanel.setMode(sessionId, 'browser') }, ensureBlank: (sessionId) => set((st) => { const cur = st.bySession[sessionId] if (cur) { return { bySession: { ...st.bySession, [sessionId]: { ...cur, isOpen: true } } } } return { bySession: { ...st.bySession, [sessionId]: empty() } } }), navigate: (sessionId, url) => set((st) => { const cur = st.bySession[sessionId] ?? empty(url) const history = [...cur.history.slice(0, Math.max(0, cur.historyIndex + 1)), url] return { bySession: { ...st.bySession, [sessionId]: withNav({ ...cur, isOpen: true, loading: 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 } } } }), setZoom: (sessionId, zoom) => set((st) => { const cur = st.bySession[sessionId]; if (!cur) return st return { bySession: { ...st.bySession, [sessionId]: { ...cur, zoom: normalizeBrowserZoom(zoom) } } } }), close: (sessionId) => set((st) => { 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 } } } }), }))