mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 08:33:36 +00:00
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:
@@ -600,8 +600,6 @@ export class SettingsService {
|
|||||||
theme: (appState.theme as GlobalSettings['theme']) || 'dark',
|
theme: (appState.theme as GlobalSettings['theme']) || 'dark',
|
||||||
sidebarOpen: appState.sidebarOpen !== undefined ? (appState.sidebarOpen as boolean) : true,
|
sidebarOpen: appState.sidebarOpen !== undefined ? (appState.sidebarOpen as boolean) : true,
|
||||||
chatHistoryOpen: (appState.chatHistoryOpen as boolean) || false,
|
chatHistoryOpen: (appState.chatHistoryOpen as boolean) || false,
|
||||||
kanbanCardDetailLevel:
|
|
||||||
(appState.kanbanCardDetailLevel as GlobalSettings['kanbanCardDetailLevel']) || 'standard',
|
|
||||||
maxConcurrency: (appState.maxConcurrency as number) || 3,
|
maxConcurrency: (appState.maxConcurrency as number) || 3,
|
||||||
defaultSkipTests:
|
defaultSkipTests:
|
||||||
appState.defaultSkipTests !== undefined ? (appState.defaultSkipTests as boolean) : true,
|
appState.defaultSkipTests !== undefined ? (appState.defaultSkipTests as boolean) : true,
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
|
|
||||||
export type {
|
export type {
|
||||||
ThemeMode,
|
ThemeMode,
|
||||||
KanbanCardDetailLevel,
|
|
||||||
ModelAlias,
|
ModelAlias,
|
||||||
PlanningMode,
|
PlanningMode,
|
||||||
ThinkingLevel,
|
ThinkingLevel,
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ const E2E_SETTINGS = {
|
|||||||
theme: 'dark',
|
theme: 'dark',
|
||||||
sidebarOpen: true,
|
sidebarOpen: true,
|
||||||
chatHistoryOpen: false,
|
chatHistoryOpen: false,
|
||||||
kanbanCardDetailLevel: 'standard',
|
|
||||||
maxConcurrency: 3,
|
maxConcurrency: 3,
|
||||||
defaultSkipTests: true,
|
defaultSkipTests: true,
|
||||||
enableDependencyBlocking: true,
|
enableDependencyBlocking: true,
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import { CreateSpecDialog } from '@/components/views/spec-view/dialogs';
|
|||||||
import {
|
import {
|
||||||
CollapseToggleButton,
|
CollapseToggleButton,
|
||||||
SidebarHeader,
|
SidebarHeader,
|
||||||
ProjectActions,
|
|
||||||
SidebarNavigation,
|
SidebarNavigation,
|
||||||
ProjectSelectorWithOptions,
|
ProjectSelectorWithOptions,
|
||||||
SidebarFooter,
|
SidebarFooter,
|
||||||
@@ -276,17 +275,6 @@ export function Sidebar() {
|
|||||||
<div className="flex-1 flex flex-col overflow-hidden">
|
<div className="flex-1 flex flex-col overflow-hidden">
|
||||||
<SidebarHeader sidebarOpen={sidebarOpen} navigate={navigate} />
|
<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
|
<ProjectSelectorWithOptions
|
||||||
sidebarOpen={sidebarOpen}
|
sidebarOpen={sidebarOpen}
|
||||||
isProjectPickerOpen={isProjectPickerOpen}
|
isProjectPickerOpen={isProjectPickerOpen}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export function AutomakerLogo({ sidebarOpen, navigate }: AutomakerLogoProps) {
|
|||||||
'flex items-center gap-3 titlebar-no-drag cursor-pointer group',
|
'flex items-center gap-3 titlebar-no-drag cursor-pointer group',
|
||||||
!sidebarOpen && 'flex-col gap-1'
|
!sidebarOpen && 'flex-col gap-1'
|
||||||
)}
|
)}
|
||||||
onClick={() => navigate({ to: '/' })}
|
onClick={() => navigate({ to: '/dashboard' })}
|
||||||
data-testid="logo-button"
|
data-testid="logo-button"
|
||||||
>
|
>
|
||||||
{/* Collapsed logo - shown when sidebar is closed OR on small screens when sidebar is open */}
|
{/* Collapsed logo - shown when sidebar is closed OR on small screens when sidebar is open */}
|
||||||
|
|||||||
@@ -7,7 +7,24 @@ import {
|
|||||||
useSensors,
|
useSensors,
|
||||||
rectIntersection,
|
rectIntersection,
|
||||||
pointerWithin,
|
pointerWithin,
|
||||||
|
type PointerEvent as DndPointerEvent,
|
||||||
} from '@dnd-kit/core';
|
} 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 { useAppStore, Feature } from '@/store/app-store';
|
||||||
import { getElectronAPI } from '@/lib/electron';
|
import { getElectronAPI } from '@/lib/electron';
|
||||||
import { getHttpApiClient } from '@/lib/http-api-client';
|
import { getHttpApiClient } from '@/lib/http-api-client';
|
||||||
@@ -73,8 +90,6 @@ export function BoardView() {
|
|||||||
maxConcurrency,
|
maxConcurrency,
|
||||||
setMaxConcurrency,
|
setMaxConcurrency,
|
||||||
defaultSkipTests,
|
defaultSkipTests,
|
||||||
kanbanCardDetailLevel,
|
|
||||||
setKanbanCardDetailLevel,
|
|
||||||
boardViewMode,
|
boardViewMode,
|
||||||
setBoardViewMode,
|
setBoardViewMode,
|
||||||
specCreatingForProject,
|
specCreatingForProject,
|
||||||
@@ -95,6 +110,8 @@ export function BoardView() {
|
|||||||
} = useAppStore();
|
} = useAppStore();
|
||||||
// Subscribe to pipelineConfigByProject to trigger re-renders when it changes
|
// Subscribe to pipelineConfigByProject to trigger re-renders when it changes
|
||||||
const pipelineConfigByProject = useAppStore((state) => state.pipelineConfigByProject);
|
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 shortcuts = useKeyboardShortcutsConfig();
|
||||||
const {
|
const {
|
||||||
features: hookFeatures,
|
features: hookFeatures,
|
||||||
@@ -246,7 +263,7 @@ export function BoardView() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
useSensor(PointerSensor, {
|
useSensor(DialogAwarePointerSensor, {
|
||||||
activationConstraint: {
|
activationConstraint: {
|
||||||
distance: 8,
|
distance: 8,
|
||||||
},
|
},
|
||||||
@@ -1139,6 +1156,7 @@ export function BoardView() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<BoardHeader
|
<BoardHeader
|
||||||
projectName={currentProject.name}
|
projectName={currentProject.name}
|
||||||
|
projectPath={currentProject.path}
|
||||||
maxConcurrency={maxConcurrency}
|
maxConcurrency={maxConcurrency}
|
||||||
runningAgentsCount={runningAutoTasks.length}
|
runningAgentsCount={runningAutoTasks.length}
|
||||||
onConcurrencyChange={setMaxConcurrency}
|
onConcurrencyChange={setMaxConcurrency}
|
||||||
@@ -1150,47 +1168,43 @@ export function BoardView() {
|
|||||||
autoMode.stop();
|
autoMode.stop();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onAddFeature={() => setShowAddDialog(true)}
|
|
||||||
onOpenPlanDialog={() => setShowPlanDialog(true)}
|
onOpenPlanDialog={() => setShowPlanDialog(true)}
|
||||||
addFeatureShortcut={{
|
|
||||||
key: shortcuts.addFeature,
|
|
||||||
action: () => setShowAddDialog(true),
|
|
||||||
description: 'Add new feature',
|
|
||||||
}}
|
|
||||||
isMounted={isMounted}
|
isMounted={isMounted}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Worktree Panel */}
|
{/* Worktree Panel - conditionally rendered based on visibility setting */}
|
||||||
<WorktreePanel
|
{(worktreePanelVisibleByProject[currentProject.path] ?? true) && (
|
||||||
refreshTrigger={worktreeRefreshKey}
|
<WorktreePanel
|
||||||
projectPath={currentProject.path}
|
refreshTrigger={worktreeRefreshKey}
|
||||||
onCreateWorktree={() => setShowCreateWorktreeDialog(true)}
|
projectPath={currentProject.path}
|
||||||
onDeleteWorktree={(worktree) => {
|
onCreateWorktree={() => setShowCreateWorktreeDialog(true)}
|
||||||
setSelectedWorktreeForAction(worktree);
|
onDeleteWorktree={(worktree) => {
|
||||||
setShowDeleteWorktreeDialog(true);
|
setSelectedWorktreeForAction(worktree);
|
||||||
}}
|
setShowDeleteWorktreeDialog(true);
|
||||||
onCommit={(worktree) => {
|
}}
|
||||||
setSelectedWorktreeForAction(worktree);
|
onCommit={(worktree) => {
|
||||||
setShowCommitWorktreeDialog(true);
|
setSelectedWorktreeForAction(worktree);
|
||||||
}}
|
setShowCommitWorktreeDialog(true);
|
||||||
onCreatePR={(worktree) => {
|
}}
|
||||||
setSelectedWorktreeForAction(worktree);
|
onCreatePR={(worktree) => {
|
||||||
setShowCreatePRDialog(true);
|
setSelectedWorktreeForAction(worktree);
|
||||||
}}
|
setShowCreatePRDialog(true);
|
||||||
onCreateBranch={(worktree) => {
|
}}
|
||||||
setSelectedWorktreeForAction(worktree);
|
onCreateBranch={(worktree) => {
|
||||||
setShowCreateBranchDialog(true);
|
setSelectedWorktreeForAction(worktree);
|
||||||
}}
|
setShowCreateBranchDialog(true);
|
||||||
onAddressPRComments={handleAddressPRComments}
|
}}
|
||||||
onResolveConflicts={handleResolveConflicts}
|
onAddressPRComments={handleAddressPRComments}
|
||||||
onRemovedWorktrees={handleRemovedWorktrees}
|
onResolveConflicts={handleResolveConflicts}
|
||||||
runningFeatureIds={runningAutoTasks}
|
onRemovedWorktrees={handleRemovedWorktrees}
|
||||||
branchCardCounts={branchCardCounts}
|
runningFeatureIds={runningAutoTasks}
|
||||||
features={hookFeatures.map((f) => ({
|
branchCardCounts={branchCardCounts}
|
||||||
id: f.id,
|
features={hookFeatures.map((f) => ({
|
||||||
branchName: f.branchName,
|
id: f.id,
|
||||||
}))}
|
branchName: f.branchName,
|
||||||
/>
|
}))}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Main Content Area */}
|
{/* Main Content Area */}
|
||||||
<div className="flex-1 flex flex-col overflow-hidden">
|
<div className="flex-1 flex flex-col overflow-hidden">
|
||||||
@@ -1210,8 +1224,6 @@ export function BoardView() {
|
|||||||
onShowBoardBackground={() => setShowBoardBackgroundModal(true)}
|
onShowBoardBackground={() => setShowBoardBackgroundModal(true)}
|
||||||
onShowCompletedModal={() => setShowCompletedModal(true)}
|
onShowCompletedModal={() => setShowCompletedModal(true)}
|
||||||
completedCount={completedFeatures.length}
|
completedCount={completedFeatures.length}
|
||||||
kanbanCardDetailLevel={kanbanCardDetailLevel}
|
|
||||||
onDetailLevelChange={setKanbanCardDetailLevel}
|
|
||||||
boardViewMode={boardViewMode}
|
boardViewMode={boardViewMode}
|
||||||
onBoardViewModeChange={setBoardViewMode}
|
onBoardViewModeChange={setBoardViewMode}
|
||||||
/>
|
/>
|
||||||
@@ -1247,6 +1259,7 @@ export function BoardView() {
|
|||||||
featuresWithContext={featuresWithContext}
|
featuresWithContext={featuresWithContext}
|
||||||
runningAutoTasks={runningAutoTasks}
|
runningAutoTasks={runningAutoTasks}
|
||||||
onArchiveAllVerified={() => setShowArchiveAllVerifiedDialog(true)}
|
onArchiveAllVerified={() => setShowArchiveAllVerifiedDialog(true)}
|
||||||
|
onAddFeature={() => setShowAddDialog(true)}
|
||||||
pipelineConfig={
|
pipelineConfig={
|
||||||
currentProject?.path ? pipelineConfigByProject[currentProject.path] || null : null
|
currentProject?.path ? pipelineConfigByProject[currentProject.path] || null : null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
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 { cn } from '@/lib/utils';
|
||||||
import { BoardViewMode } from '@/store/app-store';
|
import { BoardViewMode } from '@/store/app-store';
|
||||||
|
|
||||||
@@ -9,8 +9,6 @@ interface BoardControlsProps {
|
|||||||
onShowBoardBackground: () => void;
|
onShowBoardBackground: () => void;
|
||||||
onShowCompletedModal: () => void;
|
onShowCompletedModal: () => void;
|
||||||
completedCount: number;
|
completedCount: number;
|
||||||
kanbanCardDetailLevel: 'minimal' | 'standard' | 'detailed';
|
|
||||||
onDetailLevelChange: (level: 'minimal' | 'standard' | 'detailed') => void;
|
|
||||||
boardViewMode: BoardViewMode;
|
boardViewMode: BoardViewMode;
|
||||||
onBoardViewModeChange: (mode: BoardViewMode) => void;
|
onBoardViewModeChange: (mode: BoardViewMode) => void;
|
||||||
}
|
}
|
||||||
@@ -20,8 +18,6 @@ export function BoardControls({
|
|||||||
onShowBoardBackground,
|
onShowBoardBackground,
|
||||||
onShowCompletedModal,
|
onShowCompletedModal,
|
||||||
completedCount,
|
completedCount,
|
||||||
kanbanCardDetailLevel,
|
|
||||||
onDetailLevelChange,
|
|
||||||
boardViewMode,
|
boardViewMode,
|
||||||
onBoardViewModeChange,
|
onBoardViewModeChange,
|
||||||
}: BoardControlsProps) {
|
}: BoardControlsProps) {
|
||||||
@@ -115,70 +111,6 @@ export function BoardControls({
|
|||||||
<p>Completed Features ({completedCount})</p>
|
<p>Completed Features ({completedCount})</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</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>
|
</div>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,26 +1,25 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import { HotkeyButton } from '@/components/ui/hotkey-button';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Slider } from '@/components/ui/slider';
|
import { Slider } from '@/components/ui/slider';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Plus, Bot, Wand2, Settings2 } from 'lucide-react';
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||||
import { KeyboardShortcut } from '@/hooks/use-keyboard-shortcuts';
|
import { Bot, Wand2, Settings2, GitBranch } from 'lucide-react';
|
||||||
import { UsagePopover } from '@/components/usage-popover';
|
import { UsagePopover } from '@/components/usage-popover';
|
||||||
import { useAppStore } from '@/store/app-store';
|
import { useAppStore } from '@/store/app-store';
|
||||||
import { useSetupStore } from '@/store/setup-store';
|
import { useSetupStore } from '@/store/setup-store';
|
||||||
import { AutoModeSettingsDialog } from './dialogs/auto-mode-settings-dialog';
|
import { AutoModeSettingsDialog } from './dialogs/auto-mode-settings-dialog';
|
||||||
|
import { getHttpApiClient } from '@/lib/http-api-client';
|
||||||
|
|
||||||
interface BoardHeaderProps {
|
interface BoardHeaderProps {
|
||||||
projectName: string;
|
projectName: string;
|
||||||
|
projectPath: string;
|
||||||
maxConcurrency: number;
|
maxConcurrency: number;
|
||||||
runningAgentsCount: number;
|
runningAgentsCount: number;
|
||||||
onConcurrencyChange: (value: number) => void;
|
onConcurrencyChange: (value: number) => void;
|
||||||
isAutoModeRunning: boolean;
|
isAutoModeRunning: boolean;
|
||||||
onAutoModeToggle: (enabled: boolean) => void;
|
onAutoModeToggle: (enabled: boolean) => void;
|
||||||
onAddFeature: () => void;
|
|
||||||
onOpenPlanDialog: () => void;
|
onOpenPlanDialog: () => void;
|
||||||
addFeatureShortcut: KeyboardShortcut;
|
|
||||||
isMounted: boolean;
|
isMounted: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,14 +29,13 @@ const controlContainerClass =
|
|||||||
|
|
||||||
export function BoardHeader({
|
export function BoardHeader({
|
||||||
projectName,
|
projectName,
|
||||||
|
projectPath,
|
||||||
maxConcurrency,
|
maxConcurrency,
|
||||||
runningAgentsCount,
|
runningAgentsCount,
|
||||||
onConcurrencyChange,
|
onConcurrencyChange,
|
||||||
isAutoModeRunning,
|
isAutoModeRunning,
|
||||||
onAutoModeToggle,
|
onAutoModeToggle,
|
||||||
onAddFeature,
|
|
||||||
onOpenPlanDialog,
|
onOpenPlanDialog,
|
||||||
addFeatureShortcut,
|
|
||||||
isMounted,
|
isMounted,
|
||||||
}: BoardHeaderProps) {
|
}: BoardHeaderProps) {
|
||||||
const [showAutoModeSettings, setShowAutoModeSettings] = useState(false);
|
const [showAutoModeSettings, setShowAutoModeSettings] = useState(false);
|
||||||
@@ -47,6 +45,29 @@ export function BoardHeader({
|
|||||||
const setSkipVerificationInAutoMode = useAppStore((state) => state.setSkipVerificationInAutoMode);
|
const setSkipVerificationInAutoMode = useAppStore((state) => state.setSkipVerificationInAutoMode);
|
||||||
const codexAuthStatus = useSetupStore((state) => state.codexAuthStatus);
|
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
|
// Claude usage tracking visibility logic
|
||||||
// Hide when using API key (only show for Claude Code CLI users)
|
// Hide when using API key (only show for Claude Code CLI users)
|
||||||
// Also hide on Windows for now (CLI usage command not supported)
|
// 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 */}
|
{/* Usage Popover - show if either provider is authenticated */}
|
||||||
{isMounted && (showClaudeUsage || showCodexUsage) && <UsagePopover />}
|
{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 && (
|
{isMounted && (
|
||||||
<div className={controlContainerClass} data-testid="concurrency-slider-container">
|
<div className={controlContainerClass} data-testid="worktrees-toggle-container">
|
||||||
<Bot className="w-4 h-4 text-muted-foreground" />
|
<GitBranch className="w-4 h-4 text-muted-foreground" />
|
||||||
<span className="text-sm font-medium">Agents</span>
|
<Label htmlFor="worktrees-toggle" className="text-sm font-medium cursor-pointer">
|
||||||
<Slider
|
Worktrees
|
||||||
value={[maxConcurrency]}
|
</Label>
|
||||||
onValueChange={(value) => onConcurrencyChange(value[0])}
|
<Switch
|
||||||
min={1}
|
id="worktrees-toggle"
|
||||||
max={10}
|
checked={isWorktreePanelVisible}
|
||||||
step={1}
|
onCheckedChange={handleWorktreePanelToggle}
|
||||||
className="w-20"
|
data-testid="worktrees-toggle"
|
||||||
data-testid="concurrency-slider"
|
|
||||||
/>
|
/>
|
||||||
<span
|
|
||||||
className="text-sm text-muted-foreground min-w-[5ch] text-center"
|
|
||||||
data-testid="concurrency-value"
|
|
||||||
>
|
|
||||||
{runningAgentsCount} / {maxConcurrency}
|
|
||||||
</span>
|
|
||||||
</div>
|
</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 */}
|
{/* Auto Mode Toggle - only show after mount to prevent hydration issues */}
|
||||||
{isMounted && (
|
{isMounted && (
|
||||||
<div className={controlContainerClass} data-testid="auto-mode-toggle-container">
|
<div className={controlContainerClass} data-testid="auto-mode-toggle-container">
|
||||||
@@ -134,17 +191,6 @@ export function BoardHeader({
|
|||||||
<Wand2 className="w-4 h-4 mr-2" />
|
<Wand2 className="w-4 h-4 mr-2" />
|
||||||
Plan
|
Plan
|
||||||
</Button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import { useEffect, useState } from 'react';
|
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 type { ReasoningEffort } from '@automaker/types';
|
||||||
import { getProviderFromModel } from '@/lib/utils';
|
import { getProviderFromModel } from '@/lib/utils';
|
||||||
import {
|
import {
|
||||||
@@ -68,12 +68,9 @@ export function AgentInfoPanel({
|
|||||||
summary,
|
summary,
|
||||||
isCurrentAutoTask,
|
isCurrentAutoTask,
|
||||||
}: AgentInfoPanelProps) {
|
}: AgentInfoPanelProps) {
|
||||||
const { kanbanCardDetailLevel } = useAppStore();
|
|
||||||
const [agentInfo, setAgentInfo] = useState<AgentTaskInfo | null>(null);
|
const [agentInfo, setAgentInfo] = useState<AgentTaskInfo | null>(null);
|
||||||
const [isSummaryDialogOpen, setIsSummaryDialogOpen] = useState(false);
|
const [isSummaryDialogOpen, setIsSummaryDialogOpen] = useState(false);
|
||||||
|
|
||||||
const showAgentInfo = kanbanCardDetailLevel === 'detailed';
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadContext = async () => {
|
const loadContext = async () => {
|
||||||
if (contextContent) {
|
if (contextContent) {
|
||||||
@@ -123,7 +120,7 @@ export function AgentInfoPanel({
|
|||||||
}
|
}
|
||||||
}, [feature.id, feature.status, contextContent, isCurrentAutoTask]);
|
}, [feature.id, feature.status, contextContent, isCurrentAutoTask]);
|
||||||
// Model/Preset Info for Backlog Cards
|
// Model/Preset Info for Backlog Cards
|
||||||
if (showAgentInfo && feature.status === 'backlog') {
|
if (feature.status === 'backlog') {
|
||||||
const provider = getProviderFromModel(feature.model);
|
const provider = getProviderFromModel(feature.model);
|
||||||
const isCodex = provider === 'codex';
|
const isCodex = provider === 'codex';
|
||||||
const isClaude = provider === 'claude';
|
const isClaude = provider === 'claude';
|
||||||
@@ -160,7 +157,7 @@ export function AgentInfoPanel({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Agent Info Panel for non-backlog cards
|
// Agent Info Panel for non-backlog cards
|
||||||
if (showAgentInfo && feature.status !== 'backlog' && agentInfo) {
|
if (feature.status !== 'backlog' && agentInfo) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="mb-3 space-y-2 overflow-hidden">
|
<div className="mb-3 space-y-2 overflow-hidden">
|
||||||
@@ -255,7 +252,11 @@ export function AgentInfoPanel({
|
|||||||
<Expand className="w-3 h-3" />
|
<Expand className="w-3 h-3" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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}
|
{feature.summary || summary || agentInfo.summary}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -292,58 +293,15 @@ export function AgentInfoPanel({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show just the todo list for non-backlog features when showAgentInfo is false
|
// Always render SummaryDialog (even if no agentInfo yet)
|
||||||
// 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)
|
|
||||||
// This ensures the dialog can be opened from the expand button
|
// This ensures the dialog can be opened from the expand button
|
||||||
return (
|
return (
|
||||||
<>
|
<SummaryDialog
|
||||||
{showAgentInfo && (
|
feature={feature}
|
||||||
<SummaryDialog
|
agentInfo={agentInfo}
|
||||||
feature={feature}
|
summary={summary}
|
||||||
agentInfo={agentInfo}
|
isOpen={isSummaryDialogOpen}
|
||||||
summary={summary}
|
onOpenChange={setIsSummaryDialogOpen}
|
||||||
isOpen={isSummaryDialogOpen}
|
/>
|
||||||
onOpenChange={setIsSummaryDialogOpen}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,8 +31,10 @@ export function SummaryDialog({
|
|||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||||
<DialogContent
|
<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}`}
|
data-testid={`summary-dialog-${feature.id}`}
|
||||||
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
|
onMouseDown={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ interface KanbanColumnProps {
|
|||||||
count: number;
|
count: number;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
headerAction?: ReactNode;
|
headerAction?: ReactNode;
|
||||||
|
/** Floating action button at the bottom of the column */
|
||||||
|
footerAction?: ReactNode;
|
||||||
opacity?: number;
|
opacity?: number;
|
||||||
showBorder?: boolean;
|
showBorder?: boolean;
|
||||||
hideScrollbar?: boolean;
|
hideScrollbar?: boolean;
|
||||||
@@ -24,6 +26,7 @@ export const KanbanColumn = memo(function KanbanColumn({
|
|||||||
count,
|
count,
|
||||||
children,
|
children,
|
||||||
headerAction,
|
headerAction,
|
||||||
|
footerAction,
|
||||||
opacity = 100,
|
opacity = 100,
|
||||||
showBorder = true,
|
showBorder = true,
|
||||||
hideScrollbar = false,
|
hideScrollbar = false,
|
||||||
@@ -79,12 +82,21 @@ export const KanbanColumn = memo(function KanbanColumn({
|
|||||||
hideScrollbar &&
|
hideScrollbar &&
|
||||||
'[&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]',
|
'[&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]',
|
||||||
// Smooth scrolling
|
// Smooth scrolling
|
||||||
'scroll-smooth'
|
'scroll-smooth',
|
||||||
|
// Add padding at bottom if there's a footer action
|
||||||
|
footerAction && 'pb-14'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</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 */}
|
{/* Drop zone indicator when dragging over */}
|
||||||
{isOver && (
|
{isOver && (
|
||||||
<div className="absolute inset-0 rounded-xl bg-primary/5 pointer-events-none z-5 border-2 border-dashed border-primary/20" />
|
<div className="absolute inset-0 rounded-xl bg-primary/5 pointer-events-none z-5 border-2 border-dashed border-primary/20" />
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
import {
|
import {
|
||||||
getAncestors,
|
getAncestors,
|
||||||
formatAncestorContextForPrompt,
|
formatAncestorContextForPrompt,
|
||||||
@@ -492,23 +493,44 @@ export function AddFeatureDialog({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div className="grid gap-3 grid-cols-2">
|
||||||
className={cn(
|
<div className="space-y-1.5">
|
||||||
'grid gap-3',
|
<Label
|
||||||
modelSupportsPlanningMode ? 'grid-cols-2' : 'grid-cols-1'
|
className={cn(
|
||||||
)}
|
'text-xs text-muted-foreground',
|
||||||
>
|
!modelSupportsPlanningMode && 'opacity-50'
|
||||||
{modelSupportsPlanningMode && (
|
)}
|
||||||
<div className="space-y-1.5">
|
>
|
||||||
<Label className="text-xs text-muted-foreground">Planning</Label>
|
Planning
|
||||||
|
</Label>
|
||||||
|
{modelSupportsPlanningMode ? (
|
||||||
<PlanningModeSelect
|
<PlanningModeSelect
|
||||||
mode={planningMode}
|
mode={planningMode}
|
||||||
onModeChange={setPlanningMode}
|
onModeChange={setPlanningMode}
|
||||||
testIdPrefix="add-feature-planning"
|
testIdPrefix="add-feature-planning"
|
||||||
compact
|
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">
|
<div className="space-y-1.5">
|
||||||
<Label className="text-xs text-muted-foreground">Options</Label>
|
<Label className="text-xs text-muted-foreground">Options</Label>
|
||||||
<div className="flex flex-col gap-2 pt-1">
|
<div className="flex flex-col gap-2 pt-1">
|
||||||
@@ -526,28 +548,32 @@ export function AddFeatureDialog({
|
|||||||
Run tests
|
Run tests
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
{modelSupportsPlanningMode && (
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<Checkbox
|
||||||
<Checkbox
|
id="add-feature-require-approval"
|
||||||
id="add-feature-require-approval"
|
checked={requirePlanApproval}
|
||||||
checked={requirePlanApproval}
|
onCheckedChange={(checked) => setRequirePlanApproval(!!checked)}
|
||||||
onCheckedChange={(checked) => setRequirePlanApproval(!!checked)}
|
disabled={
|
||||||
disabled={planningMode === 'skip' || planningMode === 'lite'}
|
!modelSupportsPlanningMode ||
|
||||||
data-testid="add-feature-require-approval-checkbox"
|
planningMode === 'skip' ||
|
||||||
/>
|
planningMode === 'lite'
|
||||||
<Label
|
}
|
||||||
htmlFor="add-feature-require-approval"
|
data-testid="add-feature-require-approval-checkbox"
|
||||||
className={cn(
|
/>
|
||||||
'text-xs font-normal',
|
<Label
|
||||||
planningMode === 'skip' || planningMode === 'lite'
|
htmlFor="add-feature-require-approval"
|
||||||
? 'cursor-not-allowed text-muted-foreground'
|
className={cn(
|
||||||
: 'cursor-pointer'
|
'text-xs font-normal',
|
||||||
)}
|
!modelSupportsPlanningMode ||
|
||||||
>
|
planningMode === 'skip' ||
|
||||||
Require approval
|
planningMode === 'lite'
|
||||||
</Label>
|
? 'cursor-not-allowed text-muted-foreground'
|
||||||
</div>
|
: 'cursor-pointer'
|
||||||
)}
|
)}
|
||||||
|
>
|
||||||
|
Require approval
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||||
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
import { DependencyTreeDialog } from './dependency-tree-dialog';
|
import { DependencyTreeDialog } from './dependency-tree-dialog';
|
||||||
import { isClaudeModel, supportsReasoningEffort } from '@automaker/types';
|
import { isClaudeModel, supportsReasoningEffort } from '@automaker/types';
|
||||||
|
|
||||||
@@ -516,23 +517,44 @@ export function EditFeatureDialog({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div className="grid gap-3 grid-cols-2">
|
||||||
className={cn(
|
<div className="space-y-1.5">
|
||||||
'grid gap-3',
|
<Label
|
||||||
modelSupportsPlanningMode ? 'grid-cols-2' : 'grid-cols-1'
|
className={cn(
|
||||||
)}
|
'text-xs text-muted-foreground',
|
||||||
>
|
!modelSupportsPlanningMode && 'opacity-50'
|
||||||
{modelSupportsPlanningMode && (
|
)}
|
||||||
<div className="space-y-1.5">
|
>
|
||||||
<Label className="text-xs text-muted-foreground">Planning</Label>
|
Planning
|
||||||
|
</Label>
|
||||||
|
{modelSupportsPlanningMode ? (
|
||||||
<PlanningModeSelect
|
<PlanningModeSelect
|
||||||
mode={planningMode}
|
mode={planningMode}
|
||||||
onModeChange={setPlanningMode}
|
onModeChange={setPlanningMode}
|
||||||
testIdPrefix="edit-feature-planning"
|
testIdPrefix="edit-feature-planning"
|
||||||
compact
|
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">
|
<div className="space-y-1.5">
|
||||||
<Label className="text-xs text-muted-foreground">Options</Label>
|
<Label className="text-xs text-muted-foreground">Options</Label>
|
||||||
<div className="flex flex-col gap-2 pt-1">
|
<div className="flex flex-col gap-2 pt-1">
|
||||||
@@ -552,28 +574,32 @@ export function EditFeatureDialog({
|
|||||||
Run tests
|
Run tests
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
{modelSupportsPlanningMode && (
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<Checkbox
|
||||||
<Checkbox
|
id="edit-feature-require-approval"
|
||||||
id="edit-feature-require-approval"
|
checked={requirePlanApproval}
|
||||||
checked={requirePlanApproval}
|
onCheckedChange={(checked) => setRequirePlanApproval(!!checked)}
|
||||||
onCheckedChange={(checked) => setRequirePlanApproval(!!checked)}
|
disabled={
|
||||||
disabled={planningMode === 'skip' || planningMode === 'lite'}
|
!modelSupportsPlanningMode ||
|
||||||
data-testid="edit-feature-require-approval-checkbox"
|
planningMode === 'skip' ||
|
||||||
/>
|
planningMode === 'lite'
|
||||||
<Label
|
}
|
||||||
htmlFor="edit-feature-require-approval"
|
data-testid="edit-feature-require-approval-checkbox"
|
||||||
className={cn(
|
/>
|
||||||
'text-xs font-normal',
|
<Label
|
||||||
planningMode === 'skip' || planningMode === 'lite'
|
htmlFor="edit-feature-require-approval"
|
||||||
? 'cursor-not-allowed text-muted-foreground'
|
className={cn(
|
||||||
: 'cursor-pointer'
|
'text-xs font-normal',
|
||||||
)}
|
!modelSupportsPlanningMode ||
|
||||||
>
|
planningMode === 'skip' ||
|
||||||
Require approval
|
planningMode === 'lite'
|
||||||
</Label>
|
? 'cursor-not-allowed text-muted-foreground'
|
||||||
</div>
|
: 'cursor-pointer'
|
||||||
)}
|
)}
|
||||||
|
>
|
||||||
|
Require approval
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,8 +15,9 @@ import { modelSupportsThinking } from '@/lib/utils';
|
|||||||
import { Feature, ModelAlias, ThinkingLevel, PlanningMode } from '@/store/app-store';
|
import { Feature, ModelAlias, ThinkingLevel, PlanningMode } from '@/store/app-store';
|
||||||
import { TestingTabContent, PrioritySelect, PlanningModeSelect } from '../shared';
|
import { TestingTabContent, PrioritySelect, PlanningModeSelect } from '../shared';
|
||||||
import { PhaseModelSelector } from '@/components/views/settings-view/model-defaults/phase-model-selector';
|
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 { cn } from '@/lib/utils';
|
||||||
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
|
|
||||||
interface MassEditDialogProps {
|
interface MassEditDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -167,6 +168,7 @@ export function MassEditDialog({ open, onClose, selectedFeatures, onApply }: Mas
|
|||||||
const hasAnyApply = Object.values(applyState).some(Boolean);
|
const hasAnyApply = Object.values(applyState).some(Boolean);
|
||||||
const isCurrentModelCursor = isCursorModel(model);
|
const isCurrentModelCursor = isCursorModel(model);
|
||||||
const modelAllowsThinking = !isCurrentModelCursor && modelSupportsThinking(model);
|
const modelAllowsThinking = !isCurrentModelCursor && modelSupportsThinking(model);
|
||||||
|
const modelSupportsPlanningMode = isClaudeModel(model);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={(open) => !open && onClose()}>
|
<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" />
|
<div className="border-t border-border" />
|
||||||
|
|
||||||
{/* Planning Mode */}
|
{/* Planning Mode */}
|
||||||
<FieldWrapper
|
{modelSupportsPlanningMode ? (
|
||||||
label="Planning Mode"
|
<FieldWrapper
|
||||||
isMixed={mixedValues.planningMode || mixedValues.requirePlanApproval}
|
label="Planning Mode"
|
||||||
willApply={applyState.planningMode || applyState.requirePlanApproval}
|
isMixed={mixedValues.planningMode || mixedValues.requirePlanApproval}
|
||||||
onApplyChange={(apply) =>
|
willApply={applyState.planningMode || applyState.requirePlanApproval}
|
||||||
setApplyState((prev) => ({
|
onApplyChange={(apply) =>
|
||||||
...prev,
|
setApplyState((prev) => ({
|
||||||
planningMode: apply,
|
...prev,
|
||||||
requirePlanApproval: apply,
|
planningMode: apply,
|
||||||
}))
|
requirePlanApproval: apply,
|
||||||
}
|
}))
|
||||||
>
|
}
|
||||||
<PlanningModeSelect
|
>
|
||||||
mode={planningMode}
|
<PlanningModeSelect
|
||||||
onModeChange={(newMode) => {
|
mode={planningMode}
|
||||||
setPlanningMode(newMode);
|
onModeChange={(newMode) => {
|
||||||
// Auto-suggest approval based on mode, but user can override
|
setPlanningMode(newMode);
|
||||||
setRequirePlanApproval(newMode === 'spec' || newMode === 'full');
|
// Auto-suggest approval based on mode, but user can override
|
||||||
}}
|
setRequirePlanApproval(newMode === 'spec' || newMode === 'full');
|
||||||
requireApproval={requirePlanApproval}
|
}}
|
||||||
onRequireApprovalChange={setRequirePlanApproval}
|
requireApproval={requirePlanApproval}
|
||||||
testIdPrefix="mass-edit-planning"
|
onRequireApprovalChange={setRequirePlanApproval}
|
||||||
/>
|
testIdPrefix="mass-edit-planning"
|
||||||
</FieldWrapper>
|
/>
|
||||||
|
</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 */}
|
{/* Priority */}
|
||||||
<FieldWrapper
|
<FieldWrapper
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { DndContext, DragOverlay } from '@dnd-kit/core';
|
|||||||
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { KanbanColumn, KanbanCard } from './components';
|
import { KanbanColumn, KanbanCard } from './components';
|
||||||
import { Feature } from '@/store/app-store';
|
import { Feature, useAppStore, formatShortcut } from '@/store/app-store';
|
||||||
import { Archive, Settings2, CheckSquare, GripVertical } from 'lucide-react';
|
import { Archive, Settings2, CheckSquare, GripVertical, Plus } from 'lucide-react';
|
||||||
import { useResponsiveKanban } from '@/hooks/use-responsive-kanban';
|
import { useResponsiveKanban } from '@/hooks/use-responsive-kanban';
|
||||||
import { getColumnsWithPipeline, type ColumnId } from './constants';
|
import { getColumnsWithPipeline, type ColumnId } from './constants';
|
||||||
import type { PipelineConfig } from '@automaker/types';
|
import type { PipelineConfig } from '@automaker/types';
|
||||||
@@ -43,6 +43,7 @@ interface KanbanBoardProps {
|
|||||||
featuresWithContext: Set<string>;
|
featuresWithContext: Set<string>;
|
||||||
runningAutoTasks: string[];
|
runningAutoTasks: string[];
|
||||||
onArchiveAllVerified: () => void;
|
onArchiveAllVerified: () => void;
|
||||||
|
onAddFeature: () => void;
|
||||||
pipelineConfig: PipelineConfig | null;
|
pipelineConfig: PipelineConfig | null;
|
||||||
onOpenPipelineSettings?: () => void;
|
onOpenPipelineSettings?: () => void;
|
||||||
// Selection mode props
|
// Selection mode props
|
||||||
@@ -78,6 +79,7 @@ export function KanbanBoard({
|
|||||||
featuresWithContext,
|
featuresWithContext,
|
||||||
runningAutoTasks,
|
runningAutoTasks,
|
||||||
onArchiveAllVerified,
|
onArchiveAllVerified,
|
||||||
|
onAddFeature,
|
||||||
pipelineConfig,
|
pipelineConfig,
|
||||||
onOpenPipelineSettings,
|
onOpenPipelineSettings,
|
||||||
isSelectionMode = false,
|
isSelectionMode = false,
|
||||||
@@ -88,6 +90,10 @@ export function KanbanBoard({
|
|||||||
// Generate columns including pipeline steps
|
// Generate columns including pipeline steps
|
||||||
const columns = useMemo(() => getColumnsWithPipeline(pipelineConfig), [pipelineConfig]);
|
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
|
// Use responsive column widths based on window size
|
||||||
// containerStyle handles centering and ensures columns fit without horizontal scroll in Electron
|
// containerStyle handles centering and ensures columns fit without horizontal scroll in Electron
|
||||||
const { columnWidth, containerStyle } = useResponsiveKanban(columns.length);
|
const { columnWidth, containerStyle } = useResponsiveKanban(columns.length);
|
||||||
@@ -127,26 +133,38 @@ export function KanbanBoard({
|
|||||||
Complete All
|
Complete All
|
||||||
</Button>
|
</Button>
|
||||||
) : column.id === 'backlog' ? (
|
) : column.id === 'backlog' ? (
|
||||||
<Button
|
<div className="flex items-center gap-1">
|
||||||
variant="ghost"
|
<Button
|
||||||
size="sm"
|
variant="default"
|
||||||
className={`h-6 px-2 text-xs ${isSelectionMode ? 'text-primary bg-primary/10' : 'text-muted-foreground hover:text-foreground'}`}
|
size="sm"
|
||||||
onClick={onToggleSelectionMode}
|
className="h-6 w-6 p-0"
|
||||||
title={isSelectionMode ? 'Switch to Drag Mode' : 'Select Multiple'}
|
onClick={onAddFeature}
|
||||||
data-testid="selection-mode-button"
|
title="Add Feature"
|
||||||
>
|
data-testid="add-feature-button"
|
||||||
{isSelectionMode ? (
|
>
|
||||||
<>
|
<Plus className="w-3.5 h-3.5" />
|
||||||
<GripVertical className="w-3.5 h-3.5 mr-1" />
|
</Button>
|
||||||
Drag
|
<Button
|
||||||
</>
|
variant="ghost"
|
||||||
) : (
|
size="sm"
|
||||||
<>
|
className={`h-6 px-2 text-xs ${isSelectionMode ? 'text-primary bg-primary/10' : 'text-muted-foreground hover:text-foreground'}`}
|
||||||
<CheckSquare className="w-3.5 h-3.5 mr-1" />
|
onClick={onToggleSelectionMode}
|
||||||
Select
|
title={isSelectionMode ? 'Switch to Drag Mode' : 'Select Multiple'}
|
||||||
</>
|
data-testid="selection-mode-button"
|
||||||
)}
|
>
|
||||||
</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' ? (
|
) : column.id === 'in_progress' ? (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -171,6 +189,23 @@ export function KanbanBoard({
|
|||||||
</Button>
|
</Button>
|
||||||
) : undefined
|
) : 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
|
<SortableContext
|
||||||
items={columnFeatures.map((f) => f.id)}
|
items={columnFeatures.map((f) => f.id)}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { Button } from '@/components/ui/button';
|
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 { cn, pathsEqual } from '@/lib/utils';
|
||||||
import { useAppStore } from '@/store/app-store';
|
|
||||||
import type { WorktreePanelProps, WorktreeInfo } from './types';
|
import type { WorktreePanelProps, WorktreeInfo } from './types';
|
||||||
import {
|
import {
|
||||||
useWorktrees,
|
useWorktrees,
|
||||||
@@ -83,12 +82,6 @@ export function WorktreePanel({
|
|||||||
features,
|
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
|
// 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
|
// Reduced from 1s to 5s to minimize GPU/CPU usage from frequent re-renders
|
||||||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
@@ -104,18 +97,6 @@ export function WorktreePanel({
|
|||||||
};
|
};
|
||||||
}, [fetchWorktrees]);
|
}, [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) => {
|
const isWorktreeSelected = (worktree: WorktreeInfo) => {
|
||||||
return worktree.isMain
|
return worktree.isMain
|
||||||
? currentWorktree === null || currentWorktree === undefined || currentWorktree.path === null
|
? currentWorktree === null || currentWorktree === undefined || currentWorktree.path === null
|
||||||
@@ -138,44 +119,8 @@ export function WorktreePanel({
|
|||||||
const mainWorktree = worktrees.find((w) => w.isMain);
|
const mainWorktree = worktrees.find((w) => w.isMain);
|
||||||
const nonMainWorktrees = worktrees.filter((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 (
|
return (
|
||||||
<div className="flex items-center gap-2 px-4 py-2 border-b border-border bg-glass/50 backdrop-blur-sm">
|
<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" />
|
<GitBranch className="w-4 h-4 text-muted-foreground" />
|
||||||
<span className="text-sm text-muted-foreground mr-2">Branch:</span>
|
<span className="text-sm text-muted-foreground mr-2">Branch:</span>
|
||||||
|
|
||||||
|
|||||||
885
apps/ui/src/components/views/dashboard-view.tsx
Normal file
885
apps/ui/src/components/views/dashboard-view.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -86,8 +86,8 @@ export function SetupView() {
|
|||||||
const handleFinish = () => {
|
const handleFinish = () => {
|
||||||
logger.debug('[Setup Flow] handleFinish called - completing setup');
|
logger.debug('[Setup Flow] handleFinish called - completing setup');
|
||||||
completeSetup();
|
completeSetup();
|
||||||
logger.debug('[Setup Flow] Setup completed, redirecting to welcome view');
|
logger.debug('[Setup Flow] Setup completed, redirecting to dashboard');
|
||||||
navigate({ to: '/' });
|
navigate({ to: '/dashboard' });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export function useProjectSettingsLoader() {
|
|||||||
const setCardBorderEnabled = useAppStore((state) => state.setCardBorderEnabled);
|
const setCardBorderEnabled = useAppStore((state) => state.setCardBorderEnabled);
|
||||||
const setCardBorderOpacity = useAppStore((state) => state.setCardBorderOpacity);
|
const setCardBorderOpacity = useAppStore((state) => state.setCardBorderOpacity);
|
||||||
const setHideScrollbar = useAppStore((state) => state.setHideScrollbar);
|
const setHideScrollbar = useAppStore((state) => state.setHideScrollbar);
|
||||||
|
const setWorktreePanelVisible = useAppStore((state) => state.setWorktreePanelVisible);
|
||||||
|
|
||||||
const loadingRef = useRef<string | null>(null);
|
const loadingRef = useRef<string | null>(null);
|
||||||
const currentProjectRef = 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);
|
(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) {
|
} catch (error) {
|
||||||
console.error('Failed to load project settings:', error);
|
console.error('Failed to load project settings:', error);
|
||||||
|
|||||||
@@ -139,7 +139,6 @@ export function parseLocalStorageSettings(): Partial<GlobalSettings> | null {
|
|||||||
theme: state.theme as GlobalSettings['theme'],
|
theme: state.theme as GlobalSettings['theme'],
|
||||||
sidebarOpen: state.sidebarOpen as boolean,
|
sidebarOpen: state.sidebarOpen as boolean,
|
||||||
chatHistoryOpen: state.chatHistoryOpen as boolean,
|
chatHistoryOpen: state.chatHistoryOpen as boolean,
|
||||||
kanbanCardDetailLevel: state.kanbanCardDetailLevel as GlobalSettings['kanbanCardDetailLevel'],
|
|
||||||
maxConcurrency: state.maxConcurrency as number,
|
maxConcurrency: state.maxConcurrency as number,
|
||||||
defaultSkipTests: state.defaultSkipTests as boolean,
|
defaultSkipTests: state.defaultSkipTests as boolean,
|
||||||
enableDependencyBlocking: state.enableDependencyBlocking as boolean,
|
enableDependencyBlocking: state.enableDependencyBlocking as boolean,
|
||||||
@@ -504,6 +503,7 @@ export function hydrateStoreFromSettings(settings: GlobalSettings): void {
|
|||||||
path: ref.path,
|
path: ref.path,
|
||||||
lastOpened: ref.lastOpened,
|
lastOpened: ref.lastOpened,
|
||||||
theme: ref.theme,
|
theme: ref.theme,
|
||||||
|
isFavorite: ref.isFavorite,
|
||||||
features: [], // Features are loaded separately when project is opened
|
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,
|
theme: settings.theme as unknown as import('@/store/app-store').ThemeMode,
|
||||||
sidebarOpen: settings.sidebarOpen ?? true,
|
sidebarOpen: settings.sidebarOpen ?? true,
|
||||||
chatHistoryOpen: settings.chatHistoryOpen ?? false,
|
chatHistoryOpen: settings.chatHistoryOpen ?? false,
|
||||||
kanbanCardDetailLevel: settings.kanbanCardDetailLevel ?? 'standard',
|
|
||||||
maxConcurrency: settings.maxConcurrency ?? 3,
|
maxConcurrency: settings.maxConcurrency ?? 3,
|
||||||
defaultSkipTests: settings.defaultSkipTests ?? true,
|
defaultSkipTests: settings.defaultSkipTests ?? true,
|
||||||
enableDependencyBlocking: settings.enableDependencyBlocking ?? true,
|
enableDependencyBlocking: settings.enableDependencyBlocking ?? true,
|
||||||
@@ -581,7 +580,6 @@ function buildSettingsUpdateFromStore(): Record<string, unknown> {
|
|||||||
theme: state.theme,
|
theme: state.theme,
|
||||||
sidebarOpen: state.sidebarOpen,
|
sidebarOpen: state.sidebarOpen,
|
||||||
chatHistoryOpen: state.chatHistoryOpen,
|
chatHistoryOpen: state.chatHistoryOpen,
|
||||||
kanbanCardDetailLevel: state.kanbanCardDetailLevel,
|
|
||||||
maxConcurrency: state.maxConcurrency,
|
maxConcurrency: state.maxConcurrency,
|
||||||
defaultSkipTests: state.defaultSkipTests,
|
defaultSkipTests: state.defaultSkipTests,
|
||||||
enableDependencyBlocking: state.enableDependencyBlocking,
|
enableDependencyBlocking: state.enableDependencyBlocking,
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ const SETTINGS_FIELDS_TO_SYNC = [
|
|||||||
'theme',
|
'theme',
|
||||||
'sidebarOpen',
|
'sidebarOpen',
|
||||||
'chatHistoryOpen',
|
'chatHistoryOpen',
|
||||||
'kanbanCardDetailLevel',
|
|
||||||
'maxConcurrency',
|
'maxConcurrency',
|
||||||
'defaultSkipTests',
|
'defaultSkipTests',
|
||||||
'enableDependencyBlocking',
|
'enableDependencyBlocking',
|
||||||
@@ -379,7 +378,6 @@ export async function refreshSettingsFromServer(): Promise<boolean> {
|
|||||||
theme: serverSettings.theme as unknown as ThemeMode,
|
theme: serverSettings.theme as unknown as ThemeMode,
|
||||||
sidebarOpen: serverSettings.sidebarOpen,
|
sidebarOpen: serverSettings.sidebarOpen,
|
||||||
chatHistoryOpen: serverSettings.chatHistoryOpen,
|
chatHistoryOpen: serverSettings.chatHistoryOpen,
|
||||||
kanbanCardDetailLevel: serverSettings.kanbanCardDetailLevel,
|
|
||||||
maxConcurrency: serverSettings.maxConcurrency,
|
maxConcurrency: serverSettings.maxConcurrency,
|
||||||
defaultSkipTests: serverSettings.defaultSkipTests,
|
defaultSkipTests: serverSettings.defaultSkipTests,
|
||||||
enableDependencyBlocking: serverSettings.enableDependencyBlocking,
|
enableDependencyBlocking: serverSettings.enableDependencyBlocking,
|
||||||
|
|||||||
@@ -3003,6 +3003,7 @@ export interface Project {
|
|||||||
path: string;
|
path: string;
|
||||||
lastOpened?: string;
|
lastOpened?: string;
|
||||||
theme?: string; // Per-project theme override (uses ThemeMode from app-store)
|
theme?: string; // Per-project theme override (uses ThemeMode from app-store)
|
||||||
|
isFavorite?: boolean; // Pin project to top of dashboard
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TrashedProject extends Project {
|
export interface TrashedProject extends Project {
|
||||||
|
|||||||
@@ -1858,7 +1858,6 @@ export class HttpApiClient implements ElectronAPI {
|
|||||||
theme: string;
|
theme: string;
|
||||||
sidebarOpen: boolean;
|
sidebarOpen: boolean;
|
||||||
chatHistoryOpen: boolean;
|
chatHistoryOpen: boolean;
|
||||||
kanbanCardDetailLevel: string;
|
|
||||||
maxConcurrency: number;
|
maxConcurrency: number;
|
||||||
defaultSkipTests: boolean;
|
defaultSkipTests: boolean;
|
||||||
enableDependencyBlocking: boolean;
|
enableDependencyBlocking: boolean;
|
||||||
@@ -1951,6 +1950,7 @@ export class HttpApiClient implements ElectronAPI {
|
|||||||
cardBorderOpacity: number;
|
cardBorderOpacity: number;
|
||||||
hideScrollbar: boolean;
|
hideScrollbar: boolean;
|
||||||
};
|
};
|
||||||
|
worktreePanelVisible?: boolean;
|
||||||
lastSelectedSessionId?: string;
|
lastSelectedSessionId?: string;
|
||||||
};
|
};
|
||||||
error?: string;
|
error?: string;
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ function RootLayoutContent() {
|
|||||||
const isSetupRoute = location.pathname === '/setup';
|
const isSetupRoute = location.pathname === '/setup';
|
||||||
const isLoginRoute = location.pathname === '/login';
|
const isLoginRoute = location.pathname === '/login';
|
||||||
const isLoggedOutRoute = location.pathname === '/logged-out';
|
const isLoggedOutRoute = location.pathname === '/logged-out';
|
||||||
|
const isDashboardRoute = location.pathname === '/dashboard';
|
||||||
|
|
||||||
// Sandbox environment check state
|
// Sandbox environment check state
|
||||||
type SandboxStatus = 'pending' | 'containerized' | 'needs-confirmation' | 'denied' | 'confirmed';
|
type SandboxStatus = 'pending' | 'containerized' | 'needs-confirmation' | 'denied' | 'confirmed';
|
||||||
@@ -389,9 +390,9 @@ function RootLayoutContent() {
|
|||||||
return;
|
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') {
|
if (setupComplete && location.pathname === '/setup') {
|
||||||
navigate({ to: '/' });
|
navigate({ to: '/dashboard' });
|
||||||
}
|
}
|
||||||
}, [authChecked, isAuthenticated, setupComplete, location.pathname, navigate]);
|
}, [authChecked, isAuthenticated, setupComplete, location.pathname, navigate]);
|
||||||
|
|
||||||
@@ -425,10 +426,16 @@ function RootLayoutContent() {
|
|||||||
testConnection();
|
testConnection();
|
||||||
}, [setIpcConnected]);
|
}, [setIpcConnected]);
|
||||||
|
|
||||||
// Restore to board view if a project was previously open
|
// Redirect from welcome page based on project state
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isMounted && currentProject && location.pathname === '/') {
|
if (isMounted && location.pathname === '/') {
|
||||||
navigate({ to: '/board' });
|
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]);
|
}, [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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<main className="flex h-screen overflow-hidden" data-testid="app-container">
|
<main className="flex h-screen overflow-hidden" data-testid="app-container">
|
||||||
|
|||||||
6
apps/ui/src/routes/dashboard.tsx
Normal file
6
apps/ui/src/routes/dashboard.tsx
Normal 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,
|
||||||
|
});
|
||||||
@@ -114,8 +114,6 @@ function saveThemeToStorage(theme: ThemeMode): void {
|
|||||||
setItem(THEME_STORAGE_KEY, theme);
|
setItem(THEME_STORAGE_KEY, theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type KanbanCardDetailLevel = 'minimal' | 'standard' | 'detailed';
|
|
||||||
|
|
||||||
export type BoardViewMode = 'kanban' | 'graph';
|
export type BoardViewMode = 'kanban' | 'graph';
|
||||||
|
|
||||||
export interface ApiKeys {
|
export interface ApiKeys {
|
||||||
@@ -508,7 +506,6 @@ export interface AppState {
|
|||||||
maxConcurrency: number; // Maximum number of concurrent agent tasks
|
maxConcurrency: number; // Maximum number of concurrent agent tasks
|
||||||
|
|
||||||
// Kanban Card Display Settings
|
// Kanban Card Display Settings
|
||||||
kanbanCardDetailLevel: KanbanCardDetailLevel; // Level of detail shown on kanban cards
|
|
||||||
boardViewMode: BoardViewMode; // Whether to show kanban or dependency graph view
|
boardViewMode: BoardViewMode; // Whether to show kanban or dependency graph view
|
||||||
|
|
||||||
// Feature Default Settings
|
// Feature Default Settings
|
||||||
@@ -656,6 +653,10 @@ export interface AppState {
|
|||||||
// Pipeline Configuration (per-project, keyed by project path)
|
// Pipeline Configuration (per-project, keyed by project path)
|
||||||
pipelineConfigByProject: Record<string, PipelineConfig>;
|
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)
|
// UI State (previously in localStorage, now synced via API)
|
||||||
/** Whether worktree panel is collapsed in board view */
|
/** Whether worktree panel is collapsed in board view */
|
||||||
worktreePanelCollapsed: boolean;
|
worktreePanelCollapsed: boolean;
|
||||||
@@ -816,6 +817,7 @@ export interface AppActions {
|
|||||||
cyclePrevProject: () => void; // Cycle back through project history (Q)
|
cyclePrevProject: () => void; // Cycle back through project history (Q)
|
||||||
cycleNextProject: () => void; // Cycle forward through project history (E)
|
cycleNextProject: () => void; // Cycle forward through project history (E)
|
||||||
clearProjectHistory: () => void; // Clear history, keeping only current project
|
clearProjectHistory: () => void; // Clear history, keeping only current project
|
||||||
|
toggleProjectFavorite: (projectId: string) => void; // Toggle project favorite status
|
||||||
|
|
||||||
// View actions
|
// View actions
|
||||||
setCurrentView: (view: ViewMode) => void;
|
setCurrentView: (view: ViewMode) => void;
|
||||||
@@ -869,7 +871,6 @@ export interface AppActions {
|
|||||||
setMaxConcurrency: (max: number) => void;
|
setMaxConcurrency: (max: number) => void;
|
||||||
|
|
||||||
// Kanban Card Settings actions
|
// Kanban Card Settings actions
|
||||||
setKanbanCardDetailLevel: (level: KanbanCardDetailLevel) => void;
|
|
||||||
setBoardViewMode: (mode: BoardViewMode) => void;
|
setBoardViewMode: (mode: BoardViewMode) => void;
|
||||||
|
|
||||||
// Feature Default Settings actions
|
// Feature Default Settings actions
|
||||||
@@ -1062,6 +1063,10 @@ export interface AppActions {
|
|||||||
deletePipelineStep: (projectPath: string, stepId: string) => void;
|
deletePipelineStep: (projectPath: string, stepId: string) => void;
|
||||||
reorderPipelineSteps: (projectPath: string, stepIds: 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)
|
// UI State actions (previously in localStorage, now synced via API)
|
||||||
setWorktreePanelCollapsed: (collapsed: boolean) => void;
|
setWorktreePanelCollapsed: (collapsed: boolean) => void;
|
||||||
setLastProjectDir: (dir: string) => void;
|
setLastProjectDir: (dir: string) => void;
|
||||||
@@ -1118,7 +1123,6 @@ const initialState: AppState = {
|
|||||||
autoModeByProject: {},
|
autoModeByProject: {},
|
||||||
autoModeActivityLog: [],
|
autoModeActivityLog: [],
|
||||||
maxConcurrency: 3, // Default to 3 concurrent agents
|
maxConcurrency: 3, // Default to 3 concurrent agents
|
||||||
kanbanCardDetailLevel: 'standard', // Default to standard detail level
|
|
||||||
boardViewMode: 'kanban', // Default to kanban view
|
boardViewMode: 'kanban', // Default to kanban view
|
||||||
defaultSkipTests: true, // Default to manual verification (tests disabled)
|
defaultSkipTests: true, // Default to manual verification (tests disabled)
|
||||||
enableDependencyBlocking: true, // Default to enabled (show dependency blocking UI)
|
enableDependencyBlocking: true, // Default to enabled (show dependency blocking UI)
|
||||||
@@ -1186,6 +1190,7 @@ const initialState: AppState = {
|
|||||||
codexModelsError: null,
|
codexModelsError: null,
|
||||||
codexModelsLastFetched: null,
|
codexModelsLastFetched: null,
|
||||||
pipelineConfigByProject: {},
|
pipelineConfigByProject: {},
|
||||||
|
worktreePanelVisibleByProject: {},
|
||||||
// UI State (previously in localStorage, now synced via API)
|
// UI State (previously in localStorage, now synced via API)
|
||||||
worktreePanelCollapsed: false,
|
worktreePanelCollapsed: false,
|
||||||
lastProjectDir: '',
|
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
|
// View actions
|
||||||
setCurrentView: (view) => set({ currentView: view }),
|
setCurrentView: (view) => set({ currentView: view }),
|
||||||
toggleSidebar: () => set({ sidebarOpen: !get().sidebarOpen }),
|
toggleSidebar: () => set({ sidebarOpen: !get().sidebarOpen }),
|
||||||
@@ -1704,7 +1726,6 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
|
|||||||
setMaxConcurrency: (max) => set({ maxConcurrency: max }),
|
setMaxConcurrency: (max) => set({ maxConcurrency: max }),
|
||||||
|
|
||||||
// Kanban Card Settings actions
|
// Kanban Card Settings actions
|
||||||
setKanbanCardDetailLevel: (level) => set({ kanbanCardDetailLevel: level }),
|
|
||||||
setBoardViewMode: (mode) => set({ boardViewMode: mode }),
|
setBoardViewMode: (mode) => set({ boardViewMode: mode }),
|
||||||
|
|
||||||
// Feature Default Settings actions
|
// 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)
|
// UI State actions (previously in localStorage, now synced via API)
|
||||||
setWorktreePanelCollapsed: (collapsed) => set({ worktreePanelCollapsed: collapsed }),
|
setWorktreePanelCollapsed: (collapsed) => set({ worktreePanelCollapsed: collapsed }),
|
||||||
setLastProjectDir: (dir) => set({ lastProjectDir: dir }),
|
setLastProjectDir: (dir) => set({ lastProjectDir: dir }),
|
||||||
|
|||||||
@@ -96,7 +96,6 @@ const SETTINGS_FIELDS_TO_SYNC = [
|
|||||||
'theme',
|
'theme',
|
||||||
'sidebarOpen',
|
'sidebarOpen',
|
||||||
'chatHistoryOpen',
|
'chatHistoryOpen',
|
||||||
'kanbanCardDetailLevel',
|
|
||||||
'maxConcurrency',
|
'maxConcurrency',
|
||||||
'defaultSkipTests',
|
'defaultSkipTests',
|
||||||
'enableDependencyBlocking',
|
'enableDependencyBlocking',
|
||||||
|
|||||||
@@ -109,7 +109,6 @@ export { DEFAULT_PROMPT_CUSTOMIZATION } from './prompts.js';
|
|||||||
// Settings types and constants
|
// Settings types and constants
|
||||||
export type {
|
export type {
|
||||||
ThemeMode,
|
ThemeMode,
|
||||||
KanbanCardDetailLevel,
|
|
||||||
PlanningMode,
|
PlanningMode,
|
||||||
ThinkingLevel,
|
ThinkingLevel,
|
||||||
ModelProvider,
|
ModelProvider,
|
||||||
|
|||||||
@@ -65,9 +65,6 @@ export type ThemeMode =
|
|||||||
| 'nordlight'
|
| 'nordlight'
|
||||||
| 'blossom';
|
| 'blossom';
|
||||||
|
|
||||||
/** KanbanCardDetailLevel - Controls how much information is displayed on kanban cards */
|
|
||||||
export type KanbanCardDetailLevel = 'minimal' | 'standard' | 'detailed';
|
|
||||||
|
|
||||||
/** PlanningMode - Planning levels for feature generation workflows */
|
/** PlanningMode - Planning levels for feature generation workflows */
|
||||||
export type PlanningMode = 'skip' | 'lite' | 'spec' | 'full';
|
export type PlanningMode = 'skip' | 'lite' | 'spec' | 'full';
|
||||||
|
|
||||||
@@ -294,6 +291,8 @@ export interface ProjectRef {
|
|||||||
lastOpened?: string;
|
lastOpened?: string;
|
||||||
/** Project-specific theme override (or undefined to use global) */
|
/** Project-specific theme override (or undefined to use global) */
|
||||||
theme?: string;
|
theme?: string;
|
||||||
|
/** Whether project is pinned to favorites on dashboard */
|
||||||
|
isFavorite?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -360,8 +359,6 @@ export interface GlobalSettings {
|
|||||||
sidebarOpen: boolean;
|
sidebarOpen: boolean;
|
||||||
/** Whether chat history panel is open */
|
/** Whether chat history panel is open */
|
||||||
chatHistoryOpen: boolean;
|
chatHistoryOpen: boolean;
|
||||||
/** How much detail to show on kanban cards */
|
|
||||||
kanbanCardDetailLevel: KanbanCardDetailLevel;
|
|
||||||
|
|
||||||
// Feature Generation Defaults
|
// Feature Generation Defaults
|
||||||
/** Max features to generate concurrently */
|
/** Max features to generate concurrently */
|
||||||
@@ -595,6 +592,10 @@ export interface ProjectSettings {
|
|||||||
/** Project-specific board background settings */
|
/** Project-specific board background settings */
|
||||||
boardBackground?: BoardBackgroundSettings;
|
boardBackground?: BoardBackgroundSettings;
|
||||||
|
|
||||||
|
// UI Visibility
|
||||||
|
/** Whether the worktree panel row is visible (default: true) */
|
||||||
|
worktreePanelVisible?: boolean;
|
||||||
|
|
||||||
// Session Tracking
|
// Session Tracking
|
||||||
/** Last chat session selected in this project */
|
/** Last chat session selected in this project */
|
||||||
lastSelectedSessionId?: string;
|
lastSelectedSessionId?: string;
|
||||||
@@ -676,7 +677,6 @@ export const DEFAULT_GLOBAL_SETTINGS: GlobalSettings = {
|
|||||||
theme: 'dark',
|
theme: 'dark',
|
||||||
sidebarOpen: true,
|
sidebarOpen: true,
|
||||||
chatHistoryOpen: false,
|
chatHistoryOpen: false,
|
||||||
kanbanCardDetailLevel: 'standard',
|
|
||||||
maxConcurrency: 3,
|
maxConcurrency: 3,
|
||||||
defaultSkipTests: true,
|
defaultSkipTests: true,
|
||||||
enableDependencyBlocking: true,
|
enableDependencyBlocking: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user