Compare commits

...

4 Commits

Author SHA1 Message Date
Kacper
6e2f277f63 Merge v0.14.0rc into refactor/store-ui-slice
Resolve merge conflict in app-store.ts by keeping UI slice implementation
of getEffectiveFontSans/getEffectiveFontMono (already provided by ui-slice.ts)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 17:48:41 +01:00
Shirone
ef3f8de33b Merge pull request #715 from OG-Ken/fix/opencode-dynamic-models-404-endpoint
fix: Correct OpenCode dynamic models API endpoint URL
2026-01-27 12:02:33 +00:00
Ken Lopez
d379bf412a fix: Correct OpenCode dynamic models API endpoint URL
The fetchOpencodeModels function was calling '/api/opencode/models' which
returns 404. Changed to '/api/setup/opencode/models' which correctly
returns the dynamic models.

This fixes an issue where enabled OpenCode dynamic models (e.g., local
Ollama models) were not appearing in the Model Defaults dropdown selectors
despite being visible and enabled in the OpenCode Settings page.
2026-01-27 03:06:28 -05:00
Shirone
79236ba16e 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>
2026-01-26 00:03:58 +01:00
4 changed files with 562 additions and 234 deletions

View File

@@ -60,6 +60,8 @@ import {
type ShortcutKey, type ShortcutKey,
type KeyboardShortcuts, type KeyboardShortcuts,
type BackgroundSettings, type BackgroundSettings,
type UISliceState,
type UISliceActions,
// Settings types // Settings types
type ApiKeys, type ApiKeys,
// Chat types // Chat types
@@ -109,16 +111,13 @@ import {
} from './utils'; } from './utils';
// Import default values from modular defaults files // 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 internal theme utils (not re-exported publicly)
import { import { persistEffectiveThemeForProject } from './utils/theme-utils';
getEffectiveFont,
saveThemeToStorage,
saveFontSansToStorage,
saveFontMonoToStorage,
persistEffectiveThemeForProject,
} from './utils/theme-utils';
const logger = createLogger('AppStore'); const logger = createLogger('AppStore');
const OPENCODE_BEDROCK_PROVIDER_ID = 'amazon-bedrock'; const OPENCODE_BEDROCK_PROVIDER_ID = 'amazon-bedrock';
@@ -146,6 +145,8 @@ export type {
ShortcutKey, ShortcutKey,
KeyboardShortcuts, KeyboardShortcuts,
BackgroundSettings, BackgroundSettings,
UISliceState,
UISliceActions,
ApiKeys, ApiKeys,
ImageAttachment, ImageAttachment,
TextFileAttachment, TextFileAttachment,
@@ -213,56 +214,72 @@ export { defaultBackgroundSettings, defaultTerminalState, MAX_INIT_OUTPUT_LINES
// - defaultTerminalState (./defaults/terminal-defaults.ts) // - defaultTerminalState (./defaults/terminal-defaults.ts)
const initialState: AppState = { const initialState: AppState = {
// Spread UI slice state first
...initialUIState,
// Project state
projects: [], projects: [],
currentProject: null, currentProject: null,
trashedProjects: [], trashedProjects: [],
projectHistory: [], projectHistory: [],
projectHistoryIndex: -1, projectHistoryIndex: -1,
currentView: 'welcome',
sidebarOpen: true, // Agent Session state
sidebarStyle: 'unified',
collapsedNavSections: {},
mobileSidebarHidden: false,
lastSelectedSessionByProject: {}, lastSelectedSessionByProject: {},
theme: getStoredTheme() || 'dark',
fontFamilySans: getStoredFontSans(), // Features/Kanban
fontFamilyMono: getStoredFontMono(),
features: [], features: [],
// App spec
appSpec: '', appSpec: '',
// IPC status
ipcConnected: false, ipcConnected: false,
// API Keys
apiKeys: { apiKeys: {
anthropic: '', anthropic: '',
google: '', google: '',
openai: '', openai: '',
}, },
// Chat Sessions
chatSessions: [], chatSessions: [],
currentChatSession: null, currentChatSession: null,
chatHistoryOpen: false,
// Auto Mode
autoModeByWorktree: {}, autoModeByWorktree: {},
autoModeActivityLog: [], autoModeActivityLog: [],
maxConcurrency: DEFAULT_MAX_CONCURRENCY, maxConcurrency: DEFAULT_MAX_CONCURRENCY,
boardViewMode: 'kanban',
// Feature Default Settings
defaultSkipTests: true, defaultSkipTests: true,
enableDependencyBlocking: true, enableDependencyBlocking: true,
skipVerificationInAutoMode: false, skipVerificationInAutoMode: false,
enableAiCommitMessages: true, enableAiCommitMessages: true,
planUseSelectedWorktreeBranch: true, planUseSelectedWorktreeBranch: true,
addFeatureUseSelectedWorktreeBranch: false, addFeatureUseSelectedWorktreeBranch: false,
// Worktree Settings
useWorktrees: true, useWorktrees: true,
currentWorktreeByProject: {}, currentWorktreeByProject: {},
worktreesByProject: {}, worktreesByProject: {},
keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS,
muteDoneSound: false, // Server Settings
disableSplashScreen: false,
serverLogLevel: 'info', serverLogLevel: 'info',
enableRequestLogging: true, enableRequestLogging: true,
showQueryDevtools: true,
// Model Settings
enhancementModel: 'claude-sonnet', enhancementModel: 'claude-sonnet',
validationModel: 'claude-opus', validationModel: 'claude-opus',
phaseModels: DEFAULT_PHASE_MODELS, phaseModels: DEFAULT_PHASE_MODELS,
favoriteModels: [], favoriteModels: [],
// Cursor CLI Settings
enabledCursorModels: getAllCursorModelIds(), enabledCursorModels: getAllCursorModelIds(),
cursorDefaultModel: 'cursor-auto', cursorDefaultModel: 'cursor-auto',
// Codex CLI Settings
enabledCodexModels: getAllCodexModelIds(), enabledCodexModels: getAllCodexModelIds(),
codexDefaultModel: 'codex-gpt-5.2-codex', codexDefaultModel: 'codex-gpt-5.2-codex',
codexAutoLoadAgents: false, codexAutoLoadAgents: false,
@@ -270,6 +287,8 @@ const initialState: AppState = {
codexApprovalPolicy: 'on-request', codexApprovalPolicy: 'on-request',
codexEnableWebSearch: false, codexEnableWebSearch: false,
codexEnableImages: false, codexEnableImages: false,
// OpenCode CLI Settings
enabledOpencodeModels: getAllOpencodeModelIds(), enabledOpencodeModels: getAllOpencodeModelIds(),
opencodeDefaultModel: DEFAULT_OPENCODE_MODEL, opencodeDefaultModel: DEFAULT_OPENCODE_MODEL,
dynamicOpencodeModels: [], dynamicOpencodeModels: [],
@@ -279,61 +298,101 @@ const initialState: AppState = {
opencodeModelsError: null, opencodeModelsError: null,
opencodeModelsLastFetched: null, opencodeModelsLastFetched: null,
opencodeModelsLastFailedAt: null, opencodeModelsLastFailedAt: null,
// Gemini CLI Settings
enabledGeminiModels: getAllGeminiModelIds(), enabledGeminiModels: getAllGeminiModelIds(),
geminiDefaultModel: DEFAULT_GEMINI_MODEL, geminiDefaultModel: DEFAULT_GEMINI_MODEL,
// Copilot SDK Settings
enabledCopilotModels: getAllCopilotModelIds(), enabledCopilotModels: getAllCopilotModelIds(),
copilotDefaultModel: DEFAULT_COPILOT_MODEL, copilotDefaultModel: DEFAULT_COPILOT_MODEL,
// Provider Settings
disabledProviders: [], disabledProviders: [],
// Claude Agent SDK Settings
autoLoadClaudeMd: false, autoLoadClaudeMd: false,
skipSandboxWarning: false, skipSandboxWarning: false,
// MCP Servers
mcpServers: [], mcpServers: [],
// Editor Configuration
defaultEditorCommand: null, defaultEditorCommand: null,
// Terminal Configuration
defaultTerminalId: null, defaultTerminalId: null,
// Skills Configuration
enableSkills: true, enableSkills: true,
skillsSources: ['user', 'project'] as Array<'user' | 'project'>, skillsSources: ['user', 'project'] as Array<'user' | 'project'>,
// Subagents Configuration
enableSubagents: true, enableSubagents: true,
subagentsSources: ['user', 'project'] as Array<'user' | 'project'>, subagentsSources: ['user', 'project'] as Array<'user' | 'project'>,
// Prompt Customization
promptCustomization: {}, promptCustomization: {},
// Event Hooks
eventHooks: [], eventHooks: [],
// Claude-Compatible Providers
claudeCompatibleProviders: [], claudeCompatibleProviders: [],
claudeApiProfiles: [], claudeApiProfiles: [],
activeClaudeApiProfileId: null, activeClaudeApiProfileId: null,
// Project Analysis
projectAnalysis: null, projectAnalysis: null,
isAnalyzing: false, isAnalyzing: false,
boardBackgroundByProject: {},
previewTheme: null, // Terminal state
terminalState: defaultTerminalState, terminalState: defaultTerminalState,
terminalLayoutByProject: {}, terminalLayoutByProject: {},
// Spec Creation
specCreatingForProject: null, specCreatingForProject: null,
// Planning
defaultPlanningMode: 'skip' as PlanningMode, defaultPlanningMode: 'skip' as PlanningMode,
defaultRequirePlanApproval: false, defaultRequirePlanApproval: false,
defaultFeatureModel: DEFAULT_GLOBAL_SETTINGS.defaultFeatureModel, defaultFeatureModel: DEFAULT_GLOBAL_SETTINGS.defaultFeatureModel,
pendingPlanApproval: null, pendingPlanApproval: null,
// Claude Usage Tracking
claudeRefreshInterval: 60, claudeRefreshInterval: 60,
claudeUsage: null, claudeUsage: null,
claudeUsageLastUpdated: null, claudeUsageLastUpdated: null,
// Codex Usage Tracking
codexUsage: null, codexUsage: null,
codexUsageLastUpdated: null, codexUsageLastUpdated: null,
// Codex Models
codexModels: [], codexModels: [],
codexModelsLoading: false, codexModelsLoading: false,
codexModelsError: null, codexModelsError: null,
codexModelsLastFetched: null, codexModelsLastFetched: null,
codexModelsLastFailedAt: null, codexModelsLastFailedAt: null,
// Pipeline Configuration
pipelineConfigByProject: {}, pipelineConfigByProject: {},
worktreePanelVisibleByProject: {},
showInitScriptIndicatorByProject: {}, // Project-specific Worktree Settings
defaultDeleteBranchByProject: {}, defaultDeleteBranchByProject: {},
autoDismissInitScriptIndicatorByProject: {},
useWorktreesByProject: {}, useWorktreesByProject: {},
worktreePanelCollapsed: false,
lastProjectDir: '', // Init Script State
recentFolders: [],
initScriptState: {}, initScriptState: {},
}; };
export const useAppStore = create<AppState & AppActions>()((set, get) => ({ export const useAppStore = create<AppState & AppActions>()((set, get, store) => ({
// Spread initial non-UI state
...initialState, ...initialState,
// Spread UI slice (includes UI state and actions)
...createUISlice(set, get, store),
// Project actions // Project actions
setProjects: (projects) => set({ projects }), setProjects: (projects) => set({ projects }),
@@ -598,28 +657,9 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
saveProjects(get().projects); saveProjects(get().projects);
}, },
// View actions // View actions - provided by UI slice
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 }),
// Theme actions // Theme actions (setTheme, getEffectiveTheme, setPreviewTheme provided by UI slice)
setTheme: (theme) => {
set({ theme });
saveThemeToStorage(theme);
},
setProjectTheme: (projectId: string, theme: ThemeMode | null) => { setProjectTheme: (projectId: string, theme: ThemeMode | null) => {
set((state) => ({ set((state) => ({
projects: state.projects.map((p) => projects: state.projects.map((p) =>
@@ -644,34 +684,17 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
// Persist to storage // Persist to storage
saveProjects(get().projects); saveProjects(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 // Font actions (setFontSans, setFontMono, getEffectiveFontSans, getEffectiveFontMono provided by UI slice)
setFontSans: (fontFamily) => {
set({ fontFamilySans: fontFamily });
saveFontSansToStorage(fontFamily);
},
setFontMono: (fontFamily) => {
set({ fontFamilyMono: fontFamily });
saveFontMonoToStorage(fontFamily);
},
setProjectFontSans: (projectId: string, fontFamily: string | null) => { setProjectFontSans: (projectId: string, fontFamily: string | null) => {
set((state) => ({ set((state) => ({
projects: state.projects.map((p) => 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 // Also update currentProject if it's the one being changed
currentProject: currentProject:
state.currentProject?.id === projectId state.currentProject?.id === projectId
? { ...state.currentProject, fontSans: fontFamily ?? undefined } ? { ...state.currentProject, fontFamilySans: fontFamily ?? undefined }
: state.currentProject, : state.currentProject,
})); }));
@@ -681,28 +704,18 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
setProjectFontMono: (projectId: string, fontFamily: string | null) => { setProjectFontMono: (projectId: string, fontFamily: string | null) => {
set((state) => ({ set((state) => ({
projects: state.projects.map((p) => 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 // Also update currentProject if it's the one being changed
currentProject: currentProject:
state.currentProject?.id === projectId state.currentProject?.id === projectId
? { ...state.currentProject, fontMono: fontFamily ?? undefined } ? { ...state.currentProject, fontFamilyMono: fontFamily ?? undefined }
: state.currentProject, : state.currentProject,
})); }));
// Persist to storage // Persist to storage
saveProjects(get().projects); saveProjects(get().projects);
}, },
getEffectiveFontSans: () => {
const state = get();
const projectFont = state.currentProject?.fontFamilySans;
return getEffectiveFont(projectFont, state.fontFamilySans, UI_SANS_FONT_OPTIONS);
},
getEffectiveFontMono: () => {
const state = get();
const projectFont = state.currentProject?.fontFamilyMono;
return getEffectiveFont(projectFont, state.fontFamilyMono, UI_MONO_FONT_OPTIONS);
},
// Claude API Profile actions (per-project override) // Claude API Profile actions (per-project override)
setProjectClaudeApiProfile: (projectId: string, profileId: string | null | undefined) => { setProjectClaudeApiProfile: (projectId: string, profileId: string | null | undefined) => {
@@ -886,8 +899,7 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
currentChatSession: currentChatSession:
state.currentChatSession?.id === sessionId ? null : state.currentChatSession, state.currentChatSession?.id === sessionId ? null : state.currentChatSession,
})), })),
setChatHistoryOpen: (open) => set({ chatHistoryOpen: open }), // setChatHistoryOpen and toggleChatHistory - provided by UI slice
toggleChatHistory: () => set((state) => ({ chatHistoryOpen: !state.chatHistoryOpen })),
// Auto Mode actions (per-worktree) // Auto Mode actions (per-worktree)
getWorktreeKey: (projectId: string, branchName: string | null) => getWorktreeKey: (projectId: string, branchName: string | null) =>
@@ -1018,8 +1030,7 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
})); }));
}, },
// Kanban Card Settings actions // Kanban Card Settings actions - setBoardViewMode provided by UI slice
setBoardViewMode: (mode) => set({ boardViewMode: mode }),
// Feature Default Settings actions // Feature Default Settings actions
setDefaultSkipTests: (skip) => set({ defaultSkipTests: skip }), setDefaultSkipTests: (skip) => set({ defaultSkipTests: skip }),
@@ -1094,29 +1105,17 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
return mainWorktree?.branch ?? null; return mainWorktree?.branch ?? null;
}, },
// Keyboard Shortcuts actions // Keyboard Shortcuts actions - provided by UI slice
setKeyboardShortcut: (key, value) =>
set((state) => ({
keyboardShortcuts: { ...state.keyboardShortcuts, [key]: value },
})),
setKeyboardShortcuts: (shortcuts) =>
set((state) => ({
keyboardShortcuts: { ...state.keyboardShortcuts, ...shortcuts },
})),
resetKeyboardShortcuts: () => set({ keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS }),
// Audio Settings actions // Audio Settings actions - setMuteDoneSound provided by UI slice
setMuteDoneSound: (muted) => set({ muteDoneSound: muted }),
// Splash Screen actions // Splash Screen actions - setDisableSplashScreen provided by UI slice
setDisableSplashScreen: (disabled) => set({ disableSplashScreen: disabled }),
// Server Log Level actions // Server Log Level actions
setServerLogLevel: (level) => set({ serverLogLevel: level }), setServerLogLevel: (level) => set({ serverLogLevel: level }),
setEnableRequestLogging: (enabled) => set({ enableRequestLogging: enabled }), setEnableRequestLogging: (enabled) => set({ enableRequestLogging: enabled }),
// Developer Tools actions // Developer Tools actions - setShowQueryDevtools provided by UI slice
setShowQueryDevtools: (show) => set({ showQueryDevtools: show }),
// Enhancement Model actions // Enhancement Model actions
setEnhancementModel: (model) => set({ enhancementModel: model }), setEnhancementModel: (model) => set({ enhancementModel: model }),
@@ -1486,96 +1485,7 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
})), })),
getLastSelectedSession: (projectPath) => get().lastSelectedSessionByProject[projectPath] ?? null, getLastSelectedSession: (projectPath) => get().lastSelectedSessionByProject[projectPath] ?? null,
// Board Background actions // Board Background actions - provided by UI slice
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 };
}),
// Terminal actions // Terminal actions
setTerminalUnlocked: (unlocked, token) => setTerminalUnlocked: (unlocked, token) =>
@@ -2325,27 +2235,9 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
}; };
}), }),
// Worktree Panel Visibility actions // Worktree Panel Visibility actions - provided by UI slice
setWorktreePanelVisible: (projectPath, visible) =>
set((state) => ({
worktreePanelVisibleByProject: {
...state.worktreePanelVisibleByProject,
[projectPath]: visible,
},
})),
getWorktreePanelVisible: (projectPath) =>
get().worktreePanelVisibleByProject[projectPath] ?? true,
// Init Script Indicator Visibility actions // Init Script Indicator Visibility actions - provided by UI slice
setShowInitScriptIndicator: (projectPath, visible) =>
set((state) => ({
showInitScriptIndicatorByProject: {
...state.showInitScriptIndicatorByProject,
[projectPath]: visible,
},
})),
getShowInitScriptIndicator: (projectPath) =>
get().showInitScriptIndicatorByProject[projectPath] ?? true,
// Default Delete Branch actions // Default Delete Branch actions
setDefaultDeleteBranch: (projectPath, deleteBranch) => setDefaultDeleteBranch: (projectPath, deleteBranch) =>
@@ -2357,16 +2249,7 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
})), })),
getDefaultDeleteBranch: (projectPath) => get().defaultDeleteBranchByProject[projectPath] ?? false, getDefaultDeleteBranch: (projectPath) => get().defaultDeleteBranchByProject[projectPath] ?? false,
// Auto-dismiss Init Script Indicator actions // Auto-dismiss Init Script Indicator actions - provided by UI slice
setAutoDismissInitScriptIndicator: (projectPath, autoDismiss) =>
set((state) => ({
autoDismissInitScriptIndicatorByProject: {
...state.autoDismissInitScriptIndicatorByProject,
[projectPath]: autoDismiss,
},
})),
getAutoDismissInitScriptIndicator: (projectPath) =>
get().autoDismissInitScriptIndicatorByProject[projectPath] ?? true,
// Use Worktrees Override actions // Use Worktrees Override actions
setProjectUseWorktrees: (projectPath, useWorktrees) => setProjectUseWorktrees: (projectPath, useWorktrees) =>
@@ -2382,15 +2265,7 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
return projectOverride !== undefined ? projectOverride : get().useWorktrees; return projectOverride !== undefined ? projectOverride : get().useWorktrees;
}, },
// UI State actions // UI State actions - provided by UI slice
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) };
}),
// Claude Usage Tracking actions // Claude Usage Tracking actions
setClaudeRefreshInterval: (interval) => set({ claudeRefreshInterval: interval }), setClaudeRefreshInterval: (interval) => set({ claudeRefreshInterval: interval }),
@@ -2512,7 +2387,7 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
authMethod?: string; authMethod?: string;
}>; }>;
error?: string; error?: string;
}>('/api/opencode/models'); }>('/api/setup/opencode/models');
if (data.success && data.models) { if (data.success && data.models) {
// Filter out Bedrock models // Filter out Bedrock models

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; closeTerminal: string;
newTerminalTab: 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;
}