refactor(store): Extract UI slice from app-store.ts

- Extract UI-related state and actions into store/slices/ui-slice.ts
- Add UISliceState and UISliceActions interfaces to store/types/ui-types.ts
- First implementation of Zustand slice pattern in the codebase
- Fix pre-existing bug: fontSans/fontMono -> fontFamilySans/fontFamilyMono
- Maintain backward compatibility through re-exports

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Shirone
2026-01-26 00:03:58 +01:00
parent c848306e4c
commit 79236ba16e
4 changed files with 561 additions and 233 deletions

View File

@@ -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<AppState & AppActions>()((set, get) => ({
export const useAppStore = create<AppState & AppActions>()((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<AppState & AppActions>()((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<AppState & AppActions>()((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<AppState & AppActions>()((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<AppState & AppActions>()((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<AppState & AppActions>()((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<AppState & AppActions>()((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<AppState & AppActions>()((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<AppState & AppActions>()((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<AppState & AppActions>()((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<AppState & AppActions>()((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<AppState & AppActions>()((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 }),

View File

@@ -0,0 +1 @@
export { createUISlice, initialUIState, type UISlice } from './ui-slice';

View File

@@ -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<AppState & AppActions, [], [], UISlice> = (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<string, boolean>) =>
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<KeyboardShortcuts>) =>
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) };
}),
});

View File

@@ -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<string, boolean>;
mobileSidebarHidden: boolean;
// Theme State
theme: ThemeMode;
previewTheme: ThemeMode | null;
// Font State
fontFamilySans: string | null;
fontFamilyMono: string | null;
// Board UI State
boardViewMode: BoardViewMode;
boardBackgroundByProject: Record<string, BackgroundSettings>;
// Settings UI State
keyboardShortcuts: KeyboardShortcuts;
muteDoneSound: boolean;
disableSplashScreen: boolean;
showQueryDevtools: boolean;
chatHistoryOpen: boolean;
// Panel Visibility State
worktreePanelCollapsed: boolean;
worktreePanelVisibleByProject: Record<string, boolean>;
showInitScriptIndicatorByProject: Record<string, boolean>;
autoDismissInitScriptIndicatorByProject: Record<string, boolean>;
// 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<string, boolean>) => 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<KeyboardShortcuts>) => 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;
}