mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
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:
@@ -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 }),
|
||||
|
||||
1
apps/ui/src/store/slices/index.ts
Normal file
1
apps/ui/src/store/slices/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { createUISlice, initialUIState, type UISlice } from './ui-slice';
|
||||
343
apps/ui/src/store/slices/ui-slice.ts
Normal file
343
apps/ui/src/store/slices/ui-slice.ts
Normal 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) };
|
||||
}),
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user