Merge branch 'main' of github.com:AutoMaker-Org/automaker into improve-context-page

This commit is contained in:
Test User
2025-12-22 00:50:55 -05:00
501 changed files with 17637 additions and 17437 deletions

View File

@@ -30,6 +30,25 @@ export type ViewMode =
| 'terminal'
| 'wiki';
export type ThemeMode =
| 'light'
| 'dark'
| 'system'
| 'retro'
| 'dracula'
| 'nord'
| 'monokai'
| 'tokyonight'
| 'solarized'
| 'gruvbox'
| 'catppuccin'
| 'onedark'
| 'synthwave'
| 'red'
| 'cream'
| 'sunset'
| 'gray';
export type KanbanCardDetailLevel = 'minimal' | 'standard' | 'detailed';
export interface ApiKeys {
@@ -151,6 +170,7 @@ export interface KeyboardShortcuts {
splitTerminalRight: string;
splitTerminalDown: string;
closeTerminal: string;
newTerminalTab: string;
}
// Default keyboard shortcuts
@@ -185,6 +205,7 @@ export const DEFAULT_KEYBOARD_SHORTCUTS: KeyboardShortcuts = {
splitTerminalRight: 'Alt+D',
splitTerminalDown: 'Alt+S',
closeTerminal: 'Alt+W',
newTerminalTab: 'Alt+T',
};
export interface ImageAttachment {
@@ -242,8 +263,12 @@ export interface FeatureImage {
size: number;
}
// Available models for feature execution (alias for consistency)
export type ClaudeModel = AgentModel;
export interface FeatureImagePath {
id: string;
path: string; // Path to the temp file
filename: string;
mimeType: string;
}
// UI-specific Feature extension with UI-only fields and stricter types
export interface Feature extends Omit<
@@ -305,6 +330,7 @@ export type TerminalPanelContent =
| { type: 'terminal'; sessionId: string; size?: number; fontSize?: number }
| {
type: 'split';
id: string; // Stable ID for React key stability
direction: 'horizontal' | 'vertical';
panels: TerminalPanelContent[];
size?: number;
@@ -323,7 +349,57 @@ export interface TerminalState {
tabs: TerminalTab[];
activeTabId: string | null;
activeSessionId: string | null;
maximizedSessionId: string | null; // Session ID of the maximized terminal pane (null if none)
defaultFontSize: number; // Default font size for new terminals
defaultRunScript: string; // Script to run when a new terminal is created (e.g., "claude" to start Claude Code)
screenReaderMode: boolean; // Enable screen reader accessibility mode
fontFamily: string; // Font family for terminal text
scrollbackLines: number; // Number of lines to keep in scrollback buffer
lineHeight: number; // Line height multiplier for terminal text
maxSessions: number; // Maximum concurrent terminal sessions (server setting)
}
// Persisted terminal layout - now includes sessionIds for reconnection
// Used to restore terminal layout structure when switching projects
export type PersistedTerminalPanel =
| { type: 'terminal'; size?: number; fontSize?: number; sessionId?: string }
| {
type: 'split';
id?: string; // Optional for backwards compatibility with older persisted layouts
direction: 'horizontal' | 'vertical';
panels: PersistedTerminalPanel[];
size?: number;
};
// Helper to generate unique split IDs
const generateSplitId = () => `split-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
export interface PersistedTerminalTab {
id: string;
name: string;
layout: PersistedTerminalPanel | null;
}
export interface PersistedTerminalState {
tabs: PersistedTerminalTab[];
activeTabIndex: number; // Use index instead of ID since IDs are regenerated
defaultFontSize: number;
defaultRunScript?: string; // Optional to support existing persisted data
screenReaderMode?: boolean; // Optional to support existing persisted data
fontFamily?: string; // Optional to support existing persisted data
scrollbackLines?: number; // Optional to support existing persisted data
lineHeight?: number; // Optional to support existing persisted data
}
// Persisted terminal settings - stored globally (not per-project)
export interface PersistedTerminalSettings {
defaultFontSize: number;
defaultRunScript: string;
screenReaderMode: boolean;
fontFamily: string;
scrollbackLines: number;
lineHeight: number;
maxSessions: number;
}
export interface AppState {
@@ -437,6 +513,10 @@ export interface AppState {
// Terminal state
terminalState: TerminalState;
// Terminal layout persistence (per-project, keyed by project path)
// Stores the tab/split structure so it can be restored when switching projects
terminalLayoutByProject: Record<string, PersistedTerminalState>;
// Spec Creation State (per-project, keyed by project path)
// Tracks which project is currently having its spec generated
specCreatingForProject: string | null;
@@ -716,6 +796,7 @@ export interface AppActions {
// Terminal actions
setTerminalUnlocked: (unlocked: boolean, token?: string) => void;
setActiveTerminalSession: (sessionId: string | null) => void;
toggleTerminalMaximized: (sessionId: string) => void;
addTerminalToLayout: (
sessionId: string,
direction?: 'horizontal' | 'vertical',
@@ -725,16 +806,33 @@ export interface AppActions {
swapTerminals: (sessionId1: string, sessionId2: string) => void;
clearTerminalState: () => void;
setTerminalPanelFontSize: (sessionId: string, fontSize: number) => void;
setTerminalDefaultFontSize: (fontSize: number) => void;
setTerminalDefaultRunScript: (script: string) => void;
setTerminalScreenReaderMode: (enabled: boolean) => void;
setTerminalFontFamily: (fontFamily: string) => void;
setTerminalScrollbackLines: (lines: number) => void;
setTerminalLineHeight: (lineHeight: number) => void;
setTerminalMaxSessions: (maxSessions: number) => void;
addTerminalTab: (name?: string) => string;
removeTerminalTab: (tabId: string) => void;
setActiveTerminalTab: (tabId: string) => void;
renameTerminalTab: (tabId: string, name: string) => void;
reorderTerminalTabs: (fromTabId: string, toTabId: string) => void;
moveTerminalToTab: (sessionId: string, targetTabId: string | 'new') => void;
addTerminalToTab: (
sessionId: string,
tabId: string,
direction?: 'horizontal' | 'vertical'
) => void;
setTerminalTabLayout: (
tabId: string,
layout: TerminalPanelContent,
activeSessionId?: string
) => void;
updateTerminalPanelSizes: (tabId: string, panelKeys: string[], sizes: number[]) => void;
saveTerminalLayout: (projectPath: string) => void;
getPersistedTerminalLayout: (projectPath: string) => PersistedTerminalState | null;
clearPersistedTerminalLayout: (projectPath: string) => void;
// Spec Creation actions
setSpecCreatingForProject: (projectPath: string | null) => void;
@@ -754,11 +852,6 @@ export interface AppActions {
} | null
) => void;
// Claude Usage Tracking actions
setClaudeRefreshInterval: (interval: number) => void;
setClaudeUsageLastUpdated: (timestamp: number) => void;
setClaudeUsage: (usage: ClaudeUsage | null) => void;
// Reset
reset: () => void;
}
@@ -843,16 +936,21 @@ const initialState: AppState = {
tabs: [],
activeTabId: null,
activeSessionId: null,
maximizedSessionId: null,
defaultFontSize: 14,
defaultRunScript: '',
screenReaderMode: false,
fontFamily: "Menlo, Monaco, 'Courier New', monospace",
scrollbackLines: 5000,
lineHeight: 1.0,
maxSessions: 100,
},
terminalLayoutByProject: {},
specCreatingForProject: null,
defaultPlanningMode: 'skip' as PlanningMode,
defaultRequirePlanApproval: false,
defaultAIProfileId: null,
pendingPlanApproval: null,
claudeRefreshInterval: 60,
claudeUsage: null,
claudeUsageLastUpdated: null,
};
export const useAppStore = create<AppState & AppActions>()(
@@ -1680,6 +1778,19 @@ export const useAppStore = create<AppState & AppActions>()(
});
},
toggleTerminalMaximized: (sessionId) => {
const current = get().terminalState;
const newMaximized = current.maximizedSessionId === sessionId ? null : sessionId;
set({
terminalState: {
...current,
maximizedSessionId: newMaximized,
// Also set as active when maximizing
activeSessionId: newMaximized ?? current.activeSessionId,
},
});
},
addTerminalToLayout: (sessionId, direction = 'horizontal', targetSessionId) => {
const current = get().terminalState;
const newTerminal: TerminalPanelContent = {
@@ -1723,6 +1834,7 @@ export const useAppStore = create<AppState & AppActions>()(
// Found the target - split it
return {
type: 'split',
id: generateSplitId(),
direction: targetDirection,
panels: [{ ...node, size: 50 }, newTerminal],
};
@@ -1745,6 +1857,7 @@ export const useAppStore = create<AppState & AppActions>()(
if (node.type === 'terminal') {
return {
type: 'split',
id: generateSplitId(),
direction: targetDirection,
panels: [{ ...node, size: 50 }, newTerminal],
};
@@ -1763,6 +1876,7 @@ export const useAppStore = create<AppState & AppActions>()(
// Different direction, wrap in new split
return {
type: 'split',
id: generateSplitId(),
direction: targetDirection,
panels: [{ ...node, size: 50 }, newTerminal],
};
@@ -1816,7 +1930,13 @@ export const useAppStore = create<AppState & AppActions>()(
}
if (newPanels.length === 0) return null;
if (newPanels.length === 1) return newPanels[0];
return { ...node, panels: newPanels };
// Normalize sizes to sum to 100%
const totalSize = newPanels.reduce((sum, p) => sum + (p.size || 0), 0);
const normalizedPanels =
totalSize > 0
? newPanels.map((p) => ({ ...p, size: ((p.size || 0) / totalSize) * 100 }))
: newPanels.map((p) => ({ ...p, size: 100 / newPanels.length }));
return { ...node, panels: normalizedPanels };
};
let newTabs = current.tabs.map((tab) => {
@@ -1873,14 +1993,25 @@ export const useAppStore = create<AppState & AppActions>()(
},
clearTerminalState: () => {
const current = get().terminalState;
set({
terminalState: {
isUnlocked: false,
authToken: null,
// Preserve auth state - user shouldn't need to re-authenticate
isUnlocked: current.isUnlocked,
authToken: current.authToken,
// Clear session-specific state only
tabs: [],
activeTabId: null,
activeSessionId: null,
defaultFontSize: 14,
maximizedSessionId: null,
// Preserve user preferences - these should persist across projects
defaultFontSize: current.defaultFontSize,
defaultRunScript: current.defaultRunScript,
screenReaderMode: current.screenReaderMode,
fontFamily: current.fontFamily,
scrollbackLines: current.scrollbackLines,
lineHeight: current.lineHeight,
maxSessions: current.maxSessions,
},
});
},
@@ -1909,6 +2040,62 @@ export const useAppStore = create<AppState & AppActions>()(
});
},
setTerminalDefaultFontSize: (fontSize) => {
const current = get().terminalState;
const clampedSize = Math.max(8, Math.min(32, fontSize));
set({
terminalState: { ...current, defaultFontSize: clampedSize },
});
},
setTerminalDefaultRunScript: (script) => {
const current = get().terminalState;
set({
terminalState: { ...current, defaultRunScript: script },
});
},
setTerminalScreenReaderMode: (enabled) => {
const current = get().terminalState;
set({
terminalState: { ...current, screenReaderMode: enabled },
});
},
setTerminalFontFamily: (fontFamily) => {
const current = get().terminalState;
set({
terminalState: { ...current, fontFamily },
});
},
setTerminalScrollbackLines: (lines) => {
const current = get().terminalState;
// Clamp to reasonable range: 1000 - 100000 lines
const clampedLines = Math.max(1000, Math.min(100000, lines));
set({
terminalState: { ...current, scrollbackLines: clampedLines },
});
},
setTerminalLineHeight: (lineHeight) => {
const current = get().terminalState;
// Clamp to reasonable range: 1.0 - 2.0
const clampedHeight = Math.max(1.0, Math.min(2.0, lineHeight));
set({
terminalState: { ...current, lineHeight: clampedHeight },
});
},
setTerminalMaxSessions: (maxSessions) => {
const current = get().terminalState;
// Clamp to reasonable range: 1 - 500
const clampedMax = Math.max(1, Math.min(500, maxSessions));
set({
terminalState: { ...current, maxSessions: clampedMax },
});
},
addTerminalTab: (name) => {
const current = get().terminalState;
const newTabId = `tab-${Date.now()}`;
@@ -1985,6 +2172,9 @@ export const useAppStore = create<AppState & AppActions>()(
...current,
activeTabId: tabId,
activeSessionId: newActiveSessionId,
// Clear maximized state when switching tabs - the maximized terminal
// belongs to the previous tab and shouldn't persist across tab switches
maximizedSessionId: null,
},
});
},
@@ -1997,6 +2187,25 @@ export const useAppStore = create<AppState & AppActions>()(
});
},
reorderTerminalTabs: (fromTabId, toTabId) => {
const current = get().terminalState;
const fromIndex = current.tabs.findIndex((t) => t.id === fromTabId);
const toIndex = current.tabs.findIndex((t) => t.id === toTabId);
if (fromIndex === -1 || toIndex === -1 || fromIndex === toIndex) {
return;
}
// Reorder tabs by moving fromIndex to toIndex
const newTabs = [...current.tabs];
const [movedTab] = newTabs.splice(fromIndex, 1);
newTabs.splice(toIndex, 0, movedTab);
set({
terminalState: { ...current, tabs: newTabs },
});
},
moveTerminalToTab: (sessionId, targetTabId) => {
const current = get().terminalState;
@@ -2043,7 +2252,13 @@ export const useAppStore = create<AppState & AppActions>()(
}
if (newPanels.length === 0) return null;
if (newPanels.length === 1) return newPanels[0];
return { ...node, panels: newPanels };
// Normalize sizes to sum to 100%
const totalSize = newPanels.reduce((sum, p) => sum + (p.size || 0), 0);
const normalizedPanels =
totalSize > 0
? newPanels.map((p) => ({ ...p, size: ((p.size || 0) / totalSize) * 100 }))
: newPanels.map((p) => ({ ...p, size: 100 / newPanels.length }));
return { ...node, panels: normalizedPanels };
};
const newSourceLayout = removeAndCollapse(sourceTab.layout);
@@ -2093,6 +2308,7 @@ export const useAppStore = create<AppState & AppActions>()(
} else if (targetTab.layout.type === 'terminal') {
newTargetLayout = {
type: 'split',
id: generateSplitId(),
direction: 'horizontal',
panels: [{ ...targetTab.layout, size: 50 }, terminalNode],
};
@@ -2143,6 +2359,7 @@ export const useAppStore = create<AppState & AppActions>()(
} else if (tab.layout.type === 'terminal') {
newLayout = {
type: 'split',
id: generateSplitId(),
direction,
panels: [{ ...tab.layout, size: 50 }, terminalNode],
};
@@ -2159,6 +2376,7 @@ export const useAppStore = create<AppState & AppActions>()(
} else {
newLayout = {
type: 'split',
id: generateSplitId(),
direction,
panels: [{ ...tab.layout, size: 50 }, terminalNode],
};
@@ -2177,6 +2395,146 @@ export const useAppStore = create<AppState & AppActions>()(
});
},
setTerminalTabLayout: (tabId, layout, activeSessionId) => {
const current = get().terminalState;
const tab = current.tabs.find((t) => t.id === tabId);
if (!tab) return;
const newTabs = current.tabs.map((t) => (t.id === tabId ? { ...t, layout } : t));
// Find first terminal in layout if no activeSessionId provided
const findFirst = (node: TerminalPanelContent): string | null => {
if (node.type === 'terminal') return node.sessionId;
for (const p of node.panels) {
const found = findFirst(p);
if (found) return found;
}
return null;
};
const newActiveSessionId = activeSessionId || findFirst(layout);
set({
terminalState: {
...current,
tabs: newTabs,
activeTabId: tabId,
activeSessionId: newActiveSessionId,
},
});
},
updateTerminalPanelSizes: (tabId, panelKeys, sizes) => {
const current = get().terminalState;
const tab = current.tabs.find((t) => t.id === tabId);
if (!tab || !tab.layout) return;
// Create a map of panel key to new size
const sizeMap = new Map<string, number>();
panelKeys.forEach((key, index) => {
sizeMap.set(key, sizes[index]);
});
// Helper to generate panel key (matches getPanelKey in terminal-view.tsx)
const getPanelKey = (panel: TerminalPanelContent): string => {
if (panel.type === 'terminal') return panel.sessionId;
const childKeys = panel.panels.map(getPanelKey).join('-');
return `split-${panel.direction}-${childKeys}`;
};
// Recursively update sizes in the layout
const updateSizes = (panel: TerminalPanelContent): TerminalPanelContent => {
const key = getPanelKey(panel);
const newSize = sizeMap.get(key);
if (panel.type === 'terminal') {
return newSize !== undefined ? { ...panel, size: newSize } : panel;
}
return {
...panel,
size: newSize !== undefined ? newSize : panel.size,
panels: panel.panels.map(updateSizes),
};
};
const updatedLayout = updateSizes(tab.layout);
const newTabs = current.tabs.map((t) =>
t.id === tabId ? { ...t, layout: updatedLayout } : t
);
set({
terminalState: { ...current, tabs: newTabs },
});
},
// Convert runtime layout to persisted format (preserves sessionIds for reconnection)
saveTerminalLayout: (projectPath) => {
const current = get().terminalState;
if (current.tabs.length === 0) {
// Nothing to save, clear any existing layout
const { [projectPath]: _, ...rest } = get().terminalLayoutByProject;
set({ terminalLayoutByProject: rest });
return;
}
// Convert TerminalPanelContent to PersistedTerminalPanel
// Now preserves sessionId so we can reconnect when switching back
const persistPanel = (panel: TerminalPanelContent): PersistedTerminalPanel => {
if (panel.type === 'terminal') {
return {
type: 'terminal',
size: panel.size,
fontSize: panel.fontSize,
sessionId: panel.sessionId, // Preserve for reconnection
};
}
return {
type: 'split',
id: panel.id, // Preserve stable ID
direction: panel.direction,
panels: panel.panels.map(persistPanel),
size: panel.size,
};
};
const persistedTabs: PersistedTerminalTab[] = current.tabs.map((tab) => ({
id: tab.id,
name: tab.name,
layout: tab.layout ? persistPanel(tab.layout) : null,
}));
const activeTabIndex = current.tabs.findIndex((t) => t.id === current.activeTabId);
const persisted: PersistedTerminalState = {
tabs: persistedTabs,
activeTabIndex: activeTabIndex >= 0 ? activeTabIndex : 0,
defaultFontSize: current.defaultFontSize,
defaultRunScript: current.defaultRunScript,
screenReaderMode: current.screenReaderMode,
fontFamily: current.fontFamily,
scrollbackLines: current.scrollbackLines,
lineHeight: current.lineHeight,
};
set({
terminalLayoutByProject: {
...get().terminalLayoutByProject,
[projectPath]: persisted,
},
});
},
getPersistedTerminalLayout: (projectPath) => {
return get().terminalLayoutByProject[projectPath] || null;
},
clearPersistedTerminalLayout: (projectPath) => {
const { [projectPath]: _, ...rest } = get().terminalLayoutByProject;
set({ terminalLayoutByProject: rest });
},
// Spec Creation actions
setSpecCreatingForProject: (projectPath) => {
set({ specCreatingForProject: projectPath });
@@ -2208,6 +2566,43 @@ export const useAppStore = create<AppState & AppActions>()(
{
name: 'automaker-storage',
version: 2, // Increment when making breaking changes to persisted state
// Custom merge function to properly restore terminal settings on every load
// The default shallow merge doesn't work because we persist terminalSettings
// separately from terminalState (to avoid persisting session data like tabs)
merge: (persistedState, currentState) => {
const persisted = persistedState as Partial<AppState> & {
terminalSettings?: PersistedTerminalSettings;
};
const current = currentState as AppState & AppActions;
// Start with default shallow merge
const merged = { ...current, ...persisted } as AppState & AppActions;
// Restore terminal settings into terminalState
// terminalSettings is persisted separately from terminalState to avoid
// persisting session data (tabs, activeSessionId, etc.)
if (persisted.terminalSettings) {
merged.terminalState = {
// Start with current (initial) terminalState for session fields
...current.terminalState,
// Override with persisted settings
defaultFontSize:
persisted.terminalSettings.defaultFontSize ?? current.terminalState.defaultFontSize,
defaultRunScript:
persisted.terminalSettings.defaultRunScript ?? current.terminalState.defaultRunScript,
screenReaderMode:
persisted.terminalSettings.screenReaderMode ?? current.terminalState.screenReaderMode,
fontFamily: persisted.terminalSettings.fontFamily ?? current.terminalState.fontFamily,
scrollbackLines:
persisted.terminalSettings.scrollbackLines ?? current.terminalState.scrollbackLines,
lineHeight: persisted.terminalSettings.lineHeight ?? current.terminalState.lineHeight,
maxSessions:
persisted.terminalSettings.maxSessions ?? current.terminalState.maxSessions,
};
}
return merged;
},
migrate: (persistedState: unknown, version: number) => {
const state = persistedState as Partial<AppState>;
@@ -2234,243 +2629,87 @@ export const useAppStore = create<AppState & AppActions>()(
}
}
// Rehydrate terminal settings from persisted state
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const persistedSettings = (state as any).terminalSettings as
| PersistedTerminalSettings
| undefined;
if (persistedSettings) {
state.terminalState = {
...state.terminalState,
// Preserve session state (tabs, activeTabId, etc.) but restore settings
isUnlocked: state.terminalState?.isUnlocked ?? false,
authToken: state.terminalState?.authToken ?? null,
tabs: state.terminalState?.tabs ?? [],
activeTabId: state.terminalState?.activeTabId ?? null,
activeSessionId: state.terminalState?.activeSessionId ?? null,
maximizedSessionId: state.terminalState?.maximizedSessionId ?? null,
// Restore persisted settings
defaultFontSize: persistedSettings.defaultFontSize ?? 14,
defaultRunScript: persistedSettings.defaultRunScript ?? '',
screenReaderMode: persistedSettings.screenReaderMode ?? false,
fontFamily: persistedSettings.fontFamily ?? "Menlo, Monaco, 'Courier New', monospace",
scrollbackLines: persistedSettings.scrollbackLines ?? 5000,
lineHeight: persistedSettings.lineHeight ?? 1.0,
maxSessions: persistedSettings.maxSessions ?? 100,
};
}
return state as AppState;
},
partialize: (state) => ({
// Project management
projects: state.projects,
currentProject: state.currentProject,
trashedProjects: state.trashedProjects,
projectHistory: state.projectHistory,
projectHistoryIndex: state.projectHistoryIndex,
// Features - cached locally for faster hydration (authoritative source is server)
features: state.features,
// UI state
currentView: state.currentView,
theme: state.theme,
sidebarOpen: state.sidebarOpen,
chatHistoryOpen: state.chatHistoryOpen,
kanbanCardDetailLevel: state.kanbanCardDetailLevel,
// Settings
apiKeys: state.apiKeys,
maxConcurrency: state.maxConcurrency,
// Note: autoModeByProject is intentionally NOT persisted
// Auto-mode should always default to OFF on app refresh
defaultSkipTests: state.defaultSkipTests,
enableDependencyBlocking: state.enableDependencyBlocking,
useWorktrees: state.useWorktrees,
currentWorktreeByProject: state.currentWorktreeByProject,
worktreesByProject: state.worktreesByProject,
showProfilesOnly: state.showProfilesOnly,
keyboardShortcuts: state.keyboardShortcuts,
muteDoneSound: state.muteDoneSound,
enhancementModel: state.enhancementModel,
// Profiles and sessions
aiProfiles: state.aiProfiles,
chatSessions: state.chatSessions,
lastSelectedSessionByProject: state.lastSelectedSessionByProject,
// Board background settings
boardBackgroundByProject: state.boardBackgroundByProject,
defaultPlanningMode: state.defaultPlanningMode,
defaultRequirePlanApproval: state.defaultRequirePlanApproval,
defaultAIProfileId: state.defaultAIProfileId,
// Claude usage tracking
claudeUsage: state.claudeUsage,
claudeUsageLastUpdated: state.claudeUsageLastUpdated,
claudeRefreshInterval: state.claudeRefreshInterval,
}),
partialize: (state) =>
({
// Project management
projects: state.projects,
currentProject: state.currentProject,
trashedProjects: state.trashedProjects,
projectHistory: state.projectHistory,
projectHistoryIndex: state.projectHistoryIndex,
// Features - cached locally for faster hydration (authoritative source is server)
features: state.features,
// UI state
currentView: state.currentView,
theme: state.theme,
sidebarOpen: state.sidebarOpen,
chatHistoryOpen: state.chatHistoryOpen,
kanbanCardDetailLevel: state.kanbanCardDetailLevel,
// Settings
apiKeys: state.apiKeys,
maxConcurrency: state.maxConcurrency,
// Note: autoModeByProject is intentionally NOT persisted
// Auto-mode should always default to OFF on app refresh
defaultSkipTests: state.defaultSkipTests,
enableDependencyBlocking: state.enableDependencyBlocking,
useWorktrees: state.useWorktrees,
currentWorktreeByProject: state.currentWorktreeByProject,
worktreesByProject: state.worktreesByProject,
showProfilesOnly: state.showProfilesOnly,
keyboardShortcuts: state.keyboardShortcuts,
muteDoneSound: state.muteDoneSound,
enhancementModel: state.enhancementModel,
// Profiles and sessions
aiProfiles: state.aiProfiles,
chatSessions: state.chatSessions,
lastSelectedSessionByProject: state.lastSelectedSessionByProject,
// Board background settings
boardBackgroundByProject: state.boardBackgroundByProject,
// Terminal layout persistence (per-project)
terminalLayoutByProject: state.terminalLayoutByProject,
// Terminal settings persistence (global)
terminalSettings: {
defaultFontSize: state.terminalState.defaultFontSize,
defaultRunScript: state.terminalState.defaultRunScript,
screenReaderMode: state.terminalState.screenReaderMode,
fontFamily: state.terminalState.fontFamily,
scrollbackLines: state.terminalState.scrollbackLines,
lineHeight: state.terminalState.lineHeight,
maxSessions: state.terminalState.maxSessions,
} as PersistedTerminalSettings,
defaultPlanningMode: state.defaultPlanningMode,
defaultRequirePlanApproval: state.defaultRequirePlanApproval,
defaultAIProfileId: state.defaultAIProfileId,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}) as any,
}
)
);
// ============================================================================
// Settings Sync to Server (file-based storage)
// ============================================================================
// Debounced sync function to avoid excessive server calls
let syncTimeoutId: NodeJS.Timeout | null = null;
const SYNC_DEBOUNCE_MS = 2000; // Wait 2 seconds after last change before syncing
/**
* Schedule a sync of current settings to the server
* This is debounced to avoid excessive API calls
*/
function scheduleSyncToServer() {
// Only sync in Electron mode
if (typeof window === 'undefined') return;
// Clear any pending sync
if (syncTimeoutId) {
clearTimeout(syncTimeoutId);
}
// Schedule new sync
syncTimeoutId = setTimeout(async () => {
try {
// Dynamic import to avoid circular dependencies
const { syncSettingsToServer } = await import('@/hooks/use-settings-migration');
await syncSettingsToServer();
} catch (error) {
console.error('[AppStore] Failed to sync settings to server:', error);
}
}, SYNC_DEBOUNCE_MS);
}
// Subscribe to store changes and sync to server
// Only sync when important settings change (not every state change)
let previousState: Partial<AppState> | null = null;
let previousProjectSettings: Record<
string,
{
theme?: string;
boardBackground?: (typeof initialState.boardBackgroundByProject)[string];
currentWorktree?: (typeof initialState.currentWorktreeByProject)[string];
worktrees?: (typeof initialState.worktreesByProject)[string];
}
> = {};
// Track pending project syncs (debounced per project)
const projectSyncTimeouts: Record<string, NodeJS.Timeout> = {};
const PROJECT_SYNC_DEBOUNCE_MS = 2000;
/**
* Schedule sync of project settings to server
*/
function scheduleProjectSettingsSync(projectPath: string, updates: Record<string, unknown>) {
// Only sync in Electron mode
if (typeof window === 'undefined') return;
// Clear any pending sync for this project
if (projectSyncTimeouts[projectPath]) {
clearTimeout(projectSyncTimeouts[projectPath]);
}
// Schedule new sync
projectSyncTimeouts[projectPath] = setTimeout(async () => {
try {
const { syncProjectSettingsToServer } = await import('@/hooks/use-settings-migration');
await syncProjectSettingsToServer(projectPath, updates);
} catch (error) {
console.error(`[AppStore] Failed to sync project settings for ${projectPath}:`, error);
}
delete projectSyncTimeouts[projectPath];
}, PROJECT_SYNC_DEBOUNCE_MS);
}
useAppStore.subscribe((state) => {
// Skip if this is the initial load
if (!previousState) {
previousState = {
theme: state.theme,
projects: state.projects,
trashedProjects: state.trashedProjects,
keyboardShortcuts: state.keyboardShortcuts,
aiProfiles: state.aiProfiles,
maxConcurrency: state.maxConcurrency,
defaultSkipTests: state.defaultSkipTests,
enableDependencyBlocking: state.enableDependencyBlocking,
useWorktrees: state.useWorktrees,
showProfilesOnly: state.showProfilesOnly,
muteDoneSound: state.muteDoneSound,
enhancementModel: state.enhancementModel,
defaultPlanningMode: state.defaultPlanningMode,
defaultRequirePlanApproval: state.defaultRequirePlanApproval,
defaultAIProfileId: state.defaultAIProfileId,
};
// Initialize project settings tracking
for (const project of state.projects) {
previousProjectSettings[project.path] = {
theme: project.theme,
boardBackground: state.boardBackgroundByProject[project.path],
currentWorktree: state.currentWorktreeByProject[project.path],
worktrees: state.worktreesByProject[project.path],
};
}
return;
}
// Check if any important global settings changed
const importantSettingsChanged =
state.theme !== previousState.theme ||
state.projects !== previousState.projects ||
state.trashedProjects !== previousState.trashedProjects ||
state.keyboardShortcuts !== previousState.keyboardShortcuts ||
state.aiProfiles !== previousState.aiProfiles ||
state.maxConcurrency !== previousState.maxConcurrency ||
state.defaultSkipTests !== previousState.defaultSkipTests ||
state.enableDependencyBlocking !== previousState.enableDependencyBlocking ||
state.useWorktrees !== previousState.useWorktrees ||
state.showProfilesOnly !== previousState.showProfilesOnly ||
state.muteDoneSound !== previousState.muteDoneSound ||
state.enhancementModel !== previousState.enhancementModel ||
state.defaultPlanningMode !== previousState.defaultPlanningMode ||
state.defaultRequirePlanApproval !== previousState.defaultRequirePlanApproval ||
state.defaultAIProfileId !== previousState.defaultAIProfileId;
if (importantSettingsChanged) {
// Update previous state
previousState = {
theme: state.theme,
projects: state.projects,
trashedProjects: state.trashedProjects,
keyboardShortcuts: state.keyboardShortcuts,
aiProfiles: state.aiProfiles,
maxConcurrency: state.maxConcurrency,
defaultSkipTests: state.defaultSkipTests,
enableDependencyBlocking: state.enableDependencyBlocking,
useWorktrees: state.useWorktrees,
showProfilesOnly: state.showProfilesOnly,
muteDoneSound: state.muteDoneSound,
enhancementModel: state.enhancementModel,
defaultPlanningMode: state.defaultPlanningMode,
defaultRequirePlanApproval: state.defaultRequirePlanApproval,
defaultAIProfileId: state.defaultAIProfileId,
};
// Schedule sync to server
scheduleSyncToServer();
}
// Check for per-project settings changes
for (const project of state.projects) {
const projectPath = project.path;
const prev = previousProjectSettings[projectPath] || {};
const updates: Record<string, unknown> = {};
// Check if project theme changed
if (project.theme !== prev.theme) {
updates.theme = project.theme;
}
// Check if board background changed
const currentBg = state.boardBackgroundByProject[projectPath];
if (currentBg !== prev.boardBackground) {
updates.boardBackground = currentBg;
}
// Check if current worktree changed
const currentWt = state.currentWorktreeByProject[projectPath];
if (currentWt !== prev.currentWorktree) {
updates.currentWorktree = currentWt;
}
// Check if worktrees list changed
const worktrees = state.worktreesByProject[projectPath];
if (worktrees !== prev.worktrees) {
updates.worktrees = worktrees;
}
// If any project settings changed, sync them
if (Object.keys(updates).length > 0) {
scheduleProjectSettingsSync(projectPath, updates);
// Update tracking
previousProjectSettings[projectPath] = {
theme: project.theme,
boardBackground: currentBg,
currentWorktree: currentWt,
worktrees: worktrees,
};
}
}
});

View File

@@ -1,5 +1,5 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
// CLI Installation Status
export interface CliStatus {
@@ -22,13 +22,13 @@ export interface GhCliStatus {
// Claude Auth Method - all possible authentication sources
export type ClaudeAuthMethod =
| "oauth_token_env"
| "oauth_token" // Stored OAuth token from claude login
| "api_key_env" // ANTHROPIC_API_KEY environment variable
| "api_key" // Manually stored API key
| "credentials_file" // Generic credentials file detection
| "cli_authenticated" // Claude CLI is installed and has active sessions/activity
| "none";
| 'oauth_token_env'
| 'oauth_token' // Stored OAuth token from claude login
| 'api_key_env' // ANTHROPIC_API_KEY environment variable
| 'api_key' // Manually stored API key
| 'credentials_file' // Generic credentials file detection
| 'cli_authenticated' // Claude CLI is installed and has active sessions/activity
| 'none';
// Claude Auth Status
export interface ClaudeAuthStatus {
@@ -52,12 +52,12 @@ export interface InstallProgress {
}
export type SetupStep =
| "welcome"
| "theme"
| "claude_detect"
| "claude_auth"
| "github"
| "complete";
| 'welcome'
| 'theme'
| 'claude_detect'
| 'claude_auth'
| 'github'
| 'complete';
export interface SetupState {
// Setup wizard state
@@ -100,18 +100,18 @@ export interface SetupActions {
const initialInstallProgress: InstallProgress = {
isInstalling: false,
currentStep: "",
currentStep: '',
progress: 0,
output: [],
};
// Check if setup should be skipped (for E2E testing)
const shouldSkipSetup = import.meta.env.VITE_SKIP_SETUP === "true";
const shouldSkipSetup = import.meta.env.VITE_SKIP_SETUP === 'true';
const initialState: SetupState = {
isFirstRun: !shouldSkipSetup,
setupComplete: shouldSkipSetup,
currentStep: shouldSkipSetup ? "complete" : "welcome",
currentStep: shouldSkipSetup ? 'complete' : 'welcome',
claudeCliStatus: null,
claudeAuthStatus: null,
@@ -133,11 +133,10 @@ export const useSetupStore = create<SetupState & SetupActions>()(
setSetupComplete: (complete) =>
set({
setupComplete: complete,
currentStep: complete ? "complete" : "welcome",
currentStep: complete ? 'complete' : 'welcome',
}),
completeSetup: () =>
set({ setupComplete: true, currentStep: "complete" }),
completeSetup: () => set({ setupComplete: true, currentStep: 'complete' }),
resetSetup: () =>
set({
@@ -172,7 +171,7 @@ export const useSetupStore = create<SetupState & SetupActions>()(
setSkipClaudeSetup: (skip) => set({ skipClaudeSetup: skip }),
}),
{
name: "automaker-setup",
name: 'automaker-setup',
partialize: (state) => ({
isFirstRun: state.isFirstRun,
setupComplete: state.setupComplete,