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