diff --git a/apps/server/src/services/settings-service.ts b/apps/server/src/services/settings-service.ts index 9270b3fb..f1dfd45c 100644 --- a/apps/server/src/services/settings-service.ts +++ b/apps/server/src/services/settings-service.ts @@ -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, diff --git a/apps/server/src/types/settings.ts b/apps/server/src/types/settings.ts index e785f8ea..98bce97f 100644 --- a/apps/server/src/types/settings.ts +++ b/apps/server/src/types/settings.ts @@ -7,7 +7,6 @@ export type { ThemeMode, - KanbanCardDetailLevel, ModelAlias, PlanningMode, ThinkingLevel, diff --git a/apps/ui/scripts/setup-e2e-fixtures.mjs b/apps/ui/scripts/setup-e2e-fixtures.mjs index 26488791..356e419b 100644 --- a/apps/ui/scripts/setup-e2e-fixtures.mjs +++ b/apps/ui/scripts/setup-e2e-fixtures.mjs @@ -41,7 +41,6 @@ const E2E_SETTINGS = { theme: 'dark', sidebarOpen: true, chatHistoryOpen: false, - kanbanCardDetailLevel: 'standard', maxConcurrency: 3, defaultSkipTests: true, enableDependencyBlocking: true, diff --git a/apps/ui/src/components/layout/sidebar.tsx b/apps/ui/src/components/layout/sidebar.tsx index 98535bda..2eff16c0 100644 --- a/apps/ui/src/components/layout/sidebar.tsx +++ b/apps/ui/src/components/layout/sidebar.tsx @@ -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() {
- {/* Project Actions - Moved above project selector */} - {sidebarOpen && ( - - )} - 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 */} diff --git a/apps/ui/src/components/views/board-view.tsx b/apps/ui/src/components/views/board-view.tsx index e53688c1..810d1a91 100644 --- a/apps/ui/src/components/views/board-view.tsx +++ b/apps/ui/src/components/views/board-view.tsx @@ -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 */} setShowAddDialog(true)} onOpenPlanDialog={() => setShowPlanDialog(true)} - addFeatureShortcut={{ - key: shortcuts.addFeature, - action: () => setShowAddDialog(true), - description: 'Add new feature', - }} isMounted={isMounted} /> - {/* Worktree Panel */} - 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) && ( + 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 */}
@@ -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 } diff --git a/apps/ui/src/components/views/board-view/board-controls.tsx b/apps/ui/src/components/views/board-view/board-controls.tsx index 14733637..2d398c18 100644 --- a/apps/ui/src/components/views/board-view/board-controls.tsx +++ b/apps/ui/src/components/views/board-view/board-controls.tsx @@ -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({

Completed Features ({completedCount})

- - {/* Kanban Card Detail Level Toggle */} -
- - - - - -

Minimal - Title & category only

-
-
- - - - - -

Standard - Steps & progress

-
-
- - - - - -

Detailed - Model, tools & tasks

-
-
-
); diff --git a/apps/ui/src/components/views/board-view/board-header.tsx b/apps/ui/src/components/views/board-view/board-header.tsx index fb9f38c2..4611c843 100644 --- a/apps/ui/src/components/views/board-view/board-header.tsx +++ b/apps/ui/src/components/views/board-view/board-header.tsx @@ -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) && } - {/* Concurrency Slider - only show after mount to prevent hydration issues */} + {/* Worktrees Toggle - only show after mount to prevent hydration issues */} {isMounted && ( -
- - Agents - onConcurrencyChange(value[0])} - min={1} - max={10} - step={1} - className="w-20" - data-testid="concurrency-slider" +
+ + + - - {runningAgentsCount} / {maxConcurrency} -
)} + {/* Concurrency Control - only show after mount to prevent hydration issues */} + {isMounted && ( + + + + + +
+
+

Max Concurrent Agents

+

+ Controls how many AI agents can run simultaneously. Higher values process more + features in parallel but use more API resources. +

+
+
+ onConcurrencyChange(value[0])} + min={1} + max={10} + step={1} + className="flex-1" + data-testid="concurrency-slider" + /> + + {maxConcurrency} + +
+
+
+
+ )} + {/* Auto Mode Toggle - only show after mount to prevent hydration issues */} {isMounted && (
@@ -134,17 +191,6 @@ export function BoardHeader({ Plan - - - - Add Feature -
); diff --git a/apps/ui/src/components/views/board-view/components/kanban-card/agent-info-panel.tsx b/apps/ui/src/components/views/board-view/components/kanban-card/agent-info-panel.tsx index 5439b675..b73a8d04 100644 --- a/apps/ui/src/components/views/board-view/components/kanban-card/agent-info-panel.tsx +++ b/apps/ui/src/components/views/board-view/components/kanban-card/agent-info-panel.tsx @@ -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(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 ( <>
@@ -255,7 +252,11 @@ export function AgentInfoPanel({
-

+

e.stopPropagation()} + onMouseDown={(e) => e.stopPropagation()} + > {feature.summary || summary || agentInfo.summary}

@@ -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 ( -
-
- - - {agentInfo.todos.filter((t) => t.status === 'completed').length}/ - {agentInfo.todos.length} tasks - -
-
- {agentInfo.todos.map((todo, idx) => ( -
- {todo.status === 'completed' ? ( - - ) : todo.status === 'in_progress' ? ( - - ) : ( - - )} - - {todo.content} - -
- ))} -
-
- ); - } - - // 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 && ( - - )} - + ); } diff --git a/apps/ui/src/components/views/board-view/components/kanban-card/summary-dialog.tsx b/apps/ui/src/components/views/board-view/components/kanban-card/summary-dialog.tsx index b469da8f..db9f579d 100644 --- a/apps/ui/src/components/views/board-view/components/kanban-card/summary-dialog.tsx +++ b/apps/ui/src/components/views/board-view/components/kanban-card/summary-dialog.tsx @@ -31,8 +31,10 @@ export function SummaryDialog({ return ( e.stopPropagation()} + onMouseDown={(e) => e.stopPropagation()} > diff --git a/apps/ui/src/components/views/board-view/components/kanban-column.tsx b/apps/ui/src/components/views/board-view/components/kanban-column.tsx index 4e08cfba..4a1b62dd 100644 --- a/apps/ui/src/components/views/board-view/components/kanban-column.tsx +++ b/apps/ui/src/components/views/board-view/components/kanban-column.tsx @@ -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} + {/* Floating Footer Action */} + {footerAction && ( +
+ {footerAction} +
+ )} + {/* Drop zone indicator when dragging over */} {isOver && (
diff --git a/apps/ui/src/components/views/board-view/dialogs/add-feature-dialog.tsx b/apps/ui/src/components/views/board-view/dialogs/add-feature-dialog.tsx index bab34522..c502e2b0 100644 --- a/apps/ui/src/components/views/board-view/dialogs/add-feature-dialog.tsx +++ b/apps/ui/src/components/views/board-view/dialogs/add-feature-dialog.tsx @@ -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({ />
-
- {modelSupportsPlanningMode && ( -
- +
+
+ + {modelSupportsPlanningMode ? ( -
- )} + ) : ( + + + +
+ {}} + testIdPrefix="add-feature-planning" + compact + disabled + /> +
+
+ +

Planning modes are only available for Claude Provider

+
+
+
+ )} +
@@ -526,28 +548,32 @@ export function AddFeatureDialog({ Run tests
- {modelSupportsPlanningMode && ( -
- setRequirePlanApproval(!!checked)} - disabled={planningMode === 'skip' || planningMode === 'lite'} - data-testid="add-feature-require-approval-checkbox" - /> - -
- )} +
+ setRequirePlanApproval(!!checked)} + disabled={ + !modelSupportsPlanningMode || + planningMode === 'skip' || + planningMode === 'lite' + } + data-testid="add-feature-require-approval-checkbox" + /> + +
diff --git a/apps/ui/src/components/views/board-view/dialogs/edit-feature-dialog.tsx b/apps/ui/src/components/views/board-view/dialogs/edit-feature-dialog.tsx index addd7e83..7b4ac693 100644 --- a/apps/ui/src/components/views/board-view/dialogs/edit-feature-dialog.tsx +++ b/apps/ui/src/components/views/board-view/dialogs/edit-feature-dialog.tsx @@ -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({ /> -
- {modelSupportsPlanningMode && ( -
- +
+
+ + {modelSupportsPlanningMode ? ( -
- )} + ) : ( + + + +
+ {}} + testIdPrefix="edit-feature-planning" + compact + disabled + /> +
+
+ +

Planning modes are only available for Claude Provider

+
+
+
+ )} +
@@ -552,28 +574,32 @@ export function EditFeatureDialog({ Run tests
- {modelSupportsPlanningMode && ( -
- setRequirePlanApproval(!!checked)} - disabled={planningMode === 'skip' || planningMode === 'lite'} - data-testid="edit-feature-require-approval-checkbox" - /> - -
- )} +
+ setRequirePlanApproval(!!checked)} + disabled={ + !modelSupportsPlanningMode || + planningMode === 'skip' || + planningMode === 'lite' + } + data-testid="edit-feature-require-approval-checkbox" + /> + +
diff --git a/apps/ui/src/components/views/board-view/dialogs/mass-edit-dialog.tsx b/apps/ui/src/components/views/board-view/dialogs/mass-edit-dialog.tsx index ce391330..30042a4c 100644 --- a/apps/ui/src/components/views/board-view/dialogs/mass-edit-dialog.tsx +++ b/apps/ui/src/components/views/board-view/dialogs/mass-edit-dialog.tsx @@ -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 ( !open && onClose()}> @@ -205,30 +207,64 @@ export function MassEditDialog({ open, onClose, selectedFeatures, onApply }: Mas
{/* Planning Mode */} - - setApplyState((prev) => ({ - ...prev, - planningMode: apply, - requirePlanApproval: apply, - })) - } - > - { - 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" - /> - + {modelSupportsPlanningMode ? ( + + setApplyState((prev) => ({ + ...prev, + planningMode: apply, + requirePlanApproval: apply, + })) + } + > + { + 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" + /> + + ) : ( + + + +
+
+
+ + +
+
+
+ {}} + testIdPrefix="mass-edit-planning" + disabled + /> +
+
+
+ +

Planning modes are only available for Claude Provider

+
+
+
+ )} {/* Priority */} ; 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 ) : column.id === 'backlog' ? ( - +
+ + +
) : column.id === 'in_progress' ? ( + ) : undefined + } > f.id)} diff --git a/apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx b/apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx index e0030d09..b56f65e1 100644 --- a/apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx +++ b/apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx @@ -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(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 ( -
- - - Branch: - {selectedWorktree?.branch ?? 'main'} - {selectedWorktree?.hasChanges && ( - - {selectedWorktree.changedFilesCount ?? '!'} - - )} -
- ); - } - - // Expanded view - full worktree panel return (
- - 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() && ( +
+ + {/* 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 && ( +
+
+
+ +
+

Favorites

+
+
+ {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 */} + !open && setProjectToRemove(null)}> + + + Remove Project + + Are you sure you want to remove {projectToRemove?.name} from + Automaker? + + +
+

+ This will only remove the project from your Automaker projects list. The project files + on your computer will not be deleted. +

+
+ + + + +
+
+ + {/* Loading overlay */} + {isOpening && ( +
+
+ +

Opening project...

+
+
+ )} +
+ ); +} 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..f5d421e0 100644 --- a/apps/ui/src/hooks/use-settings-migration.ts +++ b/apps/ui/src/hooks/use-settings-migration.ts @@ -139,7 +139,6 @@ export function parseLocalStorageSettings(): Partial | 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 { theme: state.theme, sidebarOpen: state.sidebarOpen, chatHistoryOpen: state.chatHistoryOpen, - kanbanCardDetailLevel: state.kanbanCardDetailLevel, maxConcurrency: state.maxConcurrency, defaultSkipTests: state.defaultSkipTests, enableDependencyBlocking: state.enableDependencyBlocking, diff --git a/apps/ui/src/hooks/use-settings-sync.ts b/apps/ui/src/hooks/use-settings-sync.ts index 8f17a283..4788bfb1 100644 --- a/apps/ui/src/hooks/use-settings-sync.ts +++ b/apps/ui/src/hooks/use-settings-sync.ts @@ -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 { 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, 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..85392000 100644 --- a/apps/ui/src/lib/http-api-client.ts +++ b/apps/ui/src/lib/http-api-client.ts @@ -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; 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..fa1daf4c 100644 --- a/apps/ui/src/store/app-store.ts +++ b/apps/ui/src/store/app-store.ts @@ -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; + // 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 +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()((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()((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()((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/docs/settings-api-migration.md b/docs/settings-api-migration.md index 85bc6979..5a20fc21 100644 --- a/docs/settings-api-migration.md +++ b/docs/settings-api-migration.md @@ -96,7 +96,6 @@ const SETTINGS_FIELDS_TO_SYNC = [ 'theme', 'sidebarOpen', 'chatHistoryOpen', - 'kanbanCardDetailLevel', 'maxConcurrency', 'defaultSkipTests', 'enableDependencyBlocking', diff --git a/libs/types/src/index.ts b/libs/types/src/index.ts index d2e49cbf..7b402274 100644 --- a/libs/types/src/index.ts +++ b/libs/types/src/index.ts @@ -109,7 +109,6 @@ export { DEFAULT_PROMPT_CUSTOMIZATION } from './prompts.js'; // Settings types and constants export type { ThemeMode, - KanbanCardDetailLevel, PlanningMode, ThinkingLevel, ModelProvider, diff --git a/libs/types/src/settings.ts b/libs/types/src/settings.ts index b1737068..07b4290d 100644 --- a/libs/types/src/settings.ts +++ b/libs/types/src/settings.ts @@ -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,