Merge upstream/v0.10.0rc into feature/codex-cli

Sync with latest upstream changes:
- feat: enhance feature dialogs with planning mode tooltips
- refactor: remove kanbanCardDetailLevel from settings and UI components
- refactor: streamline feature addition in BoardView and KanbanBoard
- feat: implement dashboard view and enhance sidebar navigation
This commit is contained in:
DhanushSantosh
2026-01-11 00:59:36 +05:30
29 changed files with 1388 additions and 421 deletions

View File

@@ -600,8 +600,6 @@ export class SettingsService {
theme: (appState.theme as GlobalSettings['theme']) || 'dark',
sidebarOpen: appState.sidebarOpen !== undefined ? (appState.sidebarOpen as boolean) : true,
chatHistoryOpen: (appState.chatHistoryOpen as boolean) || false,
kanbanCardDetailLevel:
(appState.kanbanCardDetailLevel as GlobalSettings['kanbanCardDetailLevel']) || 'standard',
maxConcurrency: (appState.maxConcurrency as number) || 3,
defaultSkipTests:
appState.defaultSkipTests !== undefined ? (appState.defaultSkipTests as boolean) : true,

View File

@@ -7,7 +7,6 @@
export type {
ThemeMode,
KanbanCardDetailLevel,
ModelAlias,
PlanningMode,
ThinkingLevel,

View File

@@ -41,7 +41,6 @@ const E2E_SETTINGS = {
theme: 'dark',
sidebarOpen: true,
chatHistoryOpen: false,
kanbanCardDetailLevel: 'standard',
maxConcurrency: 3,
defaultSkipTests: true,
enableDependencyBlocking: true,

View File

@@ -17,7 +17,6 @@ import { CreateSpecDialog } from '@/components/views/spec-view/dialogs';
import {
CollapseToggleButton,
SidebarHeader,
ProjectActions,
SidebarNavigation,
ProjectSelectorWithOptions,
SidebarFooter,
@@ -276,17 +275,6 @@ export function Sidebar() {
<div className="flex-1 flex flex-col overflow-hidden">
<SidebarHeader sidebarOpen={sidebarOpen} navigate={navigate} />
{/* Project Actions - Moved above project selector */}
{sidebarOpen && (
<ProjectActions
setShowNewProjectModal={setShowNewProjectModal}
handleOpenFolder={handleOpenFolder}
setShowTrashDialog={setShowTrashDialog}
trashedProjects={trashedProjects}
shortcuts={{ openProject: shortcuts.openProject }}
/>
)}
<ProjectSelectorWithOptions
sidebarOpen={sidebarOpen}
isProjectPickerOpen={isProjectPickerOpen}

View File

@@ -32,7 +32,7 @@ export function AutomakerLogo({ sidebarOpen, navigate }: AutomakerLogoProps) {
'flex items-center gap-3 titlebar-no-drag cursor-pointer group',
!sidebarOpen && 'flex-col gap-1'
)}
onClick={() => navigate({ to: '/' })}
onClick={() => navigate({ to: '/dashboard' })}
data-testid="logo-button"
>
{/* Collapsed logo - shown when sidebar is closed OR on small screens when sidebar is open */}

View File

@@ -7,7 +7,24 @@ import {
useSensors,
rectIntersection,
pointerWithin,
type PointerEvent as DndPointerEvent,
} from '@dnd-kit/core';
// Custom pointer sensor that ignores drag events from within dialogs
class DialogAwarePointerSensor extends PointerSensor {
static activators = [
{
eventName: 'onPointerDown' as const,
handler: ({ nativeEvent: event }: { nativeEvent: DndPointerEvent }) => {
// Don't start drag if the event originated from inside a dialog
if ((event.target as Element)?.closest?.('[role="dialog"]')) {
return false;
}
return true;
},
},
];
}
import { useAppStore, Feature } from '@/store/app-store';
import { getElectronAPI } from '@/lib/electron';
import { getHttpApiClient } from '@/lib/http-api-client';
@@ -73,8 +90,6 @@ export function BoardView() {
maxConcurrency,
setMaxConcurrency,
defaultSkipTests,
kanbanCardDetailLevel,
setKanbanCardDetailLevel,
boardViewMode,
setBoardViewMode,
specCreatingForProject,
@@ -95,6 +110,8 @@ export function BoardView() {
} = useAppStore();
// Subscribe to pipelineConfigByProject to trigger re-renders when it changes
const pipelineConfigByProject = useAppStore((state) => state.pipelineConfigByProject);
// Subscribe to worktreePanelVisibleByProject to trigger re-renders when it changes
const worktreePanelVisibleByProject = useAppStore((state) => state.worktreePanelVisibleByProject);
const shortcuts = useKeyboardShortcutsConfig();
const {
features: hookFeatures,
@@ -246,7 +263,7 @@ export function BoardView() {
}, []);
const sensors = useSensors(
useSensor(PointerSensor, {
useSensor(DialogAwarePointerSensor, {
activationConstraint: {
distance: 8,
},
@@ -1139,6 +1156,7 @@ export function BoardView() {
{/* Header */}
<BoardHeader
projectName={currentProject.name}
projectPath={currentProject.path}
maxConcurrency={maxConcurrency}
runningAgentsCount={runningAutoTasks.length}
onConcurrencyChange={setMaxConcurrency}
@@ -1150,47 +1168,43 @@ export function BoardView() {
autoMode.stop();
}
}}
onAddFeature={() => setShowAddDialog(true)}
onOpenPlanDialog={() => setShowPlanDialog(true)}
addFeatureShortcut={{
key: shortcuts.addFeature,
action: () => setShowAddDialog(true),
description: 'Add new feature',
}}
isMounted={isMounted}
/>
{/* Worktree Panel */}
<WorktreePanel
refreshTrigger={worktreeRefreshKey}
projectPath={currentProject.path}
onCreateWorktree={() => setShowCreateWorktreeDialog(true)}
onDeleteWorktree={(worktree) => {
setSelectedWorktreeForAction(worktree);
setShowDeleteWorktreeDialog(true);
}}
onCommit={(worktree) => {
setSelectedWorktreeForAction(worktree);
setShowCommitWorktreeDialog(true);
}}
onCreatePR={(worktree) => {
setSelectedWorktreeForAction(worktree);
setShowCreatePRDialog(true);
}}
onCreateBranch={(worktree) => {
setSelectedWorktreeForAction(worktree);
setShowCreateBranchDialog(true);
}}
onAddressPRComments={handleAddressPRComments}
onResolveConflicts={handleResolveConflicts}
onRemovedWorktrees={handleRemovedWorktrees}
runningFeatureIds={runningAutoTasks}
branchCardCounts={branchCardCounts}
features={hookFeatures.map((f) => ({
id: f.id,
branchName: f.branchName,
}))}
/>
{/* Worktree Panel - conditionally rendered based on visibility setting */}
{(worktreePanelVisibleByProject[currentProject.path] ?? true) && (
<WorktreePanel
refreshTrigger={worktreeRefreshKey}
projectPath={currentProject.path}
onCreateWorktree={() => setShowCreateWorktreeDialog(true)}
onDeleteWorktree={(worktree) => {
setSelectedWorktreeForAction(worktree);
setShowDeleteWorktreeDialog(true);
}}
onCommit={(worktree) => {
setSelectedWorktreeForAction(worktree);
setShowCommitWorktreeDialog(true);
}}
onCreatePR={(worktree) => {
setSelectedWorktreeForAction(worktree);
setShowCreatePRDialog(true);
}}
onCreateBranch={(worktree) => {
setSelectedWorktreeForAction(worktree);
setShowCreateBranchDialog(true);
}}
onAddressPRComments={handleAddressPRComments}
onResolveConflicts={handleResolveConflicts}
onRemovedWorktrees={handleRemovedWorktrees}
runningFeatureIds={runningAutoTasks}
branchCardCounts={branchCardCounts}
features={hookFeatures.map((f) => ({
id: f.id,
branchName: f.branchName,
}))}
/>
)}
{/* Main Content Area */}
<div className="flex-1 flex flex-col overflow-hidden">
@@ -1210,8 +1224,6 @@ export function BoardView() {
onShowBoardBackground={() => setShowBoardBackgroundModal(true)}
onShowCompletedModal={() => setShowCompletedModal(true)}
completedCount={completedFeatures.length}
kanbanCardDetailLevel={kanbanCardDetailLevel}
onDetailLevelChange={setKanbanCardDetailLevel}
boardViewMode={boardViewMode}
onBoardViewModeChange={setBoardViewMode}
/>
@@ -1247,6 +1259,7 @@ export function BoardView() {
featuresWithContext={featuresWithContext}
runningAutoTasks={runningAutoTasks}
onArchiveAllVerified={() => setShowArchiveAllVerifiedDialog(true)}
onAddFeature={() => setShowAddDialog(true)}
pipelineConfig={
currentProject?.path ? pipelineConfigByProject[currentProject.path] || null : null
}

View File

@@ -1,6 +1,6 @@
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { ImageIcon, Archive, Minimize2, Square, Maximize2, Columns3, Network } from 'lucide-react';
import { ImageIcon, Archive, Columns3, Network } from 'lucide-react';
import { cn } from '@/lib/utils';
import { BoardViewMode } from '@/store/app-store';
@@ -9,8 +9,6 @@ interface BoardControlsProps {
onShowBoardBackground: () => void;
onShowCompletedModal: () => void;
completedCount: number;
kanbanCardDetailLevel: 'minimal' | 'standard' | 'detailed';
onDetailLevelChange: (level: 'minimal' | 'standard' | 'detailed') => void;
boardViewMode: BoardViewMode;
onBoardViewModeChange: (mode: BoardViewMode) => void;
}
@@ -20,8 +18,6 @@ export function BoardControls({
onShowBoardBackground,
onShowCompletedModal,
completedCount,
kanbanCardDetailLevel,
onDetailLevelChange,
boardViewMode,
onBoardViewModeChange,
}: BoardControlsProps) {
@@ -115,70 +111,6 @@ export function BoardControls({
<p>Completed Features ({completedCount})</p>
</TooltipContent>
</Tooltip>
{/* Kanban Card Detail Level Toggle */}
<div
className="flex items-center rounded-lg bg-secondary border border-border"
data-testid="kanban-detail-toggle"
>
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={() => onDetailLevelChange('minimal')}
className={cn(
'p-2 rounded-l-lg transition-colors',
kanbanCardDetailLevel === 'minimal'
? 'bg-brand-500/20 text-brand-500'
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
)}
data-testid="kanban-toggle-minimal"
>
<Minimize2 className="w-4 h-4" />
</button>
</TooltipTrigger>
<TooltipContent>
<p>Minimal - Title & category only</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={() => onDetailLevelChange('standard')}
className={cn(
'p-2 transition-colors',
kanbanCardDetailLevel === 'standard'
? 'bg-brand-500/20 text-brand-500'
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
)}
data-testid="kanban-toggle-standard"
>
<Square className="w-4 h-4" />
</button>
</TooltipTrigger>
<TooltipContent>
<p>Standard - Steps & progress</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={() => onDetailLevelChange('detailed')}
className={cn(
'p-2 rounded-r-lg transition-colors',
kanbanCardDetailLevel === 'detailed'
? 'bg-brand-500/20 text-brand-500'
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
)}
data-testid="kanban-toggle-detailed"
>
<Maximize2 className="w-4 h-4" />
</button>
</TooltipTrigger>
<TooltipContent>
<p>Detailed - Model, tools & tasks</p>
</TooltipContent>
</Tooltip>
</div>
</div>
</TooltipProvider>
);

View File

@@ -1,26 +1,25 @@
import { useState } from 'react';
import { HotkeyButton } from '@/components/ui/hotkey-button';
import { useState, useCallback } from 'react';
import { Button } from '@/components/ui/button';
import { Slider } from '@/components/ui/slider';
import { Switch } from '@/components/ui/switch';
import { Label } from '@/components/ui/label';
import { Plus, Bot, Wand2, Settings2 } from 'lucide-react';
import { KeyboardShortcut } from '@/hooks/use-keyboard-shortcuts';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Bot, Wand2, Settings2, GitBranch } from 'lucide-react';
import { UsagePopover } from '@/components/usage-popover';
import { useAppStore } from '@/store/app-store';
import { useSetupStore } from '@/store/setup-store';
import { AutoModeSettingsDialog } from './dialogs/auto-mode-settings-dialog';
import { getHttpApiClient } from '@/lib/http-api-client';
interface BoardHeaderProps {
projectName: string;
projectPath: string;
maxConcurrency: number;
runningAgentsCount: number;
onConcurrencyChange: (value: number) => void;
isAutoModeRunning: boolean;
onAutoModeToggle: (enabled: boolean) => void;
onAddFeature: () => void;
onOpenPlanDialog: () => void;
addFeatureShortcut: KeyboardShortcut;
isMounted: boolean;
}
@@ -30,14 +29,13 @@ const controlContainerClass =
export function BoardHeader({
projectName,
projectPath,
maxConcurrency,
runningAgentsCount,
onConcurrencyChange,
isAutoModeRunning,
onAutoModeToggle,
onAddFeature,
onOpenPlanDialog,
addFeatureShortcut,
isMounted,
}: BoardHeaderProps) {
const [showAutoModeSettings, setShowAutoModeSettings] = useState(false);
@@ -47,6 +45,29 @@ export function BoardHeader({
const setSkipVerificationInAutoMode = useAppStore((state) => state.setSkipVerificationInAutoMode);
const codexAuthStatus = useSetupStore((state) => state.codexAuthStatus);
// Worktree panel visibility (per-project)
const worktreePanelVisibleByProject = useAppStore((state) => state.worktreePanelVisibleByProject);
const setWorktreePanelVisible = useAppStore((state) => state.setWorktreePanelVisible);
const isWorktreePanelVisible = worktreePanelVisibleByProject[projectPath] ?? true;
const handleWorktreePanelToggle = useCallback(
async (visible: boolean) => {
// Update local store
setWorktreePanelVisible(projectPath, visible);
// Persist to server
try {
const httpClient = getHttpApiClient();
await httpClient.settings.updateProject(projectPath, {
worktreePanelVisible: visible,
});
} catch (error) {
console.error('Failed to persist worktree panel visibility:', error);
}
},
[projectPath, setWorktreePanelVisible]
);
// Claude usage tracking visibility logic
// Hide when using API key (only show for Claude Code CLI users)
// Also hide on Windows for now (CLI usage command not supported)
@@ -71,29 +92,65 @@ export function BoardHeader({
{/* Usage Popover - show if either provider is authenticated */}
{isMounted && (showClaudeUsage || showCodexUsage) && <UsagePopover />}
{/* Concurrency Slider - only show after mount to prevent hydration issues */}
{/* Worktrees Toggle - only show after mount to prevent hydration issues */}
{isMounted && (
<div className={controlContainerClass} data-testid="concurrency-slider-container">
<Bot className="w-4 h-4 text-muted-foreground" />
<span className="text-sm font-medium">Agents</span>
<Slider
value={[maxConcurrency]}
onValueChange={(value) => onConcurrencyChange(value[0])}
min={1}
max={10}
step={1}
className="w-20"
data-testid="concurrency-slider"
<div className={controlContainerClass} data-testid="worktrees-toggle-container">
<GitBranch className="w-4 h-4 text-muted-foreground" />
<Label htmlFor="worktrees-toggle" className="text-sm font-medium cursor-pointer">
Worktrees
</Label>
<Switch
id="worktrees-toggle"
checked={isWorktreePanelVisible}
onCheckedChange={handleWorktreePanelToggle}
data-testid="worktrees-toggle"
/>
<span
className="text-sm text-muted-foreground min-w-[5ch] text-center"
data-testid="concurrency-value"
>
{runningAgentsCount} / {maxConcurrency}
</span>
</div>
)}
{/* Concurrency Control - only show after mount to prevent hydration issues */}
{isMounted && (
<Popover>
<PopoverTrigger asChild>
<button
className={`${controlContainerClass} cursor-pointer hover:bg-accent/50 transition-colors`}
data-testid="concurrency-slider-container"
>
<Bot className="w-4 h-4 text-muted-foreground" />
<span className="text-sm font-medium">Agents</span>
<span className="text-sm text-muted-foreground" data-testid="concurrency-value">
{runningAgentsCount}/{maxConcurrency}
</span>
</button>
</PopoverTrigger>
<PopoverContent className="w-64" align="end">
<div className="space-y-4">
<div>
<h4 className="font-medium text-sm mb-1">Max Concurrent Agents</h4>
<p className="text-xs text-muted-foreground">
Controls how many AI agents can run simultaneously. Higher values process more
features in parallel but use more API resources.
</p>
</div>
<div className="flex items-center gap-3">
<Slider
value={[maxConcurrency]}
onValueChange={(value) => onConcurrencyChange(value[0])}
min={1}
max={10}
step={1}
className="flex-1"
data-testid="concurrency-slider"
/>
<span className="text-sm font-medium min-w-[2ch] text-right">
{maxConcurrency}
</span>
</div>
</div>
</PopoverContent>
</Popover>
)}
{/* Auto Mode Toggle - only show after mount to prevent hydration issues */}
{isMounted && (
<div className={controlContainerClass} data-testid="auto-mode-toggle-container">
@@ -134,17 +191,6 @@ export function BoardHeader({
<Wand2 className="w-4 h-4 mr-2" />
Plan
</Button>
<HotkeyButton
size="sm"
onClick={onAddFeature}
hotkey={addFeatureShortcut}
hotkeyActive={false}
data-testid="add-feature-button"
>
<Plus className="w-4 h-4 mr-2" />
Add Feature
</HotkeyButton>
</div>
</div>
);

View File

@@ -1,6 +1,6 @@
// @ts-nocheck
import { useEffect, useState } from 'react';
import { Feature, ThinkingLevel, useAppStore } from '@/store/app-store';
import { Feature, ThinkingLevel } from '@/store/app-store';
import type { ReasoningEffort } from '@automaker/types';
import { getProviderFromModel } from '@/lib/utils';
import {
@@ -68,12 +68,9 @@ export function AgentInfoPanel({
summary,
isCurrentAutoTask,
}: AgentInfoPanelProps) {
const { kanbanCardDetailLevel } = useAppStore();
const [agentInfo, setAgentInfo] = useState<AgentTaskInfo | null>(null);
const [isSummaryDialogOpen, setIsSummaryDialogOpen] = useState(false);
const showAgentInfo = kanbanCardDetailLevel === 'detailed';
useEffect(() => {
const loadContext = async () => {
if (contextContent) {
@@ -123,7 +120,7 @@ export function AgentInfoPanel({
}
}, [feature.id, feature.status, contextContent, isCurrentAutoTask]);
// Model/Preset Info for Backlog Cards
if (showAgentInfo && feature.status === 'backlog') {
if (feature.status === 'backlog') {
const provider = getProviderFromModel(feature.model);
const isCodex = provider === 'codex';
const isClaude = provider === 'claude';
@@ -160,7 +157,7 @@ export function AgentInfoPanel({
}
// Agent Info Panel for non-backlog cards
if (showAgentInfo && feature.status !== 'backlog' && agentInfo) {
if (feature.status !== 'backlog' && agentInfo) {
return (
<>
<div className="mb-3 space-y-2 overflow-hidden">
@@ -255,7 +252,11 @@ export function AgentInfoPanel({
<Expand className="w-3 h-3" />
</button>
</div>
<p className="text-[10px] text-muted-foreground/70 line-clamp-3 break-words hyphens-auto leading-relaxed overflow-hidden">
<p
className="text-[10px] text-muted-foreground/70 line-clamp-3 break-words hyphens-auto leading-relaxed overflow-hidden select-text cursor-text"
onPointerDown={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
>
{feature.summary || summary || agentInfo.summary}
</p>
</div>
@@ -292,58 +293,15 @@ export function AgentInfoPanel({
);
}
// Show just the todo list for non-backlog features when showAgentInfo is false
// This ensures users always see what the agent is working on
if (!showAgentInfo && feature.status !== 'backlog' && agentInfo && agentInfo.todos.length > 0) {
return (
<div className="mb-3 space-y-1 overflow-hidden">
<div className="flex items-center gap-1 text-[10px] text-muted-foreground/70">
<ListTodo className="w-3 h-3" />
<span>
{agentInfo.todos.filter((t) => t.status === 'completed').length}/
{agentInfo.todos.length} tasks
</span>
</div>
<div className="space-y-0.5 max-h-24 overflow-y-auto">
{agentInfo.todos.map((todo, idx) => (
<div key={idx} className="flex items-center gap-1.5 text-[10px]">
{todo.status === 'completed' ? (
<CheckCircle2 className="w-2.5 h-2.5 text-[var(--status-success)] shrink-0" />
) : todo.status === 'in_progress' ? (
<Loader2 className="w-2.5 h-2.5 text-[var(--status-warning)] animate-spin shrink-0" />
) : (
<Circle className="w-2.5 h-2.5 text-muted-foreground/50 shrink-0" />
)}
<span
className={cn(
'break-words hyphens-auto line-clamp-2 leading-relaxed',
todo.status === 'completed' && 'text-muted-foreground/60 line-through',
todo.status === 'in_progress' && 'text-[var(--status-warning)]',
todo.status === 'pending' && 'text-muted-foreground/80'
)}
>
{todo.content}
</span>
</div>
))}
</div>
</div>
);
}
// Always render SummaryDialog if showAgentInfo is true (even if no agentInfo yet)
// Always render SummaryDialog (even if no agentInfo yet)
// This ensures the dialog can be opened from the expand button
return (
<>
{showAgentInfo && (
<SummaryDialog
feature={feature}
agentInfo={agentInfo}
summary={summary}
isOpen={isSummaryDialogOpen}
onOpenChange={setIsSummaryDialogOpen}
/>
)}
</>
<SummaryDialog
feature={feature}
agentInfo={agentInfo}
summary={summary}
isOpen={isSummaryDialogOpen}
onOpenChange={setIsSummaryDialogOpen}
/>
);
}

View File

@@ -31,8 +31,10 @@ export function SummaryDialog({
return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent
className="max-w-4xl max-h-[80vh] overflow-hidden flex flex-col"
className="max-w-4xl max-h-[80vh] overflow-hidden flex flex-col select-text"
data-testid={`summary-dialog-${feature.id}`}
onPointerDown={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">

View File

@@ -10,6 +10,8 @@ interface KanbanColumnProps {
count: number;
children: ReactNode;
headerAction?: ReactNode;
/** Floating action button at the bottom of the column */
footerAction?: ReactNode;
opacity?: number;
showBorder?: boolean;
hideScrollbar?: boolean;
@@ -24,6 +26,7 @@ export const KanbanColumn = memo(function KanbanColumn({
count,
children,
headerAction,
footerAction,
opacity = 100,
showBorder = true,
hideScrollbar = false,
@@ -79,12 +82,21 @@ export const KanbanColumn = memo(function KanbanColumn({
hideScrollbar &&
'[&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]',
// Smooth scrolling
'scroll-smooth'
'scroll-smooth',
// Add padding at bottom if there's a footer action
footerAction && 'pb-14'
)}
>
{children}
</div>
{/* Floating Footer Action */}
{footerAction && (
<div className="absolute bottom-0 left-0 right-0 z-20 p-2 bg-gradient-to-t from-card/95 via-card/80 to-transparent pt-6">
{footerAction}
</div>
)}
{/* Drop zone indicator when dragging over */}
{isOver && (
<div className="absolute inset-0 rounded-xl bg-primary/5 pointer-events-none z-5 border-2 border-dashed border-primary/20" />

View File

@@ -53,6 +53,7 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import {
getAncestors,
formatAncestorContextForPrompt,
@@ -492,23 +493,44 @@ export function AddFeatureDialog({
/>
</div>
<div
className={cn(
'grid gap-3',
modelSupportsPlanningMode ? 'grid-cols-2' : 'grid-cols-1'
)}
>
{modelSupportsPlanningMode && (
<div className="space-y-1.5">
<Label className="text-xs text-muted-foreground">Planning</Label>
<div className="grid gap-3 grid-cols-2">
<div className="space-y-1.5">
<Label
className={cn(
'text-xs text-muted-foreground',
!modelSupportsPlanningMode && 'opacity-50'
)}
>
Planning
</Label>
{modelSupportsPlanningMode ? (
<PlanningModeSelect
mode={planningMode}
onModeChange={setPlanningMode}
testIdPrefix="add-feature-planning"
compact
/>
</div>
)}
) : (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div>
<PlanningModeSelect
mode="skip"
onModeChange={() => {}}
testIdPrefix="add-feature-planning"
compact
disabled
/>
</div>
</TooltipTrigger>
<TooltipContent>
<p>Planning modes are only available for Claude Provider</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
<div className="space-y-1.5">
<Label className="text-xs text-muted-foreground">Options</Label>
<div className="flex flex-col gap-2 pt-1">
@@ -526,28 +548,32 @@ export function AddFeatureDialog({
Run tests
</Label>
</div>
{modelSupportsPlanningMode && (
<div className="flex items-center gap-2">
<Checkbox
id="add-feature-require-approval"
checked={requirePlanApproval}
onCheckedChange={(checked) => setRequirePlanApproval(!!checked)}
disabled={planningMode === 'skip' || planningMode === 'lite'}
data-testid="add-feature-require-approval-checkbox"
/>
<Label
htmlFor="add-feature-require-approval"
className={cn(
'text-xs font-normal',
planningMode === 'skip' || planningMode === 'lite'
? 'cursor-not-allowed text-muted-foreground'
: 'cursor-pointer'
)}
>
Require approval
</Label>
</div>
)}
<div className="flex items-center gap-2">
<Checkbox
id="add-feature-require-approval"
checked={requirePlanApproval}
onCheckedChange={(checked) => setRequirePlanApproval(!!checked)}
disabled={
!modelSupportsPlanningMode ||
planningMode === 'skip' ||
planningMode === 'lite'
}
data-testid="add-feature-require-approval-checkbox"
/>
<Label
htmlFor="add-feature-require-approval"
className={cn(
'text-xs font-normal',
!modelSupportsPlanningMode ||
planningMode === 'skip' ||
planningMode === 'lite'
? 'cursor-not-allowed text-muted-foreground'
: 'cursor-pointer'
)}
>
Require approval
</Label>
</div>
</div>
</div>
</div>

View File

@@ -52,6 +52,7 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { DependencyTreeDialog } from './dependency-tree-dialog';
import { isClaudeModel, supportsReasoningEffort } from '@automaker/types';
@@ -516,23 +517,44 @@ export function EditFeatureDialog({
/>
</div>
<div
className={cn(
'grid gap-3',
modelSupportsPlanningMode ? 'grid-cols-2' : 'grid-cols-1'
)}
>
{modelSupportsPlanningMode && (
<div className="space-y-1.5">
<Label className="text-xs text-muted-foreground">Planning</Label>
<div className="grid gap-3 grid-cols-2">
<div className="space-y-1.5">
<Label
className={cn(
'text-xs text-muted-foreground',
!modelSupportsPlanningMode && 'opacity-50'
)}
>
Planning
</Label>
{modelSupportsPlanningMode ? (
<PlanningModeSelect
mode={planningMode}
onModeChange={setPlanningMode}
testIdPrefix="edit-feature-planning"
compact
/>
</div>
)}
) : (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div>
<PlanningModeSelect
mode="skip"
onModeChange={() => {}}
testIdPrefix="edit-feature-planning"
compact
disabled
/>
</div>
</TooltipTrigger>
<TooltipContent>
<p>Planning modes are only available for Claude Provider</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
<div className="space-y-1.5">
<Label className="text-xs text-muted-foreground">Options</Label>
<div className="flex flex-col gap-2 pt-1">
@@ -552,28 +574,32 @@ export function EditFeatureDialog({
Run tests
</Label>
</div>
{modelSupportsPlanningMode && (
<div className="flex items-center gap-2">
<Checkbox
id="edit-feature-require-approval"
checked={requirePlanApproval}
onCheckedChange={(checked) => setRequirePlanApproval(!!checked)}
disabled={planningMode === 'skip' || planningMode === 'lite'}
data-testid="edit-feature-require-approval-checkbox"
/>
<Label
htmlFor="edit-feature-require-approval"
className={cn(
'text-xs font-normal',
planningMode === 'skip' || planningMode === 'lite'
? 'cursor-not-allowed text-muted-foreground'
: 'cursor-pointer'
)}
>
Require approval
</Label>
</div>
)}
<div className="flex items-center gap-2">
<Checkbox
id="edit-feature-require-approval"
checked={requirePlanApproval}
onCheckedChange={(checked) => setRequirePlanApproval(!!checked)}
disabled={
!modelSupportsPlanningMode ||
planningMode === 'skip' ||
planningMode === 'lite'
}
data-testid="edit-feature-require-approval-checkbox"
/>
<Label
htmlFor="edit-feature-require-approval"
className={cn(
'text-xs font-normal',
!modelSupportsPlanningMode ||
planningMode === 'skip' ||
planningMode === 'lite'
? 'cursor-not-allowed text-muted-foreground'
: 'cursor-pointer'
)}
>
Require approval
</Label>
</div>
</div>
</div>
</div>

View File

@@ -15,8 +15,9 @@ import { modelSupportsThinking } from '@/lib/utils';
import { Feature, ModelAlias, ThinkingLevel, PlanningMode } from '@/store/app-store';
import { TestingTabContent, PrioritySelect, PlanningModeSelect } from '../shared';
import { PhaseModelSelector } from '@/components/views/settings-view/model-defaults/phase-model-selector';
import { isCursorModel, type PhaseModelEntry } from '@automaker/types';
import { isCursorModel, isClaudeModel, type PhaseModelEntry } from '@automaker/types';
import { cn } from '@/lib/utils';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
interface MassEditDialogProps {
open: boolean;
@@ -167,6 +168,7 @@ export function MassEditDialog({ open, onClose, selectedFeatures, onApply }: Mas
const hasAnyApply = Object.values(applyState).some(Boolean);
const isCurrentModelCursor = isCursorModel(model);
const modelAllowsThinking = !isCurrentModelCursor && modelSupportsThinking(model);
const modelSupportsPlanningMode = isClaudeModel(model);
return (
<Dialog open={open} onOpenChange={(open) => !open && onClose()}>
@@ -205,30 +207,64 @@ export function MassEditDialog({ open, onClose, selectedFeatures, onApply }: Mas
<div className="border-t border-border" />
{/* Planning Mode */}
<FieldWrapper
label="Planning Mode"
isMixed={mixedValues.planningMode || mixedValues.requirePlanApproval}
willApply={applyState.planningMode || applyState.requirePlanApproval}
onApplyChange={(apply) =>
setApplyState((prev) => ({
...prev,
planningMode: apply,
requirePlanApproval: apply,
}))
}
>
<PlanningModeSelect
mode={planningMode}
onModeChange={(newMode) => {
setPlanningMode(newMode);
// Auto-suggest approval based on mode, but user can override
setRequirePlanApproval(newMode === 'spec' || newMode === 'full');
}}
requireApproval={requirePlanApproval}
onRequireApprovalChange={setRequirePlanApproval}
testIdPrefix="mass-edit-planning"
/>
</FieldWrapper>
{modelSupportsPlanningMode ? (
<FieldWrapper
label="Planning Mode"
isMixed={mixedValues.planningMode || mixedValues.requirePlanApproval}
willApply={applyState.planningMode || applyState.requirePlanApproval}
onApplyChange={(apply) =>
setApplyState((prev) => ({
...prev,
planningMode: apply,
requirePlanApproval: apply,
}))
}
>
<PlanningModeSelect
mode={planningMode}
onModeChange={(newMode) => {
setPlanningMode(newMode);
// Auto-suggest approval based on mode, but user can override
setRequirePlanApproval(newMode === 'spec' || newMode === 'full');
}}
requireApproval={requirePlanApproval}
onRequireApprovalChange={setRequirePlanApproval}
testIdPrefix="mass-edit-planning"
/>
</FieldWrapper>
) : (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div
className={cn(
'p-3 rounded-lg border transition-colors border-border bg-muted/20 opacity-50 cursor-not-allowed'
)}
>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<Checkbox checked={false} disabled className="opacity-50" />
<Label className="text-sm font-medium text-muted-foreground">
Planning Mode
</Label>
</div>
</div>
<div className="opacity-50 pointer-events-none">
<PlanningModeSelect
mode="skip"
onModeChange={() => {}}
testIdPrefix="mass-edit-planning"
disabled
/>
</div>
</div>
</TooltipTrigger>
<TooltipContent>
<p>Planning modes are only available for Claude Provider</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
{/* Priority */}
<FieldWrapper

View File

@@ -3,8 +3,8 @@ import { DndContext, DragOverlay } from '@dnd-kit/core';
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
import { Button } from '@/components/ui/button';
import { KanbanColumn, KanbanCard } from './components';
import { Feature } from '@/store/app-store';
import { Archive, Settings2, CheckSquare, GripVertical } from 'lucide-react';
import { Feature, useAppStore, formatShortcut } from '@/store/app-store';
import { Archive, Settings2, CheckSquare, GripVertical, Plus } from 'lucide-react';
import { useResponsiveKanban } from '@/hooks/use-responsive-kanban';
import { getColumnsWithPipeline, type ColumnId } from './constants';
import type { PipelineConfig } from '@automaker/types';
@@ -43,6 +43,7 @@ interface KanbanBoardProps {
featuresWithContext: Set<string>;
runningAutoTasks: string[];
onArchiveAllVerified: () => void;
onAddFeature: () => void;
pipelineConfig: PipelineConfig | null;
onOpenPipelineSettings?: () => void;
// Selection mode props
@@ -78,6 +79,7 @@ export function KanbanBoard({
featuresWithContext,
runningAutoTasks,
onArchiveAllVerified,
onAddFeature,
pipelineConfig,
onOpenPipelineSettings,
isSelectionMode = false,
@@ -88,6 +90,10 @@ export function KanbanBoard({
// Generate columns including pipeline steps
const columns = useMemo(() => getColumnsWithPipeline(pipelineConfig), [pipelineConfig]);
// Get the keyboard shortcut for adding features
const { keyboardShortcuts } = useAppStore();
const addFeatureShortcut = keyboardShortcuts.addFeature || 'N';
// Use responsive column widths based on window size
// containerStyle handles centering and ensures columns fit without horizontal scroll in Electron
const { columnWidth, containerStyle } = useResponsiveKanban(columns.length);
@@ -127,26 +133,38 @@ export function KanbanBoard({
Complete All
</Button>
) : column.id === 'backlog' ? (
<Button
variant="ghost"
size="sm"
className={`h-6 px-2 text-xs ${isSelectionMode ? 'text-primary bg-primary/10' : 'text-muted-foreground hover:text-foreground'}`}
onClick={onToggleSelectionMode}
title={isSelectionMode ? 'Switch to Drag Mode' : 'Select Multiple'}
data-testid="selection-mode-button"
>
{isSelectionMode ? (
<>
<GripVertical className="w-3.5 h-3.5 mr-1" />
Drag
</>
) : (
<>
<CheckSquare className="w-3.5 h-3.5 mr-1" />
Select
</>
)}
</Button>
<div className="flex items-center gap-1">
<Button
variant="default"
size="sm"
className="h-6 w-6 p-0"
onClick={onAddFeature}
title="Add Feature"
data-testid="add-feature-button"
>
<Plus className="w-3.5 h-3.5" />
</Button>
<Button
variant="ghost"
size="sm"
className={`h-6 px-2 text-xs ${isSelectionMode ? 'text-primary bg-primary/10' : 'text-muted-foreground hover:text-foreground'}`}
onClick={onToggleSelectionMode}
title={isSelectionMode ? 'Switch to Drag Mode' : 'Select Multiple'}
data-testid="selection-mode-button"
>
{isSelectionMode ? (
<>
<GripVertical className="w-3.5 h-3.5 mr-1" />
Drag
</>
) : (
<>
<CheckSquare className="w-3.5 h-3.5 mr-1" />
Select
</>
)}
</Button>
</div>
) : column.id === 'in_progress' ? (
<Button
variant="ghost"
@@ -171,6 +189,23 @@ export function KanbanBoard({
</Button>
) : undefined
}
footerAction={
column.id === 'backlog' ? (
<Button
variant="default"
size="sm"
className="w-full h-9 text-sm"
onClick={onAddFeature}
data-testid="add-feature-floating-button"
>
<Plus className="w-4 h-4 mr-2" />
Add Feature
<span className="ml-auto pl-2 text-[10px] font-mono opacity-70 bg-black/20 px-1.5 py-0.5 rounded">
{formatShortcut(addFeatureShortcut, true)}
</span>
</Button>
) : undefined
}
>
<SortableContext
items={columnFeatures.map((f) => f.id)}

View File

@@ -1,8 +1,7 @@
import { useEffect, useRef } from 'react';
import { Button } from '@/components/ui/button';
import { GitBranch, Plus, RefreshCw, PanelLeftOpen, PanelLeftClose } from 'lucide-react';
import { GitBranch, Plus, RefreshCw } from 'lucide-react';
import { cn, pathsEqual } from '@/lib/utils';
import { useAppStore } from '@/store/app-store';
import type { WorktreePanelProps, WorktreeInfo } from './types';
import {
useWorktrees,
@@ -83,12 +82,6 @@ export function WorktreePanel({
features,
});
// Collapse state from store (synced via API)
const isCollapsed = useAppStore((s) => s.worktreePanelCollapsed);
const setWorktreePanelCollapsed = useAppStore((s) => s.setWorktreePanelCollapsed);
const toggleCollapsed = () => setWorktreePanelCollapsed(!isCollapsed);
// Periodic interval check (5 seconds) to detect branch changes on disk
// Reduced from 1s to 5s to minimize GPU/CPU usage from frequent re-renders
const intervalRef = useRef<NodeJS.Timeout | null>(null);
@@ -104,18 +97,6 @@ export function WorktreePanel({
};
}, [fetchWorktrees]);
// Get the currently selected worktree for collapsed view
const selectedWorktree = worktrees.find((w) => {
if (
currentWorktree === null ||
currentWorktree === undefined ||
currentWorktree.path === null
) {
return w.isMain;
}
return pathsEqual(w.path, currentWorktreePath);
});
const isWorktreeSelected = (worktree: WorktreeInfo) => {
return worktree.isMain
? currentWorktree === null || currentWorktree === undefined || currentWorktree.path === null
@@ -138,44 +119,8 @@ export function WorktreePanel({
const mainWorktree = worktrees.find((w) => w.isMain);
const nonMainWorktrees = worktrees.filter((w) => !w.isMain);
// Collapsed view - just show current branch and toggle
if (isCollapsed) {
return (
<div className="flex items-center gap-2 px-4 py-1.5 border-b border-border bg-glass/50 backdrop-blur-sm">
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 text-muted-foreground hover:text-foreground"
onClick={toggleCollapsed}
title="Expand worktree panel"
>
<PanelLeftOpen className="w-4 h-4" />
</Button>
<GitBranch className="w-4 h-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground">Branch:</span>
<span className="text-sm font-mono font-medium">{selectedWorktree?.branch ?? 'main'}</span>
{selectedWorktree?.hasChanges && (
<span className="inline-flex items-center justify-center h-4 min-w-[1rem] px-1 text-[10px] font-medium rounded border bg-amber-500/20 text-amber-600 dark:text-amber-400 border-amber-500/30">
{selectedWorktree.changedFilesCount ?? '!'}
</span>
)}
</div>
);
}
// Expanded view - full worktree panel
return (
<div className="flex items-center gap-2 px-4 py-2 border-b border-border bg-glass/50 backdrop-blur-sm">
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 text-muted-foreground hover:text-foreground"
onClick={toggleCollapsed}
title="Collapse worktree panel"
>
<PanelLeftClose className="w-4 h-4" />
</Button>
<GitBranch className="w-4 h-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground mr-2">Branch:</span>

View File

@@ -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`,
`<project_specification>
<project_name>${projectName}</project_name>
<overview>
Describe your project here. This file will be analyzed by an AI agent
to understand your project structure and tech stack.
</overview>
<technology_stack>
<!-- The AI agent will fill this in after analyzing your project -->
</technology_stack>
<core_capabilities>
<!-- List core features and capabilities -->
</core_capabilities>
<implemented_features>
<!-- The AI agent will populate this based on code analysis -->
</implemented_features>
</project_specification>`
);
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`,
`<project_specification>
<project_name>${projectName}</project_name>
<overview>
This project was created from the "${template.name}" starter template.
${template.description}
</overview>
<technology_stack>
${template.techStack.map((tech) => `<technology>${tech}</technology>`).join('\n ')}
</technology_stack>
<core_capabilities>
${template.features.map((feature) => `<capability>${feature}</capability>`).join('\n ')}
</core_capabilities>
<implemented_features>
<!-- The AI agent will populate this based on code analysis -->
</implemented_features>
</project_specification>`
);
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`,
`<project_specification>
<project_name>${projectName}</project_name>
<overview>
This project was cloned from ${repoUrl}.
The AI agent will analyze the project structure.
</overview>
<technology_stack>
<!-- The AI agent will fill this in after analyzing your project -->
</technology_stack>
<core_capabilities>
<!-- List core features and capabilities -->
</core_capabilities>
<implemented_features>
<!-- The AI agent will populate this based on code analysis -->
</implemented_features>
</project_specification>`
);
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 (
<div className="flex-1 flex flex-col h-screen content-bg" data-testid="dashboard-view">
{/* Header with logo */}
<header className="shrink-0 border-b border-border bg-glass backdrop-blur-md">
{/* Electron titlebar drag region */}
{isElectron() && (
<div
className={`absolute top-0 left-0 right-0 h-6 titlebar-drag-region z-40 pointer-events-none ${isMac ? 'pl-20' : ''}`}
aria-hidden="true"
/>
)}
<div className="px-8 py-4 flex items-center justify-between">
<div
className="flex items-center gap-3 cursor-pointer group titlebar-no-drag"
onClick={() => navigate({ to: '/dashboard' })}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 256 256"
role="img"
aria-label="Automaker Logo"
className="size-10 group-hover:rotate-12 transition-transform duration-300 ease-out"
>
<defs>
<linearGradient
id="bg-dashboard"
x1="0"
y1="0"
x2="256"
y2="256"
gradientUnits="userSpaceOnUse"
>
<stop offset="0%" style={{ stopColor: 'var(--brand-400)' }} />
<stop offset="100%" style={{ stopColor: 'var(--brand-600)' }} />
</linearGradient>
<filter id="iconShadow-dashboard" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow
dx="0"
dy="4"
stdDeviation="4"
floodColor="#000000"
floodOpacity="0.25"
/>
</filter>
</defs>
<rect x="16" y="16" width="224" height="224" rx="56" fill="url(#bg-dashboard)" />
<g
fill="none"
stroke="#FFFFFF"
strokeWidth="20"
strokeLinecap="round"
strokeLinejoin="round"
filter="url(#iconShadow-dashboard)"
>
<path d="M92 92 L52 128 L92 164" />
<path d="M144 72 L116 184" />
<path d="M164 92 L204 128 L164 164" />
</g>
</svg>
<div className="flex flex-col">
<span className="font-bold text-foreground text-2xl tracking-tight leading-none">
automaker<span className="text-brand-500">.</span>
</span>
<span className="text-xs text-muted-foreground leading-none font-medium mt-1">
v{appVersion} {versionSuffix}
</span>
</div>
</div>
<Button
variant="ghost"
size="icon"
onClick={() => navigate({ to: '/settings' })}
className="titlebar-no-drag"
>
<Settings className="w-5 h-5" />
</Button>
</div>
</header>
{/* Main content */}
<div className="flex-1 overflow-y-auto p-8">
<div className="max-w-6xl mx-auto">
{/* No projects - show getting started */}
{!hasProjects && (
<div className="animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="text-center mb-12">
<h2 className="text-3xl font-bold text-foreground mb-3">Welcome to Automaker</h2>
<p className="text-lg text-muted-foreground max-w-xl mx-auto">
Your autonomous AI development studio. Get started by creating a new project or
opening an existing one.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 max-w-3xl mx-auto">
{/* New Project Card */}
<div
className="group relative rounded-xl border border-border bg-card/80 backdrop-blur-sm hover:bg-card hover:border-brand-500/30 hover:shadow-xl hover:shadow-brand-500/5 transition-all duration-300 hover:-translate-y-1"
data-testid="new-project-card"
>
<div className="absolute inset-0 rounded-xl bg-linear-to-br from-brand-500/5 via-transparent to-purple-600/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
<div className="relative p-6 h-full flex flex-col">
<div className="flex items-start gap-4 flex-1">
<div className="w-12 h-12 rounded-xl bg-linear-to-br from-brand-500 to-brand-600 shadow-lg shadow-brand-500/25 flex items-center justify-center group-hover:scale-105 group-hover:shadow-brand-500/40 transition-all duration-300 shrink-0">
<Plus className="w-6 h-6 text-white" />
</div>
<div className="flex-1 min-w-0">
<h3 className="text-lg font-semibold text-foreground mb-1.5">
New Project
</h3>
<p className="text-sm text-muted-foreground leading-relaxed">
Create a new project from scratch with AI-powered development
</p>
</div>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
className="w-full mt-5 bg-linear-to-r from-brand-500 to-brand-600 hover:from-brand-600 hover:to-brand-700 text-white border-0 shadow-md shadow-brand-500/20 hover:shadow-brand-500/30 transition-all"
data-testid="create-new-project"
>
<Plus className="w-4 h-4 mr-2" />
Create Project
<ChevronDown className="w-4 h-4 ml-2" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuItem
onClick={handleNewProject}
data-testid="quick-setup-option"
>
<Plus className="w-4 h-4 mr-2" />
Quick Setup
</DropdownMenuItem>
<DropdownMenuItem
onClick={handleInteractiveMode}
data-testid="interactive-mode-option"
>
<MessageSquare className="w-4 h-4 mr-2" />
Interactive Mode
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
{/* Open Project Card */}
<div
className="group relative rounded-xl border border-border bg-card/80 backdrop-blur-sm hover:bg-card hover:border-blue-500/30 hover:shadow-xl hover:shadow-blue-500/5 transition-all duration-300 cursor-pointer hover:-translate-y-1"
onClick={handleOpenProject}
data-testid="open-project-card"
>
<div className="absolute inset-0 rounded-xl bg-linear-to-br from-blue-500/5 via-transparent to-cyan-600/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
<div className="relative p-6 h-full flex flex-col">
<div className="flex items-start gap-4 flex-1">
<div className="w-12 h-12 rounded-xl bg-muted border border-border flex items-center justify-center group-hover:bg-blue-500/10 group-hover:border-blue-500/30 group-hover:scale-105 transition-all duration-300 shrink-0">
<FolderOpen className="w-6 h-6 text-muted-foreground group-hover:text-blue-500 transition-colors duration-300" />
</div>
<div className="flex-1 min-w-0">
<h3 className="text-lg font-semibold text-foreground mb-1.5">
Open Project
</h3>
<p className="text-sm text-muted-foreground leading-relaxed">
Open an existing project folder to continue working
</p>
</div>
</div>
<Button
variant="secondary"
className="w-full mt-5 bg-secondary/80 hover:bg-secondary text-foreground border border-border hover:border-blue-500/30 transition-all"
data-testid="open-existing-project"
>
<FolderOpen className="w-4 h-4 mr-2" />
Browse Folder
</Button>
</div>
</div>
</div>
</div>
)}
{/* Has projects - show project list */}
{hasProjects && (
<div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
{/* Quick actions header */}
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold text-foreground">Your Projects</h2>
<div className="flex gap-2">
<Button variant="outline" onClick={handleOpenProject}>
<FolderOpen className="w-4 h-4 mr-2" />
Open Folder
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button className="bg-linear-to-r from-brand-500 to-brand-600 hover:from-brand-600 hover:to-brand-700 text-white">
<Plus className="w-4 h-4 mr-2" />
New Project
<ChevronDown className="w-4 h-4 ml-2" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuItem onClick={handleNewProject}>
<Plus className="w-4 h-4 mr-2" />
Quick Setup
</DropdownMenuItem>
<DropdownMenuItem onClick={handleInteractiveMode}>
<MessageSquare className="w-4 h-4 mr-2" />
Interactive Mode
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
{/* Favorites section */}
{favoriteProjects.length > 0 && (
<div>
<div className="flex items-center gap-2.5 mb-4">
<div className="w-8 h-8 rounded-lg bg-yellow-500/10 flex items-center justify-center">
<Star className="w-4 h-4 text-yellow-500 fill-yellow-500" />
</div>
<h3 className="text-lg font-semibold text-foreground">Favorites</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{favoriteProjects.map((project) => (
<div
key={project.id}
className="group relative rounded-xl border border-yellow-500/30 bg-card/60 backdrop-blur-sm hover:bg-card hover:border-yellow-500/50 hover:shadow-lg hover:shadow-yellow-500/5 transition-all duration-300 cursor-pointer hover:-translate-y-0.5"
onClick={() => handleProjectClick(project)}
data-testid={`project-card-${project.id}`}
>
<div className="absolute inset-0 rounded-xl bg-linear-to-br from-yellow-500/5 to-amber-600/5 opacity-0 group-hover:opacity-100 transition-all duration-300" />
<div className="relative p-4">
<div className="flex items-start gap-3">
<div className="w-10 h-10 rounded-lg bg-yellow-500/10 border border-yellow-500/30 flex items-center justify-center group-hover:bg-yellow-500/20 transition-all duration-300 shrink-0">
<Folder className="w-5 h-5 text-yellow-500" />
</div>
<div className="flex-1 min-w-0">
<p className="font-medium text-foreground truncate group-hover:text-yellow-500 transition-colors duration-300">
{project.name}
</p>
<p className="text-xs text-muted-foreground/70 truncate mt-1">
{project.path}
</p>
{project.lastOpened && (
<p className="text-xs text-muted-foreground mt-1.5">
{new Date(project.lastOpened).toLocaleDateString()}
</p>
)}
</div>
<div className="flex items-center gap-1">
<button
onClick={(e) => handleToggleFavorite(e, project.id)}
className="p-1.5 rounded-lg hover:bg-yellow-500/20 transition-colors"
title="Remove from favorites"
>
<Star className="w-4 h-4 text-yellow-500 fill-yellow-500" />
</button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
onClick={(e) => e.stopPropagation()}
className="p-1.5 rounded-lg hover:bg-muted transition-colors opacity-0 group-hover:opacity-100"
title="More options"
>
<MoreVertical className="w-4 h-4 text-muted-foreground" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={(e) => handleRemoveProject(e, project)}
className="text-destructive focus:text-destructive"
>
<Trash2 className="w-4 h-4 mr-2" />
Remove from Automaker
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</div>
</div>
))}
</div>
</div>
)}
{/* Recent projects section */}
{recentProjects.length > 0 && (
<div>
<div className="flex items-center gap-2.5 mb-4">
<div className="w-8 h-8 rounded-lg bg-muted/50 flex items-center justify-center">
<Clock className="w-4 h-4 text-muted-foreground" />
</div>
<h3 className="text-lg font-semibold text-foreground">Recent Projects</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{recentProjects.map((project) => (
<div
key={project.id}
className="group relative rounded-xl border border-border bg-card/60 backdrop-blur-sm hover:bg-card hover:border-brand-500/40 hover:shadow-lg hover:shadow-brand-500/5 transition-all duration-300 cursor-pointer hover:-translate-y-0.5"
onClick={() => handleProjectClick(project)}
data-testid={`project-card-${project.id}`}
>
<div className="absolute inset-0 rounded-xl bg-linear-to-br from-brand-500/0 to-purple-600/0 group-hover:from-brand-500/5 group-hover:to-purple-600/5 transition-all duration-300" />
<div className="relative p-4">
<div className="flex items-start gap-3">
<div className="w-10 h-10 rounded-lg bg-muted/80 border border-border flex items-center justify-center group-hover:bg-brand-500/10 group-hover:border-brand-500/30 transition-all duration-300 shrink-0">
<Folder className="w-5 h-5 text-muted-foreground group-hover:text-brand-500 transition-colors duration-300" />
</div>
<div className="flex-1 min-w-0">
<p className="font-medium text-foreground truncate group-hover:text-brand-500 transition-colors duration-300">
{project.name}
</p>
<p className="text-xs text-muted-foreground/70 truncate mt-1">
{project.path}
</p>
{project.lastOpened && (
<p className="text-xs text-muted-foreground mt-1.5">
{new Date(project.lastOpened).toLocaleDateString()}
</p>
)}
</div>
<div className="flex items-center gap-1">
<button
onClick={(e) => handleToggleFavorite(e, project.id)}
className="p-1.5 rounded-lg hover:bg-muted transition-colors opacity-0 group-hover:opacity-100"
title="Add to favorites"
>
<Star className="w-4 h-4 text-muted-foreground hover:text-yellow-500" />
</button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
onClick={(e) => e.stopPropagation()}
className="p-1.5 rounded-lg hover:bg-muted transition-colors opacity-0 group-hover:opacity-100"
title="More options"
>
<MoreVertical className="w-4 h-4 text-muted-foreground" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={(e) => handleRemoveProject(e, project)}
className="text-destructive focus:text-destructive"
>
<Trash2 className="w-4 h-4 mr-2" />
Remove from Automaker
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</div>
</div>
))}
</div>
</div>
)}
</div>
)}
</div>
</div>
{/* Modals */}
<NewProjectModal
open={showNewProjectModal}
onOpenChange={setShowNewProjectModal}
onCreateBlankProject={handleCreateBlankProject}
onCreateFromTemplate={handleCreateFromTemplate}
onCreateFromCustomUrl={handleCreateFromCustomUrl}
isCreating={isCreating}
/>
<WorkspacePickerModal
open={showWorkspacePicker}
onOpenChange={setShowWorkspacePicker}
onSelect={handleWorkspaceSelect}
/>
{/* Remove project confirmation dialog */}
<Dialog open={!!projectToRemove} onOpenChange={(open) => !open && setProjectToRemove(null)}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Remove Project</DialogTitle>
<DialogDescription>
Are you sure you want to remove <strong>{projectToRemove?.name}</strong> from
Automaker?
</DialogDescription>
</DialogHeader>
<div className="py-4">
<p className="text-sm text-muted-foreground">
This will only remove the project from your Automaker projects list. The project files
on your computer will not be deleted.
</p>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setProjectToRemove(null)}>
Cancel
</Button>
<Button variant="destructive" onClick={handleConfirmRemove}>
<Trash2 className="w-4 h-4 mr-2" />
Remove Project
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Loading overlay */}
{isOpening && (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm"
data-testid="project-opening-overlay"
>
<div className="flex flex-col items-center gap-4 p-8 rounded-2xl bg-card border border-border shadow-2xl">
<Loader2 className="w-10 h-10 text-brand-500 animate-spin" />
<p className="text-foreground font-medium">Opening project...</p>
</div>
</div>
)}
</div>
);
}

View File

@@ -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 (

View File

@@ -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<string | null>(null);
const currentProjectRef = useRef<string | null>(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);

View File

@@ -139,7 +139,6 @@ export function parseLocalStorageSettings(): Partial<GlobalSettings> | null {
theme: state.theme as GlobalSettings['theme'],
sidebarOpen: state.sidebarOpen as boolean,
chatHistoryOpen: state.chatHistoryOpen as boolean,
kanbanCardDetailLevel: state.kanbanCardDetailLevel as GlobalSettings['kanbanCardDetailLevel'],
maxConcurrency: state.maxConcurrency as number,
defaultSkipTests: state.defaultSkipTests as boolean,
enableDependencyBlocking: state.enableDependencyBlocking as boolean,
@@ -504,6 +503,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
}));
@@ -525,7 +525,6 @@ export function hydrateStoreFromSettings(settings: GlobalSettings): void {
theme: settings.theme as unknown as import('@/store/app-store').ThemeMode,
sidebarOpen: settings.sidebarOpen ?? true,
chatHistoryOpen: settings.chatHistoryOpen ?? false,
kanbanCardDetailLevel: settings.kanbanCardDetailLevel ?? 'standard',
maxConcurrency: settings.maxConcurrency ?? 3,
defaultSkipTests: settings.defaultSkipTests ?? true,
enableDependencyBlocking: settings.enableDependencyBlocking ?? true,
@@ -581,7 +580,6 @@ function buildSettingsUpdateFromStore(): Record<string, unknown> {
theme: state.theme,
sidebarOpen: state.sidebarOpen,
chatHistoryOpen: state.chatHistoryOpen,
kanbanCardDetailLevel: state.kanbanCardDetailLevel,
maxConcurrency: state.maxConcurrency,
defaultSkipTests: state.defaultSkipTests,
enableDependencyBlocking: state.enableDependencyBlocking,

View File

@@ -31,7 +31,6 @@ const SETTINGS_FIELDS_TO_SYNC = [
'theme',
'sidebarOpen',
'chatHistoryOpen',
'kanbanCardDetailLevel',
'maxConcurrency',
'defaultSkipTests',
'enableDependencyBlocking',
@@ -379,7 +378,6 @@ export async function refreshSettingsFromServer(): Promise<boolean> {
theme: serverSettings.theme as unknown as ThemeMode,
sidebarOpen: serverSettings.sidebarOpen,
chatHistoryOpen: serverSettings.chatHistoryOpen,
kanbanCardDetailLevel: serverSettings.kanbanCardDetailLevel,
maxConcurrency: serverSettings.maxConcurrency,
defaultSkipTests: serverSettings.defaultSkipTests,
enableDependencyBlocking: serverSettings.enableDependencyBlocking,

View File

@@ -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 {

View File

@@ -1858,7 +1858,6 @@ export class HttpApiClient implements ElectronAPI {
theme: string;
sidebarOpen: boolean;
chatHistoryOpen: boolean;
kanbanCardDetailLevel: string;
maxConcurrency: number;
defaultSkipTests: boolean;
enableDependencyBlocking: boolean;
@@ -1951,6 +1950,7 @@ export class HttpApiClient implements ElectronAPI {
cardBorderOpacity: number;
hideScrollbar: boolean;
};
worktreePanelVisible?: boolean;
lastSelectedSessionId?: string;
};
error?: string;

View File

@@ -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 (
<>
<main className="h-screen overflow-hidden" data-testid="app-container">
<Outlet />
<Toaster richColors position="bottom-right" />
</main>
<SandboxRiskDialog
open={showSandboxDialog}
onConfirm={handleSandboxConfirm}
onDeny={handleSandboxDeny}
/>
</>
);
}
return (
<>
<main className="flex h-screen overflow-hidden" data-testid="app-container">

View File

@@ -0,0 +1,6 @@
import { createFileRoute } from '@tanstack/react-router';
import { DashboardView } from '@/components/views/dashboard-view';
export const Route = createFileRoute('/dashboard')({
component: DashboardView,
});

View File

@@ -114,8 +114,6 @@ function saveThemeToStorage(theme: ThemeMode): void {
setItem(THEME_STORAGE_KEY, theme);
}
export type KanbanCardDetailLevel = 'minimal' | 'standard' | 'detailed';
export type BoardViewMode = 'kanban' | 'graph';
export interface ApiKeys {
@@ -508,7 +506,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
@@ -656,6 +653,10 @@ export interface AppState {
// 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;
@@ -816,6 +817,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;
@@ -869,7 +871,6 @@ export interface AppActions {
setMaxConcurrency: (max: number) => void;
// Kanban Card Settings actions
setKanbanCardDetailLevel: (level: KanbanCardDetailLevel) => void;
setBoardViewMode: (mode: BoardViewMode) => void;
// Feature Default Settings actions
@@ -1062,6 +1063,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;
@@ -1118,7 +1123,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)
@@ -1186,6 +1190,7 @@ const initialState: AppState = {
codexModelsError: null,
codexModelsLastFetched: null,
pipelineConfigByProject: {},
worktreePanelVisibleByProject: {},
// UI State (previously in localStorage, now synced via API)
worktreePanelCollapsed: false,
lastProjectDir: '',
@@ -1429,6 +1434,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 }),
@@ -1704,7 +1726,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
@@ -3070,6 +3091,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 }),

View File

@@ -96,7 +96,6 @@ const SETTINGS_FIELDS_TO_SYNC = [
'theme',
'sidebarOpen',
'chatHistoryOpen',
'kanbanCardDetailLevel',
'maxConcurrency',
'defaultSkipTests',
'enableDependencyBlocking',

View File

@@ -109,7 +109,6 @@ export { DEFAULT_PROMPT_CUSTOMIZATION } from './prompts.js';
// Settings types and constants
export type {
ThemeMode,
KanbanCardDetailLevel,
PlanningMode,
ThinkingLevel,
ModelProvider,

View File

@@ -65,9 +65,6 @@ export type ThemeMode =
| 'nordlight'
| 'blossom';
/** KanbanCardDetailLevel - Controls how much information is displayed on kanban cards */
export type KanbanCardDetailLevel = 'minimal' | 'standard' | 'detailed';
/** PlanningMode - Planning levels for feature generation workflows */
export type PlanningMode = 'skip' | 'lite' | 'spec' | 'full';
@@ -294,6 +291,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;
}
/**
@@ -360,8 +359,6 @@ export interface GlobalSettings {
sidebarOpen: boolean;
/** Whether chat history panel is open */
chatHistoryOpen: boolean;
/** How much detail to show on kanban cards */
kanbanCardDetailLevel: KanbanCardDetailLevel;
// Feature Generation Defaults
/** Max features to generate concurrently */
@@ -595,6 +592,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;
@@ -676,7 +677,6 @@ export const DEFAULT_GLOBAL_SETTINGS: GlobalSettings = {
theme: 'dark',
sidebarOpen: true,
chatHistoryOpen: false,
kanbanCardDetailLevel: 'standard',
maxConcurrency: 3,
defaultSkipTests: true,
enableDependencyBlocking: true,