import { create } from 'zustand' import { sessionsApi, type BatchDeleteSessionsResponse, type BranchSessionResponse, type CreateSessionRepositoryOptions, } from '../api/sessions' import { useSessionRuntimeStore } from './sessionRuntimeStore' import { useTabStore } from './tabStore' import type { SessionListItem } from '../types/session' import type { PermissionMode } from '../types/settings' import { isPlaceholderSessionTitle } from '../lib/sessionTitle' type CreateSessionOptions = { repository?: CreateSessionRepositoryOptions permissionMode?: PermissionMode } type BranchSessionResult = Pick type SessionStore = { sessions: SessionListItem[] activeSessionId: string | null isLoading: boolean error: string | null isBatchMode: boolean selectedSessionIds: Set fetchSessions: (project?: string) => Promise createSession: (workDir?: string, options?: CreateSessionOptions) => Promise branchSession: ( sourceSessionId: string, targetMessageId: string, options?: { title?: string }, ) => Promise deleteSession: (id: string) => Promise deleteSessions: (ids: string[]) => Promise enterBatchMode: () => void exitBatchMode: () => void toggleSessionSelected: (id: string) => void selectSessions: (ids: string[]) => void deselectSessions: (ids: string[]) => void clearSessionSelection: () => void renameSession: (id: string, title: string) => Promise updateSessionTitle: (id: string, title: string) => void updateSessionPermissionMode: (id: string, mode: PermissionMode) => void setActiveSession: (id: string | null) => void } export const useSessionStore = create((set, get) => ({ sessions: [], activeSessionId: null, isLoading: false, error: null, isBatchMode: false, selectedSessionIds: new Set(), fetchSessions: async (project?: string) => { set({ isLoading: true, error: null }) try { const { sessions: raw } = await sessionsApi.list({ project, limit: 100 }) let syncedSessions: SessionListItem[] = [] set((state) => { const currentById = new Map(state.sessions.map((session) => [session.id, session])) // Deduplicate by session ID - keep the most recently modified entry. const byId = new Map() for (const s of raw) { const current = currentById.get(s.id) const candidate = preserveLocalTitle(current, s) const existing = byId.get(s.id) if (!existing || new Date(candidate.modifiedAt) > new Date(existing.modifiedAt)) { byId.set(s.id, candidate) } } const sessions = [...byId.values()] syncedSessions = sessions return { sessions, isLoading: false } }) syncOpenSessionTabTitles(syncedSessions) } catch (err) { set({ error: (err as Error).message, isLoading: false }) } }, createSession: async (workDir?: string, options?: CreateSessionOptions) => { const { sessionId: id, workDir: resolvedWorkDir } = await sessionsApi.create({ ...(workDir ? { workDir } : {}), ...(options?.repository ? { repository: options.repository } : {}), ...(options?.permissionMode ? { permissionMode: options.permissionMode } : {}), }) const now = new Date().toISOString() const optimisticSession: SessionListItem = { id, title: 'New Session', createdAt: now, modifiedAt: now, messageCount: 0, projectPath: '', workDir: resolvedWorkDir ?? workDir ?? null, projectRoot: resolvedWorkDir ?? workDir ?? null, workDirExists: true, permissionMode: options?.permissionMode, } set((state) => ({ sessions: state.sessions.some((session) => session.id === id) ? state.sessions : [optimisticSession, ...state.sessions], activeSessionId: id, })) void get().fetchSessions() return id }, branchSession: async (sourceSessionId, targetMessageId, options) => { const result = await sessionsApi.branch(sourceSessionId, { targetMessageId, ...(options?.title ? { title: options.title } : {}), }) const sourceSession = get().sessions.find((session) => session.id === sourceSessionId) const now = new Date().toISOString() const optimisticSession: SessionListItem = { id: result.sessionId, title: result.title || 'New Session', createdAt: now, modifiedAt: now, messageCount: 0, projectPath: sourceSession?.projectPath ?? '', projectRoot: sourceSession?.projectRoot ?? sourceSession?.workDir ?? result.workDir ?? null, workDir: result.workDir ?? sourceSession?.workDir ?? null, workDirExists: true, } set((state) => ({ sessions: state.sessions.some((session) => session.id === result.sessionId) ? state.sessions.map((session) => session.id === result.sessionId ? { ...session, ...optimisticSession } : session) : [optimisticSession, ...state.sessions], activeSessionId: result.sessionId, })) void get().fetchSessions() return { sessionId: result.sessionId, title: result.title, workDir: result.workDir, } }, deleteSession: async (id: string) => { await sessionsApi.delete(id) useSessionRuntimeStore.getState().clearSelection(id) set((s) => ({ sessions: s.sessions.filter((session) => session.id !== id), activeSessionId: s.activeSessionId === id ? null : s.activeSessionId, selectedSessionIds: removeIdsFromSet(s.selectedSessionIds, [id]), })) }, deleteSessions: async (ids: string[]) => { const sessionIds = [...new Set(ids)].filter(Boolean) const result = await sessionsApi.batchDelete(sessionIds) for (const id of result.successes) { useSessionRuntimeStore.getState().clearSelection(id) } set((s) => ({ sessions: s.sessions.filter((session) => !result.successes.includes(session.id)), activeSessionId: s.activeSessionId && result.successes.includes(s.activeSessionId) ? null : s.activeSessionId, selectedSessionIds: removeIdsFromSet(s.selectedSessionIds, result.successes), })) return result }, enterBatchMode: () => set({ isBatchMode: true }), exitBatchMode: () => set({ isBatchMode: false, selectedSessionIds: new Set() }), toggleSessionSelected: (id) => set((s) => { const selectedSessionIds = new Set(s.selectedSessionIds) if (selectedSessionIds.has(id)) { selectedSessionIds.delete(id) } else { selectedSessionIds.add(id) } return { selectedSessionIds } }), selectSessions: (ids) => set((s) => { const selectedSessionIds = new Set(s.selectedSessionIds) for (const id of ids) selectedSessionIds.add(id) return { selectedSessionIds } }), deselectSessions: (ids) => set((s) => ({ selectedSessionIds: removeIdsFromSet(s.selectedSessionIds, ids), })), clearSessionSelection: () => set({ selectedSessionIds: new Set() }), renameSession: async (id: string, title: string) => { await sessionsApi.rename(id, title) set((s) => ({ sessions: s.sessions.map((session) => session.id === id ? { ...session, title } : session, ), })) }, updateSessionTitle: (id, title) => { set((s) => ({ sessions: s.sessions.map((session) => session.id === id ? { ...session, title } : session, ), })) }, updateSessionPermissionMode: (id, mode) => { set((s) => ({ sessions: s.sessions.map((session) => session.id === id ? { ...session, permissionMode: mode } : session, ), })) }, setActiveSession: (id) => set({ activeSessionId: id }), })) function removeIdsFromSet(selected: Set, ids: string[]): Set { if (ids.length === 0) return selected const next = new Set(selected) for (const id of ids) next.delete(id) return next } function preserveLocalTitle( current: SessionListItem | undefined, incoming: SessionListItem, ): SessionListItem { if (!current) return incoming if (isPlaceholderSessionTitle(incoming.title) && !isPlaceholderSessionTitle(current.title)) { return { ...incoming, title: current.title } } return incoming } function syncOpenSessionTabTitles(sessions: SessionListItem[]): void { const titleById = new Map(sessions.map((session) => [session.id, session.title])) const { tabs, updateTabTitle } = useTabStore.getState() for (const tab of tabs) { if (tab.type !== 'session') continue const title = titleById.get(tab.sessionId) if (title && title !== tab.title) { updateTabTitle(tab.sessionId, title) } } }