import { create } from "zustand"; import type { StoreApi, UseBoundStore } from "zustand"; import { devtools } from "zustand/middleware"; import type { Session, Message, Part } from "@opencode-ai/sdk/v2"; import type { PermissionRequest, PermissionResponse } from "@/types/permission"; import type { QuestionRequest } from "@/types/question"; import type { SessionStore, AttachedFile, EditPermissionMode, SyntheticContextPart } from "./types/sessionTypes"; import { useSessionStore as useSessionManagementStore } from "./sessionStore"; import { useMessageStore } from "./messageStore"; import { useFileStore } from "./fileStore"; import { useContextStore } from "./contextStore"; import { usePermissionStore } from "./permissionStore"; import { useQuestionStore } from "./questionStore"; import { opencodeClient } from "@/lib/opencode/client"; import { useDirectoryStore } from "./useDirectoryStore"; import { useConfigStore } from "./useConfigStore"; import { useProjectsStore } from "./useProjectsStore"; import { useSessionFoldersStore } from "./useSessionFoldersStore"; import { getSafeStorage } from "./utils/safeStorage"; import { EXECUTION_FORK_META_TEXT } from "@/lib/messages/executionMeta"; import { markPendingUserSendAnimation } from "@/lib/userSendAnimation"; import { flattenAssistantTextParts } from "@/lib/messages/messageText"; import { normalizeMessageRecordsForProjection } from "./utils/messageProjectors"; import type { ProjectEntry } from "@/lib/api/types"; import type { WorktreeMetadata } from "@/types/worktree"; import { waitForWorktreeBootstrap } from "@/lib/worktrees/worktreeBootstrap"; import { waitForPendingDraftWorktreeRequest } from "@/lib/worktrees/pendingDraftWorktree"; export type { AttachedFile, EditPermissionMode }; export { MEMORY_LIMITS, ACTIVE_SESSION_WINDOW } from "./types/sessionTypes"; declare global { interface Window { __zustand_session_store__?: UseBoundStore>; } } const normalizePath = (value?: string | null): string | null => { if (typeof value !== "string") { return null; } const trimmed = value.trim(); if (!trimmed) { return null; } const replaced = trimmed.replace(/\\/g, "/"); if (replaced === "/") { return "/"; } return replaced.length > 1 ? replaced.replace(/\/+$/, "") : replaced; }; const sessionChoiceAnalysisSignature = new Map(); const DRAFT_TARGET_STORAGE_KEY = "oc.chatInput.lastDraftTarget"; type PersistedDraftTarget = { projectId: string | null; directory: string | null; }; const safeStorage = getSafeStorage(); const readPersistedDraftTarget = (): PersistedDraftTarget | null => { try { const raw = safeStorage.getItem(DRAFT_TARGET_STORAGE_KEY); if (!raw) { return null; } const parsed = JSON.parse(raw) as { projectId?: unknown; directory?: unknown }; return { projectId: typeof parsed?.projectId === "string" ? parsed.projectId : null, directory: normalizePath(typeof parsed?.directory === "string" ? parsed.directory : null), }; } catch { return null; } }; const persistDraftTarget = (target: PersistedDraftTarget): void => { try { safeStorage.setItem(DRAFT_TARGET_STORAGE_KEY, JSON.stringify(target)); } catch { // ignored } }; const resolveProjectForDirectory = (projects: ProjectEntry[], directory: string | null): ProjectEntry | null => { const normalizedDirectory = normalizePath(directory); if (!normalizedDirectory) { return null; } let bestMatch: ProjectEntry | null = null; for (const project of projects) { const projectPath = normalizePath(project.path); if (!projectPath) { continue; } const isExact = normalizedDirectory === projectPath; const isNested = normalizedDirectory.startsWith(`${projectPath}/`); if (!isExact && !isNested) { continue; } if (!bestMatch || projectPath.length > (normalizePath(bestMatch.path)?.length ?? 0)) { bestMatch = project; } } return bestMatch; }; const resolveProjectFromWorktreeDirectory = ( projects: ProjectEntry[], availableWorktreesByProject: Map, directory: string | null, ): ProjectEntry | null => { const normalizedDirectory = normalizePath(directory); if (!normalizedDirectory) { return null; } let matchedWorktree: WorktreeMetadata | null = null; let matchedProjectPath: string | null = null; let bestPathLength = -1; for (const [projectPath, worktrees] of availableWorktreesByProject.entries()) { for (const worktree of worktrees) { const worktreePath = normalizePath(worktree.path); if (!worktreePath) { continue; } const isExact = normalizedDirectory === worktreePath; const isNested = normalizedDirectory.startsWith(`${worktreePath}/`); if (!isExact && !isNested) { continue; } if (worktreePath.length > bestPathLength) { bestPathLength = worktreePath.length; matchedWorktree = worktree; matchedProjectPath = normalizePath(projectPath); } } } if (!matchedWorktree) { return null; } const normalizedMetadataProjectPath = normalizePath(matchedWorktree.projectDirectory); const candidates = [normalizedMetadataProjectPath, matchedProjectPath].filter((value): value is string => Boolean(value)); for (const candidatePath of candidates) { const exact = projects.find((project) => normalizePath(project.path) === candidatePath) ?? null; if (exact) { return exact; } const nested = resolveProjectForDirectory(projects, candidatePath); if (nested) { return nested; } } return null; }; const resolveDraftProjectForDirectory = ( projects: ProjectEntry[], availableWorktreesByProject: Map, directory: string | null, ): ProjectEntry | null => { return resolveProjectFromWorktreeDirectory(projects, availableWorktreesByProject, directory) ?? resolveProjectForDirectory(projects, directory); }; const buildSessionChoiceAnalysisSignature = (messages: Array<{ info: Message; parts: Part[] }>): string => { const lastMessage = messages[messages.length - 1]; const lastMessageId = typeof lastMessage?.info?.id === 'string' ? lastMessage.info.id : ''; const lastAssistant = [...messages] .reverse() .find((message) => message.info?.role === 'assistant'); const lastAssistantId = typeof lastAssistant?.info?.id === 'string' ? lastAssistant.info.id : ''; return `${messages.length}:${lastMessageId}:${lastAssistantId}`; }; const resolveSessionDirectory = ( sessions: Session[], sessionId: string | null | undefined, getWorktreeMetadata: (id: string) => { path?: string } | undefined, ): string | null => { if (!sessionId) { return null; } const metadataPath = getWorktreeMetadata(sessionId)?.path; if (typeof metadataPath === "string" && metadataPath.trim().length > 0) { return normalizePath(metadataPath); } const target = sessions.find((session) => session.id === sessionId) as { directory?: string | null } | undefined; if (!target) { return null; } return normalizePath(target.directory ?? null); }; export const useSessionStore = create()( devtools( (set, get) => ({ sessions: [], archivedSessions: [], sessionsByDirectory: new Map(), currentSessionId: null, lastLoadedDirectory: null, messages: new Map(), sessionMemoryState: new Map(), sessionHistoryMeta: new Map(), messageStreamStates: new Map(), sessionCompactionUntil: new Map(), sessionAbortFlags: new Map(), permissions: new Map(), questions: new Map(), attachedFiles: [], isLoading: false, error: null, streamingMessageIds: new Map(), abortControllers: new Map(), lastUsedProvider: null, isSyncing: false, sessionModelSelections: new Map(), sessionAgentSelections: new Map(), sessionAgentModelSelections: new Map(), webUICreatedSessions: new Set(), worktreeMetadata: new Map(), availableWorktrees: [], availableWorktreesByProject: new Map(), currentAgentContext: new Map(), sessionContextUsage: new Map(), sessionAgentEditModes: new Map(), abortPromptSessionId: null, abortPromptExpiresAt: null, sessionStatus: new Map(), sessionAttentionStates: new Map(), userSummaryTitles: new Map(), pendingInputText: null, pendingInputMode: 'replace', pendingSyntheticParts: null, newSessionDraft: { open: true, selectedProjectId: null, directoryOverride: null, pendingWorktreeRequestId: null, bootstrapPendingDirectory: null, preserveDirectoryOverride: false, parentID: null }, // Voice state (initialized to disconnected/idle) voiceStatus: 'disconnected', voiceMode: 'idle', // Voice actions setVoiceStatus: (status: import("./types/sessionTypes").VoiceStatus) => { set({ voiceStatus: status }); }, setVoiceMode: (mode: import("./types/sessionTypes").VoiceMode) => { set({ voiceMode: mode }); }, getSessionAgentEditMode: (sessionId: string, agentName: string | undefined, defaultMode?: EditPermissionMode) => { return useContextStore.getState().getSessionAgentEditMode(sessionId, agentName, defaultMode); }, toggleSessionAgentEditMode: (sessionId: string, agentName: string | undefined, defaultMode?: EditPermissionMode) => { return useContextStore.getState().toggleSessionAgentEditMode(sessionId, agentName, defaultMode); }, setSessionAgentEditMode: (sessionId: string, agentName: string | undefined, mode: EditPermissionMode, defaultMode?: EditPermissionMode) => { return useContextStore.getState().setSessionAgentEditMode(sessionId, agentName, mode, defaultMode); }, loadSessions: () => useSessionManagementStore.getState().loadSessions(), openNewSessionDraft: (options) => { const projectsState = useProjectsStore.getState(); const projects = projectsState.projects; const availableWorktreesByProject = get().availableWorktreesByProject; const activeProject = projectsState.getActiveProject(); const currentDirectory = normalizePath(useDirectoryStore.getState().currentDirectory ?? null); const persistedTarget = readPersistedDraftTarget(); const explicitDirectory = options?.directoryOverride !== undefined ? normalizePath(options.directoryOverride) : null; const explicitProject = options?.projectId ? projects.find((project) => project.id === options.projectId) ?? null : null; const inferredProjectFromDirectory = resolveDraftProjectForDirectory(projects, availableWorktreesByProject, explicitDirectory); const fallbackProject = (() => { if (activeProject) { return activeProject; } if (projectsState.activeProjectId) { return projects.find((project) => project.id === projectsState.activeProjectId) ?? null; } return projects[0] ?? null; })(); const persistedProjectById = persistedTarget?.projectId ? projects.find((project) => project.id === persistedTarget.projectId) ?? null : null; const persistedProjectByDirectory = resolveDraftProjectForDirectory(projects, availableWorktreesByProject, persistedTarget?.directory ?? null); const currentDirectoryProject = resolveDraftProjectForDirectory(projects, availableWorktreesByProject, currentDirectory); const selectedProject = (() => { if (explicitProject || explicitDirectory !== null) { return explicitProject ?? inferredProjectFromDirectory ?? fallbackProject; } if (currentDirectory) { return currentDirectoryProject ?? fallbackProject; } return persistedProjectByDirectory ?? persistedProjectById ?? fallbackProject; })(); const directory = (() => { if (explicitDirectory !== null) { return explicitDirectory; } if (explicitProject) { return normalizePath(explicitProject.path ?? null); } if (currentDirectory) { return currentDirectory; } if (persistedTarget?.directory) { return persistedTarget.directory; } return normalizePath(selectedProject?.path ?? null); })(); persistDraftTarget({ projectId: selectedProject?.id ?? null, directory, }); set({ newSessionDraft: { open: true, selectedProjectId: selectedProject?.id ?? null, directoryOverride: directory, pendingWorktreeRequestId: options?.pendingWorktreeRequestId ?? null, bootstrapPendingDirectory: normalizePath(options?.bootstrapPendingDirectory ?? null), preserveDirectoryOverride: options?.preserveDirectoryOverride === true, parentID: options?.parentID ?? null, title: options?.title, initialPrompt: options?.initialPrompt, syntheticParts: options?.syntheticParts, targetFolderId: options?.targetFolderId, }, currentSessionId: null, error: null, // Set pending input text if initialPrompt is provided ...(options?.initialPrompt ? { pendingInputText: options.initialPrompt, pendingInputMode: 'replace' as const } : {}), }); try { const configState = useConfigStore.getState(); const visibleAgents = configState.getVisibleAgents(); // Priority: settingsDefaultAgent → build → first visible let agentName: string | undefined; if (configState.settingsDefaultAgent) { const settingsAgent = visibleAgents.find((a) => a.name === configState.settingsDefaultAgent); if (settingsAgent) { agentName = settingsAgent.name; } } if (!agentName) { agentName = visibleAgents.find((agent) => agent.name === 'build')?.name || visibleAgents[0]?.name; } if (agentName) { configState.setAgent(agentName); } } catch { // ignored } }, overrideNewSessionDraftTarget: (options) => { const projectsState = useProjectsStore.getState(); const projects = projectsState.projects; const availableWorktreesByProject = get().availableWorktreesByProject; const explicitDirectory = normalizePath(options?.directoryOverride ?? null); const explicitProject = options?.projectId ? projects.find((project) => project.id === options.projectId) ?? null : null; const inferredProject = resolveDraftProjectForDirectory(projects, availableWorktreesByProject, explicitDirectory); const fallbackProject = explicitProject ?? inferredProject ?? projectsState.getActiveProject() ?? projects[0] ?? null; const selectedProject = explicitProject ?? inferredProject ?? fallbackProject; const nextDirectory = explicitDirectory ?? normalizePath(selectedProject?.path ?? null); persistDraftTarget({ projectId: selectedProject?.id ?? null, directory: nextDirectory, }); set((state) => { const previousDraft = state.newSessionDraft; const hasPendingWorktreeRequestId = Object.prototype.hasOwnProperty.call(options, 'pendingWorktreeRequestId'); const hasBootstrapPendingDirectory = Object.prototype.hasOwnProperty.call(options, 'bootstrapPendingDirectory'); return { newSessionDraft: { ...previousDraft, open: true, selectedProjectId: selectedProject?.id ?? null, directoryOverride: nextDirectory, pendingWorktreeRequestId: hasPendingWorktreeRequestId ? (options.pendingWorktreeRequestId ?? null) : (previousDraft.pendingWorktreeRequestId ?? null), bootstrapPendingDirectory: hasBootstrapPendingDirectory ? normalizePath(options.bootstrapPendingDirectory ?? null) : (previousDraft.bootstrapPendingDirectory ?? null), preserveDirectoryOverride: options?.preserveDirectoryOverride === true, title: options?.title ?? previousDraft.title, initialPrompt: options?.initialPrompt ?? previousDraft.initialPrompt, }, currentSessionId: null, error: null, ...(options?.initialPrompt ? { pendingInputText: options.initialPrompt, pendingInputMode: 'replace' as const } : {}), }; }); }, setNewSessionDraftTarget: ({ projectId, directoryOverride }, options) => { const projects = useProjectsStore.getState().projects; const project = projectId ? projects.find((entry) => entry.id === projectId) ?? null : null; const normalizedDirectory = normalizePath(directoryOverride); const normalizedProjectPath = normalizePath(project?.path ?? null); const nextDirectory = normalizedDirectory ?? normalizedProjectPath ?? null; let didUpdate = false; set((state) => { if (!state.newSessionDraft?.open) { return state; } if ( options?.force !== true && ( state.newSessionDraft.pendingWorktreeRequestId || state.newSessionDraft.bootstrapPendingDirectory || state.newSessionDraft.preserveDirectoryOverride ) ) { return state; } didUpdate = true; return { newSessionDraft: { ...state.newSessionDraft, selectedProjectId: project?.id ?? null, directoryOverride: nextDirectory, pendingWorktreeRequestId: null, bootstrapPendingDirectory: null, preserveDirectoryOverride: false, parentID: null, }, }; }); if (didUpdate) { persistDraftTarget({ projectId: project?.id ?? null, directory: nextDirectory, }); } }, closeNewSessionDraft: () => { const realCurrentSessionId = useSessionManagementStore.getState().currentSessionId; set({ newSessionDraft: { open: false, selectedProjectId: null, directoryOverride: null, pendingWorktreeRequestId: null, bootstrapPendingDirectory: null, preserveDirectoryOverride: false, parentID: null, title: undefined, initialPrompt: undefined, syntheticParts: undefined, targetFolderId: undefined }, currentSessionId: realCurrentSessionId, }); }, setPendingDraftWorktreeRequest: (requestId) => { set((state) => { if (!state.newSessionDraft?.open) { return state; } return { newSessionDraft: { ...state.newSessionDraft, pendingWorktreeRequestId: requestId, }, }; }); }, resolvePendingDraftWorktreeTarget: (requestId, directory, options) => { set((state) => { if (!state.newSessionDraft?.open || state.newSessionDraft.pendingWorktreeRequestId !== requestId) { return state; } return { newSessionDraft: { ...state.newSessionDraft, selectedProjectId: options?.projectId ?? state.newSessionDraft.selectedProjectId ?? null, directoryOverride: normalizePath(directory), pendingWorktreeRequestId: null, bootstrapPendingDirectory: normalizePath(options?.bootstrapPendingDirectory ?? state.newSessionDraft.bootstrapPendingDirectory ?? null), preserveDirectoryOverride: options?.preserveDirectoryOverride ?? true, }, }; }); }, setDraftBootstrapPendingDirectory: (directory) => { set((state) => { if (!state.newSessionDraft?.open) { return state; } return { newSessionDraft: { ...state.newSessionDraft, bootstrapPendingDirectory: normalizePath(directory), }, }; }); }, setDraftPreserveDirectoryOverride: (value) => { set((state) => { if (!state.newSessionDraft?.open) { return state; } return { newSessionDraft: { ...state.newSessionDraft, preserveDirectoryOverride: value, }, }; }); }, createSession: async (title?: string, directoryOverride?: string | null, parentID?: string | null) => { const draft = get().newSessionDraft; const targetFolderId = draft.targetFolderId; get().closeNewSessionDraft(); const result = await useSessionManagementStore.getState().createSession(title, directoryOverride, parentID); if (result?.id) { await get().setCurrentSession(result.id); const finalScopeKey = directoryOverride || get().lastLoadedDirectory || result.directory; if (targetFolderId && finalScopeKey) { useSessionFoldersStore.getState().addSessionToFolder(finalScopeKey, targetFolderId, result.id); } } return result; }, createSessionFromAssistantMessage: async (sourceMessageId: string) => { if (!sourceMessageId) { return; } const messageStore = useMessageStore.getState(); const { messages, lastUsedProvider } = messageStore; let sourceEntry: { info: Message; parts: Part[] } | undefined; let sourceSessionId: string | undefined; messages.forEach((messageList, sessionId) => { const found = messageList.find((entry) => entry.info?.id === sourceMessageId); if (found && !sourceEntry) { sourceEntry = found; sourceSessionId = sessionId; } }); if (!sourceEntry || sourceEntry.info.role !== "assistant") { return; } const assistantPlanText = flattenAssistantTextParts(sourceEntry.parts); if (!assistantPlanText.trim()) { return; } const sessionManagementStore = useSessionManagementStore.getState(); const directory = resolveSessionDirectory( sessionManagementStore.sessions, sourceSessionId ?? null, sessionManagementStore.getWorktreeMetadata, ); const session = await get().createSession(undefined, directory ?? null, null); if (!session) { return; } const { currentProviderId, currentModelId, currentAgentName } = useConfigStore.getState(); const providerID = currentProviderId || lastUsedProvider?.providerID; const modelID = currentModelId || lastUsedProvider?.modelID; if (!providerID || !modelID) { return; } await opencodeClient.sendMessage({ id: session.id, providerID, modelID, text: assistantPlanText, prefaceText: EXECUTION_FORK_META_TEXT, agent: currentAgentName ?? undefined, }); }, deleteSession: (id: string, options) => useSessionManagementStore.getState().deleteSession(id, options), deleteSessions: (ids: string[], options) => useSessionManagementStore.getState().deleteSessions(ids, options), archiveSession: (id: string) => useSessionManagementStore.getState().archiveSession(id), archiveSessions: (ids: string[], options) => useSessionManagementStore.getState().archiveSessions(ids, options), updateSessionTitle: (id: string, title: string) => useSessionManagementStore.getState().updateSessionTitle(id, title), shareSession: (id: string) => useSessionManagementStore.getState().shareSession(id), unshareSession: (id: string) => useSessionManagementStore.getState().unshareSession(id), setCurrentSession: async (id: string | null) => { if (id) { get().closeNewSessionDraft(); } const previousSessionId = useSessionManagementStore.getState().currentSessionId; const sessionDirectory = resolveSessionDirectory( useSessionManagementStore.getState().sessions, id, useSessionManagementStore.getState().getWorktreeMetadata ); const fallbackDirectory = opencodeClient.getDirectory() ?? useDirectoryStore.getState().currentDirectory ?? null; const resolvedDirectory = sessionDirectory ?? fallbackDirectory; try { opencodeClient.setDirectory(resolvedDirectory ?? undefined); } catch (error) { console.warn("Failed to set OpenCode directory for session switch:", error); } if (previousSessionId && previousSessionId !== id) { const memoryState = get().sessionMemoryState.get(previousSessionId); if (!memoryState?.isStreaming) { const previousMessages = get().messages.get(previousSessionId) || []; if (previousMessages.length > 0) { get().updateViewportAnchor(previousSessionId, previousMessages.length - 1); } } } useSessionManagementStore.getState().setCurrentSession(id); if (id) { const existingMessages = get().messages.get(id); const historyMeta = get().sessionHistoryMeta.get(id); const needsHistoryBootstrap = !historyMeta || typeof historyMeta.complete !== 'boolean'; if (!existingMessages || needsHistoryBootstrap) { await get().loadMessages(id); } // Analyze session messages to extract agent/model/variant choices // This ensures context is available even when ModelControls isn't mounted const sessionMessages = get().messages.get(id); if (sessionMessages && sessionMessages.length > 0) { const agents = useConfigStore.getState().agents; if (agents.length > 0) { const analysisSignature = buildSessionChoiceAnalysisSignature(sessionMessages); if (sessionChoiceAnalysisSignature.get(id) === analysisSignature) { return; } try { await useContextStore.getState().analyzeAndSaveExternalSessionChoices( id, agents, get().messages ); sessionChoiceAnalysisSignature.set(id, analysisSignature); } catch (error) { console.warn('Failed to analyze session choices:', error); } } } } }, loadMessages: (sessionId: string, limit?: number) => useMessageStore.getState().loadMessages(sessionId, limit), sendMessage: async (content: string, providerID: string, modelID: string, agent?: string, attachments?: AttachedFile[], agentMentionName?: string, additionalParts?: Array<{ text: string; attachments?: AttachedFile[]; synthetic?: boolean }>, variant?: string, inputMode: 'normal' | 'shell' = 'normal') => { const draft = get().newSessionDraft; const trimmedAgent = typeof agent === 'string' && agent.trim().length > 0 ? agent.trim() : undefined; const setStatus = (sessionId: string, type: 'idle' | 'busy') => { set((state) => { const next = new Map(state.sessionStatus ?? new Map()); next.set(sessionId, { type }); return { sessionStatus: next }; }); }; if (draft?.open) { const draftTargetFolderId = draft.targetFolderId; let draftDirectoryOverride = draft.bootstrapPendingDirectory ?? draft.directoryOverride ?? null; const draftProjectId = draft.selectedProjectId ?? null; if (draft.pendingWorktreeRequestId) { draftDirectoryOverride = await waitForPendingDraftWorktreeRequest(draft.pendingWorktreeRequestId); get().resolvePendingDraftWorktreeTarget(draft.pendingWorktreeRequestId, draftDirectoryOverride); } const created = await useSessionManagementStore .getState() .createSession(draft.title, draftDirectoryOverride, draft.parentID ?? null); if (!created?.id) { throw new Error('Failed to create session'); } persistDraftTarget({ projectId: draftProjectId, directory: normalizePath(draftDirectoryOverride ?? created.directory ?? null), }); const configState = useConfigStore.getState(); const draftAgentName = configState.currentAgentName; const effectiveDraftAgent = trimmedAgent ?? draftAgentName; const draftProviderId = configState.currentProviderId; const draftModelId = configState.currentModelId; if (draftProviderId && draftModelId) { try { useContextStore.getState().saveSessionModelSelection(created.id, draftProviderId, draftModelId); } catch { // ignored } } if (effectiveDraftAgent) { try { useContextStore.getState().saveSessionAgentSelection(created.id, effectiveDraftAgent); } catch { // ignored } if (draftProviderId && draftModelId) { try { useContextStore .getState() .saveAgentModelForSession(created.id, effectiveDraftAgent, draftProviderId, draftModelId); } catch { // ignored } try { useContextStore .getState() .saveAgentModelVariantForSession(created.id, effectiveDraftAgent, draftProviderId, draftModelId, variant); } catch { // ignored } } } try { useSessionManagementStore .getState() .initializeNewOpenChamberSession(created.id, configState.agents); } catch { // ignored } // Capture synthetic parts before clearing draft const draftSyntheticParts = draft.syntheticParts; get().closeNewSessionDraft(); await get().setCurrentSession(created.id); // Assign to target folder if session was created from folder's + button if (draftTargetFolderId) { const scopeKey = draftDirectoryOverride || created.directory || null; if (scopeKey) { useSessionFoldersStore.getState().addSessionToFolder(scopeKey, draftTargetFolderId, created.id); } } setStatus(created.id, 'busy'); // Merge draft synthetic parts with any additional parts passed to sendMessage const mergedAdditionalParts = draftSyntheticParts?.length ? [...(additionalParts || []), ...draftSyntheticParts] : additionalParts; const createdDirectory = normalizePath(draftDirectoryOverride ?? created.directory ?? null); if (createdDirectory) { await waitForWorktreeBootstrap(createdDirectory); } try { markPendingUserSendAnimation(created.id); return await useMessageStore .getState() .sendMessage(content, providerID, modelID, effectiveDraftAgent, created.id, attachments, agentMentionName, mergedAdditionalParts, variant, inputMode); } catch (error) { setStatus(created.id, 'idle'); throw error; } } const currentSessionId = useSessionManagementStore.getState().currentSessionId; const sessionAgentSelection = currentSessionId ? useContextStore.getState().getSessionAgentSelection(currentSessionId) : null; const configAgentName = useConfigStore.getState().currentAgentName; const effectiveAgent = trimmedAgent || sessionAgentSelection || configAgentName || undefined; if (currentSessionId && effectiveAgent) { try { useContextStore.getState().saveSessionAgentSelection(currentSessionId, effectiveAgent); } catch { // ignored } try { useContextStore .getState() .saveAgentModelVariantForSession(currentSessionId, effectiveAgent, providerID, modelID, variant); } catch { // ignored } } if (currentSessionId) { setStatus(currentSessionId, 'busy'); const memoryState = get().sessionMemoryState.get(currentSessionId); if (!memoryState || !memoryState.lastUserMessageAt) { const currentMemoryState = get().sessionMemoryState; const newMemoryState = new Map(currentMemoryState); newMemoryState.set(currentSessionId, { viewportAnchor: memoryState?.viewportAnchor ?? 0, isStreaming: memoryState?.isStreaming ?? false, lastAccessedAt: Date.now(), backgroundMessageCount: memoryState?.backgroundMessageCount ?? 0, lastUserMessageAt: Date.now(), }); set({ sessionMemoryState: newMemoryState }); } } const currentSessionDirectory = currentSessionId ? normalizePath(useSessionManagementStore.getState().getDirectoryForSession(currentSessionId)) : null; if (currentSessionDirectory) { await waitForWorktreeBootstrap(currentSessionDirectory); } // Notify server that user sent a message in this session if (currentSessionId) { fetch(`/api/sessions/${currentSessionId}/message-sent`, { method: 'POST' }) .catch(() => { /* ignore */ }); } try { if (currentSessionId) { markPendingUserSendAnimation(currentSessionId); } return await useMessageStore.getState().sendMessage(content, providerID, modelID, effectiveAgent, currentSessionId || undefined, attachments, agentMentionName, additionalParts, variant, inputMode); } catch (error) { if (currentSessionId) { setStatus(currentSessionId, 'idle'); } throw error; } }, abortCurrentOperation: (sessionIdOverride?: string) => { const sessionId = sessionIdOverride || useSessionManagementStore.getState().currentSessionId; return useMessageStore.getState().abortCurrentOperation(sessionId || undefined); }, armAbortPrompt: (durationMs = 3000) => { const sessionId = useSessionManagementStore.getState().currentSessionId; if (!sessionId) { return null; } const expiresAt = Date.now() + durationMs; set({ abortPromptSessionId: sessionId, abortPromptExpiresAt: expiresAt }); return expiresAt; }, clearAbortPrompt: () => { set({ abortPromptSessionId: null, abortPromptExpiresAt: null }); }, acknowledgeSessionAbort: (sessionId: string) => { if (!sessionId) { return; } useMessageStore.getState().acknowledgeSessionAbort(sessionId); }, addStreamingPart: (sessionId: string, messageId: string, part: Part, role?: string) => { const currentSessionId = useSessionManagementStore.getState().currentSessionId; const effectiveCurrent = currentSessionId || sessionId; return useMessageStore.getState().addStreamingPart(sessionId, messageId, part, role, effectiveCurrent); }, applyPartDelta: (sessionId: string, messageId: string, partId: string, field: string, delta: string, role?: string) => { const currentSessionId = useSessionManagementStore.getState().currentSessionId; const effectiveCurrent = currentSessionId || sessionId; return useMessageStore.getState().applyPartDelta(sessionId, messageId, partId, field, delta, role, effectiveCurrent); }, completeStreamingMessage: (sessionId: string, messageId: string) => useMessageStore.getState().completeStreamingMessage(sessionId, messageId), markMessageStreamSettled: (messageId: string) => useMessageStore.getState().markMessageStreamSettled(messageId), updateMessageInfo: (sessionId: string, messageId: string, messageInfo: Record) => useMessageStore.getState().updateMessageInfo(sessionId, messageId, messageInfo), updateSessionCompaction: (sessionId: string, compactingTimestamp?: number | null) => useMessageStore.getState().updateSessionCompaction(sessionId, compactingTimestamp ?? null), addPermission: (permission: PermissionRequest) => { return usePermissionStore.getState().addPermission(permission); }, respondToPermission: (sessionId: string, requestId: string, response: PermissionResponse) => usePermissionStore.getState().respondToPermission(sessionId, requestId, response), dismissPermission: (sessionId: string, requestId: string) => usePermissionStore.getState().dismissPermission(sessionId, requestId), addQuestion: (question: QuestionRequest) => useQuestionStore.getState().addQuestion(question), dismissQuestion: (sessionId: string, requestId: string) => useQuestionStore.getState().dismissQuestion(sessionId, requestId), respondToQuestion: (sessionId: string, requestId: string, answers: string[] | string[][]) => useQuestionStore.getState().respondToQuestion(sessionId, requestId, answers), rejectQuestion: (sessionId: string, requestId: string) => useQuestionStore.getState().rejectQuestion(sessionId, requestId), clearError: () => useSessionManagementStore.getState().clearError(), getSessionsByDirectory: (directory: string) => useSessionManagementStore.getState().getSessionsByDirectory(directory), getDirectoryForSession: (sessionId: string) => useSessionManagementStore.getState().getDirectoryForSession(sessionId), getLastMessageModel: (sessionId: string) => useMessageStore.getState().getLastMessageModel(sessionId), getCurrentAgent: (sessionId: string) => useContextStore.getState().getCurrentAgent(sessionId), syncMessages: ( sessionId: string, messages: { info: Message; parts: Part[] }[], options?: { replace?: boolean } ) => useMessageStore.getState().syncMessages(sessionId, messages, options), applySessionMetadata: (sessionId: string, metadata: Partial) => useSessionManagementStore.getState().applySessionMetadata(sessionId, metadata), addAttachedFile: (file: File) => useFileStore.getState().addAttachedFile(file), addServerFile: (path: string, name: string, content?: string) => useFileStore.getState().addServerFile(path, name, content), removeAttachedFile: (id: string) => useFileStore.getState().removeAttachedFile(id), clearAttachedFiles: () => useFileStore.getState().clearAttachedFiles(), updateViewportAnchor: (sessionId: string, anchor: number) => useMessageStore.getState().updateViewportAnchor(sessionId, anchor), loadMoreMessages: (sessionId: string, direction: "up" | "down") => useMessageStore.getState().loadMoreMessages(sessionId, direction), saveSessionModelSelection: (sessionId: string, providerId: string, modelId: string) => useContextStore.getState().saveSessionModelSelection(sessionId, providerId, modelId), getSessionModelSelection: (sessionId: string) => useContextStore.getState().getSessionModelSelection(sessionId), saveSessionAgentSelection: (sessionId: string, agentName: string) => useContextStore.getState().saveSessionAgentSelection(sessionId, agentName), getSessionAgentSelection: (sessionId: string) => useContextStore.getState().getSessionAgentSelection(sessionId), saveAgentModelForSession: (sessionId: string, agentName: string, providerId: string, modelId: string) => useContextStore.getState().saveAgentModelForSession(sessionId, agentName, providerId, modelId), getAgentModelForSession: (sessionId: string, agentName: string) => useContextStore.getState().getAgentModelForSession(sessionId, agentName), saveAgentModelVariantForSession: (sessionId: string, agentName: string, providerId: string, modelId: string, variant: string | undefined) => useContextStore.getState().saveAgentModelVariantForSession(sessionId, agentName, providerId, modelId, variant), getAgentModelVariantForSession: (sessionId: string, agentName: string, providerId: string, modelId: string) => useContextStore.getState().getAgentModelVariantForSession(sessionId, agentName, providerId, modelId), analyzeAndSaveExternalSessionChoices: (sessionId: string, agents: Record[]) => { const messages = useMessageStore.getState().messages; return useContextStore.getState().analyzeAndSaveExternalSessionChoices(sessionId, agents, messages); }, isOpenChamberCreatedSession: (sessionId: string) => useSessionManagementStore.getState().isOpenChamberCreatedSession(sessionId), markSessionAsOpenChamberCreated: (sessionId: string) => useSessionManagementStore.getState().markSessionAsOpenChamberCreated(sessionId), initializeNewOpenChamberSession: (sessionId: string, agents: Record[]) => useSessionManagementStore.getState().initializeNewOpenChamberSession(sessionId, agents), setWorktreeMetadata: (sessionId: string, metadata) => useSessionManagementStore.getState().setWorktreeMetadata(sessionId, metadata), setSessionDirectory: (sessionId: string, directory: string | null) => useSessionManagementStore.getState().setSessionDirectory(sessionId, directory), getWorktreeMetadata: (sessionId: string) => useSessionManagementStore.getState().getWorktreeMetadata(sessionId), getContextUsage: (contextLimit: number, outputLimit: number) => { if (get().newSessionDraft?.open) { return null; } const currentSessionId = useSessionManagementStore.getState().currentSessionId; if (!currentSessionId) return null; const messages = useMessageStore.getState().messages; return useContextStore.getState().getContextUsage(currentSessionId, contextLimit, outputLimit, messages); }, updateSessionContextUsage: (sessionId: string, contextLimit: number, outputLimit: number) => { const messages = useMessageStore.getState().messages; return useContextStore.getState().updateSessionContextUsage(sessionId, contextLimit, outputLimit, messages); }, initializeSessionContextUsage: (sessionId: string, contextLimit: number, outputLimit: number) => { const messages = useMessageStore.getState().messages; return useContextStore.getState().initializeSessionContextUsage(sessionId, contextLimit, outputLimit, messages); }, debugSessionMessages: async (sessionId: string) => { const messages = normalizeMessageRecordsForProjection( useMessageStore.getState().messages.get(sessionId) || [] ); const session = useSessionManagementStore.getState().sessions.find(s => s.id === sessionId); console.log(`Debug session ${sessionId}:`, { session, messageCount: messages.length, messages: messages.map(m => ({ id: m.info.id, role: m.info.role, parts: m.parts.length, tokens: (m.info as Record).tokens })) }); }, pollForTokenUpdates: (sessionId: string, messageId: string, maxAttempts?: number) => { const messages = useMessageStore.getState().messages; return useContextStore.getState().pollForTokenUpdates(sessionId, messageId, messages, maxAttempts); }, updateSession: (session: Session) => useSessionManagementStore.getState().updateSession(session), removeSessionFromStore: (sessionId: string) => useSessionManagementStore.getState().removeSessionFromStore(sessionId), revertToMessage: async (sessionId: string, messageId: string) => { // Get the message text before reverting const messages = useMessageStore.getState().messages.get(sessionId) || []; const targetMessage = messages.find((m) => m.info.id === messageId); let messageText = ''; if (targetMessage && targetMessage.info.role === 'user') { // Extract text from user message parts const textParts = targetMessage.parts.filter((p) => p.type === 'text'); messageText = textParts .map((p) => { const part = p as { text?: string; content?: string }; return part.text || part.content || ''; }) .join('\n') .trim(); } // Call revert API const updatedSession = await opencodeClient.revertSession(sessionId, messageId); // Update session in store (this stores the revert.messageID) useSessionManagementStore.getState().updateSession(updatedSession); // Filter out reverted messages from the store // Messages with ID >= revert.messageID should be removed const currentMessages = useMessageStore.getState().messages.get(sessionId) || []; const revertMessageId = updatedSession.revert?.messageID; if (revertMessageId) { // Keep only messages before the revert point. // Fallback to the originally clicked message if SDK returns an id // that is not loaded in the current in-memory window. let revertIndex = currentMessages.findIndex((m) => m.info.id === revertMessageId); if (revertIndex === -1) { revertIndex = currentMessages.findIndex((m) => m.info.id === messageId); } if (revertIndex !== -1) { const filteredMessages = currentMessages.slice(0, revertIndex); useMessageStore.getState().syncMessages(sessionId, filteredMessages, { replace: true }); } } // Set pending input text for ChatInput to consume if (messageText) { set({ pendingInputText: messageText, pendingInputMode: 'replace' }); } }, handleSlashUndo: async (sessionId: string) => { const messages = get().messages.get(sessionId) || []; const userMessages = messages.filter(m => m.info.role === 'user'); const sessions = get().sessions; const currentSession = sessions.find(s => s.id === sessionId); // No-op when there is nothing to undo/redo if (userMessages.length === 0) { return; } // Get current revert state to determine which message to undo next const revertToId = currentSession?.revert?.messageID; // Find the user message AFTER the revert point (or last message if no revert) let targetMessage; if (revertToId) { const revertIndex = userMessages.findIndex(m => m.info.id === revertToId); targetMessage = userMessages[revertIndex + 1]; } else { targetMessage = userMessages[userMessages.length - 1]; } // No-op when there is nothing to undo/redo if (!targetMessage) { return; } // Helper to extract text preview const textPart = targetMessage.parts.find(p => p.type === 'text'); const preview = typeof textPart === 'object' && textPart && 'text' in textPart ? String(textPart.text).slice(0, 50) + (String(textPart.text).length > 50 ? '...' : '') : '[No text]'; await get().revertToMessage(sessionId, targetMessage.info.id); const { toast } = await import('sonner'); toast.success(`Undid to: ${preview}`); }, handleSlashRedo: async (sessionId: string) => { const sessions = get().sessions; const currentSession = sessions.find(s => s.id === sessionId); const revertToId = currentSession?.revert?.messageID; // No-op when there is nothing to undo/redo if (!revertToId) { return; } const messages = get().messages.get(sessionId) || []; const userMessages = messages.filter(m => m.info.role === 'user'); // Find the user message BEFORE the revert point const revertIndex = userMessages.findIndex(m => m.info.id === revertToId); const targetMessage = userMessages[revertIndex - 1]; if (targetMessage) { // Partial redo: move to previous message const textPart = targetMessage.parts.find(p => p.type === 'text'); const preview = typeof textPart === 'object' && textPart && 'text' in textPart ? String(textPart.text).slice(0, 50) + (String(textPart.text).length > 50 ? '...' : '') : '[No text]'; await get().revertToMessage(sessionId, targetMessage.info.id); const { toast } = await import('sonner'); toast.success(`Redid to: ${preview}`); } else { // Full unrevert: restore all const session = await opencodeClient.unrevertSession(sessionId); await useSessionManagementStore.getState().updateSession(session); await get().loadMessages(sessionId); const { toast } = await import('sonner'); toast.success('Restored all messages'); } }, forkFromMessage: async (sessionId: string, messageId: string) => { const sessions = get().sessions; const existingSession = sessions.find(s => s.id === sessionId); if (!existingSession) return; try { // 1. Call SDK fork - backend copies all messages up to messageId const result = await opencodeClient.forkSession(sessionId, messageId); if (!result || !result.id) { const { toast } = await import('sonner'); toast.error('Failed to fork session'); return; } // 2. Extract fork point content for input field (text + file attachments) const messages = get().messages.get(sessionId) || []; const message = messages.find(m => m.info.id === messageId); if (!message) { const { toast } = await import('sonner'); toast.error('Message not found'); return; } // Extract text content from non-synthetic, non-ignored text parts let inputText = ''; for (const part of message.parts) { if (part.type === 'text' && !part.synthetic && !part.ignored) { const typedPart = part as { text?: string }; inputText += typedPart.text || ''; } } // 3. Switch to new session get().setCurrentSession(result.id); // 4. Show fork point as pending input (will populate ChatInput) if (inputText) { set({ pendingInputText: inputText, pendingInputMode: 'replace' }); } // Load the new session's messages await get().loadMessages(result.id); const { toast } = await import('sonner'); toast.success(`Forked from ${existingSession.title}`); } catch (error) { console.error('Failed to fork session:', error); const { toast } = await import('sonner'); toast.error('Failed to fork session'); } }, setPendingInputText: (text: string | null, mode: 'replace' | 'append' | 'append-inline' = 'replace') => { set({ pendingInputText: text, pendingInputMode: mode }); }, consumePendingInputText: () => { const text = get().pendingInputText; const mode = get().pendingInputMode; if (text !== null) { set({ pendingInputText: null, pendingInputMode: 'replace' }); } if (text === null) { return null; } return { text, mode }; }, setPendingSyntheticParts: (parts: SyntheticContextPart[] | null) => { set({ pendingSyntheticParts: parts }); }, consumePendingSyntheticParts: () => { const parts = get().pendingSyntheticParts; if (parts !== null) { set({ pendingSyntheticParts: null }); } return parts; }, }), { name: "composed-session-store", } ), ); // rAF debounce IDs for useMessageStore -> useSessionStore sync let messageStoreSyncRafId: number | null = null; let userSummaryTitlesRafId: ReturnType | null = null; useSessionManagementStore.subscribe((state, prevState) => { if ( state.sessions === prevState.sessions && state.archivedSessions === prevState.archivedSessions && state.sessionsByDirectory === prevState.sessionsByDirectory && state.currentSessionId === prevState.currentSessionId && state.lastLoadedDirectory === prevState.lastLoadedDirectory && state.isLoading === prevState.isLoading && state.error === prevState.error && state.webUICreatedSessions === prevState.webUICreatedSessions && state.worktreeMetadata === prevState.worktreeMetadata && state.availableWorktrees === prevState.availableWorktrees && state.availableWorktreesByProject === prevState.availableWorktreesByProject ) { return; } const draftOpen = useSessionStore.getState().newSessionDraft?.open; useSessionStore.setState({ sessions: state.sessions, archivedSessions: state.archivedSessions, sessionsByDirectory: state.sessionsByDirectory, currentSessionId: draftOpen ? null : state.currentSessionId, lastLoadedDirectory: state.lastLoadedDirectory, isLoading: state.isLoading, error: state.error, webUICreatedSessions: state.webUICreatedSessions, worktreeMetadata: state.worktreeMetadata, availableWorktrees: state.availableWorktrees, availableWorktreesByProject: state.availableWorktreesByProject, }); }); useMessageStore.subscribe((state, prevState) => { // Early-return equality check stays outside the rAF so we skip scheduling // entirely when nothing relevant changed. if ( state.messages === prevState.messages && state.sessionMemoryState === prevState.sessionMemoryState && state.sessionHistoryMeta === prevState.sessionHistoryMeta && state.messageStreamStates === prevState.messageStreamStates && state.sessionCompactionUntil === prevState.sessionCompactionUntil && state.sessionAbortFlags === prevState.sessionAbortFlags && state.streamingMessageIds === prevState.streamingMessageIds && state.abortControllers === prevState.abortControllers && state.lastUsedProvider === prevState.lastUsedProvider && state.isSyncing === prevState.isSyncing ) { return; } // Debounce the expensive sessionStore update to at most once per animation // frame. Multiple messageStore updates within the same frame (e.g. several // SSE tokens arriving before the next paint) collapse into a single setState. if (messageStoreSyncRafId !== null) { cancelAnimationFrame(messageStoreSyncRafId); } messageStoreSyncRafId = requestAnimationFrame(() => { messageStoreSyncRafId = null; // Read the LATEST state at flush time, not the stale state captured by // the subscription closure. const latest = useMessageStore.getState(); useSessionStore.setState({ messages: latest.messages, sessionMemoryState: latest.sessionMemoryState, sessionHistoryMeta: latest.sessionHistoryMeta, messageStreamStates: latest.messageStreamStates, sessionCompactionUntil: latest.sessionCompactionUntil, sessionAbortFlags: latest.sessionAbortFlags, streamingMessageIds: latest.streamingMessageIds, abortControllers: latest.abortControllers, lastUsedProvider: latest.lastUsedProvider, isSyncing: latest.isSyncing, }); // Sidebar titles don't need real-time updates; debounce separately at // 500 ms so the expensive per-message iteration doesn't happen every // frame during streaming. if (userSummaryTitlesRafId !== null) { clearTimeout(userSummaryTitlesRafId); } userSummaryTitlesRafId = setTimeout(() => { userSummaryTitlesRafId = null; const titleState = useMessageStore.getState(); const userSummaryTitles = new Map(); titleState.messages.forEach((messageList, sessionId) => { if (!Array.isArray(messageList) || messageList.length === 0) { return; } for (let index = messageList.length - 1; index >= 0; index -= 1) { const entry = messageList[index]; if (!entry || !entry.info) { continue; } const info = entry.info as Message & { summary?: { title?: string | null } | null; time?: { created?: number | null }; }; if (info.role === "user") { const title = info.summary?.title; if (typeof title === "string") { const trimmed = title.trim(); if (trimmed.length > 0) { const createdAt = info.time && typeof info.time.created === "number" ? info.time.created : null; userSummaryTitles.set(sessionId, { title: trimmed, createdAt }); break; } } } } }); useSessionStore.setState({ userSummaryTitles }); }, 500); }); }); useFileStore.subscribe((state, prevState) => { if (state.attachedFiles === prevState.attachedFiles) { return; } useSessionStore.setState({ attachedFiles: state.attachedFiles, }); }); useContextStore.subscribe((state, prevState) => { if ( state.sessionModelSelections === prevState.sessionModelSelections && state.sessionAgentSelections === prevState.sessionAgentSelections && state.sessionAgentModelSelections === prevState.sessionAgentModelSelections && state.currentAgentContext === prevState.currentAgentContext && state.sessionContextUsage === prevState.sessionContextUsage && state.sessionAgentEditModes === prevState.sessionAgentEditModes ) { return; } useSessionStore.setState({ sessionModelSelections: state.sessionModelSelections, sessionAgentSelections: state.sessionAgentSelections, sessionAgentModelSelections: state.sessionAgentModelSelections, currentAgentContext: state.currentAgentContext, sessionContextUsage: state.sessionContextUsage, sessionAgentEditModes: state.sessionAgentEditModes, }); }); usePermissionStore.subscribe((state, prevState) => { if (state.permissions === prevState.permissions) { return; } useSessionStore.setState({ permissions: state.permissions, }); }); useQuestionStore.subscribe((state, prevState) => { if (state.questions === prevState.questions) { return; } useSessionStore.setState({ questions: state.questions, }); }); useDirectoryStore.subscribe((state, prevState) => { const nextDirectory = normalizePath(state.currentDirectory ?? null); const prevDirectory = normalizePath(prevState.currentDirectory ?? null); if (nextDirectory === prevDirectory) { return; } const draft = useSessionStore.getState().newSessionDraft; if (!draft?.open) { return; } if (draft.pendingWorktreeRequestId || draft.bootstrapPendingDirectory || draft.preserveDirectoryOverride) { return; } const draftDirectory = normalizePath(draft.directoryOverride); if (draftDirectory && draftDirectory !== prevDirectory) { return; } const projects = useProjectsStore.getState().projects; const resolvedProject = resolveDraftProjectForDirectory( projects, useSessionStore.getState().availableWorktreesByProject, nextDirectory, ); useSessionStore.setState((store) => ({ newSessionDraft: { ...store.newSessionDraft, selectedProjectId: resolvedProject?.id ?? store.newSessionDraft.selectedProjectId ?? null, directoryOverride: nextDirectory, parentID: null, }, })); persistDraftTarget({ projectId: resolvedProject?.id ?? draft.selectedProjectId ?? null, directory: nextDirectory, }); }); const bootDraftOpen = useSessionStore.getState().newSessionDraft?.open; useSessionStore.setState({ sessions: useSessionManagementStore.getState().sessions, currentSessionId: bootDraftOpen ? null : useSessionManagementStore.getState().currentSessionId, lastLoadedDirectory: useSessionManagementStore.getState().lastLoadedDirectory, isLoading: useSessionManagementStore.getState().isLoading, error: useSessionManagementStore.getState().error, webUICreatedSessions: useSessionManagementStore.getState().webUICreatedSessions, worktreeMetadata: useSessionManagementStore.getState().worktreeMetadata, availableWorktrees: useSessionManagementStore.getState().availableWorktrees, availableWorktreesByProject: useSessionManagementStore.getState().availableWorktreesByProject, messages: useMessageStore.getState().messages, sessionMemoryState: useMessageStore.getState().sessionMemoryState, sessionHistoryMeta: useMessageStore.getState().sessionHistoryMeta, messageStreamStates: useMessageStore.getState().messageStreamStates, sessionCompactionUntil: useMessageStore.getState().sessionCompactionUntil, sessionAbortFlags: useMessageStore.getState().sessionAbortFlags, streamingMessageIds: useMessageStore.getState().streamingMessageIds, abortControllers: useMessageStore.getState().abortControllers, lastUsedProvider: useMessageStore.getState().lastUsedProvider, isSyncing: useMessageStore.getState().isSyncing, permissions: usePermissionStore.getState().permissions, questions: useQuestionStore.getState().questions, attachedFiles: useFileStore.getState().attachedFiles, sessionModelSelections: useContextStore.getState().sessionModelSelections, sessionAgentSelections: useContextStore.getState().sessionAgentSelections, sessionAgentModelSelections: useContextStore.getState().sessionAgentModelSelections, currentAgentContext: useContextStore.getState().currentAgentContext, sessionContextUsage: useContextStore.getState().sessionContextUsage, sessionAgentEditModes: useContextStore.getState().sessionAgentEditModes, abortPromptSessionId: null, abortPromptExpiresAt: null, }); if (typeof window !== "undefined") { window.__zustand_session_store__ = useSessionStore; }