diff --git a/apps/ui/src/store/app-store.ts b/apps/ui/src/store/app-store.ts index 52744339..a6d048fb 100644 --- a/apps/ui/src/store/app-store.ts +++ b/apps/ui/src/store/app-store.ts @@ -60,6 +60,8 @@ import { type ShortcutKey, type KeyboardShortcuts, type BackgroundSettings, + type UISliceState, + type UISliceActions, // Settings types type ApiKeys, // Chat types @@ -109,16 +111,13 @@ import { } from './utils'; // Import default values from modular defaults files -import { defaultBackgroundSettings, defaultTerminalState, MAX_INIT_OUTPUT_LINES } from './defaults'; +import { defaultTerminalState, MAX_INIT_OUTPUT_LINES } from './defaults'; + +// Import UI slice +import { createUISlice, initialUIState } from './slices'; // Import internal theme utils (not re-exported publicly) -import { - getEffectiveFont, - saveThemeToStorage, - saveFontSansToStorage, - saveFontMonoToStorage, - persistEffectiveThemeForProject, -} from './utils/theme-utils'; +import { persistEffectiveThemeForProject } from './utils/theme-utils'; const logger = createLogger('AppStore'); const OPENCODE_BEDROCK_PROVIDER_ID = 'amazon-bedrock'; @@ -146,6 +145,8 @@ export type { ShortcutKey, KeyboardShortcuts, BackgroundSettings, + UISliceState, + UISliceActions, ApiKeys, ImageAttachment, TextFileAttachment, @@ -213,56 +214,72 @@ export { defaultBackgroundSettings, defaultTerminalState, MAX_INIT_OUTPUT_LINES // - defaultTerminalState (./defaults/terminal-defaults.ts) const initialState: AppState = { + // Spread UI slice state first + ...initialUIState, + + // Project state projects: [], currentProject: null, trashedProjects: [], projectHistory: [], projectHistoryIndex: -1, - currentView: 'welcome', - sidebarOpen: true, - sidebarStyle: 'unified', - collapsedNavSections: {}, - mobileSidebarHidden: false, + + // Agent Session state lastSelectedSessionByProject: {}, - theme: getStoredTheme() || 'dark', - fontFamilySans: getStoredFontSans(), - fontFamilyMono: getStoredFontMono(), + + // Features/Kanban features: [], + + // App spec appSpec: '', + + // IPC status ipcConnected: false, + + // API Keys apiKeys: { anthropic: '', google: '', openai: '', }, + + // Chat Sessions chatSessions: [], currentChatSession: null, - chatHistoryOpen: false, + + // Auto Mode autoModeByWorktree: {}, autoModeActivityLog: [], maxConcurrency: DEFAULT_MAX_CONCURRENCY, - boardViewMode: 'kanban', + + // Feature Default Settings defaultSkipTests: true, enableDependencyBlocking: true, skipVerificationInAutoMode: false, enableAiCommitMessages: true, planUseSelectedWorktreeBranch: true, addFeatureUseSelectedWorktreeBranch: false, + + // Worktree Settings useWorktrees: true, currentWorktreeByProject: {}, worktreesByProject: {}, - keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS, - muteDoneSound: false, - disableSplashScreen: false, + + // Server Settings serverLogLevel: 'info', enableRequestLogging: true, - showQueryDevtools: true, + + // Model Settings enhancementModel: 'claude-sonnet', validationModel: 'claude-opus', phaseModels: DEFAULT_PHASE_MODELS, favoriteModels: [], + + // Cursor CLI Settings enabledCursorModels: getAllCursorModelIds(), cursorDefaultModel: 'cursor-auto', + + // Codex CLI Settings enabledCodexModels: getAllCodexModelIds(), codexDefaultModel: 'codex-gpt-5.2-codex', codexAutoLoadAgents: false, @@ -270,6 +287,8 @@ const initialState: AppState = { codexApprovalPolicy: 'on-request', codexEnableWebSearch: false, codexEnableImages: false, + + // OpenCode CLI Settings enabledOpencodeModels: getAllOpencodeModelIds(), opencodeDefaultModel: DEFAULT_OPENCODE_MODEL, dynamicOpencodeModels: [], @@ -279,61 +298,101 @@ const initialState: AppState = { opencodeModelsError: null, opencodeModelsLastFetched: null, opencodeModelsLastFailedAt: null, + + // Gemini CLI Settings enabledGeminiModels: getAllGeminiModelIds(), geminiDefaultModel: DEFAULT_GEMINI_MODEL, + + // Copilot SDK Settings enabledCopilotModels: getAllCopilotModelIds(), copilotDefaultModel: DEFAULT_COPILOT_MODEL, + + // Provider Settings disabledProviders: [], + + // Claude Agent SDK Settings autoLoadClaudeMd: false, skipSandboxWarning: false, + + // MCP Servers mcpServers: [], + + // Editor Configuration defaultEditorCommand: null, + + // Terminal Configuration defaultTerminalId: null, + + // Skills Configuration enableSkills: true, skillsSources: ['user', 'project'] as Array<'user' | 'project'>, + + // Subagents Configuration enableSubagents: true, subagentsSources: ['user', 'project'] as Array<'user' | 'project'>, + + // Prompt Customization promptCustomization: {}, + + // Event Hooks eventHooks: [], + + // Claude-Compatible Providers claudeCompatibleProviders: [], claudeApiProfiles: [], activeClaudeApiProfileId: null, + + // Project Analysis projectAnalysis: null, isAnalyzing: false, - boardBackgroundByProject: {}, - previewTheme: null, + + // Terminal state terminalState: defaultTerminalState, terminalLayoutByProject: {}, + + // Spec Creation specCreatingForProject: null, + + // Planning defaultPlanningMode: 'skip' as PlanningMode, defaultRequirePlanApproval: false, defaultFeatureModel: DEFAULT_GLOBAL_SETTINGS.defaultFeatureModel, pendingPlanApproval: null, + + // Claude Usage Tracking claudeRefreshInterval: 60, claudeUsage: null, claudeUsageLastUpdated: null, + + // Codex Usage Tracking codexUsage: null, codexUsageLastUpdated: null, + + // Codex Models codexModels: [], codexModelsLoading: false, codexModelsError: null, codexModelsLastFetched: null, codexModelsLastFailedAt: null, + + // Pipeline Configuration pipelineConfigByProject: {}, - worktreePanelVisibleByProject: {}, - showInitScriptIndicatorByProject: {}, + + // Project-specific Worktree Settings defaultDeleteBranchByProject: {}, - autoDismissInitScriptIndicatorByProject: {}, useWorktreesByProject: {}, - worktreePanelCollapsed: false, - lastProjectDir: '', - recentFolders: [], + + // Init Script State initScriptState: {}, }; -export const useAppStore = create()((set, get) => ({ +export const useAppStore = create()((set, get, store) => ({ + // Spread initial non-UI state ...initialState, + // Spread UI slice (includes UI state and actions) + ...createUISlice(set, get, store), + // Project actions setProjects: (projects) => set({ projects }), @@ -616,28 +675,9 @@ export const useAppStore = create()((set, get) => ({ } }, - // View actions - setCurrentView: (view) => set({ currentView: view }), - toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })), - setSidebarOpen: (open) => set({ sidebarOpen: open }), - setSidebarStyle: (style) => set({ sidebarStyle: style }), - setCollapsedNavSections: (sections) => set({ collapsedNavSections: sections }), - toggleNavSection: (sectionLabel) => - set((state) => ({ - collapsedNavSections: { - ...state.collapsedNavSections, - [sectionLabel]: !state.collapsedNavSections[sectionLabel], - }, - })), - toggleMobileSidebarHidden: () => - set((state) => ({ mobileSidebarHidden: !state.mobileSidebarHidden })), - setMobileSidebarHidden: (hidden) => set({ mobileSidebarHidden: hidden }), + // View actions - provided by UI slice - // Theme actions - setTheme: (theme) => { - set({ theme }); - saveThemeToStorage(theme); - }, + // Theme actions (setTheme, getEffectiveTheme, setPreviewTheme provided by UI slice) setProjectTheme: (projectId: string, theme: ThemeMode | null) => { set((state) => ({ projects: state.projects.map((p) => @@ -665,34 +705,17 @@ export const useAppStore = create()((set, get) => ({ electronAPI.projects.setProjects(get().projects); } }, - getEffectiveTheme: () => { - const state = get(); - // If there's a preview theme, use it (for hover preview) - if (state.previewTheme) return state.previewTheme; - // Otherwise, use project theme if set, or fall back to global theme - const projectTheme = state.currentProject?.theme as ThemeMode | undefined; - return projectTheme ?? state.theme; - }, - setPreviewTheme: (theme) => set({ previewTheme: theme }), - // Font actions - setFontSans: (fontFamily) => { - set({ fontFamilySans: fontFamily }); - saveFontSansToStorage(fontFamily); - }, - setFontMono: (fontFamily) => { - set({ fontFamilyMono: fontFamily }); - saveFontMonoToStorage(fontFamily); - }, + // Font actions (setFontSans, setFontMono, getEffectiveFontSans, getEffectiveFontMono provided by UI slice) setProjectFontSans: (projectId: string, fontFamily: string | null) => { set((state) => ({ projects: state.projects.map((p) => - p.id === projectId ? { ...p, fontSans: fontFamily ?? undefined } : p + p.id === projectId ? { ...p, fontFamilySans: fontFamily ?? undefined } : p ), // Also update currentProject if it's the one being changed currentProject: state.currentProject?.id === projectId - ? { ...state.currentProject, fontSans: fontFamily ?? undefined } + ? { ...state.currentProject, fontFamilySans: fontFamily ?? undefined } : state.currentProject, })); @@ -705,12 +728,12 @@ export const useAppStore = create()((set, get) => ({ setProjectFontMono: (projectId: string, fontFamily: string | null) => { set((state) => ({ projects: state.projects.map((p) => - p.id === projectId ? { ...p, fontMono: fontFamily ?? undefined } : p + p.id === projectId ? { ...p, fontFamilyMono: fontFamily ?? undefined } : p ), // Also update currentProject if it's the one being changed currentProject: state.currentProject?.id === projectId - ? { ...state.currentProject, fontMono: fontFamily ?? undefined } + ? { ...state.currentProject, fontFamilyMono: fontFamily ?? undefined } : state.currentProject, })); @@ -720,16 +743,6 @@ export const useAppStore = create()((set, get) => ({ electronAPI.projects.setProjects(get().projects); } }, - getEffectiveFontSans: () => { - const state = get(); - const projectFont = state.currentProject?.fontSans; - return getEffectiveFont(projectFont, state.fontFamilySans, UI_SANS_FONT_OPTIONS); - }, - getEffectiveFontMono: () => { - const state = get(); - const projectFont = state.currentProject?.fontMono; - return getEffectiveFont(projectFont, state.fontFamilyMono, UI_MONO_FONT_OPTIONS); - }, // Claude API Profile actions (per-project override) setProjectClaudeApiProfile: (projectId: string, profileId: string | null | undefined) => { @@ -925,8 +938,7 @@ export const useAppStore = create()((set, get) => ({ currentChatSession: state.currentChatSession?.id === sessionId ? null : state.currentChatSession, })), - setChatHistoryOpen: (open) => set({ chatHistoryOpen: open }), - toggleChatHistory: () => set((state) => ({ chatHistoryOpen: !state.chatHistoryOpen })), + // setChatHistoryOpen and toggleChatHistory - provided by UI slice // Auto Mode actions (per-worktree) getWorktreeKey: (projectId: string, branchName: string | null) => @@ -1057,8 +1069,7 @@ export const useAppStore = create()((set, get) => ({ })); }, - // Kanban Card Settings actions - setBoardViewMode: (mode) => set({ boardViewMode: mode }), + // Kanban Card Settings actions - setBoardViewMode provided by UI slice // Feature Default Settings actions setDefaultSkipTests: (skip) => set({ defaultSkipTests: skip }), @@ -1133,29 +1144,17 @@ export const useAppStore = create()((set, get) => ({ return mainWorktree?.branch ?? null; }, - // Keyboard Shortcuts actions - setKeyboardShortcut: (key, value) => - set((state) => ({ - keyboardShortcuts: { ...state.keyboardShortcuts, [key]: value }, - })), - setKeyboardShortcuts: (shortcuts) => - set((state) => ({ - keyboardShortcuts: { ...state.keyboardShortcuts, ...shortcuts }, - })), - resetKeyboardShortcuts: () => set({ keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS }), + // Keyboard Shortcuts actions - provided by UI slice - // Audio Settings actions - setMuteDoneSound: (muted) => set({ muteDoneSound: muted }), + // Audio Settings actions - setMuteDoneSound provided by UI slice - // Splash Screen actions - setDisableSplashScreen: (disabled) => set({ disableSplashScreen: disabled }), + // Splash Screen actions - setDisableSplashScreen provided by UI slice // Server Log Level actions setServerLogLevel: (level) => set({ serverLogLevel: level }), setEnableRequestLogging: (enabled) => set({ enableRequestLogging: enabled }), - // Developer Tools actions - setShowQueryDevtools: (show) => set({ showQueryDevtools: show }), + // Developer Tools actions - setShowQueryDevtools provided by UI slice // Enhancement Model actions setEnhancementModel: (model) => set({ enhancementModel: model }), @@ -1525,96 +1524,7 @@ export const useAppStore = create()((set, get) => ({ })), getLastSelectedSession: (projectPath) => get().lastSelectedSessionByProject[projectPath] ?? null, - // Board Background actions - setBoardBackground: (projectPath, imagePath) => - set((state) => ({ - boardBackgroundByProject: { - ...state.boardBackgroundByProject, - [projectPath]: { - ...(state.boardBackgroundByProject[projectPath] ?? defaultBackgroundSettings), - imagePath, - imageVersion: Date.now(), // Bust cache on image change - }, - }, - })), - setCardOpacity: (projectPath, opacity) => - set((state) => ({ - boardBackgroundByProject: { - ...state.boardBackgroundByProject, - [projectPath]: { - ...(state.boardBackgroundByProject[projectPath] ?? defaultBackgroundSettings), - cardOpacity: opacity, - }, - }, - })), - setColumnOpacity: (projectPath, opacity) => - set((state) => ({ - boardBackgroundByProject: { - ...state.boardBackgroundByProject, - [projectPath]: { - ...(state.boardBackgroundByProject[projectPath] ?? defaultBackgroundSettings), - columnOpacity: opacity, - }, - }, - })), - setColumnBorderEnabled: (projectPath, enabled) => - set((state) => ({ - boardBackgroundByProject: { - ...state.boardBackgroundByProject, - [projectPath]: { - ...(state.boardBackgroundByProject[projectPath] ?? defaultBackgroundSettings), - columnBorderEnabled: enabled, - }, - }, - })), - getBoardBackground: (projectPath) => - get().boardBackgroundByProject[projectPath] ?? defaultBackgroundSettings, - setCardGlassmorphism: (projectPath, enabled) => - set((state) => ({ - boardBackgroundByProject: { - ...state.boardBackgroundByProject, - [projectPath]: { - ...(state.boardBackgroundByProject[projectPath] ?? defaultBackgroundSettings), - cardGlassmorphism: enabled, - }, - }, - })), - setCardBorderEnabled: (projectPath, enabled) => - set((state) => ({ - boardBackgroundByProject: { - ...state.boardBackgroundByProject, - [projectPath]: { - ...(state.boardBackgroundByProject[projectPath] ?? defaultBackgroundSettings), - cardBorderEnabled: enabled, - }, - }, - })), - setCardBorderOpacity: (projectPath, opacity) => - set((state) => ({ - boardBackgroundByProject: { - ...state.boardBackgroundByProject, - [projectPath]: { - ...(state.boardBackgroundByProject[projectPath] ?? defaultBackgroundSettings), - cardBorderOpacity: opacity, - }, - }, - })), - setHideScrollbar: (projectPath, hide) => - set((state) => ({ - boardBackgroundByProject: { - ...state.boardBackgroundByProject, - [projectPath]: { - ...(state.boardBackgroundByProject[projectPath] ?? defaultBackgroundSettings), - hideScrollbar: hide, - }, - }, - })), - clearBoardBackground: (projectPath) => - set((state) => { - const newBackgrounds = { ...state.boardBackgroundByProject }; - delete newBackgrounds[projectPath]; - return { boardBackgroundByProject: newBackgrounds }; - }), + // Board Background actions - provided by UI slice // Terminal actions setTerminalUnlocked: (unlocked, token) => @@ -2364,27 +2274,9 @@ export const useAppStore = create()((set, get) => ({ }; }), - // Worktree Panel Visibility actions - setWorktreePanelVisible: (projectPath, visible) => - set((state) => ({ - worktreePanelVisibleByProject: { - ...state.worktreePanelVisibleByProject, - [projectPath]: visible, - }, - })), - getWorktreePanelVisible: (projectPath) => - get().worktreePanelVisibleByProject[projectPath] ?? true, + // Worktree Panel Visibility actions - provided by UI slice - // Init Script Indicator Visibility actions - setShowInitScriptIndicator: (projectPath, visible) => - set((state) => ({ - showInitScriptIndicatorByProject: { - ...state.showInitScriptIndicatorByProject, - [projectPath]: visible, - }, - })), - getShowInitScriptIndicator: (projectPath) => - get().showInitScriptIndicatorByProject[projectPath] ?? true, + // Init Script Indicator Visibility actions - provided by UI slice // Default Delete Branch actions setDefaultDeleteBranch: (projectPath, deleteBranch) => @@ -2396,16 +2288,7 @@ export const useAppStore = create()((set, get) => ({ })), getDefaultDeleteBranch: (projectPath) => get().defaultDeleteBranchByProject[projectPath] ?? false, - // Auto-dismiss Init Script Indicator actions - setAutoDismissInitScriptIndicator: (projectPath, autoDismiss) => - set((state) => ({ - autoDismissInitScriptIndicatorByProject: { - ...state.autoDismissInitScriptIndicatorByProject, - [projectPath]: autoDismiss, - }, - })), - getAutoDismissInitScriptIndicator: (projectPath) => - get().autoDismissInitScriptIndicatorByProject[projectPath] ?? true, + // Auto-dismiss Init Script Indicator actions - provided by UI slice // Use Worktrees Override actions setProjectUseWorktrees: (projectPath, useWorktrees) => @@ -2421,15 +2304,7 @@ export const useAppStore = create()((set, get) => ({ return projectOverride !== undefined ? projectOverride : get().useWorktrees; }, - // UI State actions - setWorktreePanelCollapsed: (collapsed) => set({ worktreePanelCollapsed: collapsed }), - setLastProjectDir: (dir) => set({ lastProjectDir: dir }), - setRecentFolders: (folders) => set({ recentFolders: folders }), - addRecentFolder: (folder) => - set((state) => { - const filtered = state.recentFolders.filter((f) => f !== folder); - return { recentFolders: [folder, ...filtered].slice(0, 10) }; - }), + // UI State actions - provided by UI slice // Claude Usage Tracking actions setClaudeRefreshInterval: (interval) => set({ claudeRefreshInterval: interval }), diff --git a/apps/ui/src/store/slices/index.ts b/apps/ui/src/store/slices/index.ts new file mode 100644 index 00000000..d179edc1 --- /dev/null +++ b/apps/ui/src/store/slices/index.ts @@ -0,0 +1 @@ +export { createUISlice, initialUIState, type UISlice } from './ui-slice'; diff --git a/apps/ui/src/store/slices/ui-slice.ts b/apps/ui/src/store/slices/ui-slice.ts new file mode 100644 index 00000000..d39c9b8d --- /dev/null +++ b/apps/ui/src/store/slices/ui-slice.ts @@ -0,0 +1,343 @@ +import type { StateCreator } from 'zustand'; +import { UI_SANS_FONT_OPTIONS, UI_MONO_FONT_OPTIONS } from '@/config/ui-font-options'; +import type { SidebarStyle } from '@automaker/types'; +import type { + ViewMode, + ThemeMode, + BoardViewMode, + KeyboardShortcuts, + BackgroundSettings, + UISliceState, + UISliceActions, +} from '../types/ui-types'; +import type { AppState, AppActions } from '../types/state-types'; +import { + getStoredTheme, + getStoredFontSans, + getStoredFontMono, + DEFAULT_KEYBOARD_SHORTCUTS, +} from '../utils'; +import { defaultBackgroundSettings } from '../defaults'; +import { + getEffectiveFont, + saveThemeToStorage, + saveFontSansToStorage, + saveFontMonoToStorage, +} from '../utils/theme-utils'; + +/** + * UI Slice + * Contains all UI-related state and actions extracted from the main app store. + * This is the first slice pattern implementation in the codebase. + */ +export type UISlice = UISliceState & UISliceActions; + +/** + * Initial UI state values + */ +export const initialUIState: UISliceState = { + // Core UI State + currentView: 'welcome', + sidebarOpen: true, + sidebarStyle: 'unified', + collapsedNavSections: {}, + mobileSidebarHidden: false, + + // Theme State + theme: getStoredTheme() || 'dark', + previewTheme: null, + + // Font State + fontFamilySans: getStoredFontSans(), + fontFamilyMono: getStoredFontMono(), + + // Board UI State + boardViewMode: 'kanban', + boardBackgroundByProject: {}, + + // Settings UI State + keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS, + muteDoneSound: false, + disableSplashScreen: false, + showQueryDevtools: true, + chatHistoryOpen: false, + + // Panel Visibility State + worktreePanelCollapsed: false, + worktreePanelVisibleByProject: {}, + showInitScriptIndicatorByProject: {}, + autoDismissInitScriptIndicatorByProject: {}, + + // File Picker UI State + lastProjectDir: '', + recentFolders: [], +}; + +/** + * Creates the UI slice for the Zustand store. + * + * Uses the StateCreator pattern to allow the slice to access other parts + * of the combined store state (e.g., currentProject for theme resolution). + */ +export const createUISlice: StateCreator = (set, get) => ({ + // Spread initial state + ...initialUIState, + + // ============================================================================ + // View Actions + // ============================================================================ + + setCurrentView: (view: ViewMode) => set({ currentView: view }), + + toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })), + + setSidebarOpen: (open: boolean) => set({ sidebarOpen: open }), + + setSidebarStyle: (style: SidebarStyle) => set({ sidebarStyle: style }), + + setCollapsedNavSections: (sections: Record) => + set({ collapsedNavSections: sections }), + + toggleNavSection: (sectionLabel: string) => + set((state) => ({ + collapsedNavSections: { + ...state.collapsedNavSections, + [sectionLabel]: !state.collapsedNavSections[sectionLabel], + }, + })), + + toggleMobileSidebarHidden: () => + set((state) => ({ mobileSidebarHidden: !state.mobileSidebarHidden })), + + setMobileSidebarHidden: (hidden: boolean) => set({ mobileSidebarHidden: hidden }), + + // ============================================================================ + // Theme Actions + // ============================================================================ + + setTheme: (theme: ThemeMode) => { + set({ theme }); + saveThemeToStorage(theme); + }, + + getEffectiveTheme: (): ThemeMode => { + const state = get(); + // If there's a preview theme, use it (for hover preview) + if (state.previewTheme) return state.previewTheme; + // Otherwise, use project theme if set, or fall back to global theme + const projectTheme = state.currentProject?.theme as ThemeMode | undefined; + return projectTheme ?? state.theme; + }, + + setPreviewTheme: (theme: ThemeMode | null) => set({ previewTheme: theme }), + + // ============================================================================ + // Font Actions + // ============================================================================ + + setFontSans: (fontFamily: string | null) => { + set({ fontFamilySans: fontFamily }); + saveFontSansToStorage(fontFamily); + }, + + setFontMono: (fontFamily: string | null) => { + set({ fontFamilyMono: fontFamily }); + saveFontMonoToStorage(fontFamily); + }, + + getEffectiveFontSans: (): string | null => { + const state = get(); + const projectFont = state.currentProject?.fontFamilySans; + return getEffectiveFont(projectFont, state.fontFamilySans, UI_SANS_FONT_OPTIONS); + }, + + getEffectiveFontMono: (): string | null => { + const state = get(); + const projectFont = state.currentProject?.fontFamilyMono; + return getEffectiveFont(projectFont, state.fontFamilyMono, UI_MONO_FONT_OPTIONS); + }, + + // ============================================================================ + // Board View Actions + // ============================================================================ + + setBoardViewMode: (mode: BoardViewMode) => set({ boardViewMode: mode }), + + setBoardBackground: (projectPath: string, imagePath: string | null) => + set((state) => ({ + boardBackgroundByProject: { + ...state.boardBackgroundByProject, + [projectPath]: { + ...(state.boardBackgroundByProject[projectPath] ?? defaultBackgroundSettings), + imagePath, + imageVersion: Date.now(), // Bust cache on image change + }, + }, + })), + + setCardOpacity: (projectPath: string, opacity: number) => + set((state) => ({ + boardBackgroundByProject: { + ...state.boardBackgroundByProject, + [projectPath]: { + ...(state.boardBackgroundByProject[projectPath] ?? defaultBackgroundSettings), + cardOpacity: opacity, + }, + }, + })), + + setColumnOpacity: (projectPath: string, opacity: number) => + set((state) => ({ + boardBackgroundByProject: { + ...state.boardBackgroundByProject, + [projectPath]: { + ...(state.boardBackgroundByProject[projectPath] ?? defaultBackgroundSettings), + columnOpacity: opacity, + }, + }, + })), + + setColumnBorderEnabled: (projectPath: string, enabled: boolean) => + set((state) => ({ + boardBackgroundByProject: { + ...state.boardBackgroundByProject, + [projectPath]: { + ...(state.boardBackgroundByProject[projectPath] ?? defaultBackgroundSettings), + columnBorderEnabled: enabled, + }, + }, + })), + + setCardGlassmorphism: (projectPath: string, enabled: boolean) => + set((state) => ({ + boardBackgroundByProject: { + ...state.boardBackgroundByProject, + [projectPath]: { + ...(state.boardBackgroundByProject[projectPath] ?? defaultBackgroundSettings), + cardGlassmorphism: enabled, + }, + }, + })), + + setCardBorderEnabled: (projectPath: string, enabled: boolean) => + set((state) => ({ + boardBackgroundByProject: { + ...state.boardBackgroundByProject, + [projectPath]: { + ...(state.boardBackgroundByProject[projectPath] ?? defaultBackgroundSettings), + cardBorderEnabled: enabled, + }, + }, + })), + + setCardBorderOpacity: (projectPath: string, opacity: number) => + set((state) => ({ + boardBackgroundByProject: { + ...state.boardBackgroundByProject, + [projectPath]: { + ...(state.boardBackgroundByProject[projectPath] ?? defaultBackgroundSettings), + cardBorderOpacity: opacity, + }, + }, + })), + + setHideScrollbar: (projectPath: string, hide: boolean) => + set((state) => ({ + boardBackgroundByProject: { + ...state.boardBackgroundByProject, + [projectPath]: { + ...(state.boardBackgroundByProject[projectPath] ?? defaultBackgroundSettings), + hideScrollbar: hide, + }, + }, + })), + + getBoardBackground: (projectPath: string): BackgroundSettings => + get().boardBackgroundByProject[projectPath] ?? defaultBackgroundSettings, + + clearBoardBackground: (projectPath: string) => + set((state) => { + const newBackgrounds = { ...state.boardBackgroundByProject }; + delete newBackgrounds[projectPath]; + return { boardBackgroundByProject: newBackgrounds }; + }), + + // ============================================================================ + // Settings UI Actions + // ============================================================================ + + setKeyboardShortcut: (key: keyof KeyboardShortcuts, value: string) => + set((state) => ({ + keyboardShortcuts: { ...state.keyboardShortcuts, [key]: value }, + })), + + setKeyboardShortcuts: (shortcuts: Partial) => + set((state) => ({ + keyboardShortcuts: { ...state.keyboardShortcuts, ...shortcuts }, + })), + + resetKeyboardShortcuts: () => set({ keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS }), + + setMuteDoneSound: (muted: boolean) => set({ muteDoneSound: muted }), + + setDisableSplashScreen: (disabled: boolean) => set({ disableSplashScreen: disabled }), + + setShowQueryDevtools: (show: boolean) => set({ showQueryDevtools: show }), + + setChatHistoryOpen: (open: boolean) => set({ chatHistoryOpen: open }), + + toggleChatHistory: () => set((state) => ({ chatHistoryOpen: !state.chatHistoryOpen })), + + // ============================================================================ + // Panel Visibility Actions + // ============================================================================ + + setWorktreePanelCollapsed: (collapsed: boolean) => set({ worktreePanelCollapsed: collapsed }), + + setWorktreePanelVisible: (projectPath: string, visible: boolean) => + set((state) => ({ + worktreePanelVisibleByProject: { + ...state.worktreePanelVisibleByProject, + [projectPath]: visible, + }, + })), + + getWorktreePanelVisible: (projectPath: string): boolean => + get().worktreePanelVisibleByProject[projectPath] ?? true, + + setShowInitScriptIndicator: (projectPath: string, visible: boolean) => + set((state) => ({ + showInitScriptIndicatorByProject: { + ...state.showInitScriptIndicatorByProject, + [projectPath]: visible, + }, + })), + + getShowInitScriptIndicator: (projectPath: string): boolean => + get().showInitScriptIndicatorByProject[projectPath] ?? true, + + setAutoDismissInitScriptIndicator: (projectPath: string, autoDismiss: boolean) => + set((state) => ({ + autoDismissInitScriptIndicatorByProject: { + ...state.autoDismissInitScriptIndicatorByProject, + [projectPath]: autoDismiss, + }, + })), + + getAutoDismissInitScriptIndicator: (projectPath: string): boolean => + get().autoDismissInitScriptIndicatorByProject[projectPath] ?? true, + + // ============================================================================ + // File Picker UI Actions + // ============================================================================ + + setLastProjectDir: (dir: string) => set({ lastProjectDir: dir }), + + setRecentFolders: (folders: string[]) => set({ recentFolders: folders }), + + addRecentFolder: (folder: string) => + set((state) => { + const filtered = state.recentFolders.filter((f) => f !== folder); + return { recentFolders: [folder, ...filtered].slice(0, 10) }; + }), +}); diff --git a/apps/ui/src/store/types/ui-types.ts b/apps/ui/src/store/types/ui-types.ts index e586d015..7f1359ec 100644 --- a/apps/ui/src/store/types/ui-types.ts +++ b/apps/ui/src/store/types/ui-types.ts @@ -117,3 +117,112 @@ export interface KeyboardShortcuts { closeTerminal: string; newTerminalTab: string; } + +// Import SidebarStyle from @automaker/types for UI slice +import type { SidebarStyle } from '@automaker/types'; + +/** + * UI Slice State + * Contains all UI-related state that is extracted into the UI slice. + */ +export interface UISliceState { + // Core UI State + currentView: ViewMode; + sidebarOpen: boolean; + sidebarStyle: SidebarStyle; + collapsedNavSections: Record; + mobileSidebarHidden: boolean; + + // Theme State + theme: ThemeMode; + previewTheme: ThemeMode | null; + + // Font State + fontFamilySans: string | null; + fontFamilyMono: string | null; + + // Board UI State + boardViewMode: BoardViewMode; + boardBackgroundByProject: Record; + + // Settings UI State + keyboardShortcuts: KeyboardShortcuts; + muteDoneSound: boolean; + disableSplashScreen: boolean; + showQueryDevtools: boolean; + chatHistoryOpen: boolean; + + // Panel Visibility State + worktreePanelCollapsed: boolean; + worktreePanelVisibleByProject: Record; + showInitScriptIndicatorByProject: Record; + autoDismissInitScriptIndicatorByProject: Record; + + // File Picker UI State + lastProjectDir: string; + recentFolders: string[]; +} + +/** + * UI Slice Actions + * Contains all UI-related actions that are extracted into the UI slice. + */ +export interface UISliceActions { + // View Actions + setCurrentView: (view: ViewMode) => void; + toggleSidebar: () => void; + setSidebarOpen: (open: boolean) => void; + setSidebarStyle: (style: SidebarStyle) => void; + setCollapsedNavSections: (sections: Record) => void; + toggleNavSection: (sectionLabel: string) => void; + toggleMobileSidebarHidden: () => void; + setMobileSidebarHidden: (hidden: boolean) => void; + + // Theme Actions (Pure UI only - project theme actions stay in main store) + setTheme: (theme: ThemeMode) => void; + getEffectiveTheme: () => ThemeMode; + setPreviewTheme: (theme: ThemeMode | null) => void; + + // Font Actions (Pure UI only - project font actions stay in main store) + setFontSans: (fontFamily: string | null) => void; + setFontMono: (fontFamily: string | null) => void; + getEffectiveFontSans: () => string | null; + getEffectiveFontMono: () => string | null; + + // Board View Actions + setBoardViewMode: (mode: BoardViewMode) => void; + 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; + setCardGlassmorphism: (projectPath: string, enabled: boolean) => void; + setCardBorderEnabled: (projectPath: string, enabled: boolean) => void; + setCardBorderOpacity: (projectPath: string, opacity: number) => void; + setHideScrollbar: (projectPath: string, hide: boolean) => void; + getBoardBackground: (projectPath: string) => BackgroundSettings; + clearBoardBackground: (projectPath: string) => void; + + // Settings UI Actions + setKeyboardShortcut: (key: keyof KeyboardShortcuts, value: string) => void; + setKeyboardShortcuts: (shortcuts: Partial) => void; + resetKeyboardShortcuts: () => void; + setMuteDoneSound: (muted: boolean) => void; + setDisableSplashScreen: (disabled: boolean) => void; + setShowQueryDevtools: (show: boolean) => void; + setChatHistoryOpen: (open: boolean) => void; + toggleChatHistory: () => void; + + // Panel Visibility Actions + setWorktreePanelCollapsed: (collapsed: boolean) => void; + setWorktreePanelVisible: (projectPath: string, visible: boolean) => void; + getWorktreePanelVisible: (projectPath: string) => boolean; + setShowInitScriptIndicator: (projectPath: string, visible: boolean) => void; + getShowInitScriptIndicator: (projectPath: string) => boolean; + setAutoDismissInitScriptIndicator: (projectPath: string, autoDismiss: boolean) => void; + getAutoDismissInitScriptIndicator: (projectPath: string) => boolean; + + // File Picker UI Actions + setLastProjectDir: (dir: string) => void; + setRecentFolders: (folders: string[]) => void; + addRecentFolder: (folder: string) => void; +}