-
-
Branch:
diff --git a/apps/ui/src/components/views/dashboard-view.tsx b/apps/ui/src/components/views/dashboard-view.tsx
new file mode 100644
index 00000000..b49755d0
--- /dev/null
+++ b/apps/ui/src/components/views/dashboard-view.tsx
@@ -0,0 +1,885 @@
+import { useState, useCallback } from 'react';
+import { createLogger } from '@automaker/utils/logger';
+import { useNavigate } from '@tanstack/react-router';
+import { useAppStore, type ThemeMode } from '@/store/app-store';
+import { useOSDetection } from '@/hooks/use-os-detection';
+import { getElectronAPI, isElectron } from '@/lib/electron';
+import { initializeProject } from '@/lib/project-init';
+import { getHttpApiClient } from '@/lib/http-api-client';
+import { isMac } from '@/lib/utils';
+import { toast } from 'sonner';
+import { Button } from '@/components/ui/button';
+import { NewProjectModal } from '@/components/dialogs/new-project-modal';
+import { WorkspacePickerModal } from '@/components/dialogs/workspace-picker-modal';
+import type { StarterTemplate } from '@/lib/templates';
+import {
+ FolderOpen,
+ Plus,
+ Folder,
+ Star,
+ Clock,
+ Loader2,
+ ChevronDown,
+ MessageSquare,
+ Settings,
+ MoreVertical,
+ Trash2,
+} from 'lucide-react';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog';
+
+const logger = createLogger('DashboardView');
+
+function getOSAbbreviation(os: string): string {
+ switch (os) {
+ case 'mac':
+ return 'M';
+ case 'windows':
+ return 'W';
+ case 'linux':
+ return 'L';
+ default:
+ return '?';
+ }
+}
+
+export function DashboardView() {
+ const navigate = useNavigate();
+ const { os } = useOSDetection();
+ const appVersion = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '0.0.0';
+ const appMode = import.meta.env.VITE_APP_MODE || '?';
+ const versionSuffix = `${getOSAbbreviation(os)}${appMode}`;
+
+ const {
+ projects,
+ trashedProjects,
+ currentProject,
+ upsertAndSetCurrentProject,
+ addProject,
+ setCurrentProject,
+ toggleProjectFavorite,
+ moveProjectToTrash,
+ theme: globalTheme,
+ } = useAppStore();
+
+ const [showNewProjectModal, setShowNewProjectModal] = useState(false);
+ const [showWorkspacePicker, setShowWorkspacePicker] = useState(false);
+ const [isCreating, setIsCreating] = useState(false);
+ const [isOpening, setIsOpening] = useState(false);
+ const [projectToRemove, setProjectToRemove] = useState<{ id: string; name: string } | null>(null);
+
+ // Sort projects: favorites first, then by last opened
+ const sortedProjects = [...projects].sort((a, b) => {
+ // Favorites first
+ if (a.isFavorite && !b.isFavorite) return -1;
+ if (!a.isFavorite && b.isFavorite) return 1;
+ // Then by last opened
+ const dateA = a.lastOpened ? new Date(a.lastOpened).getTime() : 0;
+ const dateB = b.lastOpened ? new Date(b.lastOpened).getTime() : 0;
+ return dateB - dateA;
+ });
+
+ const favoriteProjects = sortedProjects.filter((p) => p.isFavorite);
+ const recentProjects = sortedProjects.filter((p) => !p.isFavorite);
+
+ /**
+ * Initialize project and navigate to board
+ */
+ const initializeAndOpenProject = useCallback(
+ async (path: string, name: string) => {
+ setIsOpening(true);
+ try {
+ const initResult = await initializeProject(path);
+
+ if (!initResult.success) {
+ toast.error('Failed to initialize project', {
+ description: initResult.error || 'Unknown error occurred',
+ });
+ return;
+ }
+
+ const trashedProject = trashedProjects.find((p) => p.path === path);
+ const effectiveTheme =
+ (trashedProject?.theme as ThemeMode | undefined) ||
+ (currentProject?.theme as ThemeMode | undefined) ||
+ globalTheme;
+ upsertAndSetCurrentProject(path, name, effectiveTheme);
+
+ toast.success('Project opened', {
+ description: `Opened ${name}`,
+ });
+
+ navigate({ to: '/board' });
+ } catch (error) {
+ logger.error('[Dashboard] Failed to open project:', error);
+ toast.error('Failed to open project', {
+ description: error instanceof Error ? error.message : 'Unknown error',
+ });
+ } finally {
+ setIsOpening(false);
+ }
+ },
+ [trashedProjects, currentProject, globalTheme, upsertAndSetCurrentProject, navigate]
+ );
+
+ const handleOpenProject = useCallback(async () => {
+ try {
+ const httpClient = getHttpApiClient();
+ const configResult = await httpClient.workspace.getConfig();
+
+ if (configResult.success && configResult.configured) {
+ setShowWorkspacePicker(true);
+ } else {
+ const api = getElectronAPI();
+ const result = await api.openDirectory();
+
+ if (!result.canceled && result.filePaths[0]) {
+ const path = result.filePaths[0];
+ const name = path.split(/[/\\]/).filter(Boolean).pop() || 'Untitled Project';
+ await initializeAndOpenProject(path, name);
+ }
+ }
+ } catch (error) {
+ logger.error('[Dashboard] Failed to check workspace config:', error);
+ const api = getElectronAPI();
+ const result = await api.openDirectory();
+
+ if (!result.canceled && result.filePaths[0]) {
+ const path = result.filePaths[0];
+ const name = path.split(/[/\\]/).filter(Boolean).pop() || 'Untitled Project';
+ await initializeAndOpenProject(path, name);
+ }
+ }
+ }, [initializeAndOpenProject]);
+
+ const handleWorkspaceSelect = useCallback(
+ async (path: string, name: string) => {
+ setShowWorkspacePicker(false);
+ await initializeAndOpenProject(path, name);
+ },
+ [initializeAndOpenProject]
+ );
+
+ const handleProjectClick = useCallback(
+ async (project: { id: string; name: string; path: string }) => {
+ await initializeAndOpenProject(project.path, project.name);
+ },
+ [initializeAndOpenProject]
+ );
+
+ const handleToggleFavorite = useCallback(
+ (e: React.MouseEvent, projectId: string) => {
+ e.stopPropagation();
+ toggleProjectFavorite(projectId);
+ },
+ [toggleProjectFavorite]
+ );
+
+ const handleRemoveProject = useCallback(
+ (e: React.MouseEvent, project: { id: string; name: string }) => {
+ e.stopPropagation();
+ setProjectToRemove(project);
+ },
+ []
+ );
+
+ const handleConfirmRemove = useCallback(() => {
+ if (projectToRemove) {
+ moveProjectToTrash(projectToRemove.id);
+ toast.success('Project removed', {
+ description: `${projectToRemove.name} has been removed from your projects list`,
+ });
+ setProjectToRemove(null);
+ }
+ }, [projectToRemove, moveProjectToTrash]);
+
+ const handleNewProject = () => {
+ setShowNewProjectModal(true);
+ };
+
+ const handleInteractiveMode = () => {
+ navigate({ to: '/interview' });
+ };
+
+ const handleCreateBlankProject = async (projectName: string, parentDir: string) => {
+ setIsCreating(true);
+ try {
+ const api = getElectronAPI();
+ const projectPath = `${parentDir}/${projectName}`;
+
+ const parentExists = await api.exists(parentDir);
+ if (!parentExists) {
+ toast.error('Parent directory does not exist', {
+ description: `Cannot create project in non-existent directory: ${parentDir}`,
+ });
+ return;
+ }
+
+ const parentStat = await api.stat(parentDir);
+ if (parentStat && !parentStat.stats?.isDirectory) {
+ toast.error('Parent path is not a directory', {
+ description: `${parentDir} is not a directory`,
+ });
+ return;
+ }
+
+ const mkdirResult = await api.mkdir(projectPath);
+ if (!mkdirResult.success) {
+ toast.error('Failed to create project directory', {
+ description: mkdirResult.error || 'Unknown error occurred',
+ });
+ return;
+ }
+
+ const initResult = await initializeProject(projectPath);
+ if (!initResult.success) {
+ toast.error('Failed to initialize project', {
+ description: initResult.error || 'Unknown error occurred',
+ });
+ return;
+ }
+
+ await api.writeFile(
+ `${projectPath}/.automaker/app_spec.txt`,
+ `
+ ${projectName}
+
+
+ Describe your project here. This file will be analyzed by an AI agent
+ to understand your project structure and tech stack.
+
+
+
+
+
+
+
+
+
+
+
+
+
+`
+ );
+
+ const project = {
+ id: `project-${Date.now()}`,
+ name: projectName,
+ path: projectPath,
+ lastOpened: new Date().toISOString(),
+ };
+
+ addProject(project);
+ setCurrentProject(project);
+ setShowNewProjectModal(false);
+
+ toast.success('Project created', {
+ description: `Created ${projectName}`,
+ });
+
+ navigate({ to: '/board' });
+ } catch (error) {
+ logger.error('Failed to create project:', error);
+ toast.error('Failed to create project', {
+ description: error instanceof Error ? error.message : 'Unknown error',
+ });
+ } finally {
+ setIsCreating(false);
+ }
+ };
+
+ const handleCreateFromTemplate = async (
+ template: StarterTemplate,
+ projectName: string,
+ parentDir: string
+ ) => {
+ setIsCreating(true);
+ try {
+ const httpClient = getHttpApiClient();
+ const api = getElectronAPI();
+
+ const cloneResult = await httpClient.templates.clone(
+ template.repoUrl,
+ projectName,
+ parentDir
+ );
+ if (!cloneResult.success || !cloneResult.projectPath) {
+ toast.error('Failed to clone template', {
+ description: cloneResult.error || 'Unknown error occurred',
+ });
+ return;
+ }
+
+ const projectPath = cloneResult.projectPath;
+ const initResult = await initializeProject(projectPath);
+ if (!initResult.success) {
+ toast.error('Failed to initialize project', {
+ description: initResult.error || 'Unknown error occurred',
+ });
+ return;
+ }
+
+ await api.writeFile(
+ `${projectPath}/.automaker/app_spec.txt`,
+ `
+ ${projectName}
+
+
+ This project was created from the "${template.name}" starter template.
+ ${template.description}
+
+
+
+ ${template.techStack.map((tech) => `${tech}`).join('\n ')}
+
+
+
+ ${template.features.map((feature) => `${feature}`).join('\n ')}
+
+
+
+
+
+`
+ );
+
+ const project = {
+ id: `project-${Date.now()}`,
+ name: projectName,
+ path: projectPath,
+ lastOpened: new Date().toISOString(),
+ };
+
+ addProject(project);
+ setCurrentProject(project);
+ setShowNewProjectModal(false);
+
+ toast.success('Project created from template', {
+ description: `Created ${projectName} from ${template.name}`,
+ });
+
+ navigate({ to: '/board' });
+ } catch (error) {
+ logger.error('Failed to create project from template:', error);
+ toast.error('Failed to create project', {
+ description: error instanceof Error ? error.message : 'Unknown error',
+ });
+ } finally {
+ setIsCreating(false);
+ }
+ };
+
+ const handleCreateFromCustomUrl = async (
+ repoUrl: string,
+ projectName: string,
+ parentDir: string
+ ) => {
+ setIsCreating(true);
+ try {
+ const httpClient = getHttpApiClient();
+ const api = getElectronAPI();
+
+ const cloneResult = await httpClient.templates.clone(repoUrl, projectName, parentDir);
+ if (!cloneResult.success || !cloneResult.projectPath) {
+ toast.error('Failed to clone repository', {
+ description: cloneResult.error || 'Unknown error occurred',
+ });
+ return;
+ }
+
+ const projectPath = cloneResult.projectPath;
+ const initResult = await initializeProject(projectPath);
+ if (!initResult.success) {
+ toast.error('Failed to initialize project', {
+ description: initResult.error || 'Unknown error occurred',
+ });
+ return;
+ }
+
+ await api.writeFile(
+ `${projectPath}/.automaker/app_spec.txt`,
+ `
+ ${projectName}
+
+
+ This project was cloned from ${repoUrl}.
+ The AI agent will analyze the project structure.
+
+
+
+
+
+
+
+
+
+
+
+
+
+`
+ );
+
+ const project = {
+ id: `project-${Date.now()}`,
+ name: projectName,
+ path: projectPath,
+ lastOpened: new Date().toISOString(),
+ };
+
+ addProject(project);
+ setCurrentProject(project);
+ setShowNewProjectModal(false);
+
+ toast.success('Project created from repository', {
+ description: `Created ${projectName}`,
+ });
+
+ navigate({ to: '/board' });
+ } catch (error) {
+ logger.error('Failed to create project from custom URL:', error);
+ toast.error('Failed to create project', {
+ description: error instanceof Error ? error.message : 'Unknown error',
+ });
+ } finally {
+ setIsCreating(false);
+ }
+ };
+
+ const hasProjects = projects.length > 0;
+
+ return (
+
+ {/* Header with logo */}
+
+ {/* Electron titlebar drag region */}
+ {isElectron() && (
+
+ )}
+
+
navigate({ to: '/dashboard' })}
+ >
+
+
+
+ automaker.
+
+
+ v{appVersion} {versionSuffix}
+
+
+
+
+
+
+
+ {/* Main content */}
+
+
+ {/* No projects - show getting started */}
+ {!hasProjects && (
+
+
+
Welcome to Automaker
+
+ Your autonomous AI development studio. Get started by creating a new project or
+ opening an existing one.
+
+
+
+
+ {/* New Project Card */}
+
+
+
+
+
+
+
+ New Project
+
+
+ Create a new project from scratch with AI-powered development
+
+
+
+
+
+
+
+
+
+
+ Quick Setup
+
+
+
+ Interactive Mode
+
+
+
+
+
+
+ {/* Open Project Card */}
+
+
+
+
+
+
+
+
+
+ Open Project
+
+
+ Open an existing project folder to continue working
+
+
+
+
+
+
+
+
+ )}
+
+ {/* Has projects - show project list */}
+ {hasProjects && (
+
+ {/* Quick actions header */}
+
+
Your Projects
+
+
+
+
+
+
+
+
+
+ Quick Setup
+
+
+
+ Interactive Mode
+
+
+
+
+
+
+ {/* Favorites section */}
+ {favoriteProjects.length > 0 && (
+
+
+
+ {favoriteProjects.map((project) => (
+
handleProjectClick(project)}
+ data-testid={`project-card-${project.id}`}
+ >
+
+
+
+
+
+
+
+
+ {project.name}
+
+
+ {project.path}
+
+ {project.lastOpened && (
+
+ {new Date(project.lastOpened).toLocaleDateString()}
+
+ )}
+
+
+
+
+
+
+
+
+ handleRemoveProject(e, project)}
+ className="text-destructive focus:text-destructive"
+ >
+
+ Remove from Automaker
+
+
+
+
+
+
+
+ ))}
+
+
+ )}
+
+ {/* Recent projects section */}
+ {recentProjects.length > 0 && (
+
+
+
+
+
+
Recent Projects
+
+
+ {recentProjects.map((project) => (
+
handleProjectClick(project)}
+ data-testid={`project-card-${project.id}`}
+ >
+
+
+
+
+
+
+
+
+ {project.name}
+
+
+ {project.path}
+
+ {project.lastOpened && (
+
+ {new Date(project.lastOpened).toLocaleDateString()}
+
+ )}
+
+
+
+
+
+
+
+
+ handleRemoveProject(e, project)}
+ className="text-destructive focus:text-destructive"
+ >
+
+ Remove from Automaker
+
+
+
+
+
+
+
+ ))}
+
+
+ )}
+
+ )}
+
+
+
+ {/* Modals */}
+
+
+
+
+ {/* Remove project confirmation dialog */}
+
+
+ {/* Loading overlay */}
+ {isOpening && (
+
+ )}
+
+ );
+}
diff --git a/apps/ui/src/components/views/setup-view.tsx b/apps/ui/src/components/views/setup-view.tsx
index f3e9d1dd..82a73291 100644
--- a/apps/ui/src/components/views/setup-view.tsx
+++ b/apps/ui/src/components/views/setup-view.tsx
@@ -86,8 +86,8 @@ export function SetupView() {
const handleFinish = () => {
logger.debug('[Setup Flow] handleFinish called - completing setup');
completeSetup();
- logger.debug('[Setup Flow] Setup completed, redirecting to welcome view');
- navigate({ to: '/' });
+ logger.debug('[Setup Flow] Setup completed, redirecting to dashboard');
+ navigate({ to: '/dashboard' });
};
return (
diff --git a/apps/ui/src/hooks/use-project-settings-loader.ts b/apps/ui/src/hooks/use-project-settings-loader.ts
index ee7a9d8c..62784f5f 100644
--- a/apps/ui/src/hooks/use-project-settings-loader.ts
+++ b/apps/ui/src/hooks/use-project-settings-loader.ts
@@ -17,6 +17,7 @@ export function useProjectSettingsLoader() {
const setCardBorderEnabled = useAppStore((state) => state.setCardBorderEnabled);
const setCardBorderOpacity = useAppStore((state) => state.setCardBorderOpacity);
const setHideScrollbar = useAppStore((state) => state.setHideScrollbar);
+ const setWorktreePanelVisible = useAppStore((state) => state.setWorktreePanelVisible);
const loadingRef = useRef
(null);
const currentProjectRef = useRef(null);
@@ -72,6 +73,11 @@ export function useProjectSettingsLoader() {
(setter as (path: string, val: typeof value) => void)(requestedProjectPath, value);
}
}
+
+ // Apply worktreePanelVisible if present
+ if (result.settings.worktreePanelVisible !== undefined) {
+ setWorktreePanelVisible(requestedProjectPath, result.settings.worktreePanelVisible);
+ }
}
} catch (error) {
console.error('Failed to load project settings:', error);
diff --git a/apps/ui/src/hooks/use-settings-migration.ts b/apps/ui/src/hooks/use-settings-migration.ts
index 7e97832c..3449abf5 100644
--- a/apps/ui/src/hooks/use-settings-migration.ts
+++ b/apps/ui/src/hooks/use-settings-migration.ts
@@ -504,6 +504,7 @@ export function hydrateStoreFromSettings(settings: GlobalSettings): void {
path: ref.path,
lastOpened: ref.lastOpened,
theme: ref.theme,
+ isFavorite: ref.isFavorite,
features: [], // Features are loaded separately when project is opened
}));
diff --git a/apps/ui/src/lib/electron.ts b/apps/ui/src/lib/electron.ts
index 9a746a6a..2b52a2ac 100644
--- a/apps/ui/src/lib/electron.ts
+++ b/apps/ui/src/lib/electron.ts
@@ -3003,6 +3003,7 @@ export interface Project {
path: string;
lastOpened?: string;
theme?: string; // Per-project theme override (uses ThemeMode from app-store)
+ isFavorite?: boolean; // Pin project to top of dashboard
}
export interface TrashedProject extends Project {
diff --git a/apps/ui/src/lib/http-api-client.ts b/apps/ui/src/lib/http-api-client.ts
index 3e1e602c..c46005bd 100644
--- a/apps/ui/src/lib/http-api-client.ts
+++ b/apps/ui/src/lib/http-api-client.ts
@@ -1951,6 +1951,7 @@ export class HttpApiClient implements ElectronAPI {
cardBorderOpacity: number;
hideScrollbar: boolean;
};
+ worktreePanelVisible?: boolean;
lastSelectedSessionId?: string;
};
error?: string;
diff --git a/apps/ui/src/routes/__root.tsx b/apps/ui/src/routes/__root.tsx
index 6a883071..6ca6535c 100644
--- a/apps/ui/src/routes/__root.tsx
+++ b/apps/ui/src/routes/__root.tsx
@@ -84,6 +84,7 @@ function RootLayoutContent() {
const isSetupRoute = location.pathname === '/setup';
const isLoginRoute = location.pathname === '/login';
const isLoggedOutRoute = location.pathname === '/logged-out';
+ const isDashboardRoute = location.pathname === '/dashboard';
// Sandbox environment check state
type SandboxStatus = 'pending' | 'containerized' | 'needs-confirmation' | 'denied' | 'confirmed';
@@ -389,9 +390,9 @@ function RootLayoutContent() {
return;
}
- // Setup complete but user is still on /setup -> go to app
+ // Setup complete but user is still on /setup -> go to dashboard
if (setupComplete && location.pathname === '/setup') {
- navigate({ to: '/' });
+ navigate({ to: '/dashboard' });
}
}, [authChecked, isAuthenticated, setupComplete, location.pathname, navigate]);
@@ -425,10 +426,16 @@ function RootLayoutContent() {
testConnection();
}, [setIpcConnected]);
- // Restore to board view if a project was previously open
+ // Redirect from welcome page based on project state
useEffect(() => {
- if (isMounted && currentProject && location.pathname === '/') {
- navigate({ to: '/board' });
+ if (isMounted && location.pathname === '/') {
+ if (currentProject) {
+ // Project is selected, go to board
+ navigate({ to: '/board' });
+ } else {
+ // No project selected, go to dashboard
+ navigate({ to: '/dashboard' });
+ }
}
}, [isMounted, currentProject, location.pathname, navigate]);
@@ -514,6 +521,23 @@ function RootLayoutContent() {
);
}
+ // Show dashboard page (full screen, no sidebar) - authenticated only
+ if (isDashboardRoute) {
+ return (
+ <>
+
+
+
+
+
+ >
+ );
+ }
+
return (
<>
diff --git a/apps/ui/src/routes/dashboard.tsx b/apps/ui/src/routes/dashboard.tsx
new file mode 100644
index 00000000..e204ecf0
--- /dev/null
+++ b/apps/ui/src/routes/dashboard.tsx
@@ -0,0 +1,6 @@
+import { createFileRoute } from '@tanstack/react-router';
+import { DashboardView } from '@/components/views/dashboard-view';
+
+export const Route = createFileRoute('/dashboard')({
+ component: DashboardView,
+});
diff --git a/apps/ui/src/store/app-store.ts b/apps/ui/src/store/app-store.ts
index 54bf18eb..91dea9bd 100644
--- a/apps/ui/src/store/app-store.ts
+++ b/apps/ui/src/store/app-store.ts
@@ -656,6 +656,10 @@ export interface AppState {
// Pipeline Configuration (per-project, keyed by project path)
pipelineConfigByProject: Record;
+ // Worktree Panel Visibility (per-project, keyed by project path)
+ // Whether the worktree panel row is visible (default: true)
+ worktreePanelVisibleByProject: Record;
+
// UI State (previously in localStorage, now synced via API)
/** Whether worktree panel is collapsed in board view */
worktreePanelCollapsed: boolean;
@@ -816,6 +820,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;
@@ -1062,6 +1067,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;
@@ -1186,6 +1195,7 @@ const initialState: AppState = {
codexModelsError: null,
codexModelsLastFetched: null,
pipelineConfigByProject: {},
+ worktreePanelVisibleByProject: {},
// UI State (previously in localStorage, now synced via API)
worktreePanelCollapsed: false,
lastProjectDir: '',
@@ -1429,6 +1439,23 @@ export const useAppStore = create()((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 }),
@@ -3070,6 +3097,21 @@ export const useAppStore = create()((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 }),
diff --git a/libs/types/src/settings.ts b/libs/types/src/settings.ts
index b1737068..3ce87a76 100644
--- a/libs/types/src/settings.ts
+++ b/libs/types/src/settings.ts
@@ -294,6 +294,8 @@ export interface ProjectRef {
lastOpened?: string;
/** Project-specific theme override (or undefined to use global) */
theme?: string;
+ /** Whether project is pinned to favorites on dashboard */
+ isFavorite?: boolean;
}
/**
@@ -595,6 +597,10 @@ export interface ProjectSettings {
/** Project-specific board background settings */
boardBackground?: BoardBackgroundSettings;
+ // UI Visibility
+ /** Whether the worktree panel row is visible (default: true) */
+ worktreePanelVisible?: boolean;
+
// Session Tracking
/** Last chat session selected in this project */
lastSelectedSessionId?: string;