diff --git a/desktop/src/stores/tabStore.ts b/desktop/src/stores/tabStore.ts new file mode 100644 index 00000000..3c531e6d --- /dev/null +++ b/desktop/src/stores/tabStore.ts @@ -0,0 +1,128 @@ +import { create } from 'zustand' +import { sessionsApi } from '../api/sessions' + +const TAB_STORAGE_KEY = 'cc-haha-open-tabs' + +export type Tab = { + sessionId: string + title: string + status: 'idle' | 'running' | 'error' +} + +type TabPersistence = { + openTabs: Array<{ sessionId: string; title: string }> + activeTabId: string | null +} + +type TabStore = { + tabs: Tab[] + activeTabId: string | null + + openTab: (sessionId: string, title: string) => void + closeTab: (sessionId: string) => void + setActiveTab: (sessionId: string) => void + updateTabTitle: (sessionId: string, title: string) => void + updateTabStatus: (sessionId: string, status: Tab['status']) => void + + saveTabs: () => void + restoreTabs: () => Promise +} + +export const useTabStore = create((set, get) => ({ + tabs: [], + activeTabId: null, + + openTab: (sessionId, title) => { + const { tabs } = get() + const existing = tabs.find((t) => t.sessionId === sessionId) + if (existing) { + set({ activeTabId: sessionId }) + } else { + set({ + tabs: [...tabs, { sessionId, title, status: 'idle' }], + activeTabId: sessionId, + }) + } + get().saveTabs() + }, + + closeTab: (sessionId) => { + const { tabs, activeTabId } = get() + const index = tabs.findIndex((t) => t.sessionId === sessionId) + if (index < 0) return + + const newTabs = tabs.filter((t) => t.sessionId !== sessionId) + let newActiveId = activeTabId + + if (activeTabId === sessionId) { + if (newTabs.length === 0) { + newActiveId = null + } else if (index >= newTabs.length) { + newActiveId = newTabs[newTabs.length - 1]!.sessionId + } else { + newActiveId = newTabs[index]!.sessionId + } + } + + set({ tabs: newTabs, activeTabId: newActiveId }) + get().saveTabs() + }, + + setActiveTab: (sessionId) => { + set({ activeTabId: sessionId }) + get().saveTabs() + }, + + updateTabTitle: (sessionId, title) => { + set((s) => ({ + tabs: s.tabs.map((t) => (t.sessionId === sessionId ? { ...t, title } : t)), + })) + get().saveTabs() + }, + + updateTabStatus: (sessionId, status) => { + set((s) => ({ + tabs: s.tabs.map((t) => (t.sessionId === sessionId ? { ...t, status } : t)), + })) + }, + + saveTabs: () => { + const { tabs, activeTabId } = get() + const data: TabPersistence = { + openTabs: tabs.map((t) => ({ sessionId: t.sessionId, title: t.title })), + activeTabId, + } + try { + localStorage.setItem(TAB_STORAGE_KEY, JSON.stringify(data)) + } catch { /* noop */ } + }, + + restoreTabs: async () => { + try { + const raw = localStorage.getItem(TAB_STORAGE_KEY) + if (!raw) return + + const data = JSON.parse(raw) as TabPersistence + if (!data.openTabs || data.openTabs.length === 0) return + + const { sessions } = await sessionsApi.list({ limit: 200 }) + const existingIds = new Set(sessions.map((s) => s.id)) + + const validTabs: Tab[] = data.openTabs + .filter((t) => existingIds.has(t.sessionId)) + .map((t) => ({ + sessionId: t.sessionId, + title: sessions.find((s) => s.id === t.sessionId)?.title || t.title, + status: 'idle' as const, + })) + + if (validTabs.length === 0) return + + const activeId = data.activeTabId && validTabs.some((t) => t.sessionId === data.activeTabId) + ? data.activeTabId + : validTabs[0]!.sessionId + + set({ tabs: validTabs, activeTabId: activeId }) + } catch { /* noop */ } + }, +}))