import { create } from "zustand"; import { persist } from "zustand/middleware"; import type { Project, TrashedProject } from "@/lib/electron"; export type ViewMode = | "welcome" | "setup" | "spec" | "board" | "agent" | "settings" | "interview" | "context" | "profiles" | "running-agents" | "terminal" | "wiki"; export type ThemeMode = | "light" | "dark" | "system" | "retro" | "dracula" | "nord" | "monokai" | "tokyonight" | "solarized" | "gruvbox" | "catppuccin" | "onedark" | "synthwave" | "red"; export type KanbanCardDetailLevel = "minimal" | "standard" | "detailed"; export interface ApiKeys { anthropic: string; google: string; openai: string; } // Keyboard Shortcut with optional modifiers export interface ShortcutKey { key: string; // The main key (e.g., "K", "N", "1") shift?: boolean; // Shift key modifier cmdCtrl?: boolean; // Cmd on Mac, Ctrl on Windows/Linux alt?: boolean; // Alt/Option key modifier } // Helper to parse shortcut string to ShortcutKey object export function parseShortcut( shortcut: string | undefined | null ): ShortcutKey { if (!shortcut) return { key: "" }; const parts = shortcut.split("+").map((p) => p.trim()); const result: ShortcutKey = { key: parts[parts.length - 1] }; // Normalize common OS-specific modifiers (Cmd/Ctrl/Win/Super symbols) into cmdCtrl for (let i = 0; i < parts.length - 1; i++) { const modifier = parts[i].toLowerCase(); if (modifier === "shift") result.shift = true; else if ( modifier === "cmd" || modifier === "ctrl" || modifier === "win" || modifier === "super" || modifier === "⌘" || modifier === "^" || modifier === "⊞" || modifier === "◆" ) result.cmdCtrl = true; else if ( modifier === "alt" || modifier === "opt" || modifier === "option" || modifier === "⌥" ) result.alt = true; } return result; } // Helper to format ShortcutKey to display string export function formatShortcut( shortcut: string | undefined | null, forDisplay = false ): string { if (!shortcut) return ""; const parsed = parseShortcut(shortcut); const parts: string[] = []; // Prefer User-Agent Client Hints when available; fall back to legacy const platform: "darwin" | "win32" | "linux" = (() => { if (typeof navigator === "undefined") return "linux"; const uaPlatform = ( navigator as Navigator & { userAgentData?: { platform?: string } } ).userAgentData?.platform?.toLowerCase?.(); const legacyPlatform = navigator.platform?.toLowerCase?.(); const platformString = uaPlatform || legacyPlatform || ""; if (platformString.includes("mac")) return "darwin"; if (platformString.includes("win")) return "win32"; return "linux"; })(); // Primary modifier - OS-specific if (parsed.cmdCtrl) { if (forDisplay) { parts.push( platform === "darwin" ? "⌘" : platform === "win32" ? "⊞" : "◆" ); } else { parts.push( platform === "darwin" ? "Cmd" : platform === "win32" ? "Win" : "Super" ); } } // Alt/Option if (parsed.alt) { parts.push( forDisplay ? platform === "darwin" ? "⌥" : "Alt" : platform === "darwin" ? "Opt" : "Alt" ); } // Shift if (parsed.shift) { parts.push(forDisplay ? "⇧" : "Shift"); } parts.push(parsed.key.toUpperCase()); // Add spacing when displaying symbols return parts.join(forDisplay ? " " : "+"); } // Keyboard Shortcuts - stored as strings like "K", "Shift+N", "Cmd+K" export interface KeyboardShortcuts { // Navigation shortcuts board: string; agent: string; spec: string; context: string; settings: string; profiles: string; terminal: string; // UI shortcuts toggleSidebar: string; // Action shortcuts addFeature: string; addContextFile: string; startNext: string; newSession: string; openProject: string; projectPicker: string; cyclePrevProject: string; cycleNextProject: string; addProfile: string; // Terminal shortcuts splitTerminalRight: string; splitTerminalDown: string; closeTerminal: string; } // Default keyboard shortcuts export const DEFAULT_KEYBOARD_SHORTCUTS: KeyboardShortcuts = { // Navigation board: "K", agent: "A", spec: "D", context: "C", settings: "S", profiles: "M", terminal: "T", // UI toggleSidebar: "`", // Actions // Note: Some shortcuts share the same key (e.g., "N" for addFeature, newSession, addProfile) // This is intentional as they are context-specific and only active in their respective views addFeature: "N", // Only active in board view addContextFile: "N", // Only active in context view startNext: "G", // Only active in board view newSession: "N", // Only active in agent view openProject: "O", // Global shortcut projectPicker: "P", // Global shortcut cyclePrevProject: "Q", // Global shortcut cycleNextProject: "E", // Global shortcut addProfile: "N", // Only active in profiles view // Terminal shortcuts (only active in terminal view) // Using Alt modifier to avoid conflicts with both terminal signals AND browser shortcuts splitTerminalRight: "Alt+D", splitTerminalDown: "Alt+S", closeTerminal: "Alt+W", }; export interface ImageAttachment { id: string; data: string; // base64 encoded image data mimeType: string; // e.g., "image/png", "image/jpeg" filename: string; size: number; // file size in bytes } export interface ChatMessage { id: string; role: "user" | "assistant"; content: string; timestamp: Date; images?: ImageAttachment[]; } export interface ChatSession { id: string; title: string; projectId: string; messages: ChatMessage[]; createdAt: Date; updatedAt: Date; archived: boolean; } export interface FeatureImage { id: string; data: string; // base64 encoded mimeType: string; filename: string; size: number; } export interface FeatureImagePath { id: string; path: string; // Path to the temp file filename: string; mimeType: string; } // Available models for feature execution export type ClaudeModel = "opus" | "sonnet" | "haiku"; export type AgentModel = ClaudeModel; // Model provider type export type ModelProvider = "claude"; // Thinking level (budget_tokens) options export type ThinkingLevel = "none" | "low" | "medium" | "high" | "ultrathink"; // AI Provider Profile - user-defined presets for model configurations export interface AIProfile { id: string; name: string; description: string; model: AgentModel; thinkingLevel: ThinkingLevel; provider: ModelProvider; isBuiltIn: boolean; // Built-in profiles cannot be deleted icon?: string; // Optional icon name from lucide } export interface Feature { id: string; category: string; description: string; steps: string[]; status: "backlog" | "in_progress" | "waiting_approval" | "verified"; images?: FeatureImage[]; imagePaths?: FeatureImagePath[]; // Paths to temp files for agent context startedAt?: string; // ISO timestamp for when the card moved to in_progress skipTests?: boolean; // When true, skip TDD approach and require manual verification summary?: string; // Summary of what was done/modified by the agent model?: AgentModel; // Model to use for this feature (defaults to opus) thinkingLevel?: ThinkingLevel; // Thinking level for extended thinking (defaults to none) error?: string; // Error message if the agent errored during processing // Worktree info - set when a feature is being worked on in an isolated git worktree worktreePath?: string; // Path to the worktree directory branchName?: string; // Name of the feature branch justFinishedAt?: string; // ISO timestamp when agent just finished and moved to waiting_approval (shows badge for 2 minutes) } // File tree node for project analysis export interface FileTreeNode { name: string; path: string; isDirectory: boolean; extension?: string; children?: FileTreeNode[]; } // Project analysis result export interface ProjectAnalysis { fileTree: FileTreeNode[]; totalFiles: number; totalDirectories: number; filesByExtension: Record; analyzedAt: string; } // Terminal panel layout types (recursive for splits) export type TerminalPanelContent = | { type: "terminal"; sessionId: string; size?: number; fontSize?: number } | { type: "split"; direction: "horizontal" | "vertical"; panels: TerminalPanelContent[]; size?: number; }; // Terminal tab - each tab has its own layout export interface TerminalTab { id: string; name: string; layout: TerminalPanelContent | null; } export interface TerminalState { isUnlocked: boolean; authToken: string | null; tabs: TerminalTab[]; activeTabId: string | null; activeSessionId: string | null; defaultFontSize: number; // Default font size for new terminals } export interface AppState { // Project state projects: Project[]; currentProject: Project | null; trashedProjects: TrashedProject[]; projectHistory: string[]; // Array of project IDs in MRU order (most recent first) projectHistoryIndex: number; // Current position in project history for cycling // View state currentView: ViewMode; sidebarOpen: boolean; // Agent Session state (per-project, keyed by project path) lastSelectedSessionByProject: Record; // projectPath -> sessionId // Theme theme: ThemeMode; // Features/Kanban features: Feature[]; // App spec appSpec: string; // IPC status ipcConnected: boolean; // API Keys apiKeys: ApiKeys; // Chat Sessions chatSessions: ChatSession[]; currentChatSession: ChatSession | null; chatHistoryOpen: boolean; // Auto Mode (per-project state, keyed by project ID) autoModeByProject: Record< string, { isRunning: boolean; runningTasks: string[]; // Feature IDs being worked on } >; autoModeActivityLog: AutoModeActivity[]; maxConcurrency: number; // Maximum number of concurrent agent tasks // Kanban Card Display Settings kanbanCardDetailLevel: KanbanCardDetailLevel; // Level of detail shown on kanban cards // Feature Default Settings defaultSkipTests: boolean; // Default value for skip tests when creating new features // Worktree Settings useWorktrees: boolean; // Whether to use git worktree isolation for features (default: false) // AI Profiles aiProfiles: AIProfile[]; // Profile Display Settings showProfilesOnly: boolean; // When true, hide model tweaking options and show only profile selection // Keyboard Shortcuts keyboardShortcuts: KeyboardShortcuts; // User-defined keyboard shortcuts // Audio Settings muteDoneSound: boolean; // When true, mute the notification sound when agents complete (default: false) // Project Analysis projectAnalysis: ProjectAnalysis | null; isAnalyzing: boolean; // Board Background Settings (per-project, keyed by project path) boardBackgroundByProject: Record< string, { imagePath: string | null; // Path to background image in .automaker directory imageVersion?: number; // Timestamp to bust browser cache when image is updated cardOpacity: number; // Opacity of cards (0-100) columnOpacity: number; // Opacity of columns (0-100) columnBorderEnabled: boolean; // Whether to show column borders cardGlassmorphism: boolean; // Whether to use glassmorphism (backdrop-blur) on cards cardBorderEnabled: boolean; // Whether to show card borders cardBorderOpacity: number; // Opacity of card borders (0-100) hideScrollbar: boolean; // Whether to hide the board scrollbar } >; // Theme Preview (for hover preview in theme selectors) previewTheme: ThemeMode | null; // Terminal state terminalState: TerminalState; } // Default background settings for board backgrounds export const defaultBackgroundSettings: { imagePath: string | null; imageVersion?: number; cardOpacity: number; columnOpacity: number; columnBorderEnabled: boolean; cardGlassmorphism: boolean; cardBorderEnabled: boolean; cardBorderOpacity: number; hideScrollbar: boolean; } = { imagePath: null, cardOpacity: 100, columnOpacity: 100, columnBorderEnabled: true, cardGlassmorphism: true, cardBorderEnabled: true, cardBorderOpacity: 100, hideScrollbar: false, }; export interface AutoModeActivity { id: string; featureId: string; timestamp: Date; type: | "start" | "progress" | "tool" | "complete" | "error" | "planning" | "action" | "verification"; message: string; tool?: string; passes?: boolean; phase?: "planning" | "action" | "verification"; errorType?: "authentication" | "execution"; } export interface AppActions { // Project actions setProjects: (projects: Project[]) => void; addProject: (project: Project) => void; removeProject: (projectId: string) => void; moveProjectToTrash: (projectId: string) => void; restoreTrashedProject: (projectId: string) => void; deleteTrashedProject: (projectId: string) => void; emptyTrash: () => void; setCurrentProject: (project: Project | null) => void; upsertAndSetCurrentProject: ( path: string, name: string, theme?: ThemeMode ) => Project; // Upsert project by path and set as current reorderProjects: (oldIndex: number, newIndex: number) => void; cyclePrevProject: () => void; // Cycle back through project history (Q) cycleNextProject: () => void; // Cycle forward through project history (E) clearProjectHistory: () => void; // Clear history, keeping only current project // View actions setCurrentView: (view: ViewMode) => void; toggleSidebar: () => void; setSidebarOpen: (open: boolean) => void; // Theme actions setTheme: (theme: ThemeMode) => void; setProjectTheme: (projectId: string, theme: ThemeMode | null) => void; // Set per-project theme (null to clear) getEffectiveTheme: () => ThemeMode; // Get the effective theme (project, global, or preview if set) setPreviewTheme: (theme: ThemeMode | null) => void; // Set preview theme for hover preview (null to clear) // Feature actions setFeatures: (features: Feature[]) => void; updateFeature: (id: string, updates: Partial) => void; addFeature: ( feature: Omit & Partial> ) => Feature; removeFeature: (id: string) => void; moveFeature: (id: string, newStatus: Feature["status"]) => void; // App spec actions setAppSpec: (spec: string) => void; // IPC actions setIpcConnected: (connected: boolean) => void; // API Keys actions setApiKeys: (keys: Partial) => void; // Chat Session actions createChatSession: (title?: string) => ChatSession; updateChatSession: (sessionId: string, updates: Partial) => void; addMessageToSession: (sessionId: string, message: ChatMessage) => void; setCurrentChatSession: (session: ChatSession | null) => void; archiveChatSession: (sessionId: string) => void; unarchiveChatSession: (sessionId: string) => void; deleteChatSession: (sessionId: string) => void; setChatHistoryOpen: (open: boolean) => void; toggleChatHistory: () => void; // Auto Mode actions (per-project) setAutoModeRunning: (projectId: string, running: boolean) => void; addRunningTask: (projectId: string, taskId: string) => void; removeRunningTask: (projectId: string, taskId: string) => void; clearRunningTasks: (projectId: string) => void; getAutoModeState: (projectId: string) => { isRunning: boolean; runningTasks: string[]; }; addAutoModeActivity: ( activity: Omit ) => void; clearAutoModeActivity: () => void; setMaxConcurrency: (max: number) => void; // Kanban Card Settings actions setKanbanCardDetailLevel: (level: KanbanCardDetailLevel) => void; // Feature Default Settings actions setDefaultSkipTests: (skip: boolean) => void; // Worktree Settings actions setUseWorktrees: (enabled: boolean) => void; // Profile Display Settings actions setShowProfilesOnly: (enabled: boolean) => void; // Keyboard Shortcuts actions setKeyboardShortcut: (key: keyof KeyboardShortcuts, value: string) => void; setKeyboardShortcuts: (shortcuts: Partial) => void; resetKeyboardShortcuts: () => void; // Audio Settings actions setMuteDoneSound: (muted: boolean) => void; // AI Profile actions addAIProfile: (profile: Omit) => void; updateAIProfile: (id: string, updates: Partial) => void; removeAIProfile: (id: string) => void; reorderAIProfiles: (oldIndex: number, newIndex: number) => void; resetAIProfiles: () => void; // Project Analysis actions setProjectAnalysis: (analysis: ProjectAnalysis | null) => void; setIsAnalyzing: (analyzing: boolean) => void; clearAnalysis: () => void; // Agent Session actions setLastSelectedSession: ( projectPath: string, sessionId: string | null ) => void; getLastSelectedSession: (projectPath: string) => string | null; // Board Background actions setBoardBackground: (projectPath: string, imagePath: string | null) => void; setCardOpacity: (projectPath: string, opacity: number) => void; setColumnOpacity: (projectPath: string, opacity: number) => void; setColumnBorderEnabled: (projectPath: string, enabled: boolean) => void; getBoardBackground: (projectPath: string) => { imagePath: string | null; cardOpacity: number; columnOpacity: number; columnBorderEnabled: boolean; cardGlassmorphism: boolean; cardBorderEnabled: boolean; cardBorderOpacity: number; hideScrollbar: boolean; }; setCardGlassmorphism: (projectPath: string, enabled: boolean) => void; setCardBorderEnabled: (projectPath: string, enabled: boolean) => void; setCardBorderOpacity: (projectPath: string, opacity: number) => void; setHideScrollbar: (projectPath: string, hide: boolean) => void; clearBoardBackground: (projectPath: string) => void; // Terminal actions setTerminalUnlocked: (unlocked: boolean, token?: string) => void; setActiveTerminalSession: (sessionId: string | null) => void; addTerminalToLayout: ( sessionId: string, direction?: "horizontal" | "vertical", targetSessionId?: string ) => void; removeTerminalFromLayout: (sessionId: string) => void; swapTerminals: (sessionId1: string, sessionId2: string) => void; clearTerminalState: () => void; setTerminalPanelFontSize: (sessionId: string, fontSize: number) => void; addTerminalTab: (name?: string) => string; removeTerminalTab: (tabId: string) => void; setActiveTerminalTab: (tabId: string) => void; renameTerminalTab: (tabId: string, name: string) => void; moveTerminalToTab: (sessionId: string, targetTabId: string | "new") => void; addTerminalToTab: ( sessionId: string, tabId: string, direction?: "horizontal" | "vertical" ) => void; // Reset reset: () => void; } // Default built-in AI profiles const DEFAULT_AI_PROFILES: AIProfile[] = [ { id: "profile-heavy-task", name: "Heavy Task", description: "Claude Opus with Ultrathink for complex architecture, migrations, or deep debugging.", model: "opus", thinkingLevel: "ultrathink", provider: "claude", isBuiltIn: true, icon: "Brain", }, { id: "profile-balanced", name: "Balanced", description: "Claude Sonnet with medium thinking for typical development tasks.", model: "sonnet", thinkingLevel: "medium", provider: "claude", isBuiltIn: true, icon: "Scale", }, { id: "profile-quick-edit", name: "Quick Edit", description: "Claude Haiku for fast, simple edits and minor fixes.", model: "haiku", thinkingLevel: "none", provider: "claude", isBuiltIn: true, icon: "Zap", }, ]; const initialState: AppState = { projects: [], currentProject: null, trashedProjects: [], projectHistory: [], projectHistoryIndex: -1, currentView: "welcome", sidebarOpen: true, lastSelectedSessionByProject: {}, theme: "dark", features: [], appSpec: "", ipcConnected: false, apiKeys: { anthropic: "", google: "", openai: "", }, chatSessions: [], currentChatSession: null, chatHistoryOpen: false, autoModeByProject: {}, autoModeActivityLog: [], maxConcurrency: 3, // Default to 3 concurrent agents kanbanCardDetailLevel: "standard", // Default to standard detail level defaultSkipTests: true, // Default to manual verification (tests disabled) useWorktrees: false, // Default to disabled (worktree feature is experimental) showProfilesOnly: false, // Default to showing all options (not profiles only) keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS, // Default keyboard shortcuts muteDoneSound: false, // Default to sound enabled (not muted) aiProfiles: DEFAULT_AI_PROFILES, projectAnalysis: null, isAnalyzing: false, boardBackgroundByProject: {}, previewTheme: null, terminalState: { isUnlocked: false, authToken: null, tabs: [], activeTabId: null, activeSessionId: null, defaultFontSize: 14, }, }; export const useAppStore = create()( persist( (set, get) => ({ ...initialState, // Project actions setProjects: (projects) => set({ projects }), addProject: (project) => { const projects = get().projects; const existing = projects.findIndex((p) => p.path === project.path); if (existing >= 0) { const updated = [...projects]; updated[existing] = { ...project, lastOpened: new Date().toISOString(), }; set({ projects: updated }); } else { set({ projects: [ ...projects, { ...project, lastOpened: new Date().toISOString() }, ], }); } }, removeProject: (projectId) => { set({ projects: get().projects.filter((p) => p.id !== projectId) }); }, moveProjectToTrash: (projectId) => { const project = get().projects.find((p) => p.id === projectId); if (!project) return; const remainingProjects = get().projects.filter( (p) => p.id !== projectId ); const existingTrash = get().trashedProjects.filter( (p) => p.id !== projectId ); const trashedProject: TrashedProject = { ...project, trashedAt: new Date().toISOString(), deletedFromDisk: false, }; const isCurrent = get().currentProject?.id === projectId; set({ projects: remainingProjects, trashedProjects: [trashedProject, ...existingTrash], currentProject: isCurrent ? null : get().currentProject, currentView: isCurrent ? "welcome" : get().currentView, }); }, restoreTrashedProject: (projectId) => { const trashed = get().trashedProjects.find((p) => p.id === projectId); if (!trashed) return; const remainingTrash = get().trashedProjects.filter( (p) => p.id !== projectId ); const existingProjects = get().projects; const samePathProject = existingProjects.find( (p) => p.path === trashed.path ); const projectsWithoutId = existingProjects.filter( (p) => p.id !== projectId ); // If a project with the same path already exists, keep it and just remove from trash if (samePathProject) { set({ trashedProjects: remainingTrash, currentProject: samePathProject, currentView: "board", }); return; } const restoredProject: Project = { id: trashed.id, name: trashed.name, path: trashed.path, lastOpened: new Date().toISOString(), theme: trashed.theme, // Preserve theme from trashed project }; set({ trashedProjects: remainingTrash, projects: [...projectsWithoutId, restoredProject], currentProject: restoredProject, currentView: "board", }); }, deleteTrashedProject: (projectId) => { set({ trashedProjects: get().trashedProjects.filter( (p) => p.id !== projectId ), }); }, emptyTrash: () => set({ trashedProjects: [] }), reorderProjects: (oldIndex, newIndex) => { const projects = [...get().projects]; const [movedProject] = projects.splice(oldIndex, 1); projects.splice(newIndex, 0, movedProject); set({ projects }); }, setCurrentProject: (project) => { set({ currentProject: project }); if (project) { set({ currentView: "board" }); // Add to project history (MRU order) const currentHistory = get().projectHistory; // Remove this project if it's already in history const filteredHistory = currentHistory.filter( (id) => id !== project.id ); // Add to the front (most recent) const newHistory = [project.id, ...filteredHistory]; // Reset history index to 0 (current project) set({ projectHistory: newHistory, projectHistoryIndex: 0 }); } else { set({ currentView: "welcome" }); } }, upsertAndSetCurrentProject: (path, name, theme) => { const { projects, trashedProjects, currentProject, theme: globalTheme, } = get(); const existingProject = projects.find((p) => p.path === path); let project: Project; if (existingProject) { // Update existing project, preserving theme and other properties project = { ...existingProject, name, // Update name in case it changed lastOpened: new Date().toISOString(), }; // Update the project in the store const updatedProjects = projects.map((p) => p.id === existingProject.id ? project : p ); set({ projects: updatedProjects }); } else { // Create new project - check for trashed project with same path first (preserves theme if deleted/recreated) // Then fall back to provided theme, then current project theme, then global theme const trashedProject = trashedProjects.find((p) => p.path === path); const effectiveTheme = theme || trashedProject?.theme || currentProject?.theme || globalTheme; project = { id: `project-${Date.now()}`, name, path, lastOpened: new Date().toISOString(), theme: effectiveTheme, }; // Add the new project to the store set({ projects: [ ...projects, { ...project, lastOpened: new Date().toISOString() }, ], }); } // Set as current project (this will also update history and view) get().setCurrentProject(project); return project; }, cyclePrevProject: () => { const { projectHistory, projectHistoryIndex, projects } = get(); // Filter history to only include valid projects const validHistory = projectHistory.filter((id) => projects.some((p) => p.id === id) ); if (validHistory.length <= 1) return; // Need at least 2 valid projects to cycle // Find current position in valid history const currentProjectId = get().currentProject?.id; let currentIndex = currentProjectId ? validHistory.indexOf(currentProjectId) : projectHistoryIndex; // If current project not found in valid history, start from 0 if (currentIndex === -1) currentIndex = 0; // Move to the next index (going back in history = higher index), wrapping around const newIndex = (currentIndex + 1) % validHistory.length; const targetProjectId = validHistory[newIndex]; const targetProject = projects.find((p) => p.id === targetProjectId); if (targetProject) { // Update history to only include valid projects and set new index set({ currentProject: targetProject, projectHistory: validHistory, projectHistoryIndex: newIndex, currentView: "board", }); } }, cycleNextProject: () => { const { projectHistory, projectHistoryIndex, projects } = get(); // Filter history to only include valid projects const validHistory = projectHistory.filter((id) => projects.some((p) => p.id === id) ); if (validHistory.length <= 1) return; // Need at least 2 valid projects to cycle // Find current position in valid history const currentProjectId = get().currentProject?.id; let currentIndex = currentProjectId ? validHistory.indexOf(currentProjectId) : projectHistoryIndex; // If current project not found in valid history, start from 0 if (currentIndex === -1) currentIndex = 0; // Move to the previous index (going forward = lower index), wrapping around const newIndex = currentIndex <= 0 ? validHistory.length - 1 : currentIndex - 1; const targetProjectId = validHistory[newIndex]; const targetProject = projects.find((p) => p.id === targetProjectId); if (targetProject) { // Update history to only include valid projects and set new index set({ currentProject: targetProject, projectHistory: validHistory, projectHistoryIndex: newIndex, currentView: "board", }); } }, clearProjectHistory: () => { const currentProject = get().currentProject; if (currentProject) { // Keep only the current project in history set({ projectHistory: [currentProject.id], projectHistoryIndex: 0, }); } else { // No current project, clear everything set({ projectHistory: [], projectHistoryIndex: -1, }); } }, // View actions setCurrentView: (view) => set({ currentView: view }), toggleSidebar: () => set({ sidebarOpen: !get().sidebarOpen }), setSidebarOpen: (open) => set({ sidebarOpen: open }), // Theme actions setTheme: (theme) => set({ theme }), setProjectTheme: (projectId, theme) => { // Update the project's theme property const projects = get().projects.map((p) => p.id === projectId ? { ...p, theme: theme === null ? undefined : theme } : p ); set({ projects }); // Also update currentProject if it's the same project const currentProject = get().currentProject; if (currentProject?.id === projectId) { set({ currentProject: { ...currentProject, theme: theme === null ? undefined : theme, }, }); } }, getEffectiveTheme: () => { // If preview theme is set, use it (for hover preview) const previewTheme = get().previewTheme; if (previewTheme) { return previewTheme; } const currentProject = get().currentProject; // If current project has a theme set, use it if (currentProject?.theme) { return currentProject.theme as ThemeMode; } // Otherwise fall back to global theme return get().theme; }, setPreviewTheme: (theme) => set({ previewTheme: theme }), // Feature actions setFeatures: (features) => set({ features }), updateFeature: (id, updates) => { set({ features: get().features.map((f) => f.id === id ? { ...f, ...updates } : f ), }); }, addFeature: (feature) => { const id = feature.id || `feature-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; const featureWithId = { ...feature, id } as Feature; set({ features: [...get().features, featureWithId] }); return featureWithId; }, removeFeature: (id) => { set({ features: get().features.filter((f) => f.id !== id) }); }, moveFeature: (id, newStatus) => { set({ features: get().features.map((f) => f.id === id ? { ...f, status: newStatus } : f ), }); }, // App spec actions setAppSpec: (spec) => set({ appSpec: spec }), // IPC actions setIpcConnected: (connected) => set({ ipcConnected: connected }), // API Keys actions setApiKeys: (keys) => set({ apiKeys: { ...get().apiKeys, ...keys } }), // Chat Session actions createChatSession: (title) => { const currentProject = get().currentProject; if (!currentProject) { throw new Error("No project selected"); } const now = new Date(); const session: ChatSession = { id: `chat-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, title: title || `Chat ${new Date().toLocaleDateString()} ${new Date().toLocaleTimeString()}`, projectId: currentProject.id, messages: [ { id: "welcome", role: "assistant", content: "Hello! I'm the Automaker Agent. I can help you build software autonomously. What would you like to create today?", timestamp: now, }, ], createdAt: now, updatedAt: now, archived: false, }; set({ chatSessions: [...get().chatSessions, session], currentChatSession: session, }); return session; }, updateChatSession: (sessionId, updates) => { set({ chatSessions: get().chatSessions.map((session) => session.id === sessionId ? { ...session, ...updates, updatedAt: new Date() } : session ), }); // Update current session if it's the one being updated const currentSession = get().currentChatSession; if (currentSession && currentSession.id === sessionId) { set({ currentChatSession: { ...currentSession, ...updates, updatedAt: new Date(), }, }); } }, addMessageToSession: (sessionId, message) => { const sessions = get().chatSessions; const sessionIndex = sessions.findIndex((s) => s.id === sessionId); if (sessionIndex >= 0) { const updatedSessions = [...sessions]; updatedSessions[sessionIndex] = { ...updatedSessions[sessionIndex], messages: [...updatedSessions[sessionIndex].messages, message], updatedAt: new Date(), }; set({ chatSessions: updatedSessions }); // Update current session if it's the one being updated const currentSession = get().currentChatSession; if (currentSession && currentSession.id === sessionId) { set({ currentChatSession: updatedSessions[sessionIndex], }); } } }, setCurrentChatSession: (session) => { set({ currentChatSession: session }); }, archiveChatSession: (sessionId) => { get().updateChatSession(sessionId, { archived: true }); }, unarchiveChatSession: (sessionId) => { get().updateChatSession(sessionId, { archived: false }); }, deleteChatSession: (sessionId) => { const currentSession = get().currentChatSession; set({ chatSessions: get().chatSessions.filter((s) => s.id !== sessionId), currentChatSession: currentSession?.id === sessionId ? null : currentSession, }); }, setChatHistoryOpen: (open) => set({ chatHistoryOpen: open }), toggleChatHistory: () => set({ chatHistoryOpen: !get().chatHistoryOpen }), // Auto Mode actions (per-project) setAutoModeRunning: (projectId, running) => { const current = get().autoModeByProject; const projectState = current[projectId] || { isRunning: false, runningTasks: [], }; set({ autoModeByProject: { ...current, [projectId]: { ...projectState, isRunning: running }, }, }); }, addRunningTask: (projectId, taskId) => { const current = get().autoModeByProject; const projectState = current[projectId] || { isRunning: false, runningTasks: [], }; if (!projectState.runningTasks.includes(taskId)) { set({ autoModeByProject: { ...current, [projectId]: { ...projectState, runningTasks: [...projectState.runningTasks, taskId], }, }, }); } }, removeRunningTask: (projectId, taskId) => { const current = get().autoModeByProject; const projectState = current[projectId] || { isRunning: false, runningTasks: [], }; set({ autoModeByProject: { ...current, [projectId]: { ...projectState, runningTasks: projectState.runningTasks.filter( (id) => id !== taskId ), }, }, }); }, clearRunningTasks: (projectId) => { const current = get().autoModeByProject; const projectState = current[projectId] || { isRunning: false, runningTasks: [], }; set({ autoModeByProject: { ...current, [projectId]: { ...projectState, runningTasks: [] }, }, }); }, getAutoModeState: (projectId) => { const projectState = get().autoModeByProject[projectId]; return projectState || { isRunning: false, runningTasks: [] }; }, addAutoModeActivity: (activity) => { const id = `activity-${Date.now()}-${Math.random() .toString(36) .substr(2, 9)}`; const newActivity: AutoModeActivity = { ...activity, id, timestamp: new Date(), }; // Keep only the last 100 activities to avoid memory issues const currentLog = get().autoModeActivityLog; const updatedLog = [...currentLog, newActivity].slice(-100); set({ autoModeActivityLog: updatedLog }); }, clearAutoModeActivity: () => set({ autoModeActivityLog: [] }), setMaxConcurrency: (max) => set({ maxConcurrency: max }), // Kanban Card Settings actions setKanbanCardDetailLevel: (level) => set({ kanbanCardDetailLevel: level }), // Feature Default Settings actions setDefaultSkipTests: (skip) => set({ defaultSkipTests: skip }), // Worktree Settings actions setUseWorktrees: (enabled) => set({ useWorktrees: enabled }), // Profile Display Settings actions setShowProfilesOnly: (enabled) => set({ showProfilesOnly: enabled }), // Keyboard Shortcuts actions setKeyboardShortcut: (key, value) => { set({ keyboardShortcuts: { ...get().keyboardShortcuts, [key]: value, }, }); }, setKeyboardShortcuts: (shortcuts) => { set({ keyboardShortcuts: { ...get().keyboardShortcuts, ...shortcuts, }, }); }, resetKeyboardShortcuts: () => { set({ keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS }); }, // Audio Settings actions setMuteDoneSound: (muted) => set({ muteDoneSound: muted }), // AI Profile actions addAIProfile: (profile) => { const id = `profile-${Date.now()}-${Math.random() .toString(36) .substr(2, 9)}`; set({ aiProfiles: [...get().aiProfiles, { ...profile, id }] }); }, updateAIProfile: (id, updates) => { set({ aiProfiles: get().aiProfiles.map((p) => p.id === id ? { ...p, ...updates } : p ), }); }, removeAIProfile: (id) => { // Only allow removing non-built-in profiles const profile = get().aiProfiles.find((p) => p.id === id); if (profile && !profile.isBuiltIn) { set({ aiProfiles: get().aiProfiles.filter((p) => p.id !== id) }); } }, reorderAIProfiles: (oldIndex, newIndex) => { const profiles = [...get().aiProfiles]; const [movedProfile] = profiles.splice(oldIndex, 1); profiles.splice(newIndex, 0, movedProfile); set({ aiProfiles: profiles }); }, resetAIProfiles: () => { // Merge: keep user-created profiles, but refresh all built-in profiles to latest defaults const defaultProfileIds = new Set(DEFAULT_AI_PROFILES.map((p) => p.id)); const userProfiles = get().aiProfiles.filter( (p) => !p.isBuiltIn && !defaultProfileIds.has(p.id) ); set({ aiProfiles: [...DEFAULT_AI_PROFILES, ...userProfiles] }); }, // Project Analysis actions setProjectAnalysis: (analysis) => set({ projectAnalysis: analysis }), setIsAnalyzing: (analyzing) => set({ isAnalyzing: analyzing }), clearAnalysis: () => set({ projectAnalysis: null }), // Agent Session actions setLastSelectedSession: (projectPath, sessionId) => { const current = get().lastSelectedSessionByProject; if (sessionId === null) { // Remove the entry for this project const rest = Object.fromEntries( Object.entries(current).filter(([key]) => key !== projectPath) ); set({ lastSelectedSessionByProject: rest }); } else { set({ lastSelectedSessionByProject: { ...current, [projectPath]: sessionId, }, }); } }, getLastSelectedSession: (projectPath) => { return get().lastSelectedSessionByProject[projectPath] || null; }, // Board Background actions setBoardBackground: (projectPath, imagePath) => { const current = get().boardBackgroundByProject; const existing = current[projectPath] || { imagePath: null, cardOpacity: 100, columnOpacity: 100, columnBorderEnabled: true, cardGlassmorphism: true, cardBorderEnabled: true, cardBorderOpacity: 100, hideScrollbar: false, }; set({ boardBackgroundByProject: { ...current, [projectPath]: { ...existing, imagePath, // Update imageVersion timestamp to bust browser cache when image changes imageVersion: imagePath ? Date.now() : undefined, }, }, }); }, setCardOpacity: (projectPath, opacity) => { const current = get().boardBackgroundByProject; const existing = current[projectPath] || defaultBackgroundSettings; set({ boardBackgroundByProject: { ...current, [projectPath]: { ...existing, cardOpacity: opacity, }, }, }); }, setColumnOpacity: (projectPath, opacity) => { const current = get().boardBackgroundByProject; const existing = current[projectPath] || defaultBackgroundSettings; set({ boardBackgroundByProject: { ...current, [projectPath]: { ...existing, columnOpacity: opacity, }, }, }); }, getBoardBackground: (projectPath) => { const settings = get().boardBackgroundByProject[projectPath]; return settings || defaultBackgroundSettings; }, setColumnBorderEnabled: (projectPath, enabled) => { const current = get().boardBackgroundByProject; const existing = current[projectPath] || defaultBackgroundSettings; set({ boardBackgroundByProject: { ...current, [projectPath]: { ...existing, columnBorderEnabled: enabled, }, }, }); }, setCardGlassmorphism: (projectPath, enabled) => { const current = get().boardBackgroundByProject; const existing = current[projectPath] || defaultBackgroundSettings; set({ boardBackgroundByProject: { ...current, [projectPath]: { ...existing, cardGlassmorphism: enabled, }, }, }); }, setCardBorderEnabled: (projectPath, enabled) => { const current = get().boardBackgroundByProject; const existing = current[projectPath] || defaultBackgroundSettings; set({ boardBackgroundByProject: { ...current, [projectPath]: { ...existing, cardBorderEnabled: enabled, }, }, }); }, setCardBorderOpacity: (projectPath, opacity) => { const current = get().boardBackgroundByProject; const existing = current[projectPath] || defaultBackgroundSettings; set({ boardBackgroundByProject: { ...current, [projectPath]: { ...existing, cardBorderOpacity: opacity, }, }, }); }, setHideScrollbar: (projectPath, hide) => { const current = get().boardBackgroundByProject; const existing = current[projectPath] || defaultBackgroundSettings; set({ boardBackgroundByProject: { ...current, [projectPath]: { ...existing, hideScrollbar: hide, }, }, }); }, clearBoardBackground: (projectPath) => { const current = get().boardBackgroundByProject; const existing = current[projectPath] || defaultBackgroundSettings; set({ boardBackgroundByProject: { ...current, [projectPath]: { ...existing, imagePath: null, // Only clear the image, preserve other settings imageVersion: undefined, // Clear version when clearing image }, }, }); }, // Terminal actions setTerminalUnlocked: (unlocked, token) => { set({ terminalState: { ...get().terminalState, isUnlocked: unlocked, authToken: token || null, }, }); }, setActiveTerminalSession: (sessionId) => { set({ terminalState: { ...get().terminalState, activeSessionId: sessionId, }, }); }, addTerminalToLayout: ( sessionId, direction = "horizontal", targetSessionId ) => { const current = get().terminalState; const newTerminal: TerminalPanelContent = { type: "terminal", sessionId, size: 50, }; // If no tabs, create first tab if (current.tabs.length === 0) { const newTabId = `tab-${Date.now()}`; set({ terminalState: { ...current, tabs: [ { id: newTabId, name: "Terminal 1", layout: { type: "terminal", sessionId, size: 100 }, }, ], activeTabId: newTabId, activeSessionId: sessionId, }, }); return; } // Add to active tab's layout const activeTab = current.tabs.find( (t) => t.id === current.activeTabId ); if (!activeTab) return; // If targetSessionId is provided, find and split that specific terminal const splitTargetTerminal = ( node: TerminalPanelContent, targetId: string, targetDirection: "horizontal" | "vertical" ): TerminalPanelContent => { if (node.type === "terminal") { if (node.sessionId === targetId) { // Found the target - split it return { type: "split", direction: targetDirection, panels: [{ ...node, size: 50 }, newTerminal], }; } // Not the target, return unchanged return node; } // It's a split - recurse into panels return { ...node, panels: node.panels.map((p) => splitTargetTerminal(p, targetId, targetDirection) ), }; }; // Legacy behavior: add to root layout (when no targetSessionId) const addToRootLayout = ( node: TerminalPanelContent, targetDirection: "horizontal" | "vertical" ): TerminalPanelContent => { if (node.type === "terminal") { return { type: "split", direction: targetDirection, panels: [{ ...node, size: 50 }, newTerminal], }; } // If same direction, add to existing split if (node.direction === targetDirection) { const newSize = 100 / (node.panels.length + 1); return { ...node, panels: [ ...node.panels.map((p) => ({ ...p, size: newSize })), { ...newTerminal, size: newSize }, ], }; } // Different direction, wrap in new split return { type: "split", direction: targetDirection, panels: [{ ...node, size: 50 }, newTerminal], }; }; let newLayout: TerminalPanelContent; if (!activeTab.layout) { newLayout = { type: "terminal", sessionId, size: 100 }; } else if (targetSessionId) { newLayout = splitTargetTerminal( activeTab.layout, targetSessionId, direction ); } else { newLayout = addToRootLayout(activeTab.layout, direction); } const newTabs = current.tabs.map((t) => t.id === current.activeTabId ? { ...t, layout: newLayout } : t ); set({ terminalState: { ...current, tabs: newTabs, activeSessionId: sessionId, }, }); }, removeTerminalFromLayout: (sessionId) => { const current = get().terminalState; if (current.tabs.length === 0) return; // Find which tab contains this session const findFirstTerminal = ( node: TerminalPanelContent | null ): string | null => { if (!node) return null; if (node.type === "terminal") return node.sessionId; for (const panel of node.panels) { const found = findFirstTerminal(panel); if (found) return found; } return null; }; const removeAndCollapse = ( node: TerminalPanelContent ): TerminalPanelContent | null => { if (node.type === "terminal") { return node.sessionId === sessionId ? null : node; } const newPanels: TerminalPanelContent[] = []; for (const panel of node.panels) { const result = removeAndCollapse(panel); if (result !== null) newPanels.push(result); } if (newPanels.length === 0) return null; if (newPanels.length === 1) return newPanels[0]; return { ...node, panels: newPanels }; }; let newTabs = current.tabs.map((tab) => { if (!tab.layout) return tab; const newLayout = removeAndCollapse(tab.layout); return { ...tab, layout: newLayout }; }); // Remove empty tabs newTabs = newTabs.filter((tab) => tab.layout !== null); // Determine new active session const newActiveTabId = newTabs.length > 0 ? current.activeTabId && newTabs.find((t) => t.id === current.activeTabId) ? current.activeTabId : newTabs[0].id : null; const newActiveSessionId = newActiveTabId ? findFirstTerminal( newTabs.find((t) => t.id === newActiveTabId)?.layout || null ) : null; set({ terminalState: { ...current, tabs: newTabs, activeTabId: newActiveTabId, activeSessionId: newActiveSessionId, }, }); }, swapTerminals: (sessionId1, sessionId2) => { const current = get().terminalState; if (current.tabs.length === 0) return; const swapInLayout = ( node: TerminalPanelContent ): TerminalPanelContent => { if (node.type === "terminal") { if (node.sessionId === sessionId1) return { ...node, sessionId: sessionId2 }; if (node.sessionId === sessionId2) return { ...node, sessionId: sessionId1 }; return node; } return { ...node, panels: node.panels.map(swapInLayout) }; }; const newTabs = current.tabs.map((tab) => ({ ...tab, layout: tab.layout ? swapInLayout(tab.layout) : null, })); set({ terminalState: { ...current, tabs: newTabs }, }); }, clearTerminalState: () => { set({ terminalState: { isUnlocked: false, authToken: null, tabs: [], activeTabId: null, activeSessionId: null, defaultFontSize: 14, }, }); }, setTerminalPanelFontSize: (sessionId, fontSize) => { const current = get().terminalState; const clampedSize = Math.max(8, Math.min(32, fontSize)); const updateFontSize = ( node: TerminalPanelContent ): TerminalPanelContent => { if (node.type === "terminal") { if (node.sessionId === sessionId) { return { ...node, fontSize: clampedSize }; } return node; } return { ...node, panels: node.panels.map(updateFontSize) }; }; const newTabs = current.tabs.map((tab) => { if (!tab.layout) return tab; return { ...tab, layout: updateFontSize(tab.layout) }; }); set({ terminalState: { ...current, tabs: newTabs }, }); }, addTerminalTab: (name) => { const current = get().terminalState; const newTabId = `tab-${Date.now()}`; const tabNumber = current.tabs.length + 1; const newTab: TerminalTab = { id: newTabId, name: name || `Terminal ${tabNumber}`, layout: null, }; set({ terminalState: { ...current, tabs: [...current.tabs, newTab], activeTabId: newTabId, }, }); return newTabId; }, removeTerminalTab: (tabId) => { const current = get().terminalState; const newTabs = current.tabs.filter((t) => t.id !== tabId); let newActiveTabId = current.activeTabId; let newActiveSessionId = current.activeSessionId; if (current.activeTabId === tabId) { newActiveTabId = newTabs.length > 0 ? newTabs[0].id : null; if (newActiveTabId) { const newActiveTab = newTabs.find((t) => t.id === newActiveTabId); const findFirst = (node: TerminalPanelContent): string | null => { if (node.type === "terminal") return node.sessionId; for (const p of node.panels) { const f = findFirst(p); if (f) return f; } return null; }; newActiveSessionId = newActiveTab?.layout ? findFirst(newActiveTab.layout) : null; } else { newActiveSessionId = null; } } set({ terminalState: { ...current, tabs: newTabs, activeTabId: newActiveTabId, activeSessionId: newActiveSessionId, }, }); }, setActiveTerminalTab: (tabId) => { const current = get().terminalState; const tab = current.tabs.find((t) => t.id === tabId); if (!tab) return; let newActiveSessionId = current.activeSessionId; if (tab.layout) { const findFirst = (node: TerminalPanelContent): string | null => { if (node.type === "terminal") return node.sessionId; for (const p of node.panels) { const f = findFirst(p); if (f) return f; } return null; }; newActiveSessionId = findFirst(tab.layout); } set({ terminalState: { ...current, activeTabId: tabId, activeSessionId: newActiveSessionId, }, }); }, renameTerminalTab: (tabId, name) => { const current = get().terminalState; const newTabs = current.tabs.map((t) => t.id === tabId ? { ...t, name } : t ); set({ terminalState: { ...current, tabs: newTabs }, }); }, moveTerminalToTab: (sessionId, targetTabId) => { const current = get().terminalState; let sourceTabId: string | null = null; let originalTerminalNode: | (TerminalPanelContent & { type: "terminal" }) | null = null; const findTerminal = ( node: TerminalPanelContent ): (TerminalPanelContent & { type: "terminal" }) | null => { if (node.type === "terminal") { return node.sessionId === sessionId ? node : null; } for (const panel of node.panels) { const found = findTerminal(panel); if (found) return found; } return null; }; for (const tab of current.tabs) { if (tab.layout) { const found = findTerminal(tab.layout); if (found) { sourceTabId = tab.id; originalTerminalNode = found; break; } } } if (!sourceTabId || !originalTerminalNode) return; if (sourceTabId === targetTabId) return; const sourceTab = current.tabs.find((t) => t.id === sourceTabId); if (!sourceTab?.layout) return; const removeAndCollapse = ( node: TerminalPanelContent ): TerminalPanelContent | null => { if (node.type === "terminal") { return node.sessionId === sessionId ? null : node; } const newPanels: TerminalPanelContent[] = []; for (const panel of node.panels) { const result = removeAndCollapse(panel); if (result !== null) newPanels.push(result); } if (newPanels.length === 0) return null; if (newPanels.length === 1) return newPanels[0]; return { ...node, panels: newPanels }; }; const newSourceLayout = removeAndCollapse(sourceTab.layout); let finalTargetTabId = targetTabId; let newTabs = current.tabs; if (targetTabId === "new") { const newTabId = `tab-${Date.now()}`; const sourceWillBeRemoved = !newSourceLayout; const tabName = sourceWillBeRemoved ? sourceTab.name : `Terminal ${current.tabs.length + 1}`; newTabs = [ ...current.tabs, { id: newTabId, name: tabName, layout: { type: "terminal", sessionId, size: 100, fontSize: originalTerminalNode.fontSize, }, }, ]; finalTargetTabId = newTabId; } else { const targetTab = current.tabs.find((t) => t.id === targetTabId); if (!targetTab) return; const terminalNode: TerminalPanelContent = { type: "terminal", sessionId, size: 50, fontSize: originalTerminalNode.fontSize, }; let newTargetLayout: TerminalPanelContent; if (!targetTab.layout) { newTargetLayout = { type: "terminal", sessionId, size: 100, fontSize: originalTerminalNode.fontSize, }; } else if (targetTab.layout.type === "terminal") { newTargetLayout = { type: "split", direction: "horizontal", panels: [{ ...targetTab.layout, size: 50 }, terminalNode], }; } else { newTargetLayout = { ...targetTab.layout, panels: [...targetTab.layout.panels, terminalNode], }; } newTabs = current.tabs.map((t) => t.id === targetTabId ? { ...t, layout: newTargetLayout } : t ); } if (!newSourceLayout) { newTabs = newTabs.filter((t) => t.id !== sourceTabId); } else { newTabs = newTabs.map((t) => t.id === sourceTabId ? { ...t, layout: newSourceLayout } : t ); } set({ terminalState: { ...current, tabs: newTabs, activeTabId: finalTargetTabId, activeSessionId: sessionId, }, }); }, addTerminalToTab: (sessionId, tabId, direction = "horizontal") => { const current = get().terminalState; const tab = current.tabs.find((t) => t.id === tabId); if (!tab) return; const terminalNode: TerminalPanelContent = { type: "terminal", sessionId, size: 50, }; let newLayout: TerminalPanelContent; if (!tab.layout) { newLayout = { type: "terminal", sessionId, size: 100 }; } else if (tab.layout.type === "terminal") { newLayout = { type: "split", direction, panels: [{ ...tab.layout, size: 50 }, terminalNode], }; } else { if (tab.layout.direction === direction) { const newSize = 100 / (tab.layout.panels.length + 1); newLayout = { ...tab.layout, panels: [ ...tab.layout.panels.map((p) => ({ ...p, size: newSize })), { ...terminalNode, size: newSize }, ], }; } else { newLayout = { type: "split", direction, panels: [{ ...tab.layout, size: 50 }, terminalNode], }; } } const newTabs = current.tabs.map((t) => t.id === tabId ? { ...t, layout: newLayout } : t ); set({ terminalState: { ...current, tabs: newTabs, activeTabId: tabId, activeSessionId: sessionId, }, }); }, // Reset reset: () => set(initialState), }), { name: "automaker-storage", version: 2, // Increment when making breaking changes to persisted state migrate: (persistedState: unknown, version: number) => { const state = persistedState as Partial; // Migration from version 0 (no version) to version 1: // - Change addContextFile shortcut from "F" to "N" if (version === 0) { if (state.keyboardShortcuts?.addContextFile === "F") { state.keyboardShortcuts.addContextFile = "N"; } } // Migration from version 1 to version 2: // - Change terminal shortcut from "Cmd+`" to "T" if (version <= 1) { if ( state.keyboardShortcuts?.terminal === "Cmd+`" || state.keyboardShortcuts?.terminal === undefined ) { state.keyboardShortcuts = { ...DEFAULT_KEYBOARD_SHORTCUTS, ...state.keyboardShortcuts, terminal: "T", }; } } return state as AppState; }, partialize: (state) => ({ // Project management projects: state.projects, currentProject: state.currentProject, trashedProjects: state.trashedProjects, projectHistory: state.projectHistory, projectHistoryIndex: state.projectHistoryIndex, // Features - cached locally for faster hydration (authoritative source is server) features: state.features, // UI state currentView: state.currentView, theme: state.theme, sidebarOpen: state.sidebarOpen, chatHistoryOpen: state.chatHistoryOpen, kanbanCardDetailLevel: state.kanbanCardDetailLevel, // Settings apiKeys: state.apiKeys, maxConcurrency: state.maxConcurrency, autoModeByProject: state.autoModeByProject, defaultSkipTests: state.defaultSkipTests, useWorktrees: state.useWorktrees, showProfilesOnly: state.showProfilesOnly, keyboardShortcuts: state.keyboardShortcuts, muteDoneSound: state.muteDoneSound, // Profiles and sessions aiProfiles: state.aiProfiles, chatSessions: state.chatSessions, lastSelectedSessionByProject: state.lastSelectedSessionByProject, // Board background settings boardBackgroundByProject: state.boardBackgroundByProject, }), } ) );