import { create } from 'zustand'; import { devtools, persist } from 'zustand/middleware'; import type { SidebarSection } from '@/constants/sidebar'; import { createDeferredSafeJSONStorage } from './utils/safeStorage'; import { SEMANTIC_TYPOGRAPHY, getTypographyVariable, type SemanticTypographyKey } from '@/lib/typography'; import type { ShortcutCombo } from '@/lib/shortcuts'; import type { DraftStarterRef } from '@/lib/draftStarters'; import { DEFAULT_MONO_FONT, DEFAULT_UI_FONT, type MonoFontOption, type UiFontOption } from '@/lib/fontOptions'; import { getStoredMobileKeyboardMode, type MobileKeyboardMode } from '@/lib/mobileKeyboardMode'; import { getRuntimeKey } from '@/lib/runtime-switch'; import type { TerminalShell } from '@/lib/api/types'; import { useFilesViewTabsStore } from './useFilesViewTabsStore'; export type MainTab = 'chat' | 'plan' | 'git' | 'diff' | 'terminal' | 'files' | 'context' | 'diagram'; export type PendingDiffScope = 'working' | 'staged' | 'turn'; export type ContextPanelMode = 'diff' | 'file' | 'context' | 'plan' | 'chat' | 'preview' | 'browser' | 'git' | 'pr' | 'notes' | 'terminal'; export type MermaidRenderingMode = 'svg' | 'ascii'; export type UserMessageRenderingMode = 'markdown' | 'plain'; export type ChatRenderMode = 'sorted' | 'live'; export type ActivityRenderMode = 'collapsed' | 'summary'; export type SessionRetentionAction = 'archive' | 'delete'; export type TimeFormatPreference = 'auto' | '12h' | '24h'; export type WeekStartPreference = 'auto' | 'sunday' | 'monday'; export type DesktopWindowControlsPosition = 'left' | 'right'; export type DesktopWindowControlsStyle = 'classic' | 'traffic-lights'; export type FileEditorKeymap = 'default' | 'vim'; function normalizeFileEditorKeymap(value: unknown): FileEditorKeymap { return value === 'vim' ? 'vim' : 'default'; } type ContextPanelTab = { id: string; mode: ContextPanelMode; targetPath: string | null; dedupeKey: string; label: string | null; sessionTitleFallback: string | null; readOnly: boolean; stagedDiff: boolean; diffScope: PendingDiffScope | null; touchedAt: number; }; type ContextPanelTabDescriptor = { mode: ContextPanelMode; targetPath?: string | null; dedupeKey?: string | null; label?: string | null; sessionTitleFallback?: string | null; readOnly?: boolean; stagedDiff?: boolean; diffScope?: PendingDiffScope | null; }; type ContextPanelDirectoryState = { isOpen: boolean; expanded: boolean; tabs: ContextPanelTab[]; activeTabId: string | null; // Manual per-surface widths (px), populated only by user resize; surfaces // without an entry fall back to their registry defaultWidthFraction. widthByMode: Partial>; touchedAt: number; }; type PendingFileNavigation = { path: string; line: number; column: number; }; export type MainTabGuard = (nextTab: MainTab) => boolean; export type EventStreamStatus = | 'idle' | 'connecting' | 'connected' | 'reconnecting' | 'paused' | 'offline' | 'error'; const LEGACY_DEFAULT_NOTIFICATION_TEMPLATES = { completion: { title: '{agent_name} is ready', message: '{last_message}' }, error: { title: 'Tool error', message: '{last_message}' }, question: { title: '{agent_name} needs input', message: '{last_message}' }, subtask: { title: 'Subtask complete', message: '{last_message}' }, } as const; const EMPTY_NOTIFICATION_TEMPLATES = { completion: { title: '', message: '' }, error: { title: '', message: '' }, question: { title: '', message: '' }, subtask: { title: '', message: '' }, } as const; const isSameTemplateValue = ( a: { title: string; message: string } | undefined, b: { title: string; message: string } ) => { if (!a) return false; return a.title === b.title && a.message === b.message; }; const isLegacyDefaultTemplates = (value: unknown): boolean => { if (!value || typeof value !== 'object') { return false; } const candidate = value as Record; return ( isSameTemplateValue(candidate.completion, LEGACY_DEFAULT_NOTIFICATION_TEMPLATES.completion) && isSameTemplateValue(candidate.error, LEGACY_DEFAULT_NOTIFICATION_TEMPLATES.error) && isSameTemplateValue(candidate.question, LEGACY_DEFAULT_NOTIFICATION_TEMPLATES.question) && isSameTemplateValue(candidate.subtask, LEGACY_DEFAULT_NOTIFICATION_TEMPLATES.subtask) ); }; const CONTEXT_PANEL_DEFAULT_WIDTH = 380; const CONTEXT_PANEL_MIN_WIDTH = 380; const CONTEXT_PANEL_MAX_WIDTH = 1400; const CONTEXT_PANEL_MAX_TABS = 12; const CONTEXT_PANEL_MAX_LABEL_LENGTH = 120; const LEFT_SIDEBAR_MIN_WIDTH = 280; const activeMainTabByRuntime = new Map(); const runtimeMemoryKey = (value?: string | null): string => { const key = (value ?? getRuntimeKey()).trim(); return key || 'default'; }; // Shared with rail/panel consumers so contextPanelByDirectory lookups agree on keys. export const normalizeContextPanelDirectoryKey = (value: string): string => normalizeDirectoryPath(value); const normalizeDirectoryPath = (value: string): string => { if (!value) return ''; const raw = value.replace(/\\/g, '/'); const hadUncPrefix = raw.startsWith('//'); let normalized = raw.replace(/\/+$/g, ''); normalized = normalized.replace(/\/+/g, '/'); if (hadUncPrefix && !normalized.startsWith('//')) { normalized = `/${normalized}`; } if (normalized === '') { return raw.startsWith('/') ? '/' : ''; } return normalized; }; const clampContextPanelWidth = (width: number): number => { if (!Number.isFinite(width)) { return CONTEXT_PANEL_DEFAULT_WIDTH; } return Math.min(CONTEXT_PANEL_MAX_WIDTH, Math.max(CONTEXT_PANEL_MIN_WIDTH, Math.round(width))); }; const normalizeContextTargetPath = (value: string | null | undefined): string | null => { if (typeof value !== 'string') { return null; } const trimmed = value.trim(); if (!trimmed) { return null; } return trimmed.replace(/\\/g, '/'); }; const normalizeContextTabLabel = (value: string | null | undefined): string | null => { if (typeof value !== 'string') { return null; } const trimmed = value.trim(); if (!trimmed) { return null; } return trimmed.length > CONTEXT_PANEL_MAX_LABEL_LENGTH ? trimmed.slice(0, CONTEXT_PANEL_MAX_LABEL_LENGTH) : trimmed; }; const normalizePendingDiffScope = (value: unknown): PendingDiffScope | null => { return value === 'working' || value === 'staged' || value === 'turn' ? value : null; }; const buildDefaultContextPanelTabDedupeKey = (mode: ContextPanelMode, targetPath: string | null): string => { if (mode === 'file') { return targetPath || mode; } if (mode === 'preview') { return targetPath || mode; } return mode; }; const normalizeContextPanelTabDedupeKey = ( mode: ContextPanelMode, targetPath: string | null, dedupeKey: string | null | undefined, ): string => { if (mode === 'diff') { return mode; } if (typeof dedupeKey === 'string') { const trimmed = dedupeKey.trim(); if (trimmed) { return trimmed; } } return buildDefaultContextPanelTabDedupeKey(mode, targetPath); }; const buildContextPanelTabID = (mode: ContextPanelMode, dedupeKey: string): string => { return dedupeKey === mode ? mode : `${mode}:${dedupeKey}`; }; const createContextPanelTab = (descriptor: ContextPanelTabDescriptor): ContextPanelTab => { const normalizedTargetPath = normalizeContextTargetPath(descriptor.targetPath); const dedupeKey = normalizeContextPanelTabDedupeKey( descriptor.mode, normalizedTargetPath, descriptor.dedupeKey, ); return { id: buildContextPanelTabID(descriptor.mode, dedupeKey), mode: descriptor.mode, targetPath: normalizedTargetPath, dedupeKey, label: normalizeContextTabLabel(descriptor.label), sessionTitleFallback: normalizeContextTabLabel(descriptor.sessionTitleFallback), readOnly: descriptor.readOnly === true, stagedDiff: descriptor.stagedDiff === true, diffScope: normalizePendingDiffScope(descriptor.diffScope) ?? (descriptor.stagedDiff === true ? 'staged' : 'working'), touchedAt: Date.now(), }; }; const clampContextPanelTabs = (tabs: ContextPanelTab[], maxTabs: number, activeTabId: string | null): ContextPanelTab[] => { if (tabs.length <= maxTabs) { return tabs; } const tabsByTouch = [...tabs].sort((a, b) => a.touchedAt - b.touchedAt); const removable = tabsByTouch.filter((tab) => tab.id !== activeTabId); const removeCount = tabs.length - maxTabs; if (removeCount <= 0 || removable.length === 0) { return tabs.slice(-maxTabs); } const removeSet = new Set(removable.slice(0, removeCount).map((tab) => tab.id)); return tabs.filter((tab) => !removeSet.has(tab.id)); }; const sanitizeContextPanelTabs = (tabs: unknown): ContextPanelTab[] => { if (!Array.isArray(tabs)) { return []; } const result: ContextPanelTab[] = []; const seen = new Set(); for (const entry of tabs) { if (!entry || typeof entry !== 'object') { continue; } const candidate = entry as { mode?: unknown; targetPath?: unknown; dedupeKey?: unknown; label?: unknown; sessionTitleFallback?: unknown; readOnly?: unknown; stagedDiff?: unknown; diffScope?: unknown; touchedAt?: unknown; }; if (candidate.mode !== 'diff' && candidate.mode !== 'file' && candidate.mode !== 'context' && candidate.mode !== 'plan' && candidate.mode !== 'chat' && candidate.mode !== 'preview' && candidate.mode !== 'browser' && candidate.mode !== 'git' && candidate.mode !== 'pr' && candidate.mode !== 'notes' && candidate.mode !== 'terminal') { continue; } const targetPath = normalizeContextTargetPath(typeof candidate.targetPath === 'string' ? candidate.targetPath : null); const dedupeKey = normalizeContextPanelTabDedupeKey( candidate.mode, targetPath, typeof candidate.dedupeKey === 'string' ? candidate.dedupeKey : null, ); const id = buildContextPanelTabID(candidate.mode, dedupeKey); if (!id || seen.has(id)) { continue; } seen.add(id); result.push({ id, mode: candidate.mode, targetPath, dedupeKey, label: normalizeContextTabLabel(typeof candidate.label === 'string' ? candidate.label : null), sessionTitleFallback: normalizeContextTabLabel(typeof candidate.sessionTitleFallback === 'string' ? candidate.sessionTitleFallback : null), readOnly: candidate.readOnly === true, stagedDiff: candidate.stagedDiff === true, diffScope: normalizePendingDiffScope(candidate.diffScope) ?? (candidate.stagedDiff === true ? 'staged' : 'working'), touchedAt: typeof candidate.touchedAt === 'number' && Number.isFinite(candidate.touchedAt) ? candidate.touchedAt : Date.now(), }); } return result; }; const resolveActiveContextPanelTabID = (tabs: ContextPanelTab[], activeTabId: string | null): string | null => { if (activeTabId && tabs.some((tab) => tab.id === activeTabId)) { return activeTabId; } if (tabs.length === 0) { return null; } return tabs[tabs.length - 1].id; }; const touchContextPanelState = (prev?: ContextPanelDirectoryState): ContextPanelDirectoryState => { if (prev) { const tabs = sanitizeContextPanelTabs(prev.tabs); const activeTabId = resolveActiveContextPanelTabID(tabs, prev.activeTabId); return { ...prev, tabs, activeTabId, touchedAt: Date.now(), }; } return { isOpen: false, expanded: false, tabs: [], activeTabId: null, widthByMode: {}, touchedAt: Date.now(), }; }; const upsertContextPanelTab = ( current: ContextPanelDirectoryState, descriptor: ContextPanelTabDescriptor, ): ContextPanelDirectoryState => { const nextTab = createContextPanelTab(descriptor); // A real file tab replaces the empty editor placeholder ('file' with no // target) that the rail can open before any file is picked. const baseTabs = nextTab.mode === 'file' && nextTab.targetPath ? current.tabs.filter((tab) => !(tab.mode === 'file' && !tab.targetPath)) : current.tabs; const existingIndex = baseTabs.findIndex((tab) => tab.id === nextTab.id); const tabs = existingIndex === -1 ? [...baseTabs, nextTab] : baseTabs.map((tab, index) => (index === existingIndex ? { ...tab, mode: nextTab.mode, targetPath: nextTab.targetPath || tab.targetPath, dedupeKey: nextTab.dedupeKey, label: nextTab.label, sessionTitleFallback: nextTab.sessionTitleFallback || tab.sessionTitleFallback, stagedDiff: nextTab.stagedDiff, diffScope: nextTab.diffScope, readOnly: nextTab.readOnly, touchedAt: Date.now(), } : tab)); const activeTabId = nextTab.id; const clampedTabs = clampContextPanelTabs(tabs, CONTEXT_PANEL_MAX_TABS, activeTabId); return { ...current, isOpen: true, tabs: clampedTabs, activeTabId: resolveActiveContextPanelTabID(clampedTabs, activeTabId), touchedAt: Date.now(), }; }; const closeContextPanelTab = ( current: ContextPanelDirectoryState, tabID: string, ): ContextPanelDirectoryState => { const closedTab = current.tabs.find((tab) => tab.id === tabID) ?? null; const nextTabs = current.tabs.filter((tab) => tab.id !== tabID); if (current.activeTabId !== tabID) { return { ...current, tabs: nextTabs, activeTabId: resolveActiveContextPanelTabID(nextTabs, current.activeTabId), isOpen: nextTabs.length > 0 ? current.isOpen : false, touchedAt: Date.now(), }; } // Closing the active tab stays inside the active surface: activate the most // recent remaining tab of the same mode, and when it was the last one just // close the panel instead of jumping to another surface. const sameModeTabs = closedTab ? nextTabs.filter((tab) => tab.mode === closedTab.mode) : []; const nextSameModeTab = sameModeTabs.length > 0 ? sameModeTabs.reduce((best, tab) => (tab.touchedAt >= best.touchedAt ? tab : best)) : null; return { ...current, tabs: nextTabs, activeTabId: nextSameModeTab?.id ?? resolveActiveContextPanelTabID(nextTabs, null), isOpen: nextSameModeTab ? current.isOpen : false, touchedAt: Date.now(), }; }; const reorderContextPanelTabs = ( current: ContextPanelDirectoryState, activeTabID: string, overTabID: string, ): ContextPanelDirectoryState => { if (activeTabID === overTabID) { return current; } const fromIndex = current.tabs.findIndex((tab) => tab.id === activeTabID); const toIndex = current.tabs.findIndex((tab) => tab.id === overTabID); if (fromIndex === -1 || toIndex === -1) { return current; } const tabs = [...current.tabs]; const [moved] = tabs.splice(fromIndex, 1); if (!moved) { return current; } tabs.splice(toIndex, 0, moved); return { ...current, tabs, touchedAt: Date.now(), }; }; const setContextPanelTabTargetPath = ( current: ContextPanelDirectoryState, tabID: string, targetPath: string, ): ContextPanelDirectoryState => ({ ...current, tabs: current.tabs.map((tab) => tab.id === tabID ? { ...tab, targetPath } : tab, ), }); const sanitizeContextPanelByDirectory = ( value: unknown, ): Record => { if (!value || typeof value !== 'object') { return {}; } const source = value as Record; const next: Record = {}; for (const [rawDirectory, rawState] of Object.entries(source)) { const directory = normalizeDirectoryPath(rawDirectory); if (!directory || !rawState || typeof rawState !== 'object') { continue; } const candidate = rawState as { isOpen?: unknown; expanded?: unknown; tabs?: unknown; activeTabId?: unknown; widthByMode?: unknown; touchedAt?: unknown; mode?: unknown; targetPath?: unknown; dedupeKey?: unknown; label?: unknown; }; let tabs = sanitizeContextPanelTabs(candidate.tabs); let activeTabId = typeof candidate.activeTabId === 'string' ? candidate.activeTabId : null; if (tabs.length === 0 && (candidate.mode === 'diff' || candidate.mode === 'file' || candidate.mode === 'context' || candidate.mode === 'plan' || candidate.mode === 'chat')) { tabs = [createContextPanelTab({ mode: candidate.mode, targetPath: typeof candidate.targetPath === 'string' ? candidate.targetPath : null, dedupeKey: typeof candidate.dedupeKey === 'string' ? candidate.dedupeKey : null, label: typeof candidate.label === 'string' ? candidate.label : null, })]; activeTabId = tabs[0]?.id ?? null; } const resolvedActiveTabId = resolveActiveContextPanelTabID(tabs, activeTabId); const clampedTabs = clampContextPanelTabs(tabs, CONTEXT_PANEL_MAX_TABS, resolvedActiveTabId); // Legacy single `width` values are intentionally dropped: widths are now // per-surface, seeded from registry defaults until the user resizes. const widthByMode: Partial> = {}; if (candidate.widthByMode && typeof candidate.widthByMode === 'object') { for (const [mode, value] of Object.entries(candidate.widthByMode as Record)) { if ( (mode === 'diff' || mode === 'file' || mode === 'context' || mode === 'plan' || mode === 'chat' || mode === 'preview' || mode === 'browser' || mode === 'git' || mode === 'pr' || mode === 'notes' || mode === 'terminal') && typeof value === 'number' && Number.isFinite(value) ) { widthByMode[mode] = clampContextPanelWidth(value); } } } next[directory] = { isOpen: candidate.isOpen === true, expanded: candidate.expanded === true, tabs: clampedTabs, activeTabId: resolveActiveContextPanelTabID(clampedTabs, resolvedActiveTabId), widthByMode, touchedAt: typeof candidate.touchedAt === 'number' && Number.isFinite(candidate.touchedAt) ? candidate.touchedAt : Date.now(), }; } return next; }; const clampContextPanelRoots = ( byDirectory: Record, maxRoots: number ): Record => { const entries = Object.entries(byDirectory); if (entries.length <= maxRoots) { return byDirectory; } entries.sort((a, b) => (b[1]?.touchedAt ?? 0) - (a[1]?.touchedAt ?? 0)); const next: Record = {}; for (const [directory, state] of entries.slice(0, maxRoots)) { next[directory] = state; } return next; }; interface UIStore { theme: 'light' | 'dark' | 'system'; isMultiRunLauncherOpen: boolean; multiRunLauncherPrefillPrompt: string; isSidebarOpen: boolean; sidebarWidth: number; hasManuallyResizedLeftSidebar: boolean; contextPanelByDirectory: Record; contextRailOrder: string[]; contextEditorTreeVisible: boolean; contextEditorTreeWidth: number; notesPanelHeight: number; todoPanelHeight: number; isSessionSwitcherOpen: boolean; isSessionDropdownOpen: boolean; activeMainTab: MainTab; mainTabGuard: MainTabGuard | null; sidebarOpenBeforeFullscreenTab: boolean | null; pendingDiffFile: string | null; pendingDiffStaged: boolean; pendingDiffScope: PendingDiffScope | null; pendingDiagramFile: string | null; pendingFileNavigation: PendingFileNavigation | null; pendingFileFocusPath: string | null; isMobile: boolean; isCommandPaletteOpen: boolean; isHelpDialogOpen: boolean; isAboutDialogOpen: boolean; isOpenCodeStatusDialogOpen: boolean; openCodeStatusText: string; isSessionCreateDialogOpen: boolean; isScheduledTasksDialogOpen: boolean; isArchivePageOpen: boolean; worktreesPageProjectId: string | null; isSettingsDialogOpen: boolean; isNewWorktreeDialogOpen: boolean; isModelSelectorOpen: boolean; sidebarSection: SidebarSection; // Settings IA (new shell) settingsPage: string; settingsHasOpenedOnce: boolean; settingsProjectsSelectedId: string | null; settingsRemoteInstancesSelectedId: string | null; eventStreamStatus: EventStreamStatus; eventStreamHint: string | null; showReasoningTraces: boolean; sessionRecapEnabled: boolean; sessionSuggestionEnabled: boolean; sessionGoalEnabled: boolean; sessionGoalDefaultBudgetEnabled: boolean; sessionGoalDefaultBudget: number; collapsibleThinkingBlocks: boolean; chatRenderMode: ChatRenderMode; activityRenderMode: ActivityRenderMode; showDeletionDialog: boolean; autoDeleteEnabled: boolean; autoDeleteAfterDays: number; sessionRetentionAction: SessionRetentionAction; autoDeleteLastRunAt: number | null; messageLimit: number; fontSize: number; // Global draft welcome starters; null = unset (use the default built-in set). globalDraftStarters: DraftStarterRef[] | null; draftStartersVisible: boolean; terminalFontSize: number; terminalShell: TerminalShell; terminalLoginShells: TerminalShell[]; editorFontSize: number; uiFont: UiFontOption; monoFont: MonoFontOption; padding: number; cornerRadius: number; inputBarOffset: number; mobileKeyboardMode: MobileKeyboardMode; favoriteModels: Array<{ providerID: string; modelID: string }>; hiddenModels: Array<{ providerID: string; modelID: string }>; providerOrder: string[]; collapsedModelProviders: string[]; recentModels: Array<{ providerID: string; modelID: string }>; recentAgents: string[]; recentEfforts: Record; diffLayoutPreference: 'dynamic' | 'inline' | 'side-by-side'; diffFileLayout: Record; diffWrapLines: boolean; gitChangesViewMode: 'flat' | 'tree'; isTimelineDialogOpen: boolean; isPromptNavigatorPanelOpen: boolean; isImagePreviewOpen: boolean; nativeNotificationsEnabled: boolean; notificationMode: 'always' | 'hidden-only'; notifyOnSubtasks: boolean; // Desktop dock badge showing the count of sessions with unseen activity (macOS). dockBadgeEnabled: boolean; // Event toggles (which events trigger notifications) notifyOnCompletion: boolean; notifyOnError: boolean; notifyOnQuestion: boolean; // Per-event notification templates notificationTemplates: { completion: { title: string; message: string }; error: { title: string; message: string }; question: { title: string; message: string }; subtask: { title: string; message: string }; }; // Summarization settings summarizeLastMessage: boolean; summaryThreshold: number; // chars — messages longer than this get summarized summaryLength: number; // chars — target length for summary maxLastMessageLength: number; // chars — truncate {last_message} when summarization is off showTerminalQuickKeysOnDesktop: boolean; persistChatDraft: boolean; showOpenCodeUpdateNotifications: boolean; agentControlToolEnabled: boolean; inputSpellcheckEnabled: boolean; wideChatLayoutEnabled: boolean; codeBlockLineWrap: boolean; showToolFileIcons: boolean; showTurnChangedFiles: boolean; showExpandedBashTools: boolean; showExpandedEditTools: boolean; timeFormatPreference: TimeFormatPreference; weekStartPreference: WeekStartPreference; desktopWindowControlsPosition: DesktopWindowControlsPosition; desktopWindowControlsStyle: DesktopWindowControlsStyle; mermaidRenderingMode: MermaidRenderingMode; userMessageRenderingMode: UserMessageRenderingMode; collapsibleUserMessages: boolean; stickyUserHeader: boolean; promptNavigatorEnabled: boolean; expandedEditorToolbar: boolean; showSplitAssistantMessageActions: boolean; allowPromptingSubagentSessions: boolean; isMobileSessionStatusBarCollapsed: boolean; mobileSessionPanelOpen: boolean; mobileSessionFilterProjectId: string | null; isExpandedInput: boolean; reportUsage: boolean; shortcutOverrides: Record; fileEditorKeymap: FileEditorKeymap; setTheme: (theme: 'light' | 'dark' | 'system') => void; toggleSidebar: () => void; setSidebarOpen: (open: boolean) => void; setSidebarWidth: (width: number) => void; setContextRailOrder: (order: string[]) => void; toggleContextEditorTree: () => void; setContextEditorTreeWidth: (width: number) => void; openContextSurface: (directory: string, mode: ContextPanelMode) => void; openContextPanelTab: (directory: string, tab: ContextPanelTabDescriptor) => void; openContextDiff: (directory: string, filePath: string, staged?: boolean, scope?: PendingDiffScope | null) => void; openContextFile: (directory: string, filePath: string) => void; openContextFileAtLine: (directory: string, filePath: string, line: number, column?: number) => void; openContextOverview: (directory: string) => void; openContextPlan: (directory: string) => void; openContextPreview: (directory: string, url: string) => void; openContextBrowser: (directory: string, url?: string) => void; setContextPanelTabTargetPath: (directory: string, tabID: string, targetPath: string) => void; setActiveContextPanelTab: (directory: string, tabID: string) => void; reorderContextPanelTabs: (directory: string, activeTabID: string, overTabID: string) => void; closeContextPanelTab: (directory: string, tabID: string) => void; closeContextPanel: (directory: string) => void; toggleContextPanelExpanded: (directory: string) => void; setContextPanelWidth: (directory: string, mode: ContextPanelMode, width: number) => void; setNotesPanelHeight: (height: number) => void; setTodoPanelHeight: (height: number) => void; setSessionSwitcherOpen: (open: boolean) => void; setSessionDropdownOpen: (open: boolean) => void; setActiveMainTab: (tab: MainTab) => void; prepareForRuntimeSwitch: (runtimeKey?: string | null) => void; restoreForRuntimeSwitch: (runtimeKey?: string | null) => void; setMainTabGuard: (guard: MainTabGuard | null) => void; setPendingDiffFile: (filePath: string | null, staged?: boolean, scope?: PendingDiffScope | null) => void; setPendingDiagramFile: (filePath: string | null) => void; setPendingFileNavigation: (navigation: PendingFileNavigation | null) => void; setPendingFileFocusPath: (path: string | null) => void; navigateToDiff: (filePath: string, staged?: boolean, scope?: PendingDiffScope | null) => void; consumePendingDiffFile: () => string | null; navigateToDiagram: (filePath: string) => void; consumePendingDiagramFile: () => string | null; setIsMobile: (isMobile: boolean) => void; toggleCommandPalette: () => void; setCommandPaletteOpen: (open: boolean) => void; toggleHelpDialog: () => void; setHelpDialogOpen: (open: boolean) => void; setAboutDialogOpen: (open: boolean) => void; setOpenCodeStatusDialogOpen: (open: boolean) => void; setOpenCodeStatusText: (text: string) => void; setSessionCreateDialogOpen: (open: boolean) => void; setScheduledTasksDialogOpen: (open: boolean) => void; setArchivePageOpen: (open: boolean) => void; setWorktreesPageProjectId: (projectId: string | null) => void; /** Close every full-page surface (Scheduled, Archive, Worktrees, Multi-run). */ closeMainSurfaces: () => void; setSettingsDialogOpen: (open: boolean) => void; setNewWorktreeDialogOpen: (open: boolean) => void; setModelSelectorOpen: (open: boolean) => void; applyTheme: () => void; setSidebarSection: (section: SidebarSection) => void; setSettingsPage: (slug: string) => void; setSettingsProjectsSelectedId: (projectId: string | null) => void; setSettingsRemoteInstancesSelectedId: (instanceId: string | null) => void; setEventStreamStatus: (status: EventStreamStatus, hint?: string | null) => void; setShowReasoningTraces: (value: boolean) => void; setSessionRecapEnabled: (value: boolean) => void; setSessionSuggestionEnabled: (value: boolean) => void; setSessionGoalEnabled: (value: boolean) => void; setSessionGoalDefaultBudgetEnabled: (value: boolean) => void; setSessionGoalDefaultBudget: (value: number) => void; setCollapsibleThinkingBlocks: (value: boolean) => void; setChatRenderMode: (value: ChatRenderMode) => void; setActivityRenderMode: (value: ActivityRenderMode) => void; setShowDeletionDialog: (value: boolean) => void; setAutoDeleteEnabled: (value: boolean) => void; setAutoDeleteAfterDays: (days: number) => void; setSessionRetentionAction: (value: SessionRetentionAction) => void; setAutoDeleteLastRunAt: (timestamp: number | null) => void; setMessageLimit: (value: number) => void; setFontSize: (size: number) => void; setGlobalDraftStarters: (refs: DraftStarterRef[]) => void; setDraftStartersVisible: (value: boolean) => void; setTerminalFontSize: (size: number) => void; setTerminalShell: (shell: TerminalShell) => void; setTerminalLoginShells: (shells: TerminalShell[]) => void; setEditorFontSize: (size: number) => void; setUiFont: (font: UiFontOption) => void; setMonoFont: (font: MonoFontOption) => void; setPadding: (size: number) => void; setCornerRadius: (radius: number) => void; setInputBarOffset: (offset: number) => void; setMobileKeyboardMode: (mode: MobileKeyboardMode) => void; applyTypography: () => void; applyPadding: () => void; toggleFavoriteModel: (providerID: string, modelID: string) => void; reorderFavoriteModel: ( activeProviderID: string, activeModelID: string, overProviderID: string, overModelID: string, ) => void; setProviderOrder: (orderedProviderIDs: string[]) => void; toggleHiddenModel: (providerID: string, modelID: string) => void; isHiddenModel: (providerID: string, modelID: string) => boolean; hideAllModels: (providerID: string, modelIDs: string[]) => void; showAllModels: (providerID: string) => void; toggleModelProviderCollapsed: (providerID: string) => void; setModelProvidersCollapsed: (providerIDs: string[], collapsed: boolean) => void; isFavoriteModel: (providerID: string, modelID: string) => boolean; addRecentModel: (providerID: string, modelID: string) => void; addRecentAgent: (agentName: string) => void; addRecentEffort: (providerID: string, modelID: string, variant: string | undefined) => void; setDiffLayoutPreference: (mode: 'dynamic' | 'inline' | 'side-by-side') => void; setDiffFileLayout: (filePath: string, mode: 'inline' | 'side-by-side') => void; setDiffWrapLines: (wrap: boolean) => void; setGitChangesViewMode: (mode: 'flat' | 'tree') => void; setMultiRunLauncherOpen: (open: boolean) => void; setTimelineDialogOpen: (open: boolean) => void; setPromptNavigatorPanelOpen: (open: boolean) => void; togglePromptNavigatorPanel: () => void; setImagePreviewOpen: (open: boolean) => void; setNativeNotificationsEnabled: (value: boolean) => void; setNotificationMode: (mode: 'always' | 'hidden-only') => void; setShowTerminalQuickKeysOnDesktop: (value: boolean) => void; setNotifyOnSubtasks: (value: boolean) => void; setDockBadgeEnabled: (value: boolean) => void; setNotifyOnCompletion: (value: boolean) => void; setNotifyOnError: (value: boolean) => void; setNotifyOnQuestion: (value: boolean) => void; setNotificationTemplates: (templates: UIStore['notificationTemplates']) => void; setSummarizeLastMessage: (value: boolean) => void; setSummaryThreshold: (value: number) => void; setSummaryLength: (value: number) => void; setMaxLastMessageLength: (value: number) => void; setPersistChatDraft: (value: boolean) => void; setShowOpenCodeUpdateNotifications: (value: boolean) => void; setAgentControlToolEnabled: (value: boolean) => void; setInputSpellcheckEnabled: (value: boolean) => void; setWideChatLayoutEnabled: (value: boolean) => void; setCodeBlockLineWrap: (value: boolean) => void; setShowToolFileIcons: (value: boolean) => void; setShowTurnChangedFiles: (value: boolean) => void; setShowExpandedBashTools: (value: boolean) => void; setShowExpandedEditTools: (value: boolean) => void; setTimeFormatPreference: (value: TimeFormatPreference) => void; setWeekStartPreference: (value: WeekStartPreference) => void; setDesktopWindowControlsPosition: (value: DesktopWindowControlsPosition) => void; setDesktopWindowControlsStyle: (value: DesktopWindowControlsStyle) => void; setMermaidRenderingMode: (value: MermaidRenderingMode) => void; setUserMessageRenderingMode: (value: UserMessageRenderingMode) => void; setCollapsibleUserMessages: (value: boolean) => void; setStickyUserHeader: (value: boolean) => void; setPromptNavigatorEnabled: (value: boolean) => void; setExpandedEditorToolbar: (value: boolean) => void; setShowSplitAssistantMessageActions: (value: boolean) => void; setAllowPromptingSubagentSessions: (value: boolean) => void; setIsMobileSessionStatusBarCollapsed: (value: boolean) => void; setMobileSessionPanelOpen: (value: boolean) => void; setMobileSessionFilterProjectId: (value: string | null) => void; viewPagerPage: 'left' | 'center' | 'right'; setViewPagerPage: (page: 'left' | 'center' | 'right') => void; toggleExpandedInput: () => void; setExpandedInput: (value: boolean) => void; openMultiRunLauncher: () => void; openMultiRunLauncherWithPrompt: (prompt: string) => void; setReportUsage: (value: boolean) => void; setShortcutOverride: (actionId: string, combo: ShortcutCombo) => void; clearShortcutOverride: (actionId: string) => void; resetAllShortcutOverrides: () => void; setFileEditorKeymap: (value: FileEditorKeymap) => void; } export const useUIStore = create()( devtools( persist( (set, get) => ({ theme: 'system', isMultiRunLauncherOpen: false, multiRunLauncherPrefillPrompt: '', isSidebarOpen: true, sidebarWidth: LEFT_SIDEBAR_MIN_WIDTH, hasManuallyResizedLeftSidebar: false, contextPanelByDirectory: {}, contextRailOrder: [], contextEditorTreeVisible: true, contextEditorTreeWidth: 240, notesPanelHeight: 112, todoPanelHeight: 259, isSessionSwitcherOpen: false, isSessionDropdownOpen: false, activeMainTab: 'chat', mainTabGuard: null, sidebarOpenBeforeFullscreenTab: null, pendingDiffFile: null, pendingDiffStaged: false, pendingDiffScope: null, pendingDiagramFile: null, pendingFileNavigation: null, pendingFileFocusPath: null, isMobile: false, isCommandPaletteOpen: false, isHelpDialogOpen: false, isAboutDialogOpen: false, isOpenCodeStatusDialogOpen: false, openCodeStatusText: '', isSessionCreateDialogOpen: false, isScheduledTasksDialogOpen: false, isArchivePageOpen: false, worktreesPageProjectId: null, isSettingsDialogOpen: false, isNewWorktreeDialogOpen: false, isModelSelectorOpen: false, sidebarSection: 'sessions', settingsPage: 'home', settingsHasOpenedOnce: false, settingsProjectsSelectedId: null, settingsRemoteInstancesSelectedId: null, eventStreamStatus: 'idle', eventStreamHint: null, showReasoningTraces: true, sessionRecapEnabled: true, sessionSuggestionEnabled: true, sessionGoalEnabled: true, sessionGoalDefaultBudgetEnabled: false, sessionGoalDefaultBudget: 200_000, collapsibleThinkingBlocks: true, chatRenderMode: 'live', activityRenderMode: 'summary', showDeletionDialog: true, autoDeleteEnabled: false, autoDeleteAfterDays: 30, sessionRetentionAction: 'archive', autoDeleteLastRunAt: null, messageLimit: 200, fontSize: 100, globalDraftStarters: null, terminalFontSize: 14, terminalShell: 'auto', terminalLoginShells: [], editorFontSize: 13, uiFont: DEFAULT_UI_FONT, monoFont: DEFAULT_MONO_FONT, padding: 100, cornerRadius: 18, inputBarOffset: 0, mobileKeyboardMode: getStoredMobileKeyboardMode(), favoriteModels: [], hiddenModels: [], providerOrder: [], collapsedModelProviders: [], recentModels: [], recentAgents: [], recentEfforts: {}, diffLayoutPreference: 'inline', diffFileLayout: {}, diffWrapLines: false, gitChangesViewMode: 'flat', isTimelineDialogOpen: false, isPromptNavigatorPanelOpen: false, isImagePreviewOpen: false, nativeNotificationsEnabled: false, notificationMode: 'hidden-only', notifyOnSubtasks: true, dockBadgeEnabled: true, // Event toggles (which events trigger notifications) notifyOnCompletion: true, notifyOnError: true, notifyOnQuestion: true, notificationTemplates: { completion: { ...EMPTY_NOTIFICATION_TEMPLATES.completion }, error: { ...EMPTY_NOTIFICATION_TEMPLATES.error }, question: { ...EMPTY_NOTIFICATION_TEMPLATES.question }, subtask: { ...EMPTY_NOTIFICATION_TEMPLATES.subtask }, }, // Summarization settings summarizeLastMessage: false, summaryThreshold: 200, summaryLength: 100, maxLastMessageLength: 250, showTerminalQuickKeysOnDesktop: false, persistChatDraft: true, showOpenCodeUpdateNotifications: true, agentControlToolEnabled: true, inputSpellcheckEnabled: false, wideChatLayoutEnabled: false, codeBlockLineWrap: true, showToolFileIcons: true, showTurnChangedFiles: false, showExpandedBashTools: false, showExpandedEditTools: false, timeFormatPreference: 'auto', weekStartPreference: 'auto', desktopWindowControlsPosition: 'right', desktopWindowControlsStyle: 'classic', mermaidRenderingMode: 'svg', userMessageRenderingMode: 'markdown', collapsibleUserMessages: true, stickyUserHeader: false, promptNavigatorEnabled: true, expandedEditorToolbar: false, showSplitAssistantMessageActions: false, allowPromptingSubagentSessions: false, draftStartersVisible: true, isMobileSessionStatusBarCollapsed: false, mobileSessionPanelOpen: false, mobileSessionFilterProjectId: null, isExpandedInput: false, reportUsage: true, shortcutOverrides: {}, fileEditorKeymap: 'default', setTheme: (theme) => { set({ theme }); get().applyTheme(); }, toggleSidebar: () => { set((state) => { const newOpen = !state.isSidebarOpen; if (newOpen && !state.hasManuallyResizedLeftSidebar) { return { isSidebarOpen: newOpen, sidebarWidth: LEFT_SIDEBAR_MIN_WIDTH, }; } return { isSidebarOpen: newOpen }; }); }, setSidebarOpen: (open) => { set((state) => { if (state.isSidebarOpen === open) { if (!open) { return state; } if (!state.hasManuallyResizedLeftSidebar && state.sidebarWidth !== LEFT_SIDEBAR_MIN_WIDTH) { return { isSidebarOpen: open, sidebarWidth: LEFT_SIDEBAR_MIN_WIDTH, }; } return state; } if (open && !state.hasManuallyResizedLeftSidebar) { return { isSidebarOpen: open, sidebarWidth: LEFT_SIDEBAR_MIN_WIDTH, }; } return { isSidebarOpen: open }; }); }, setSidebarWidth: (width) => { set({ sidebarWidth: width, hasManuallyResizedLeftSidebar: true }); }, setContextRailOrder: (order) => { const sanitized = Array.isArray(order) ? order.filter((id, index) => typeof id === 'string' && id.trim() !== '' && order.indexOf(id) === index) : []; set({ contextRailOrder: sanitized }); }, toggleContextEditorTree: () => { set((state) => ({ contextEditorTreeVisible: !state.contextEditorTreeVisible })); }, setContextEditorTreeWidth: (width) => { if (!Number.isFinite(width)) { return; } set({ contextEditorTreeWidth: Math.min(480, Math.max(200, Math.round(width))) }); }, // Rail entry point: activates the most recent tab of the requested // mode, opens a fresh singleton tab when none exists, and toggles the // panel closed when the requested mode is already active and visible. openContextSurface: (directory, mode) => { const normalizedDirectory = normalizeDirectoryPath((directory || '').trim()); if (!normalizedDirectory) { return; } const state = get(); const panelState = state.contextPanelByDirectory[normalizedDirectory]; const tabs = panelState?.tabs ?? []; const activeTab = tabs.find((tab) => tab.id === panelState?.activeTabId) ?? null; if (panelState?.isOpen && activeTab?.mode === mode) { state.closeContextPanel(normalizedDirectory); return; } const tabsOfMode = tabs.filter((tab) => tab.mode === mode); if (tabsOfMode.length > 0) { // `>=` so equal timestamps (same-millisecond opens) resolve to the // later tab in insertion order. const mostRecent = tabsOfMode.reduce((best, tab) => (tab.touchedAt >= best.touchedAt ? tab : best)); state.setActiveContextPanelTab(normalizedDirectory, mostRecent.id); return; } // Content-driven modes need a payload (a preview URL or session); // the rail renders them disabled until content exists. 'file' opens // an empty editor whose embedded tree picks the first file. if (mode === 'preview' || mode === 'chat') { return; } state.openContextPanelTab(normalizedDirectory, { mode }); }, openContextPanelTab: (directory, tab) => { const normalizedDirectory = normalizeDirectoryPath((directory || '').trim()); if (!normalizedDirectory) { return; } set((state) => { const prev = state.contextPanelByDirectory[normalizedDirectory]; const current = touchContextPanelState(prev); const byDirectory = { ...state.contextPanelByDirectory, [normalizedDirectory]: upsertContextPanelTab(current, tab), }; return { contextPanelByDirectory: clampContextPanelRoots(byDirectory, 20) }; }); }, openContextDiff: (directory, filePath, staged = false, scope = null) => { const normalizedDirectory = normalizeDirectoryPath((directory || '').trim()); const normalizedFilePath = (filePath || '').trim(); if (!normalizedDirectory || !normalizedFilePath) { return; } const diffScope = normalizePendingDiffScope(scope) ?? (staged ? 'staged' : 'working'); get().openContextPanelTab(normalizedDirectory, { mode: 'diff', targetPath: normalizedFilePath, stagedDiff: diffScope === 'staged', diffScope, }); }, openContextFile: (directory, filePath) => { const normalizedDirectory = normalizeDirectoryPath((directory || '').trim()); const normalizedFilePath = normalizeContextTargetPath(filePath); if (!normalizedDirectory || !normalizedFilePath) { return; } get().openContextPanelTab(normalizedDirectory, { mode: 'file', targetPath: normalizedFilePath }); get().setPendingFileFocusPath(normalizedFilePath); get().setPendingFileNavigation(null); }, openContextFileAtLine: (directory, filePath, line, column) => { const normalizedDirectory = normalizeDirectoryPath((directory || '').trim()); const normalizedFilePath = normalizeContextTargetPath(filePath); const normalizedLine = Number.isFinite(line) ? Math.max(1, Math.trunc(line)) : 1; const normalizedColumn = Number.isFinite(column) ? Math.max(1, Math.trunc(column as number)) : 1; if (!normalizedDirectory || !normalizedFilePath) { return; } get().openContextPanelTab(normalizedDirectory, { mode: 'file', targetPath: normalizedFilePath }); get().setPendingFileFocusPath(null); get().setPendingFileNavigation({ path: normalizedFilePath, line: normalizedLine, column: normalizedColumn, }); }, openContextOverview: (directory) => { const normalizedDirectory = normalizeDirectoryPath((directory || '').trim()); if (!normalizedDirectory) { return; } get().openContextPanelTab(normalizedDirectory, { mode: 'context' }); }, openContextPlan: (directory) => { const normalizedDirectory = normalizeDirectoryPath((directory || '').trim()); if (!normalizedDirectory) { return; } get().openContextPanelTab(normalizedDirectory, { mode: 'plan' }); }, openContextPreview: (directory, url) => { const normalizedDirectory = normalizeDirectoryPath((directory || '').trim()); const normalizedUrl = (url || '').trim(); if (!normalizedDirectory || !normalizedUrl) { return; } let label: string | null = null; try { const parsed = new URL(normalizedUrl); if (parsed.protocol === 'http:' || parsed.protocol === 'https:') { label = parsed.host || parsed.hostname || 'Preview'; } } catch { // ignore invalid URL } get().openContextPanelTab(normalizedDirectory, { mode: 'preview', targetPath: normalizedUrl, dedupeKey: normalizedUrl, label, }); }, openContextBrowser: (directory, url = '') => { const normalizedDirectory = normalizeDirectoryPath((directory || '').trim()); if (!normalizedDirectory) return; const targetUrl = typeof url === 'string' && url.trim().length > 0 ? url.trim() : ''; get().openContextPanelTab(normalizedDirectory, { mode: 'browser', targetPath: targetUrl, dedupeKey: 'desktop-browser', label: 'Browser', }); }, setContextPanelTabTargetPath: (directory, tabID, targetPath) => { const normalizedDirectory = normalizeDirectoryPath((directory || '').trim()); const normalizedTabID = (tabID || '').trim(); if (!normalizedDirectory || !normalizedTabID) return; set((state) => { const current = state.contextPanelByDirectory[normalizedDirectory]; if (!current) return state; return { contextPanelByDirectory: { ...state.contextPanelByDirectory, [normalizedDirectory]: setContextPanelTabTargetPath(current, normalizedTabID, targetPath), }, }; }); }, setActiveContextPanelTab: (directory, tabID) => { const normalizedDirectory = normalizeDirectoryPath((directory || '').trim()); const normalizedTabID = (tabID || '').trim(); if (!normalizedDirectory || !normalizedTabID) { return; } set((state) => { const prev = state.contextPanelByDirectory[normalizedDirectory]; const current = touchContextPanelState(prev); if (!current.tabs.some((tab) => tab.id === normalizedTabID)) { return state; } if (current.activeTabId === normalizedTabID && current.isOpen) { return state; } const byDirectory = { ...state.contextPanelByDirectory, [normalizedDirectory]: { ...current, isOpen: true, activeTabId: normalizedTabID, touchedAt: Date.now(), tabs: current.tabs.map((tab) => (tab.id === normalizedTabID ? { ...tab, touchedAt: Date.now() } : tab)), }, }; return { contextPanelByDirectory: clampContextPanelRoots(byDirectory, 20) }; }); }, reorderContextPanelTabs: (directory, activeTabID, overTabID) => { const normalizedDirectory = normalizeDirectoryPath((directory || '').trim()); const normalizedActiveTabID = (activeTabID || '').trim(); const normalizedOverTabID = (overTabID || '').trim(); if (!normalizedDirectory || !normalizedActiveTabID || !normalizedOverTabID) { return; } set((state) => { const prev = state.contextPanelByDirectory[normalizedDirectory]; const current = touchContextPanelState(prev); if (!current.tabs.some((tab) => tab.id === normalizedActiveTabID) || !current.tabs.some((tab) => tab.id === normalizedOverTabID)) { return state; } const next = reorderContextPanelTabs(current, normalizedActiveTabID, normalizedOverTabID); if (next.tabs === current.tabs) { return state; } const byDirectory = { ...state.contextPanelByDirectory, [normalizedDirectory]: next, }; return { contextPanelByDirectory: clampContextPanelRoots(byDirectory, 20) }; }); }, closeContextPanelTab: (directory, tabID) => { const normalizedDirectory = normalizeDirectoryPath((directory || '').trim()); const normalizedTabID = (tabID || '').trim(); if (!normalizedDirectory || !normalizedTabID) { return; } const closingTab = get().contextPanelByDirectory[normalizedDirectory]?.tabs .find((tab) => tab.id === normalizedTabID); set((state) => { const prev = state.contextPanelByDirectory[normalizedDirectory]; const current = touchContextPanelState(prev); if (!current.tabs.some((tab) => tab.id === normalizedTabID)) { return state; } const byDirectory = { ...state.contextPanelByDirectory, [normalizedDirectory]: closeContextPanelTab(current, normalizedTabID), }; return { contextPanelByDirectory: clampContextPanelRoots(byDirectory, 20) }; }); // Keep the editor's own open-file state in sync so a reopened // editor surface does not resurrect the closed file. if (closingTab?.mode === 'file' && closingTab.targetPath) { useFilesViewTabsStore.getState().removeOpenPath(normalizedDirectory, closingTab.targetPath); } }, closeContextPanel: (directory) => { const normalizedDirectory = normalizeDirectoryPath((directory || '').trim()); if (!normalizedDirectory) { return; } set((state) => { const prev = state.contextPanelByDirectory[normalizedDirectory]; if (!prev || !prev.isOpen) { return state; } const byDirectory = { ...state.contextPanelByDirectory, [normalizedDirectory]: { ...touchContextPanelState(prev), isOpen: false, }, }; return { contextPanelByDirectory: clampContextPanelRoots(byDirectory, 20) }; }); }, toggleContextPanelExpanded: (directory) => { const normalizedDirectory = normalizeDirectoryPath((directory || '').trim()); if (!normalizedDirectory) { return; } set((state) => { const prev = state.contextPanelByDirectory[normalizedDirectory]; const current = touchContextPanelState(prev); const byDirectory = { ...state.contextPanelByDirectory, [normalizedDirectory]: { ...current, expanded: !current.expanded, }, }; return { contextPanelByDirectory: clampContextPanelRoots(byDirectory, 20) }; }); }, setContextPanelWidth: (directory, mode, width) => { const normalizedDirectory = normalizeDirectoryPath((directory || '').trim()); if (!normalizedDirectory) { return; } set((state) => { const prev = state.contextPanelByDirectory[normalizedDirectory]; const current = touchContextPanelState(prev); const byDirectory = { ...state.contextPanelByDirectory, [normalizedDirectory]: { ...current, widthByMode: { ...current.widthByMode, [mode]: clampContextPanelWidth(width), }, }, }; return { contextPanelByDirectory: clampContextPanelRoots(byDirectory, 20) }; }); }, setNotesPanelHeight: (height) => { set({ notesPanelHeight: height }); }, setTodoPanelHeight: (height) => { set({ todoPanelHeight: height }); }, setSessionSwitcherOpen: (open) => { if (get().isSessionSwitcherOpen === open) { return; } set({ isSessionSwitcherOpen: open }); }, setSessionDropdownOpen: (open) => { if (get().isSessionDropdownOpen === open) { return; } set({ isSessionDropdownOpen: open }); }, setMainTabGuard: (guard) => { if (get().mainTabGuard === guard) { return; } set({ mainTabGuard: guard }); }, setActiveMainTab: (tab) => { const guard = get().mainTabGuard; if (guard && !guard(tab)) { return; } activeMainTabByRuntime.set(runtimeMemoryKey(), tab); set({ activeMainTab: tab }); }, prepareForRuntimeSwitch: (runtimeKey?: string | null) => { activeMainTabByRuntime.set(runtimeMemoryKey(runtimeKey), get().activeMainTab); }, restoreForRuntimeSwitch: (runtimeKey?: string | null) => { const restored = activeMainTabByRuntime.get(runtimeMemoryKey(runtimeKey)) ?? 'chat'; set({ activeMainTab: restored }); }, setPendingDiffFile: (filePath, staged = false, scope = null) => { set({ pendingDiffFile: filePath, pendingDiffStaged: filePath ? staged : false, pendingDiffScope: filePath ? scope : null, }); }, setPendingDiagramFile: (filePath) => { set({ pendingDiagramFile: filePath }); }, setPendingFileNavigation: (navigation) => { set({ pendingFileNavigation: navigation }); }, setPendingFileFocusPath: (path) => { set({ pendingFileFocusPath: path }); }, navigateToDiff: (filePath, staged = false, scope = null) => { const guard = get().mainTabGuard; if (guard && !guard('diff')) { return; } set({ pendingDiffFile: filePath, pendingDiffStaged: staged, pendingDiffScope: scope, activeMainTab: 'diff' }); }, consumePendingDiffFile: () => { const { pendingDiffFile } = get(); if (pendingDiffFile) { set({ pendingDiffFile: null, pendingDiffStaged: false, pendingDiffScope: null }); } return pendingDiffFile; }, navigateToDiagram: (filePath) => { const guard = get().mainTabGuard; if (guard && !guard('diagram')) { return; } set({ pendingDiagramFile: filePath, activeMainTab: 'diagram' }); }, consumePendingDiagramFile: () => { const { pendingDiagramFile } = get(); if (pendingDiagramFile) { set({ pendingDiagramFile: null }); } return pendingDiagramFile; }, setIsMobile: (isMobile) => { set({ isMobile }); }, toggleCommandPalette: () => { set((state) => ({ isCommandPaletteOpen: !state.isCommandPaletteOpen })); }, setCommandPaletteOpen: (open) => { set({ isCommandPaletteOpen: open }); }, toggleHelpDialog: () => { set((state) => ({ isHelpDialogOpen: !state.isHelpDialogOpen })); }, setHelpDialogOpen: (open) => { set({ isHelpDialogOpen: open }); }, setAboutDialogOpen: (open) => { set({ isAboutDialogOpen: open }); }, setOpenCodeStatusDialogOpen: (open) => { set({ isOpenCodeStatusDialogOpen: open }); }, setOpenCodeStatusText: (text) => { set({ openCodeStatusText: text }); }, setSessionCreateDialogOpen: (open) => { set({ isSessionCreateDialogOpen: open }); }, setScheduledTasksDialogOpen: (open) => { set(open ? { isScheduledTasksDialogOpen: true, isArchivePageOpen: false, worktreesPageProjectId: null, isMultiRunLauncherOpen: false } : { isScheduledTasksDialogOpen: false }); }, setArchivePageOpen: (open) => { set(open ? { isArchivePageOpen: true, isScheduledTasksDialogOpen: false, worktreesPageProjectId: null, isMultiRunLauncherOpen: false } : { isArchivePageOpen: false }); }, setWorktreesPageProjectId: (projectId) => { set(projectId ? { worktreesPageProjectId: projectId, isScheduledTasksDialogOpen: false, isArchivePageOpen: false, isMultiRunLauncherOpen: false } : { worktreesPageProjectId: null }); }, closeMainSurfaces: () => { const state = get(); if (!state.isScheduledTasksDialogOpen && !state.isArchivePageOpen && !state.worktreesPageProjectId && !state.isMultiRunLauncherOpen) { return; } set({ isScheduledTasksDialogOpen: false, isArchivePageOpen: false, worktreesPageProjectId: null, isMultiRunLauncherOpen: false, multiRunLauncherPrefillPrompt: '', }); }, setSettingsDialogOpen: (open) => { set((state) => { if (!open) { return { isSettingsDialogOpen: false }; } if (state.settingsHasOpenedOnce) { return { isSettingsDialogOpen: true }; } return { isSettingsDialogOpen: true, settingsHasOpenedOnce: true }; }); }, setNewWorktreeDialogOpen: (open) => { set({ isNewWorktreeDialogOpen: open }); }, setModelSelectorOpen: (open) => { set({ isModelSelectorOpen: open }); }, setSidebarSection: (section) => { set({ sidebarSection: section }); }, setSettingsPage: (slug) => { set({ settingsPage: slug }); }, setSettingsProjectsSelectedId: (projectId) => { set({ settingsProjectsSelectedId: projectId }); }, setSettingsRemoteInstancesSelectedId: (instanceId) => { set({ settingsRemoteInstancesSelectedId: instanceId }); }, setEventStreamStatus: (status, hint) => { set({ eventStreamStatus: status, eventStreamHint: hint ?? null, }); }, setShowReasoningTraces: (value) => { set({ showReasoningTraces: value }); }, setSessionRecapEnabled: (value) => { set({ sessionRecapEnabled: value }); }, setSessionSuggestionEnabled: (value) => { set({ sessionSuggestionEnabled: value }); }, setSessionGoalEnabled: (value) => { set({ sessionGoalEnabled: value }); }, setSessionGoalDefaultBudgetEnabled: (value) => { set({ sessionGoalDefaultBudgetEnabled: value }); }, setSessionGoalDefaultBudget: (value) => { set({ sessionGoalDefaultBudget: value }); }, setCollapsibleThinkingBlocks: (value) => { set({ collapsibleThinkingBlocks: value }); }, setChatRenderMode: (value) => { set({ chatRenderMode: value }); }, setActivityRenderMode: (value) => { set({ activityRenderMode: value }); }, setShowDeletionDialog: (value) => { set({ showDeletionDialog: value }); }, setAutoDeleteEnabled: (value) => { set({ autoDeleteEnabled: value }); }, setAutoDeleteAfterDays: (days) => { const clampedDays = Math.max(1, Math.min(365, days)); set({ autoDeleteAfterDays: clampedDays }); }, setSessionRetentionAction: (value) => { set({ sessionRetentionAction: value }); }, setAutoDeleteLastRunAt: (timestamp) => { set({ autoDeleteLastRunAt: timestamp }); }, setMessageLimit: (value) => { const clamped = Math.max(10, Math.min(500, Math.round(value))); set({ messageLimit: clamped }); }, setFontSize: (size) => { // Clamp between 50% and 200% const clampedSize = Math.max(50, Math.min(200, size)); set({ fontSize: clampedSize }); get().applyTypography(); }, setGlobalDraftStarters: (refs) => { set({ globalDraftStarters: refs }); }, setDraftStartersVisible: (value) => { set({ draftStartersVisible: value }); }, setTerminalFontSize: (size) => { const rounded = Math.round(size); const clamped = Math.max(9, Math.min(52, rounded)); set({ terminalFontSize: clamped }); }, setTerminalShell: (shell) => { set({ terminalShell: shell }); }, setTerminalLoginShells: (shells) => { set({ terminalLoginShells: [...new Set(shells)] }); }, setEditorFontSize: (size) => { const rounded = Math.round(size); const clamped = Math.max(9, Math.min(32, rounded)); set({ editorFontSize: clamped }); }, setUiFont: (font) => { set({ uiFont: font }); }, setMonoFont: (font) => { set({ monoFont: font }); }, setPadding: (size) => { // Clamp between 50% and 200% const clampedSize = Math.max(50, Math.min(200, size)); set({ padding: clampedSize }); get().applyPadding(); }, setCornerRadius: (radius) => { set({ cornerRadius: radius }); }, applyTypography: () => { const { fontSize } = get(); const root = document.documentElement; // 100 = default (1.0x), 50 = half size (0.5x), 200 = double (2.0x) const scale = fontSize / 100; const entries = Object.entries(SEMANTIC_TYPOGRAPHY) as Array<[SemanticTypographyKey, string]>; // Default must be SEMANTIC_TYPOGRAPHY (from CSS). Remove overrides. if (scale === 1) { for (const [key] of entries) { root.style.removeProperty(getTypographyVariable(key)); } return; } for (const [key, baseValue] of entries) { const numericValue = parseFloat(baseValue); if (!Number.isFinite(numericValue)) { continue; } root.style.setProperty(getTypographyVariable(key), `${numericValue * scale}rem`); } }, applyPadding: () => { const { padding } = get(); const root = document.documentElement; const scale = padding / 100; if (scale === 1) { root.style.removeProperty('--padding-scale'); root.style.removeProperty('--line-height-tight'); root.style.removeProperty('--line-height-normal'); root.style.removeProperty('--line-height-relaxed'); root.style.removeProperty('--line-height-loose'); return; } // Apply padding as a percentage scale with non-linear scaling // Use square root for more natural scaling at extremes const adjustedScale = Math.sqrt(scale); // Set the CSS custom property that all spacing tokens reference root.style.setProperty('--padding-scale', adjustedScale.toString()); // Dampened line-height scaling at extremes const lineHeightScale = 1 + (scale - 1) * 0.15; root.style.setProperty('--line-height-tight', (1.25 * lineHeightScale).toFixed(3)); root.style.setProperty('--line-height-normal', (1.5 * lineHeightScale).toFixed(3)); root.style.setProperty('--line-height-relaxed', (1.625 * lineHeightScale).toFixed(3)); root.style.setProperty('--line-height-loose', (2 * lineHeightScale).toFixed(3)); }, setDiffLayoutPreference: (mode) => { set({ diffLayoutPreference: mode }); }, setDiffFileLayout: (filePath, mode) => { set((state) => ({ diffFileLayout: { ...state.diffFileLayout, [filePath]: mode, }, })); }, setDiffWrapLines: (wrap) => { set({ diffWrapLines: wrap }); }, setGitChangesViewMode: (mode) => { set({ gitChangesViewMode: mode }); }, setInputBarOffset: (offset) => { set({ inputBarOffset: offset }); }, setMobileKeyboardMode: (mode) => { set((state) => state.mobileKeyboardMode === mode ? state : { mobileKeyboardMode: mode }); }, toggleFavoriteModel: (providerID, modelID) => { set((state) => { const exists = state.favoriteModels.some( (fav) => fav.providerID === providerID && fav.modelID === modelID ); if (exists) { // Remove from favorites return { favoriteModels: state.favoriteModels.filter( (fav) => !(fav.providerID === providerID && fav.modelID === modelID) ), }; } else { // Add to favorites (newest first) return { favoriteModels: [{ providerID, modelID }, ...state.favoriteModels], }; } }); }, reorderFavoriteModel: (activeProviderID, activeModelID, overProviderID, overModelID) => { set((state) => { const oldIndex = state.favoriteModels.findIndex( (fav) => fav.providerID === activeProviderID && fav.modelID === activeModelID ); const newIndex = state.favoriteModels.findIndex( (fav) => fav.providerID === overProviderID && fav.modelID === overModelID ); if (oldIndex === -1 || newIndex === -1 || oldIndex === newIndex) { return state; } const nextFavorites = state.favoriteModels.slice(); const [moved] = nextFavorites.splice(oldIndex, 1); if (!moved) { return state; } nextFavorites.splice(newIndex, 0, moved); return { favoriteModels: nextFavorites }; }); }, setProviderOrder: (orderedProviderIDs) => { set((state) => { const next = orderedProviderIDs.filter((id) => typeof id === 'string' && id.length > 0); const current = state.providerOrder; if (current.length === next.length && current.every((id, index) => id === next[index])) { return state; } return { providerOrder: next }; }); }, toggleHiddenModel: (providerID, modelID) => { set((state) => { const exists = state.hiddenModels.some( (item) => item.providerID === providerID && item.modelID === modelID ); if (exists) { return { hiddenModels: state.hiddenModels.filter( (item) => !(item.providerID === providerID && item.modelID === modelID) ), }; } return { hiddenModels: [{ providerID, modelID }, ...state.hiddenModels], }; }); }, isHiddenModel: (providerID, modelID) => { const { hiddenModels } = get(); return hiddenModels.some( (item) => item.providerID === providerID && item.modelID === modelID ); }, hideAllModels: (providerID, modelIDs) => { set((state) => { const current = state.hiddenModels.filter((item) => item.providerID !== providerID); const additions = modelIDs .filter((modelID) => typeof modelID === 'string' && modelID.length > 0) .map((modelID) => ({ providerID, modelID })); return { hiddenModels: [...additions, ...current] }; }); }, showAllModels: (providerID) => { set((state) => ({ hiddenModels: state.hiddenModels.filter((item) => item.providerID !== providerID), })); }, toggleModelProviderCollapsed: (providerID) => { const normalizedProviderID = typeof providerID === 'string' ? providerID.trim() : ''; if (!normalizedProviderID) { return; } set((state) => { const isCollapsed = state.collapsedModelProviders.includes(normalizedProviderID); if (isCollapsed) { return { collapsedModelProviders: state.collapsedModelProviders.filter((id) => id !== normalizedProviderID), }; } return { collapsedModelProviders: [...state.collapsedModelProviders, normalizedProviderID], }; }); }, setModelProvidersCollapsed: (providerIDs, collapsed) => { const normalizedProviderIDs = Array.from(new Set( providerIDs .filter((providerID): providerID is string => typeof providerID === 'string') .map((providerID) => providerID.trim()) .filter(Boolean) )); if (normalizedProviderIDs.length === 0) { return; } set((state) => { const scopedProviderIDs = new Set(normalizedProviderIDs); const untouchedProviders = state.collapsedModelProviders.filter((providerID) => !scopedProviderIDs.has(providerID)); return { collapsedModelProviders: collapsed ? [...untouchedProviders, ...normalizedProviderIDs] : untouchedProviders, }; }); }, isFavoriteModel: (providerID, modelID) => { const { favoriteModels } = get(); return favoriteModels.some( (fav) => fav.providerID === providerID && fav.modelID === modelID ); }, addRecentModel: (providerID, modelID) => { set((state) => { // Remove existing instance if any const filtered = state.recentModels.filter( (m) => !(m.providerID === providerID && m.modelID === modelID) ); // Add to front, limit to 5 return { recentModels: [{ providerID, modelID }, ...filtered].slice(0, 5), }; }); }, addRecentAgent: (agentName) => { const normalized = typeof agentName === 'string' ? agentName.trim() : ''; if (!normalized) { return; } set((state) => { if (state.recentAgents.includes(normalized)) { return state; } const filtered = state.recentAgents; return { recentAgents: [normalized, ...filtered].slice(0, 5), }; }); }, addRecentEffort: (providerID, modelID, variant) => { const provider = typeof providerID === 'string' ? providerID.trim() : ''; const model = typeof modelID === 'string' ? modelID.trim() : ''; if (!provider || !model) { return; } const key = `${provider}/${model}`; const normalizedVariant = typeof variant === 'string' && variant.trim().length > 0 ? variant.trim() : 'default'; set((state) => { const current = state.recentEfforts[key] ?? []; if (current.includes(normalizedVariant)) { return state; } const filtered = current; return { recentEfforts: { ...state.recentEfforts, [key]: [normalizedVariant, ...filtered].slice(0, 5), }, }; }); }, applyTheme: () => { const { theme } = get(); const root = document.documentElement; root.classList.remove('light', 'dark'); if (theme === 'system') { const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; root.classList.add(systemTheme); } else { root.classList.add(theme); } }, // Multi-run is one of the mutually exclusive full-page surfaces: // opening it closes the other surfaces and vice versa. setMultiRunLauncherOpen: (open) => { set((state) => ({ isMultiRunLauncherOpen: open, multiRunLauncherPrefillPrompt: open ? state.multiRunLauncherPrefillPrompt : '', ...(open ? { isScheduledTasksDialogOpen: false, isArchivePageOpen: false, worktreesPageProjectId: null } : {}), })); }, openMultiRunLauncher: () => { set({ isMultiRunLauncherOpen: true, multiRunLauncherPrefillPrompt: '', isSessionSwitcherOpen: false, isScheduledTasksDialogOpen: false, isArchivePageOpen: false, worktreesPageProjectId: null, }); }, openMultiRunLauncherWithPrompt: (prompt) => { set({ isMultiRunLauncherOpen: true, multiRunLauncherPrefillPrompt: prompt, isSessionSwitcherOpen: false, isScheduledTasksDialogOpen: false, isArchivePageOpen: false, worktreesPageProjectId: null, }); }, setTimelineDialogOpen: (open) => { set({ isTimelineDialogOpen: open }); }, setPromptNavigatorPanelOpen: (open) => { set({ isPromptNavigatorPanelOpen: open }); }, togglePromptNavigatorPanel: () => { set((state) => ({ isPromptNavigatorPanelOpen: !state.isPromptNavigatorPanelOpen })); }, setImagePreviewOpen: (open) => { set({ isImagePreviewOpen: open }); }, setNativeNotificationsEnabled: (value) => { set({ nativeNotificationsEnabled: value }); }, setNotificationMode: (mode) => { set({ notificationMode: mode }); }, setShowTerminalQuickKeysOnDesktop: (value) => { set({ showTerminalQuickKeysOnDesktop: value }); }, setNotifyOnSubtasks: (value) => { set({ notifyOnSubtasks: value }); }, setDockBadgeEnabled: (value) => { set({ dockBadgeEnabled: value }); }, setNotifyOnCompletion: (value) => { set({ notifyOnCompletion: value }); }, setNotifyOnError: (value) => { set({ notifyOnError: value }); }, setNotifyOnQuestion: (value) => { set({ notifyOnQuestion: value }); }, setNotificationTemplates: (templates) => { set({ notificationTemplates: templates }); }, setSummarizeLastMessage: (value) => { set({ summarizeLastMessage: value }); }, setSummaryThreshold: (value) => { set({ summaryThreshold: value }); }, setSummaryLength: (value) => { set({ summaryLength: value }); }, setMaxLastMessageLength: (value) => { set({ maxLastMessageLength: value }); }, setPersistChatDraft: (value) => { set({ persistChatDraft: value }); }, setShowOpenCodeUpdateNotifications: (value) => { set({ showOpenCodeUpdateNotifications: value }); }, setAgentControlToolEnabled: (value) => { set({ agentControlToolEnabled: value }); }, setInputSpellcheckEnabled: (value) => { set({ inputSpellcheckEnabled: value }); }, setWideChatLayoutEnabled: (value) => { set({ wideChatLayoutEnabled: value }); }, setCodeBlockLineWrap: (value) => { set({ codeBlockLineWrap: value }); }, setShowToolFileIcons: (value) => { set({ showToolFileIcons: value }); }, setShowTurnChangedFiles: (value) => { set({ showTurnChangedFiles: value }); }, setShowExpandedBashTools: (value) => { set({ showExpandedBashTools: value }); }, setShowExpandedEditTools: (value) => { set({ showExpandedEditTools: value }); }, setTimeFormatPreference: (value) => { set({ timeFormatPreference: value }); }, setWeekStartPreference: (value) => { set({ weekStartPreference: value }); }, setDesktopWindowControlsPosition: (value) => { set({ desktopWindowControlsPosition: value === 'left' ? 'left' : 'right' }); }, setDesktopWindowControlsStyle: (value) => { set({ desktopWindowControlsStyle: value === 'traffic-lights' ? 'traffic-lights' : 'classic' }); }, setMermaidRenderingMode: (value) => { set({ mermaidRenderingMode: value }); }, setUserMessageRenderingMode: (value) => { set({ userMessageRenderingMode: value }); }, setCollapsibleUserMessages: (value) => { set({ collapsibleUserMessages: value }); }, setStickyUserHeader: (value) => { set({ stickyUserHeader: value }); }, setPromptNavigatorEnabled: (value) => { set({ promptNavigatorEnabled: value }); }, setExpandedEditorToolbar: (value: boolean) => { set({ expandedEditorToolbar: value }); }, setShowSplitAssistantMessageActions: (value) => { set({ showSplitAssistantMessageActions: value }); }, setAllowPromptingSubagentSessions: (value) => { set({ allowPromptingSubagentSessions: value }); }, setIsMobileSessionStatusBarCollapsed: (value) => { set({ isMobileSessionStatusBarCollapsed: value }); }, setMobileSessionPanelOpen: (value) => { set({ mobileSessionPanelOpen: value }); }, setMobileSessionFilterProjectId: (value) => { set({ mobileSessionFilterProjectId: value }); }, setReportUsage: (value) => { set({ reportUsage: value }); }, viewPagerPage: 'center', setViewPagerPage: (page: 'left' | 'center' | 'right') => { set({ viewPagerPage: page }); set({ isSessionSwitcherOpen: page === 'left' }); }, setShortcutOverride: (actionId, combo) => { set((state) => ({ shortcutOverrides: { ...state.shortcutOverrides, [actionId]: combo, }, })); }, clearShortcutOverride: (actionId) => { set((state) => { const rest = { ...state.shortcutOverrides }; delete rest[actionId]; return { shortcutOverrides: rest }; }); }, resetAllShortcutOverrides: () => { set({ shortcutOverrides: {} }); }, setFileEditorKeymap: (value) => { set({ fileEditorKeymap: normalizeFileEditorKeymap(value) }); }, toggleExpandedInput: () => { set((state) => ({ isExpandedInput: !state.isExpandedInput })); }, setExpandedInput: (value) => { set({ isExpandedInput: value }); }, }), { name: 'ui-store', storage: createDeferredSafeJSONStorage(), version: 12, migrate: (persistedState, version) => { if (!persistedState || typeof persistedState !== 'object') { return persistedState; } const state = persistedState as Record; // v11 -> v12: drop legacy window-controls "auto" (always meant right). if (version < 12) { if (state.desktopWindowControlsPosition === 'auto' || state.desktopWindowControlsPosition == null) { state.desktopWindowControlsPosition = 'right'; } } // v10 -> v11: move the previous terminal font default forward. if (version < 11 && state.terminalFontSize === 13) { state.terminalFontSize = 14; } // v9 -> v10: remove obsolete single-file diff view mode setting if (version < 10) { delete state.diffViewMode; } // v8 -> v9: initialize notes/todo panel height fields if (version < 9) { if (typeof state.notesPanelHeight !== 'number' || !Number.isFinite(state.notesPanelHeight)) { state.notesPanelHeight = 112; } if (typeof state.todoPanelHeight !== 'number' || !Number.isFinite(state.todoPanelHeight)) { state.todoPanelHeight = 259; } } // v0 -> v1: reset legacy notification templates if (version < 1) { if (isLegacyDefaultTemplates(state.notificationTemplates)) { state.notificationTemplates = { completion: { ...EMPTY_NOTIFICATION_TEMPLATES.completion }, error: { ...EMPTY_NOTIFICATION_TEMPLATES.error }, question: { ...EMPTY_NOTIFICATION_TEMPLATES.question }, subtask: { ...EMPTY_NOTIFICATION_TEMPLATES.subtask }, }; } } // v2 -> v3: collapse 3 memory-limit fields into single messageLimit. // Pick the best user-customised value (prefer historical, fall back to active). // Discard old defaults (90/120/180) — they become the new single default (200). if (version < 3) { const OLD_DEFAULTS = new Set([90, 120, 180, 220]); const hist = state.memoryLimitHistorical as number | undefined; const active = state.memoryLimitActiveSession as number | undefined; // If user had a non-default custom value, keep it as the new messageLimit. if (typeof hist === 'number' && !OLD_DEFAULTS.has(hist)) { state.messageLimit = hist; } else if (typeof active === 'number' && !OLD_DEFAULTS.has(active)) { state.messageLimit = active; } // Otherwise leave undefined → Zustand uses the initial default (200). delete state.memoryLimitHistorical; delete state.memoryLimitViewport; delete state.memoryLimitActiveSession; } // Right-sidebar state was removed with the sidebar itself; drop // stale persisted fields. delete state.isRightSidebarOpen; delete state.rightSidebarWidth; delete state.rightSidebarTab; state.contextPanelByDirectory = sanitizeContextPanelByDirectory(state.contextPanelByDirectory); if (version < 5) { if (!state.shortcutOverrides || typeof state.shortcutOverrides !== 'object') { state.shortcutOverrides = {}; } else { const overrides = state.shortcutOverrides as Record; const cleaned: Record = {}; for (const [key, value] of Object.entries(overrides)) { if (typeof key === 'string' && typeof value === 'string') { cleaned[key] = value; } } state.shortcutOverrides = cleaned; } } if (version < 6) { state.contextPanelByDirectory = sanitizeContextPanelByDirectory(state.contextPanelByDirectory); } if (version < 7) { state.contextPanelByDirectory = sanitizeContextPanelByDirectory(state.contextPanelByDirectory); } if (version < 8) { if (state.gitChangesViewMode !== 'flat' && state.gitChangesViewMode !== 'tree') { state.gitChangesViewMode = 'flat'; } } state.fileEditorKeymap = normalizeFileEditorKeymap(state.fileEditorKeymap); state.contextRailOrder = Array.isArray(state.contextRailOrder) ? (state.contextRailOrder as unknown[]).filter((id): id is string => typeof id === 'string' && id.trim() !== '') : []; return state; }, partialize: (state) => ({ theme: state.theme, isSidebarOpen: state.isSidebarOpen, sidebarWidth: state.sidebarWidth, contextPanelByDirectory: state.contextPanelByDirectory, contextRailOrder: state.contextRailOrder, contextEditorTreeVisible: state.contextEditorTreeVisible, contextEditorTreeWidth: state.contextEditorTreeWidth, notesPanelHeight: state.notesPanelHeight, todoPanelHeight: state.todoPanelHeight, isSessionSwitcherOpen: state.isSessionSwitcherOpen, activeMainTab: state.activeMainTab, sidebarSection: state.sidebarSection, settingsPage: state.settingsPage, settingsHasOpenedOnce: state.settingsHasOpenedOnce, settingsProjectsSelectedId: state.settingsProjectsSelectedId, settingsRemoteInstancesSelectedId: state.settingsRemoteInstancesSelectedId, isSessionCreateDialogOpen: state.isSessionCreateDialogOpen, // Note: isSettingsDialogOpen intentionally NOT persisted showReasoningTraces: state.showReasoningTraces, sessionRecapEnabled: state.sessionRecapEnabled, sessionSuggestionEnabled: state.sessionSuggestionEnabled, sessionGoalEnabled: state.sessionGoalEnabled, sessionGoalDefaultBudgetEnabled: state.sessionGoalDefaultBudgetEnabled, sessionGoalDefaultBudget: state.sessionGoalDefaultBudget, collapsibleThinkingBlocks: state.collapsibleThinkingBlocks, chatRenderMode: state.chatRenderMode, activityRenderMode: state.activityRenderMode, showDeletionDialog: state.showDeletionDialog, autoDeleteEnabled: state.autoDeleteEnabled, autoDeleteAfterDays: state.autoDeleteAfterDays, sessionRetentionAction: state.sessionRetentionAction, autoDeleteLastRunAt: state.autoDeleteLastRunAt, messageLimit: state.messageLimit, fontSize: state.fontSize, globalDraftStarters: state.globalDraftStarters, terminalFontSize: state.terminalFontSize, terminalShell: state.terminalShell, terminalLoginShells: state.terminalLoginShells, editorFontSize: state.editorFontSize, uiFont: state.uiFont, monoFont: state.monoFont, padding: state.padding, cornerRadius: state.cornerRadius, favoriteModels: state.favoriteModels, hiddenModels: state.hiddenModels, providerOrder: state.providerOrder, collapsedModelProviders: state.collapsedModelProviders, recentModels: state.recentModels, recentAgents: state.recentAgents, recentEfforts: state.recentEfforts, diffLayoutPreference: state.diffLayoutPreference, diffWrapLines: state.diffWrapLines, gitChangesViewMode: state.gitChangesViewMode, nativeNotificationsEnabled: state.nativeNotificationsEnabled, notificationMode: state.notificationMode, showTerminalQuickKeysOnDesktop: state.showTerminalQuickKeysOnDesktop, notifyOnSubtasks: state.notifyOnSubtasks, dockBadgeEnabled: state.dockBadgeEnabled, notifyOnCompletion: state.notifyOnCompletion, notifyOnError: state.notifyOnError, notifyOnQuestion: state.notifyOnQuestion, notificationTemplates: state.notificationTemplates, summarizeLastMessage: state.summarizeLastMessage, summaryThreshold: state.summaryThreshold, summaryLength: state.summaryLength, maxLastMessageLength: state.maxLastMessageLength, persistChatDraft: state.persistChatDraft, showOpenCodeUpdateNotifications: state.showOpenCodeUpdateNotifications, agentControlToolEnabled: state.agentControlToolEnabled, inputSpellcheckEnabled: state.inputSpellcheckEnabled, wideChatLayoutEnabled: state.wideChatLayoutEnabled, codeBlockLineWrap: state.codeBlockLineWrap, showToolFileIcons: state.showToolFileIcons, showTurnChangedFiles: state.showTurnChangedFiles, showExpandedBashTools: state.showExpandedBashTools, showExpandedEditTools: state.showExpandedEditTools, timeFormatPreference: state.timeFormatPreference, weekStartPreference: state.weekStartPreference, desktopWindowControlsPosition: state.desktopWindowControlsPosition, desktopWindowControlsStyle: state.desktopWindowControlsStyle, mermaidRenderingMode: state.mermaidRenderingMode, userMessageRenderingMode: state.userMessageRenderingMode, collapsibleUserMessages: state.collapsibleUserMessages, stickyUserHeader: state.stickyUserHeader, promptNavigatorEnabled: state.promptNavigatorEnabled, expandedEditorToolbar: state.expandedEditorToolbar, showSplitAssistantMessageActions: state.showSplitAssistantMessageActions, allowPromptingSubagentSessions: state.allowPromptingSubagentSessions, draftStartersVisible: state.draftStartersVisible, isMobileSessionStatusBarCollapsed: state.isMobileSessionStatusBarCollapsed, mobileSessionFilterProjectId: state.mobileSessionFilterProjectId, shortcutOverrides: state.shortcutOverrides, fileEditorKeymap: state.fileEditorKeymap, }) } ), { name: 'ui-store' } ) );