Merge remote-tracking branch 'origin/v0.10.0rc' into stefandevo/main

This commit is contained in:
Kacper
2026-01-11 17:34:19 +01:00
156 changed files with 8389 additions and 5916 deletions

View File

@@ -1,6 +1,7 @@
import { create } from 'zustand';
// Note: persist middleware removed - settings now sync via API (use-settings-sync.ts)
import type { Project, TrashedProject } from '@/lib/electron';
import { getElectronAPI } from '@/lib/electron';
import { createLogger } from '@automaker/utils/logger';
import { setItem, getItem } from '@/lib/storage';
import type {
@@ -11,7 +12,6 @@ import type {
PlanningMode,
ThinkingLevel,
ModelProvider,
AIProfile,
CursorModelId,
CodexModelId,
OpencodeModelId,
@@ -40,7 +40,6 @@ export type {
PlanningMode,
ThinkingLevel,
ModelProvider,
AIProfile,
FeatureTextFilePath,
FeatureImagePath,
};
@@ -54,7 +53,6 @@ export type ViewMode =
| 'settings'
| 'interview'
| 'context'
| 'profiles'
| 'running-agents'
| 'terminal'
| 'wiki'
@@ -116,7 +114,11 @@ function saveThemeToStorage(theme: ThemeMode): void {
setItem(THEME_STORAGE_KEY, theme);
}
export type KanbanCardDetailLevel = 'minimal' | 'standard' | 'detailed';
function persistEffectiveThemeForProject(project: Project | null, fallbackTheme: ThemeMode): void {
const projectTheme = project?.theme as ThemeMode | undefined;
const themeToStore = projectTheme ?? fallbackTheme;
saveThemeToStorage(themeToStore);
}
export type BoardViewMode = 'kanban' | 'graph';
@@ -214,11 +216,12 @@ export function formatShortcut(shortcut: string | undefined | null, forDisplay =
export interface KeyboardShortcuts {
// Navigation shortcuts
board: string;
graph: string;
agent: string;
spec: string;
context: string;
memory: string;
settings: string;
profiles: string;
terminal: string;
ideation: string;
githubIssues: string;
@@ -236,7 +239,6 @@ export interface KeyboardShortcuts {
projectPicker: string;
cyclePrevProject: string;
cycleNextProject: string;
addProfile: string;
// Terminal shortcuts
splitTerminalRight: string;
@@ -249,11 +251,12 @@ export interface KeyboardShortcuts {
export const DEFAULT_KEYBOARD_SHORTCUTS: KeyboardShortcuts = {
// Navigation
board: 'K',
graph: 'H',
agent: 'A',
spec: 'D',
context: 'C',
memory: 'Y',
settings: 'S',
profiles: 'M',
terminal: 'T',
ideation: 'I',
githubIssues: 'G',
@@ -263,7 +266,7 @@ export const DEFAULT_KEYBOARD_SHORTCUTS: KeyboardShortcuts = {
toggleSidebar: '`',
// Actions
// Note: Some shortcuts share the same key (e.g., "N" for addFeature, newSession, addProfile)
// Note: Some shortcuts share the same key (e.g., "N" for addFeature, newSession)
// This is intentional as they are context-specific and only active in their respective views
addFeature: 'N', // Only active in board view
addContextFile: 'N', // Only active in context view
@@ -273,7 +276,6 @@ export const DEFAULT_KEYBOARD_SHORTCUTS: KeyboardShortcuts = {
projectPicker: 'P', // Global shortcut
cyclePrevProject: 'Q', // Global shortcut
cycleNextProject: 'E', // Global shortcut
addProfile: 'N', // Only active in profiles view
// Terminal shortcuts (only active in terminal view)
// Using Alt modifier to avoid conflicts with both terminal signals AND browser shortcuts
@@ -514,7 +516,6 @@ export interface AppState {
maxConcurrency: number; // Maximum number of concurrent agent tasks
// Kanban Card Display Settings
kanbanCardDetailLevel: KanbanCardDetailLevel; // Level of detail shown on kanban cards
boardViewMode: BoardViewMode; // Whether to show kanban or dependency graph view
// Feature Default Settings
@@ -539,12 +540,6 @@ export interface AppState {
}>
>;
// AI Profiles
aiProfiles: AIProfile[];
// Profile Display Settings
showProfilesOnly: boolean; // When true, hide model tweaking options and show only profile selection
// Keyboard Shortcuts
keyboardShortcuts: KeyboardShortcuts; // User-defined keyboard shortcuts
@@ -635,7 +630,6 @@ export interface AppState {
defaultPlanningMode: PlanningMode;
defaultRequirePlanApproval: boolean;
defaultAIProfileId: string | null;
// Plan Approval State
// When a plan requires user approval, this holds the pending approval details
@@ -655,9 +649,27 @@ export interface AppState {
codexUsage: CodexUsage | null;
codexUsageLastUpdated: number | null;
// Codex Models (dynamically fetched)
codexModels: Array<{
id: string;
label: string;
description: string;
hasThinking: boolean;
supportsVision: boolean;
tier: 'premium' | 'standard' | 'basic';
isDefault: boolean;
}>;
codexModelsLoading: boolean;
codexModelsError: string | null;
codexModelsLastFetched: number | null;
// Pipeline Configuration (per-project, keyed by project path)
pipelineConfigByProject: Record<string, PipelineConfig>;
// Worktree Panel Visibility (per-project, keyed by project path)
// Whether the worktree panel row is visible (default: true)
worktreePanelVisibleByProject: Record<string, boolean>;
// UI State (previously in localStorage, now synced via API)
/** Whether worktree panel is collapsed in board view */
worktreePanelCollapsed: boolean;
@@ -707,12 +719,6 @@ export type CodexPlanType =
| 'edu'
| 'unknown';
export interface CodexCreditsSnapshot {
balance?: string;
unlimited?: boolean;
hasCredits?: boolean;
}
export interface CodexRateLimitWindow {
limit: number;
used: number;
@@ -726,7 +732,6 @@ export interface CodexUsage {
rateLimits: {
primary?: CodexRateLimitWindow;
secondary?: CodexRateLimitWindow;
credits?: CodexCreditsSnapshot;
planType?: CodexPlanType;
} | null;
lastUpdated: string;
@@ -825,6 +830,7 @@ export interface AppActions {
cyclePrevProject: () => void; // Cycle back through project history (Q)
cycleNextProject: () => void; // Cycle forward through project history (E)
clearProjectHistory: () => void; // Clear history, keeping only current project
toggleProjectFavorite: (projectId: string) => void; // Toggle project favorite status
// View actions
setCurrentView: (view: ViewMode) => void;
@@ -878,7 +884,6 @@ export interface AppActions {
setMaxConcurrency: (max: number) => void;
// Kanban Card Settings actions
setKanbanCardDetailLevel: (level: KanbanCardDetailLevel) => void;
setBoardViewMode: (mode: BoardViewMode) => void;
// Feature Default Settings actions
@@ -910,9 +915,6 @@ export interface AppActions {
isPrimaryWorktreeBranch: (projectPath: string, branchName: string) => boolean;
getPrimaryWorktreeBranch: (projectPath: string) => string | null;
// Profile Display Settings actions
setShowProfilesOnly: (enabled: boolean) => void;
// Keyboard Shortcuts actions
setKeyboardShortcut: (key: keyof KeyboardShortcuts, value: string) => void;
setKeyboardShortcuts: (shortcuts: Partial<KeyboardShortcuts>) => void;
@@ -967,13 +969,6 @@ export interface AppActions {
// Prompt Customization actions
setPromptCustomization: (customization: PromptCustomization) => Promise<void>;
// AI Profile actions
addAIProfile: (profile: Omit<AIProfile, 'id'>) => void;
updateAIProfile: (id: string, updates: Partial<AIProfile>) => void;
removeAIProfile: (id: string) => void;
reorderAIProfiles: (oldIndex: number, newIndex: number) => void;
resetAIProfiles: () => void;
// MCP Server actions
addMCPServer: (server: Omit<MCPServerConfig, 'id'>) => void;
updateMCPServer: (id: string, updates: Partial<MCPServerConfig>) => void;
@@ -1058,7 +1053,6 @@ export interface AppActions {
setDefaultPlanningMode: (mode: PlanningMode) => void;
setDefaultRequirePlanApproval: (require: boolean) => void;
setDefaultAIProfileId: (profileId: string | null) => void;
// Plan Approval actions
setPendingPlanApproval: (
@@ -1085,6 +1079,10 @@ export interface AppActions {
deletePipelineStep: (projectPath: string, stepId: string) => void;
reorderPipelineSteps: (projectPath: string, stepIds: string[]) => void;
// Worktree Panel Visibility actions (per-project)
setWorktreePanelVisible: (projectPath: string, visible: boolean) => void;
getWorktreePanelVisible: (projectPath: string) => boolean;
// UI State actions (previously in localStorage, now synced via API)
setWorktreePanelCollapsed: (collapsed: boolean) => void;
setLastProjectDir: (dir: string) => void;
@@ -1099,56 +1097,24 @@ export interface AppActions {
// Codex Usage Tracking actions
setCodexUsage: (usage: CodexUsage | null) => void;
// Codex Models actions
fetchCodexModels: (forceRefresh?: boolean) => Promise<void>;
setCodexModels: (
models: Array<{
id: string;
label: string;
description: string;
hasThinking: boolean;
supportsVision: boolean;
tier: 'premium' | 'standard' | 'basic';
isDefault: boolean;
}>
) => void;
// Reset
reset: () => void;
}
// Default built-in AI profiles
const DEFAULT_AI_PROFILES: AIProfile[] = [
// Claude profiles
{
id: 'profile-heavy-task',
name: 'Heavy Task',
description:
'Claude Opus with Ultrathink for complex architecture, migrations, or deep debugging.',
model: 'opus',
thinkingLevel: 'ultrathink',
provider: 'claude',
isBuiltIn: true,
icon: 'Brain',
},
{
id: 'profile-balanced',
name: 'Balanced',
description: 'Claude Sonnet with medium thinking for typical development tasks.',
model: 'sonnet',
thinkingLevel: 'medium',
provider: 'claude',
isBuiltIn: true,
icon: 'Scale',
},
{
id: 'profile-quick-edit',
name: 'Quick Edit',
description: 'Claude Haiku for fast, simple edits and minor fixes.',
model: 'haiku',
thinkingLevel: 'none',
provider: 'claude',
isBuiltIn: true,
icon: 'Zap',
},
// Cursor profiles
{
id: 'profile-cursor-refactoring',
name: 'Cursor Refactoring',
description: 'Cursor Composer 1 for refactoring tasks.',
provider: 'cursor',
cursorModel: 'composer-1',
isBuiltIn: true,
icon: 'Sparkles',
},
];
const initialState: AppState = {
projects: [],
currentProject: null,
@@ -1173,7 +1139,6 @@ const initialState: AppState = {
autoModeByProject: {},
autoModeActivityLog: [],
maxConcurrency: 3, // Default to 3 concurrent agents
kanbanCardDetailLevel: 'standard', // Default to standard detail level
boardViewMode: 'kanban', // Default to kanban view
defaultSkipTests: true, // Default to manual verification (tests disabled)
enableDependencyBlocking: true, // Default to enabled (show dependency blocking UI)
@@ -1181,7 +1146,6 @@ const initialState: AppState = {
useWorktrees: true, // Default to enabled (git worktree isolation)
currentWorktreeByProject: {},
worktreesByProject: {},
showProfilesOnly: false, // Default to showing all options (not profiles only)
keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS, // Default keyboard shortcuts
muteDoneSound: false, // Default to sound enabled (not muted)
enhancementModel: 'sonnet', // Default to sonnet for feature enhancement
@@ -1208,7 +1172,6 @@ const initialState: AppState = {
enableSubagents: true, // Subagents enabled by default
subagentsSources: ['user', 'project'] as Array<'user' | 'project'>, // Load from both sources by default
promptCustomization: {}, // Empty by default - all prompts use built-in defaults
aiProfiles: DEFAULT_AI_PROFILES,
projectAnalysis: null,
isAnalyzing: false,
boardBackgroundByProject: {},
@@ -1233,14 +1196,18 @@ const initialState: AppState = {
specCreatingForProject: null,
defaultPlanningMode: 'skip' as PlanningMode,
defaultRequirePlanApproval: false,
defaultAIProfileId: null,
pendingPlanApproval: null,
claudeRefreshInterval: 60,
claudeUsage: null,
claudeUsageLastUpdated: null,
codexUsage: null,
codexUsageLastUpdated: null,
codexModels: [],
codexModelsLoading: false,
codexModelsError: null,
codexModelsLastFetched: null,
pipelineConfigByProject: {},
worktreePanelVisibleByProject: {},
// UI State (previously in localStorage, now synced via API)
worktreePanelCollapsed: false,
lastProjectDir: '',
@@ -1287,13 +1254,16 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
};
const isCurrent = get().currentProject?.id === projectId;
const nextCurrentProject = isCurrent ? null : get().currentProject;
set({
projects: remainingProjects,
trashedProjects: [trashedProject, ...existingTrash],
currentProject: isCurrent ? null : get().currentProject,
currentProject: nextCurrentProject,
currentView: isCurrent ? 'welcome' : get().currentView,
});
persistEffectiveThemeForProject(nextCurrentProject, get().theme);
},
restoreTrashedProject: (projectId) => {
@@ -1312,6 +1282,7 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
currentProject: samePathProject,
currentView: 'board',
});
persistEffectiveThemeForProject(samePathProject, get().theme);
return;
}
@@ -1329,6 +1300,7 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
currentProject: restoredProject,
currentView: 'board',
});
persistEffectiveThemeForProject(restoredProject, get().theme);
},
deleteTrashedProject: (projectId) => {
@@ -1348,6 +1320,7 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
setCurrentProject: (project) => {
set({ currentProject: project });
persistEffectiveThemeForProject(project, get().theme);
if (project) {
set({ currentView: 'board' });
// Add to project history (MRU order)
@@ -1431,6 +1404,7 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
projectHistoryIndex: newIndex,
currentView: 'board',
});
persistEffectiveThemeForProject(targetProject, get().theme);
}
},
@@ -1464,6 +1438,7 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
projectHistoryIndex: newIndex,
currentView: 'board',
});
persistEffectiveThemeForProject(targetProject, get().theme);
}
},
@@ -1484,6 +1459,23 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
}
},
toggleProjectFavorite: (projectId) => {
const { projects, currentProject } = get();
const updatedProjects = projects.map((p) =>
p.id === projectId ? { ...p, isFavorite: !p.isFavorite } : p
);
set({ projects: updatedProjects });
// Also update currentProject if it matches
if (currentProject?.id === projectId) {
set({
currentProject: {
...currentProject,
isFavorite: !currentProject.isFavorite,
},
});
}
},
// View actions
setCurrentView: (view) => set({ currentView: view }),
toggleSidebar: () => set({ sidebarOpen: !get().sidebarOpen }),
@@ -1506,12 +1498,14 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
// Also update currentProject if it's the same project
const currentProject = get().currentProject;
if (currentProject?.id === projectId) {
const updatedTheme = theme === null ? undefined : theme;
set({
currentProject: {
...currentProject,
theme: theme === null ? undefined : theme,
theme: updatedTheme,
},
});
persistEffectiveThemeForProject({ ...currentProject, theme: updatedTheme }, get().theme);
}
},
@@ -1759,7 +1753,6 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
setMaxConcurrency: (max) => set({ maxConcurrency: max }),
// Kanban Card Settings actions
setKanbanCardDetailLevel: (level) => set({ kanbanCardDetailLevel: level }),
setBoardViewMode: (mode) => set({ boardViewMode: mode }),
// Feature Default Settings actions
@@ -1815,9 +1808,6 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
return primary?.branch ?? null;
},
// Profile Display Settings actions
setShowProfilesOnly: (enabled) => set({ showProfilesOnly: enabled }),
// Keyboard Shortcuts actions
setKeyboardShortcut: (key, value) => {
set({
@@ -1977,46 +1967,6 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
await syncSettingsToServer();
},
// AI Profile actions
addAIProfile: (profile) => {
const id = `profile-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
set({ aiProfiles: [...get().aiProfiles, { ...profile, id }] });
},
updateAIProfile: (id, updates) => {
set({
aiProfiles: get().aiProfiles.map((p) => (p.id === id ? { ...p, ...updates } : p)),
});
},
removeAIProfile: (id) => {
// Only allow removing non-built-in profiles
const profile = get().aiProfiles.find((p) => p.id === id);
if (profile && !profile.isBuiltIn) {
// Clear default if this profile was selected
if (get().defaultAIProfileId === id) {
set({ defaultAIProfileId: null });
}
set({ aiProfiles: get().aiProfiles.filter((p) => p.id !== id) });
}
},
reorderAIProfiles: (oldIndex, newIndex) => {
const profiles = [...get().aiProfiles];
const [movedProfile] = profiles.splice(oldIndex, 1);
profiles.splice(newIndex, 0, movedProfile);
set({ aiProfiles: profiles });
},
resetAIProfiles: () => {
// Merge: keep user-created profiles, but refresh all built-in profiles to latest defaults
const defaultProfileIds = new Set(DEFAULT_AI_PROFILES.map((p) => p.id));
const userProfiles = get().aiProfiles.filter(
(p) => !p.isBuiltIn && !defaultProfileIds.has(p.id)
);
set({ aiProfiles: [...DEFAULT_AI_PROFILES, ...userProfiles] });
},
// MCP Server actions
addMCPServer: (server) => {
const id = `mcp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
@@ -3005,7 +2955,6 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
setDefaultPlanningMode: (mode) => set({ defaultPlanningMode: mode }),
setDefaultRequirePlanApproval: (require) => set({ defaultRequirePlanApproval: require }),
setDefaultAIProfileId: (profileId) => set({ defaultAIProfileId: profileId }),
// Plan Approval actions
setPendingPlanApproval: (approval) => set({ pendingPlanApproval: approval }),
@@ -3026,6 +2975,53 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
codexUsageLastUpdated: usage ? Date.now() : null,
}),
// Codex Models actions
fetchCodexModels: async (forceRefresh = false) => {
const { codexModelsLastFetched, codexModelsLoading } = get();
// Skip if already loading
if (codexModelsLoading) return;
// Skip if recently fetched (< 5 minutes ago) and not forcing refresh
if (!forceRefresh && codexModelsLastFetched && Date.now() - codexModelsLastFetched < 300000) {
return;
}
set({ codexModelsLoading: true, codexModelsError: null });
try {
const api = getElectronAPI();
if (!api.codex) {
throw new Error('Codex API not available');
}
const result = await api.codex.getModels(forceRefresh);
if (!result.success) {
throw new Error(result.error || 'Failed to fetch Codex models');
}
set({
codexModels: result.models || [],
codexModelsLastFetched: Date.now(),
codexModelsLoading: false,
codexModelsError: null,
});
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
set({
codexModelsError: errorMessage,
codexModelsLoading: false,
});
}
},
setCodexModels: (models) =>
set({
codexModels: models,
codexModelsLastFetched: Date.now(),
}),
// Pipeline actions
setPipelineConfig: (projectPath, config) => {
set({
@@ -3125,6 +3121,21 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
});
},
// Worktree Panel Visibility actions (per-project)
setWorktreePanelVisible: (projectPath, visible) => {
set({
worktreePanelVisibleByProject: {
...get().worktreePanelVisibleByProject,
[projectPath]: visible,
},
});
},
getWorktreePanelVisible: (projectPath) => {
// Default to true (visible) if not set
return get().worktreePanelVisibleByProject[projectPath] ?? true;
},
// UI State actions (previously in localStorage, now synced via API)
setWorktreePanelCollapsed: (collapsed) => set({ worktreePanelCollapsed: collapsed }),
setLastProjectDir: (dir) => set({ lastProjectDir: dir }),