🗂️ refactor: implement Phase 2 folder-pattern compliance

- Move dialogs to src/components/dialogs/ folder:
  - delete-session-dialog.tsx
  - delete-all-archived-sessions-dialog.tsx
  - new-project-modal.tsx
  - workspace-picker-modal.tsx
- Update all imports to reference new dialog locations
- Create barrel export (index.ts) for board-view/components/kanban-card/
- Create barrel exports (index.ts) for all 11 settings-view subfolders:
  - api-keys/, api-keys/hooks/, appearance/, audio/, cli-status/
  - components/, config/, danger-zone/, feature-defaults/
  - keyboard-shortcuts/, shared/

This is Phase 2 of folder-pattern.md compliance: organizing dialogs
and establishing consistent barrel export patterns across all view subfolders.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Kacper
2025-12-21 19:43:17 +01:00
parent 6365cc137c
commit e47b34288b
20 changed files with 304 additions and 406 deletions

View File

@@ -1,4 +1,3 @@
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -6,9 +5,9 @@ import {
DialogFooter, DialogFooter,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from '@/components/ui/dialog';
import { Button } from "@/components/ui/button"; import { Button } from '@/components/ui/button';
import { Trash2 } from "lucide-react"; import { Trash2 } from 'lucide-react';
interface DeleteAllArchivedSessionsDialogProps { interface DeleteAllArchivedSessionsDialogProps {
open: boolean; open: boolean;
@@ -29,8 +28,7 @@ export function DeleteAllArchivedSessionsDialog({
<DialogHeader> <DialogHeader>
<DialogTitle>Delete All Archived Sessions</DialogTitle> <DialogTitle>Delete All Archived Sessions</DialogTitle>
<DialogDescription> <DialogDescription>
Are you sure you want to delete all archived sessions? This action Are you sure you want to delete all archived sessions? This action cannot be undone.
cannot be undone.
{archivedCount > 0 && ( {archivedCount > 0 && (
<span className="block mt-2 text-yellow-500"> <span className="block mt-2 text-yellow-500">
{archivedCount} session(s) will be deleted. {archivedCount} session(s) will be deleted.

View File

@@ -1,6 +1,6 @@
import { MessageSquare } from "lucide-react"; import { MessageSquare } from 'lucide-react';
import { DeleteConfirmDialog } from "@/components/ui/delete-confirm-dialog"; import { DeleteConfirmDialog } from '@/components/ui/delete-confirm-dialog';
import type { SessionListItem } from "@/types/electron"; import type { SessionListItem } from '@/types/electron';
interface DeleteSessionDialogProps { interface DeleteSessionDialogProps {
open: boolean; open: boolean;
@@ -38,12 +38,8 @@ export function DeleteSessionDialog({
<MessageSquare className="w-5 h-5 text-brand-500" /> <MessageSquare className="w-5 h-5 text-brand-500" />
</div> </div>
<div className="min-w-0"> <div className="min-w-0">
<p className="font-medium text-foreground truncate"> <p className="font-medium text-foreground truncate">{session.name}</p>
{session.name} <p className="text-xs text-muted-foreground">{session.messageCount} messages</p>
</p>
<p className="text-xs text-muted-foreground">
{session.messageCount} messages
</p>
</div> </div>
</div> </div>
)} )}

View File

@@ -1,2 +1,6 @@
export { BoardBackgroundModal } from './board-background-modal'; export { BoardBackgroundModal } from './board-background-modal';
export { DeleteAllArchivedSessionsDialog } from './delete-all-archived-sessions-dialog';
export { DeleteSessionDialog } from './delete-session-dialog';
export { FileBrowserDialog } from './file-browser-dialog'; export { FileBrowserDialog } from './file-browser-dialog';
export { NewProjectModal } from './new-project-modal';
export { WorkspacePickerModal } from './workspace-picker-modal';

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from 'react';
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -6,13 +6,13 @@ import {
DialogFooter, DialogFooter,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from '@/components/ui/dialog';
import { Button } from "@/components/ui/button"; import { Button } from '@/components/ui/button';
import { HotkeyButton } from "@/components/ui/hotkey-button"; import { HotkeyButton } from '@/components/ui/hotkey-button';
import { Input } from "@/components/ui/input"; import { Input } from '@/components/ui/input';
import { Label } from "@/components/ui/label"; import { Label } from '@/components/ui/label';
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Badge } from "@/components/ui/badge"; import { Badge } from '@/components/ui/badge';
import { import {
FolderPlus, FolderPlus,
FolderOpen, FolderOpen,
@@ -22,15 +22,12 @@ import {
Loader2, Loader2,
Link, Link,
Folder, Folder,
} from "lucide-react"; } from 'lucide-react';
import { starterTemplates, type StarterTemplate } from "@/lib/templates"; import { starterTemplates, type StarterTemplate } from '@/lib/templates';
import { getElectronAPI } from "@/lib/electron"; import { getElectronAPI } from '@/lib/electron';
import { cn } from "@/lib/utils"; import { cn } from '@/lib/utils';
import { useFileBrowser } from "@/contexts/file-browser-context"; import { useFileBrowser } from '@/contexts/file-browser-context';
import { import { getDefaultWorkspaceDirectory, saveLastProjectDirectory } from '@/lib/workspace-config';
getDefaultWorkspaceDirectory,
saveLastProjectDirectory,
} from "@/lib/workspace-config";
interface ValidationErrors { interface ValidationErrors {
projectName?: boolean; projectName?: boolean;
@@ -42,20 +39,13 @@ interface ValidationErrors {
interface NewProjectModalProps { interface NewProjectModalProps {
open: boolean; open: boolean;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
onCreateBlankProject: ( onCreateBlankProject: (projectName: string, parentDir: string) => Promise<void>;
projectName: string,
parentDir: string
) => Promise<void>;
onCreateFromTemplate: ( onCreateFromTemplate: (
template: StarterTemplate, template: StarterTemplate,
projectName: string, projectName: string,
parentDir: string parentDir: string
) => Promise<void>; ) => Promise<void>;
onCreateFromCustomUrl: ( onCreateFromCustomUrl: (repoUrl: string, projectName: string, parentDir: string) => Promise<void>;
repoUrl: string,
projectName: string,
parentDir: string
) => Promise<void>;
isCreating: boolean; isCreating: boolean;
} }
@@ -67,14 +57,13 @@ export function NewProjectModal({
onCreateFromCustomUrl, onCreateFromCustomUrl,
isCreating, isCreating,
}: NewProjectModalProps) { }: NewProjectModalProps) {
const [activeTab, setActiveTab] = useState<"blank" | "template">("blank"); const [activeTab, setActiveTab] = useState<'blank' | 'template'>('blank');
const [projectName, setProjectName] = useState(""); const [projectName, setProjectName] = useState('');
const [workspaceDir, setWorkspaceDir] = useState<string>(""); const [workspaceDir, setWorkspaceDir] = useState<string>('');
const [isLoadingWorkspace, setIsLoadingWorkspace] = useState(false); const [isLoadingWorkspace, setIsLoadingWorkspace] = useState(false);
const [selectedTemplate, setSelectedTemplate] = const [selectedTemplate, setSelectedTemplate] = useState<StarterTemplate | null>(null);
useState<StarterTemplate | null>(null);
const [useCustomUrl, setUseCustomUrl] = useState(false); const [useCustomUrl, setUseCustomUrl] = useState(false);
const [customUrl, setCustomUrl] = useState(""); const [customUrl, setCustomUrl] = useState('');
const [errors, setErrors] = useState<ValidationErrors>({}); const [errors, setErrors] = useState<ValidationErrors>({});
const { openFileBrowser } = useFileBrowser(); const { openFileBrowser } = useFileBrowser();
@@ -89,7 +78,7 @@ export function NewProjectModal({
} }
}) })
.catch((error) => { .catch((error) => {
console.error("Failed to get default workspace directory:", error); console.error('Failed to get default workspace directory:', error);
}) })
.finally(() => { .finally(() => {
setIsLoadingWorkspace(false); setIsLoadingWorkspace(false);
@@ -100,11 +89,11 @@ export function NewProjectModal({
// Reset form when modal closes // Reset form when modal closes
useEffect(() => { useEffect(() => {
if (!open) { if (!open) {
setProjectName(""); setProjectName('');
setSelectedTemplate(null); setSelectedTemplate(null);
setUseCustomUrl(false); setUseCustomUrl(false);
setCustomUrl(""); setCustomUrl('');
setActiveTab("blank"); setActiveTab('blank');
setErrors({}); setErrors({});
} }
}, [open]); }, [open]);
@@ -117,10 +106,7 @@ export function NewProjectModal({
}, [projectName, errors.projectName]); }, [projectName, errors.projectName]);
useEffect(() => { useEffect(() => {
if ( if ((selectedTemplate || (useCustomUrl && customUrl)) && errors.templateSelection) {
(selectedTemplate || (useCustomUrl && customUrl)) &&
errors.templateSelection
) {
setErrors((prev) => ({ ...prev, templateSelection: false })); setErrors((prev) => ({ ...prev, templateSelection: false }));
} }
}, [selectedTemplate, useCustomUrl, customUrl, errors.templateSelection]); }, [selectedTemplate, useCustomUrl, customUrl, errors.templateSelection]);
@@ -145,7 +131,7 @@ export function NewProjectModal({
} }
// Check template selection (only for template tab) // Check template selection (only for template tab)
if (activeTab === "template") { if (activeTab === 'template') {
if (useCustomUrl) { if (useCustomUrl) {
if (!customUrl.trim()) { if (!customUrl.trim()) {
newErrors.customUrl = true; newErrors.customUrl = true;
@@ -164,7 +150,7 @@ export function NewProjectModal({
// Clear errors and proceed // Clear errors and proceed
setErrors({}); setErrors({});
if (activeTab === "blank") { if (activeTab === 'blank') {
await onCreateBlankProject(projectName, workspaceDir); await onCreateBlankProject(projectName, workspaceDir);
} else if (useCustomUrl && customUrl) { } else if (useCustomUrl && customUrl) {
await onCreateFromCustomUrl(customUrl, projectName, workspaceDir); await onCreateFromCustomUrl(customUrl, projectName, workspaceDir);
@@ -181,7 +167,7 @@ export function NewProjectModal({
const handleSelectTemplate = (template: StarterTemplate) => { const handleSelectTemplate = (template: StarterTemplate) => {
setSelectedTemplate(template); setSelectedTemplate(template);
setUseCustomUrl(false); setUseCustomUrl(false);
setCustomUrl(""); setCustomUrl('');
}; };
const handleToggleCustomUrl = () => { const handleToggleCustomUrl = () => {
@@ -193,9 +179,8 @@ export function NewProjectModal({
const handleBrowseDirectory = async () => { const handleBrowseDirectory = async () => {
const selectedPath = await openFileBrowser({ const selectedPath = await openFileBrowser({
title: "Select Base Project Directory", title: 'Select Base Project Directory',
description: description: 'Choose the parent directory where your project will be created',
"Choose the parent directory where your project will be created",
initialPath: workspaceDir || undefined, initialPath: workspaceDir || undefined,
}); });
if (selectedPath) { if (selectedPath) {
@@ -211,15 +196,12 @@ export function NewProjectModal({
// Use platform-specific path separator // Use platform-specific path separator
const pathSep = const pathSep =
typeof window !== "undefined" && (window as any).electronAPI typeof window !== 'undefined' && (window as any).electronAPI
? navigator.platform.indexOf("Win") !== -1 ? navigator.platform.indexOf('Win') !== -1
? "\\" ? '\\'
: "/" : '/'
: "/"; : '/';
const projectPath = const projectPath = workspaceDir && projectName ? `${workspaceDir}${pathSep}${projectName}` : '';
workspaceDir && projectName
? `${workspaceDir}${pathSep}${projectName}`
: "";
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
@@ -228,9 +210,7 @@ export function NewProjectModal({
data-testid="new-project-modal" data-testid="new-project-modal"
> >
<DialogHeader className="pb-2"> <DialogHeader className="pb-2">
<DialogTitle className="text-foreground"> <DialogTitle className="text-foreground">Create New Project</DialogTitle>
Create New Project
</DialogTitle>
<DialogDescription className="text-muted-foreground"> <DialogDescription className="text-muted-foreground">
Start with a blank project or choose from a starter template. Start with a blank project or choose from a starter template.
</DialogDescription> </DialogDescription>
@@ -241,13 +221,9 @@ export function NewProjectModal({
<div className="space-y-2"> <div className="space-y-2">
<Label <Label
htmlFor="project-name" htmlFor="project-name"
className={cn( className={cn('text-foreground', errors.projectName && 'text-red-500')}
"text-foreground",
errors.projectName && "text-red-500"
)}
> >
Project Name{" "} Project Name {errors.projectName && <span className="text-red-500">*</span>}
{errors.projectName && <span className="text-red-500">*</span>}
</Label> </Label>
<Input <Input
id="project-name" id="project-name"
@@ -255,33 +231,31 @@ export function NewProjectModal({
value={projectName} value={projectName}
onChange={(e) => setProjectName(e.target.value)} onChange={(e) => setProjectName(e.target.value)}
className={cn( className={cn(
"bg-input text-foreground placeholder:text-muted-foreground", 'bg-input text-foreground placeholder:text-muted-foreground',
errors.projectName errors.projectName
? "border-red-500 focus:border-red-500 focus:ring-red-500/20" ? 'border-red-500 focus:border-red-500 focus:ring-red-500/20'
: "border-border" : 'border-border'
)} )}
data-testid="project-name-input" data-testid="project-name-input"
autoFocus autoFocus
/> />
{errors.projectName && ( {errors.projectName && <p className="text-xs text-red-500">Project name is required</p>}
<p className="text-xs text-red-500">Project name is required</p>
)}
</div> </div>
{/* Workspace Directory Display */} {/* Workspace Directory Display */}
<div <div
className={cn( className={cn(
"flex items-center gap-2 text-sm", 'flex items-center gap-2 text-sm',
errors.workspaceDir ? "text-red-500" : "text-muted-foreground" errors.workspaceDir ? 'text-red-500' : 'text-muted-foreground'
)} )}
> >
<Folder className="w-4 h-4 shrink-0" /> <Folder className="w-4 h-4 shrink-0" />
<span className="flex-1 min-w-0"> <span className="flex-1 min-w-0">
{isLoadingWorkspace ? ( {isLoadingWorkspace ? (
"Loading workspace..." 'Loading workspace...'
) : workspaceDir ? ( ) : workspaceDir ? (
<> <>
Will be created at:{" "} Will be created at:{' '}
<code className="text-xs bg-muted px-1.5 py-0.5 rounded truncate"> <code className="text-xs bg-muted px-1.5 py-0.5 rounded truncate">
{projectPath || workspaceDir} {projectPath || workspaceDir}
</code> </code>
@@ -305,7 +279,7 @@ export function NewProjectModal({
<Tabs <Tabs
value={activeTab} value={activeTab}
onValueChange={(v) => setActiveTab(v as "blank" | "template")} onValueChange={(v) => setActiveTab(v as 'blank' | 'template')}
className="flex-1 flex flex-col overflow-hidden" className="flex-1 flex flex-col overflow-hidden"
> >
<TabsList className="w-full justify-start"> <TabsList className="w-full justify-start">
@@ -323,9 +297,8 @@ export function NewProjectModal({
<TabsContent value="blank" className="mt-0"> <TabsContent value="blank" className="mt-0">
<div className="p-4 rounded-lg bg-muted/50 border border-border"> <div className="p-4 rounded-lg bg-muted/50 border border-border">
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Create an empty project with the standard .automaker directory Create an empty project with the standard .automaker directory structure. Perfect
structure. Perfect for starting from scratch or importing an for starting from scratch or importing an existing codebase.
existing codebase.
</p> </p>
</div> </div>
</TabsContent> </TabsContent>
@@ -342,18 +315,18 @@ export function NewProjectModal({
{/* Preset Templates */} {/* Preset Templates */}
<div <div
className={cn( className={cn(
"space-y-3 rounded-lg p-1 -m-1", 'space-y-3 rounded-lg p-1 -m-1',
errors.templateSelection && "ring-2 ring-red-500/50" errors.templateSelection && 'ring-2 ring-red-500/50'
)} )}
> >
{starterTemplates.map((template) => ( {starterTemplates.map((template) => (
<div <div
key={template.id} key={template.id}
className={cn( className={cn(
"p-4 rounded-lg border cursor-pointer transition-all", 'p-4 rounded-lg border cursor-pointer transition-all',
selectedTemplate?.id === template.id && !useCustomUrl selectedTemplate?.id === template.id && !useCustomUrl
? "border-brand-500 bg-brand-500/10" ? 'border-brand-500 bg-brand-500/10'
: "border-border bg-muted/30 hover:border-border-glass hover:bg-muted/50" : 'border-border bg-muted/30 hover:border-border-glass hover:bg-muted/50'
)} )}
onClick={() => handleSelectTemplate(template)} onClick={() => handleSelectTemplate(template)}
data-testid={`template-${template.id}`} data-testid={`template-${template.id}`}
@@ -361,13 +334,10 @@ export function NewProjectModal({
<div className="flex items-start justify-between gap-4"> <div className="flex items-start justify-between gap-4">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<h4 className="font-medium text-foreground"> <h4 className="font-medium text-foreground">{template.name}</h4>
{template.name} {selectedTemplate?.id === template.id && !useCustomUrl && (
</h4> <Check className="w-4 h-4 text-brand-500" />
{selectedTemplate?.id === template.id && )}
!useCustomUrl && (
<Check className="w-4 h-4 text-brand-500" />
)}
</div> </div>
<p className="text-sm text-muted-foreground mb-3"> <p className="text-sm text-muted-foreground mb-3">
{template.description} {template.description}
@@ -376,11 +346,7 @@ export function NewProjectModal({
{/* Tech Stack */} {/* Tech Stack */}
<div className="flex flex-wrap gap-1.5 mb-3"> <div className="flex flex-wrap gap-1.5 mb-3">
{template.techStack.slice(0, 6).map((tech) => ( {template.techStack.slice(0, 6).map((tech) => (
<Badge <Badge key={tech} variant="secondary" className="text-xs">
key={tech}
variant="secondary"
className="text-xs"
>
{tech} {tech}
</Badge> </Badge>
))} ))}
@@ -394,7 +360,7 @@ export function NewProjectModal({
{/* Key Features */} {/* Key Features */}
<div className="text-xs text-muted-foreground"> <div className="text-xs text-muted-foreground">
<span className="font-medium">Features: </span> <span className="font-medium">Features: </span>
{template.features.slice(0, 3).join(" · ")} {template.features.slice(0, 3).join(' · ')}
{template.features.length > 3 && {template.features.length > 3 &&
` · +${template.features.length - 3} more`} ` · +${template.features.length - 3} more`}
</div> </div>
@@ -419,47 +385,38 @@ export function NewProjectModal({
{/* Custom URL Option */} {/* Custom URL Option */}
<div <div
className={cn( className={cn(
"p-4 rounded-lg border cursor-pointer transition-all", 'p-4 rounded-lg border cursor-pointer transition-all',
useCustomUrl useCustomUrl
? "border-brand-500 bg-brand-500/10" ? 'border-brand-500 bg-brand-500/10'
: "border-border bg-muted/30 hover:border-border-glass hover:bg-muted/50" : 'border-border bg-muted/30 hover:border-border-glass hover:bg-muted/50'
)} )}
onClick={handleToggleCustomUrl} onClick={handleToggleCustomUrl}
> >
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<Link className="w-4 h-4 text-muted-foreground" /> <Link className="w-4 h-4 text-muted-foreground" />
<h4 className="font-medium text-foreground"> <h4 className="font-medium text-foreground">Custom GitHub URL</h4>
Custom GitHub URL {useCustomUrl && <Check className="w-4 h-4 text-brand-500" />}
</h4>
{useCustomUrl && (
<Check className="w-4 h-4 text-brand-500" />
)}
</div> </div>
<p className="text-sm text-muted-foreground mb-3"> <p className="text-sm text-muted-foreground mb-3">
Clone any public GitHub repository as a starting point. Clone any public GitHub repository as a starting point.
</p> </p>
{useCustomUrl && ( {useCustomUrl && (
<div <div onClick={(e) => e.stopPropagation()} className="space-y-1">
onClick={(e) => e.stopPropagation()}
className="space-y-1"
>
<Input <Input
placeholder="https://github.com/username/repository" placeholder="https://github.com/username/repository"
value={customUrl} value={customUrl}
onChange={(e) => setCustomUrl(e.target.value)} onChange={(e) => setCustomUrl(e.target.value)}
className={cn( className={cn(
"bg-input text-foreground placeholder:text-muted-foreground", 'bg-input text-foreground placeholder:text-muted-foreground',
errors.customUrl errors.customUrl
? "border-red-500 focus:border-red-500 focus:ring-red-500/20" ? 'border-red-500 focus:border-red-500 focus:ring-red-500/20'
: "border-border" : 'border-border'
)} )}
data-testid="custom-url-input" data-testid="custom-url-input"
/> />
{errors.customUrl && ( {errors.customUrl && (
<p className="text-xs text-red-500"> <p className="text-xs text-red-500">GitHub URL is required</p>
GitHub URL is required
</p>
)} )}
</div> </div>
)} )}
@@ -482,14 +439,14 @@ export function NewProjectModal({
onClick={validateAndCreate} onClick={validateAndCreate}
disabled={isCreating} disabled={isCreating}
className="bg-gradient-to-r from-brand-500 to-brand-600 hover:from-brand-600 hover:to-brand-600 text-white border-0" className="bg-gradient-to-r from-brand-500 to-brand-600 hover:from-brand-600 hover:to-brand-600 text-white border-0"
hotkey={{ key: "Enter", cmdCtrl: true }} hotkey={{ key: 'Enter', cmdCtrl: true }}
hotkeyActive={open} hotkeyActive={open}
data-testid="confirm-create-project" data-testid="confirm-create-project"
> >
{isCreating ? ( {isCreating ? (
<> <>
<Loader2 className="w-4 h-4 mr-2 animate-spin" /> <Loader2 className="w-4 h-4 mr-2 animate-spin" />
{activeTab === "template" ? "Cloning..." : "Creating..."} {activeTab === 'template' ? 'Cloning...' : 'Creating...'}
</> </>
) : ( ) : (
<>Create Project</> <>Create Project</>

View File

@@ -1,5 +1,4 @@
import { useState, useEffect, useCallback } from 'react';
import { useState, useEffect, useCallback } from "react";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -7,10 +6,10 @@ import {
DialogFooter, DialogFooter,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from '@/components/ui/dialog';
import { Button } from "@/components/ui/button"; import { Button } from '@/components/ui/button';
import { Folder, Loader2, FolderOpen, AlertCircle } from "lucide-react"; import { Folder, Loader2, FolderOpen, AlertCircle } from 'lucide-react';
import { getHttpApiClient } from "@/lib/http-api-client"; import { getHttpApiClient } from '@/lib/http-api-client';
interface WorkspaceDirectory { interface WorkspaceDirectory {
name: string; name: string;
@@ -23,11 +22,7 @@ interface WorkspacePickerModalProps {
onSelect: (path: string, name: string) => void; onSelect: (path: string, name: string) => void;
} }
export function WorkspacePickerModal({ export function WorkspacePickerModal({ open, onOpenChange, onSelect }: WorkspacePickerModalProps) {
open,
onOpenChange,
onSelect,
}: WorkspacePickerModalProps) {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [directories, setDirectories] = useState<WorkspaceDirectory[]>([]); const [directories, setDirectories] = useState<WorkspaceDirectory[]>([]);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@@ -43,10 +38,10 @@ export function WorkspacePickerModal({
if (result.success && result.directories) { if (result.success && result.directories) {
setDirectories(result.directories); setDirectories(result.directories);
} else { } else {
setError(result.error || "Failed to load directories"); setError(result.error || 'Failed to load directories');
} }
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : "Failed to load directories"); setError(err instanceof Error ? err.message : 'Failed to load directories');
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
@@ -90,12 +85,7 @@ export function WorkspacePickerModal({
<AlertCircle className="w-6 h-6 text-destructive" /> <AlertCircle className="w-6 h-6 text-destructive" />
</div> </div>
<p className="text-sm text-destructive">{error}</p> <p className="text-sm text-destructive">{error}</p>
<Button <Button variant="secondary" size="sm" onClick={loadDirectories} className="mt-2">
variant="secondary"
size="sm"
onClick={loadDirectories}
className="mt-2"
>
Try Again Try Again
</Button> </Button>
</div> </div>
@@ -128,9 +118,7 @@ export function WorkspacePickerModal({
<p className="font-medium text-foreground truncate group-hover:text-brand-500 transition-colors"> <p className="font-medium text-foreground truncate group-hover:text-brand-500 transition-colors">
{dir.name} {dir.name}
</p> </p>
<p className="text-xs text-muted-foreground/70 truncate"> <p className="text-xs text-muted-foreground/70 truncate">{dir.path}</p>
{dir.path}
</p>
</div> </div>
</button> </button>
))} ))}

View File

@@ -72,7 +72,7 @@ import { toast } from 'sonner';
import { themeOptions } from '@/config/theme-options'; import { themeOptions } from '@/config/theme-options';
import type { SpecRegenerationEvent } from '@/types/electron'; import type { SpecRegenerationEvent } from '@/types/electron';
import { DeleteProjectDialog } from '@/components/views/settings-view/components/delete-project-dialog'; import { DeleteProjectDialog } from '@/components/views/settings-view/components/delete-project-dialog';
import { NewProjectModal } from '@/components/new-project-modal'; import { NewProjectModal } from '@/components/dialogs/new-project-modal';
import { CreateSpecDialog } from '@/components/views/spec-view/dialogs'; import { CreateSpecDialog } from '@/components/views/spec-view/dialogs';
import type { FeatureCount } from '@/components/views/spec-view/types'; import type { FeatureCount } from '@/components/views/spec-view/types';
import { import {

View File

@@ -1,10 +1,9 @@
import { useState, useEffect } from 'react';
import { useState, useEffect } from "react"; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from '@/components/ui/button';
import { Button } from "@/components/ui/button"; import { HotkeyButton } from '@/components/ui/hotkey-button';
import { HotkeyButton } from "@/components/ui/hotkey-button"; import { Input } from '@/components/ui/input';
import { Input } from "@/components/ui/input"; import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { import {
Plus, Plus,
MessageSquare, MessageSquare,
@@ -15,66 +14,66 @@ import {
X, X,
ArchiveRestore, ArchiveRestore,
Loader2, Loader2,
} from "lucide-react"; } from 'lucide-react';
import { cn } from "@/lib/utils"; import { cn } from '@/lib/utils';
import type { SessionListItem } from "@/types/electron"; import type { SessionListItem } from '@/types/electron';
import { useKeyboardShortcutsConfig } from "@/hooks/use-keyboard-shortcuts"; import { useKeyboardShortcutsConfig } from '@/hooks/use-keyboard-shortcuts';
import { getElectronAPI } from "@/lib/electron"; import { getElectronAPI } from '@/lib/electron';
import { DeleteSessionDialog } from "@/components/delete-session-dialog"; import { DeleteSessionDialog } from '@/components/dialogs/delete-session-dialog';
import { DeleteAllArchivedSessionsDialog } from "@/components/delete-all-archived-sessions-dialog"; import { DeleteAllArchivedSessionsDialog } from '@/components/dialogs/delete-all-archived-sessions-dialog';
// Random session name generator // Random session name generator
const adjectives = [ const adjectives = [
"Swift", 'Swift',
"Bright", 'Bright',
"Clever", 'Clever',
"Dynamic", 'Dynamic',
"Eager", 'Eager',
"Focused", 'Focused',
"Gentle", 'Gentle',
"Happy", 'Happy',
"Inventive", 'Inventive',
"Jolly", 'Jolly',
"Keen", 'Keen',
"Lively", 'Lively',
"Mighty", 'Mighty',
"Noble", 'Noble',
"Optimal", 'Optimal',
"Peaceful", 'Peaceful',
"Quick", 'Quick',
"Radiant", 'Radiant',
"Smart", 'Smart',
"Tranquil", 'Tranquil',
"Unique", 'Unique',
"Vibrant", 'Vibrant',
"Wise", 'Wise',
"Zealous", 'Zealous',
]; ];
const nouns = [ const nouns = [
"Agent", 'Agent',
"Builder", 'Builder',
"Coder", 'Coder',
"Developer", 'Developer',
"Explorer", 'Explorer',
"Forge", 'Forge',
"Garden", 'Garden',
"Helper", 'Helper',
"Innovator", 'Innovator',
"Journey", 'Journey',
"Kernel", 'Kernel',
"Lighthouse", 'Lighthouse',
"Mission", 'Mission',
"Navigator", 'Navigator',
"Oracle", 'Oracle',
"Project", 'Project',
"Quest", 'Quest',
"Runner", 'Runner',
"Spark", 'Spark',
"Task", 'Task',
"Unicorn", 'Unicorn',
"Voyage", 'Voyage',
"Workshop", 'Workshop',
]; ];
function generateRandomSessionName(): string { function generateRandomSessionName(): string {
@@ -101,19 +100,15 @@ export function SessionManager({
}: SessionManagerProps) { }: SessionManagerProps) {
const shortcuts = useKeyboardShortcutsConfig(); const shortcuts = useKeyboardShortcutsConfig();
const [sessions, setSessions] = useState<SessionListItem[]>([]); const [sessions, setSessions] = useState<SessionListItem[]>([]);
const [activeTab, setActiveTab] = useState<"active" | "archived">("active"); const [activeTab, setActiveTab] = useState<'active' | 'archived'>('active');
const [editingSessionId, setEditingSessionId] = useState<string | null>(null); const [editingSessionId, setEditingSessionId] = useState<string | null>(null);
const [editingName, setEditingName] = useState(""); const [editingName, setEditingName] = useState('');
const [isCreating, setIsCreating] = useState(false); const [isCreating, setIsCreating] = useState(false);
const [newSessionName, setNewSessionName] = useState(""); const [newSessionName, setNewSessionName] = useState('');
const [runningSessions, setRunningSessions] = useState<Set<string>>( const [runningSessions, setRunningSessions] = useState<Set<string>>(new Set());
new Set()
);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [sessionToDelete, setSessionToDelete] = const [sessionToDelete, setSessionToDelete] = useState<SessionListItem | null>(null);
useState<SessionListItem | null>(null); const [isDeleteAllArchivedDialogOpen, setIsDeleteAllArchivedDialogOpen] = useState(false);
const [isDeleteAllArchivedDialogOpen, setIsDeleteAllArchivedDialogOpen] =
useState(false);
// Check running state for all sessions // Check running state for all sessions
const checkRunningSessions = async (sessionList: SessionListItem[]) => { const checkRunningSessions = async (sessionList: SessionListItem[]) => {
@@ -131,10 +126,7 @@ export function SessionManager({
} }
} catch (err) { } catch (err) {
// Ignore errors for individual session checks // Ignore errors for individual session checks
console.warn( console.warn(`[SessionManager] Failed to check running state for ${session.id}:`, err);
`[SessionManager] Failed to check running state for ${session.id}:`,
err
);
} }
} }
@@ -180,14 +172,10 @@ export function SessionManager({
const sessionName = newSessionName.trim() || generateRandomSessionName(); const sessionName = newSessionName.trim() || generateRandomSessionName();
const result = await api.sessions.create( const result = await api.sessions.create(sessionName, projectPath, projectPath);
sessionName,
projectPath,
projectPath
);
if (result.success && result.session?.id) { if (result.success && result.session?.id) {
setNewSessionName(""); setNewSessionName('');
setIsCreating(false); setIsCreating(false);
await loadSessions(); await loadSessions();
onSelectSession(result.session.id); onSelectSession(result.session.id);
@@ -201,11 +189,7 @@ export function SessionManager({
const sessionName = generateRandomSessionName(); const sessionName = generateRandomSessionName();
const result = await api.sessions.create( const result = await api.sessions.create(sessionName, projectPath, projectPath);
sessionName,
projectPath,
projectPath
);
if (result.success && result.session?.id) { if (result.success && result.session?.id) {
await loadSessions(); await loadSessions();
@@ -234,7 +218,7 @@ export function SessionManager({
if (result.success) { if (result.success) {
setEditingSessionId(null); setEditingSessionId(null);
setEditingName(""); setEditingName('');
await loadSessions(); await loadSessions();
} }
}; };
@@ -243,7 +227,7 @@ export function SessionManager({
const handleArchiveSession = async (sessionId: string) => { const handleArchiveSession = async (sessionId: string) => {
const api = getElectronAPI(); const api = getElectronAPI();
if (!api?.sessions) { if (!api?.sessions) {
console.error("[SessionManager] Sessions API not available"); console.error('[SessionManager] Sessions API not available');
return; return;
} }
@@ -256,10 +240,10 @@ export function SessionManager({
} }
await loadSessions(); await loadSessions();
} else { } else {
console.error("[SessionManager] Archive failed:", result.error); console.error('[SessionManager] Archive failed:', result.error);
} }
} catch (error) { } catch (error) {
console.error("[SessionManager] Archive error:", error); console.error('[SessionManager] Archive error:', error);
} }
}; };
@@ -267,7 +251,7 @@ export function SessionManager({
const handleUnarchiveSession = async (sessionId: string) => { const handleUnarchiveSession = async (sessionId: string) => {
const api = getElectronAPI(); const api = getElectronAPI();
if (!api?.sessions) { if (!api?.sessions) {
console.error("[SessionManager] Sessions API not available"); console.error('[SessionManager] Sessions API not available');
return; return;
} }
@@ -276,10 +260,10 @@ export function SessionManager({
if (result.success) { if (result.success) {
await loadSessions(); await loadSessions();
} else { } else {
console.error("[SessionManager] Unarchive failed:", result.error); console.error('[SessionManager] Unarchive failed:', result.error);
} }
} catch (error) { } catch (error) {
console.error("[SessionManager] Unarchive error:", error); console.error('[SessionManager] Unarchive error:', error);
} }
}; };
@@ -324,8 +308,7 @@ export function SessionManager({
const activeSessions = sessions.filter((s) => !s.isArchived); const activeSessions = sessions.filter((s) => !s.isArchived);
const archivedSessions = sessions.filter((s) => s.isArchived); const archivedSessions = sessions.filter((s) => s.isArchived);
const displayedSessions = const displayedSessions = activeTab === 'active' ? activeSessions : archivedSessions;
activeTab === "active" ? activeSessions : archivedSessions;
return ( return (
<Card className="h-full flex flex-col rounded-none"> <Card className="h-full flex flex-col rounded-none">
@@ -337,8 +320,8 @@ export function SessionManager({
size="sm" size="sm"
onClick={() => { onClick={() => {
// Switch to active tab if on archived tab // Switch to active tab if on archived tab
if (activeTab === "archived") { if (activeTab === 'archived') {
setActiveTab("active"); setActiveTab('active');
} }
handleQuickCreateSession(); handleQuickCreateSession();
}} }}
@@ -354,9 +337,7 @@ export function SessionManager({
<Tabs <Tabs
value={activeTab} value={activeTab}
onValueChange={(value) => onValueChange={(value) => setActiveTab(value as 'active' | 'archived')}
setActiveTab(value as "active" | "archived")
}
className="w-full" className="w-full"
> >
<TabsList className="w-full"> <TabsList className="w-full">
@@ -372,10 +353,7 @@ export function SessionManager({
</Tabs> </Tabs>
</CardHeader> </CardHeader>
<CardContent <CardContent className="flex-1 overflow-y-auto space-y-2" data-testid="session-list">
className="flex-1 overflow-y-auto space-y-2"
data-testid="session-list"
>
{/* Create new session */} {/* Create new session */}
{isCreating && ( {isCreating && (
<div className="p-3 border rounded-lg bg-muted/50"> <div className="p-3 border rounded-lg bg-muted/50">
@@ -385,10 +363,10 @@ export function SessionManager({
value={newSessionName} value={newSessionName}
onChange={(e) => setNewSessionName(e.target.value)} onChange={(e) => setNewSessionName(e.target.value)}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Enter") handleCreateSession(); if (e.key === 'Enter') handleCreateSession();
if (e.key === "Escape") { if (e.key === 'Escape') {
setIsCreating(false); setIsCreating(false);
setNewSessionName(""); setNewSessionName('');
} }
}} }}
autoFocus autoFocus
@@ -401,7 +379,7 @@ export function SessionManager({
variant="ghost" variant="ghost"
onClick={() => { onClick={() => {
setIsCreating(false); setIsCreating(false);
setNewSessionName(""); setNewSessionName('');
}} }}
> >
<X className="w-4 h-4" /> <X className="w-4 h-4" />
@@ -411,7 +389,7 @@ export function SessionManager({
)} )}
{/* Delete All Archived button - shown at the top of archived sessions */} {/* Delete All Archived button - shown at the top of archived sessions */}
{activeTab === "archived" && archivedSessions.length > 0 && ( {activeTab === 'archived' && archivedSessions.length > 0 && (
<div className="pb-2 border-b mb-2"> <div className="pb-2 border-b mb-2">
<Button <Button
variant="destructive" variant="destructive"
@@ -431,9 +409,9 @@ export function SessionManager({
<div <div
key={session.id} key={session.id}
className={cn( className={cn(
"p-3 border rounded-lg cursor-pointer transition-colors hover:bg-accent/50", 'p-3 border rounded-lg cursor-pointer transition-colors hover:bg-accent/50',
currentSessionId === session.id && "bg-primary/10 border-primary", currentSessionId === session.id && 'bg-primary/10 border-primary',
session.isArchived && "opacity-60" session.isArchived && 'opacity-60'
)} )}
onClick={() => !session.isArchived && onSelectSession(session.id)} onClick={() => !session.isArchived && onSelectSession(session.id)}
data-testid={`session-item-${session.id}`} data-testid={`session-item-${session.id}`}
@@ -446,10 +424,10 @@ export function SessionManager({
value={editingName} value={editingName}
onChange={(e) => setEditingName(e.target.value)} onChange={(e) => setEditingName(e.target.value)}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Enter") handleRenameSession(session.id); if (e.key === 'Enter') handleRenameSession(session.id);
if (e.key === "Escape") { if (e.key === 'Escape') {
setEditingSessionId(null); setEditingSessionId(null);
setEditingName(""); setEditingName('');
} }
}} }}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
@@ -472,7 +450,7 @@ export function SessionManager({
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
setEditingSessionId(null); setEditingSessionId(null);
setEditingName(""); setEditingName('');
}} }}
className="h-7" className="h-7"
> >
@@ -483,16 +461,14 @@ export function SessionManager({
<> <>
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
{/* Show loading indicator if this session is running (either current session thinking or any session in runningSessions) */} {/* Show loading indicator if this session is running (either current session thinking or any session in runningSessions) */}
{(currentSessionId === session.id && {(currentSessionId === session.id && isCurrentSessionThinking) ||
isCurrentSessionThinking) ||
runningSessions.has(session.id) ? ( runningSessions.has(session.id) ? (
<Loader2 className="w-4 h-4 text-primary animate-spin shrink-0" /> <Loader2 className="w-4 h-4 text-primary animate-spin shrink-0" />
) : ( ) : (
<MessageSquare className="w-4 h-4 text-muted-foreground shrink-0" /> <MessageSquare className="w-4 h-4 text-muted-foreground shrink-0" />
)} )}
<h3 className="font-medium truncate">{session.name}</h3> <h3 className="font-medium truncate">{session.name}</h3>
{((currentSessionId === session.id && {((currentSessionId === session.id && isCurrentSessionThinking) ||
isCurrentSessionThinking) ||
runningSessions.has(session.id)) && ( runningSessions.has(session.id)) && (
<span className="text-xs text-primary bg-primary/10 px-2 py-0.5 rounded-full"> <span className="text-xs text-primary bg-primary/10 px-2 py-0.5 rounded-full">
thinking... thinking...
@@ -500,9 +476,7 @@ export function SessionManager({
)} )}
</div> </div>
{session.preview && ( {session.preview && (
<p className="text-xs text-muted-foreground truncate"> <p className="text-xs text-muted-foreground truncate">{session.preview}</p>
{session.preview}
</p>
)} )}
<div className="flex items-center gap-2 mt-2"> <div className="flex items-center gap-2 mt-2">
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
@@ -519,10 +493,7 @@ export function SessionManager({
{/* Actions */} {/* Actions */}
{!session.isArchived && ( {!session.isArchived && (
<div <div className="flex gap-1" onClick={(e) => e.stopPropagation()}>
className="flex gap-1"
onClick={(e) => e.stopPropagation()}
>
<Button <Button
size="sm" size="sm"
variant="ghost" variant="ghost"
@@ -547,10 +518,7 @@ export function SessionManager({
)} )}
{session.isArchived && ( {session.isArchived && (
<div <div className="flex gap-1" onClick={(e) => e.stopPropagation()}>
className="flex gap-1"
onClick={(e) => e.stopPropagation()}
>
<Button <Button
size="sm" size="sm"
variant="ghost" variant="ghost"
@@ -578,14 +546,12 @@ export function SessionManager({
<div className="text-center py-8 text-muted-foreground"> <div className="text-center py-8 text-muted-foreground">
<MessageSquare className="w-12 h-12 mx-auto mb-2 opacity-50" /> <MessageSquare className="w-12 h-12 mx-auto mb-2 opacity-50" />
<p className="text-sm"> <p className="text-sm">
{activeTab === "active" {activeTab === 'active' ? 'No active sessions' : 'No archived sessions'}
? "No active sessions"
: "No archived sessions"}
</p> </p>
<p className="text-xs"> <p className="text-xs">
{activeTab === "active" {activeTab === 'active'
? "Create your first session to get started" ? 'Create your first session to get started'
: "Archive sessions to see them here"} : 'Archive sessions to see them here'}
</p> </p>
</div> </div>
)} )}

View File

@@ -0,0 +1,7 @@
export { AgentInfoPanel } from './agent-info-panel';
export { CardActions } from './card-actions';
export { CardBadges, PriorityBadges } from './card-badges';
export { CardContentSections } from './card-content-sections';
export { CardHeaderSection } from './card-header';
export { KanbanCard } from './kanban-card';
export { SummaryDialog } from './summary-dialog';

View File

@@ -0,0 +1 @@
export { useApiKeyManagement } from './use-api-key-management';

View File

@@ -0,0 +1,4 @@
export { ApiKeyField } from './api-key-field';
export { ApiKeysSection } from './api-keys-section';
export { AuthenticationStatusDisplay } from './authentication-status-display';
export { SecurityNotice } from './security-notice';

View File

@@ -0,0 +1 @@
export { AppearanceSection } from './appearance-section';

View File

@@ -0,0 +1 @@
export { AudioSection } from './audio-section';

View File

@@ -0,0 +1 @@
export { ClaudeCliStatus } from './claude-cli-status';

View File

@@ -0,0 +1,4 @@
export { DeleteProjectDialog } from './delete-project-dialog';
export { KeyboardMapDialog } from './keyboard-map-dialog';
export { SettingsHeader } from './settings-header';
export { SettingsNavigation } from './settings-navigation';

View File

@@ -0,0 +1,2 @@
export { NAV_ITEMS } from './navigation';
export type { NavigationItem } from './navigation';

View File

@@ -0,0 +1 @@
export { DangerZoneSection } from './danger-zone-section';

View File

@@ -0,0 +1 @@
export { FeatureDefaultsSection } from './feature-defaults-section';

View File

@@ -0,0 +1 @@
export { KeyboardShortcutsSection } from './keyboard-shortcuts-section';

View File

@@ -0,0 +1,2 @@
export type { Theme } from '@/config/theme-options';
export type { CliStatus, KanbanDetailLevel, Project, ApiKeys } from './types';

View File

@@ -1,6 +1,5 @@
import { useState, useCallback } from 'react';
import { useState, useCallback } from "react"; import { Button } from '@/components/ui/button';
import { Button } from "@/components/ui/button";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -8,10 +7,10 @@ import {
DialogFooter, DialogFooter,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from '@/components/ui/dialog';
import { useAppStore, type ThemeMode } from "@/store/app-store"; import { useAppStore, type ThemeMode } from '@/store/app-store';
import { getElectronAPI, type Project } from "@/lib/electron"; import { getElectronAPI, type Project } from '@/lib/electron';
import { initializeProject } from "@/lib/project-init"; import { initializeProject } from '@/lib/project-init';
import { import {
FolderOpen, FolderOpen,
Plus, Plus,
@@ -21,19 +20,19 @@ import {
MessageSquare, MessageSquare,
ChevronDown, ChevronDown,
Loader2, Loader2,
} from "lucide-react"; } from 'lucide-react';
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from '@/components/ui/dropdown-menu';
import { toast } from "sonner"; import { toast } from 'sonner';
import { WorkspacePickerModal } from "@/components/workspace-picker-modal"; import { WorkspacePickerModal } from '@/components/dialogs/workspace-picker-modal';
import { NewProjectModal } from "@/components/new-project-modal"; import { NewProjectModal } from '@/components/dialogs/new-project-modal';
import { getHttpApiClient } from "@/lib/http-api-client"; import { getHttpApiClient } from '@/lib/http-api-client';
import type { StarterTemplate } from "@/lib/templates"; import type { StarterTemplate } from '@/lib/templates';
import { useNavigate } from "@tanstack/react-router"; import { useNavigate } from '@tanstack/react-router';
export function WelcomeView() { export function WelcomeView() {
const { const {
@@ -66,24 +65,24 @@ export function WelcomeView() {
const api = getElectronAPI(); const api = getElectronAPI();
if (!api.autoMode?.analyzeProject) { if (!api.autoMode?.analyzeProject) {
console.log("[Welcome] Auto mode API not available, skipping analysis"); console.log('[Welcome] Auto mode API not available, skipping analysis');
return; return;
} }
setIsAnalyzing(true); setIsAnalyzing(true);
try { try {
console.log("[Welcome] Starting project analysis for:", projectPath); console.log('[Welcome] Starting project analysis for:', projectPath);
const result = await api.autoMode.analyzeProject(projectPath); const result = await api.autoMode.analyzeProject(projectPath);
if (result.success) { if (result.success) {
toast.success("Project analyzed", { toast.success('Project analyzed', {
description: "AI agent has analyzed your project structure", description: 'AI agent has analyzed your project structure',
}); });
} else { } else {
console.error("[Welcome] Project analysis failed:", result.error); console.error('[Welcome] Project analysis failed:', result.error);
} }
} catch (error) { } catch (error) {
console.error("[Welcome] Failed to analyze project:", error); console.error('[Welcome] Failed to analyze project:', error);
} finally { } finally {
setIsAnalyzing(false); setIsAnalyzing(false);
} }
@@ -100,8 +99,8 @@ export function WelcomeView() {
const initResult = await initializeProject(path); const initResult = await initializeProject(path);
if (!initResult.success) { if (!initResult.success) {
toast.error("Failed to initialize project", { toast.error('Failed to initialize project', {
description: initResult.error || "Unknown error occurred", description: initResult.error || 'Unknown error occurred',
}); });
return; return;
} }
@@ -126,26 +125,23 @@ export function WelcomeView() {
setShowInitDialog(true); setShowInitDialog(true);
// Kick off agent to analyze the project and update app_spec.txt // Kick off agent to analyze the project and update app_spec.txt
console.log( console.log('[Welcome] Project initialized, created files:', initResult.createdFiles);
"[Welcome] Project initialized, created files:", console.log('[Welcome] Kicking off project analysis agent...');
initResult.createdFiles
);
console.log("[Welcome] Kicking off project analysis agent...");
// Start analysis in background (don't await, let it run async) // Start analysis in background (don't await, let it run async)
analyzeProject(path); analyzeProject(path);
} else { } else {
toast.success("Project opened", { toast.success('Project opened', {
description: `Opened ${name}`, description: `Opened ${name}`,
}); });
} }
// Navigate to the board view // Navigate to the board view
navigate({ to: "/board" }); navigate({ to: '/board' });
} catch (error) { } catch (error) {
console.error("[Welcome] Failed to open project:", error); console.error('[Welcome] Failed to open project:', error);
toast.error("Failed to open project", { toast.error('Failed to open project', {
description: error instanceof Error ? error.message : "Unknown error", description: error instanceof Error ? error.message : 'Unknown error',
}); });
} finally { } finally {
setIsOpening(false); setIsOpening(false);
@@ -178,21 +174,19 @@ export function WelcomeView() {
if (!result.canceled && result.filePaths[0]) { if (!result.canceled && result.filePaths[0]) {
const path = result.filePaths[0]; const path = result.filePaths[0];
// Extract folder name from path (works on both Windows and Mac/Linux) // Extract folder name from path (works on both Windows and Mac/Linux)
const name = const name = path.split(/[/\\]/).filter(Boolean).pop() || 'Untitled Project';
path.split(/[/\\]/).filter(Boolean).pop() || "Untitled Project";
await initializeAndOpenProject(path, name); await initializeAndOpenProject(path, name);
} }
} }
} catch (error) { } catch (error) {
console.error("[Welcome] Failed to check workspace config:", error); console.error('[Welcome] Failed to check workspace config:', error);
// Fall back to current behavior on error // Fall back to current behavior on error
const api = getElectronAPI(); const api = getElectronAPI();
const result = await api.openDirectory(); const result = await api.openDirectory();
if (!result.canceled && result.filePaths[0]) { if (!result.canceled && result.filePaths[0]) {
const path = result.filePaths[0]; const path = result.filePaths[0];
const name = const name = path.split(/[/\\]/).filter(Boolean).pop() || 'Untitled Project';
path.split(/[/\\]/).filter(Boolean).pop() || "Untitled Project";
await initializeAndOpenProject(path, name); await initializeAndOpenProject(path, name);
} }
} }
@@ -224,16 +218,13 @@ export function WelcomeView() {
}; };
const handleInteractiveMode = () => { const handleInteractiveMode = () => {
navigate({ to: "/interview" }); navigate({ to: '/interview' });
}; };
/** /**
* Create a blank project with just .automaker directory structure * Create a blank project with just .automaker directory structure
*/ */
const handleCreateBlankProject = async ( const handleCreateBlankProject = async (projectName: string, parentDir: string) => {
projectName: string,
parentDir: string
) => {
setIsCreating(true); setIsCreating(true);
try { try {
const api = getElectronAPI(); const api = getElectronAPI();
@@ -242,7 +233,7 @@ export function WelcomeView() {
// Validate that parent directory exists // Validate that parent directory exists
const parentExists = await api.exists(parentDir); const parentExists = await api.exists(parentDir);
if (!parentExists) { if (!parentExists) {
toast.error("Parent directory does not exist", { toast.error('Parent directory does not exist', {
description: `Cannot create project in non-existent directory: ${parentDir}`, description: `Cannot create project in non-existent directory: ${parentDir}`,
}); });
return; return;
@@ -251,7 +242,7 @@ export function WelcomeView() {
// Verify parent is actually a directory // Verify parent is actually a directory
const parentStat = await api.stat(parentDir); const parentStat = await api.stat(parentDir);
if (parentStat && !parentStat.isDirectory) { if (parentStat && !parentStat.isDirectory) {
toast.error("Parent path is not a directory", { toast.error('Parent path is not a directory', {
description: `${parentDir} is not a directory`, description: `${parentDir} is not a directory`,
}); });
return; return;
@@ -260,8 +251,8 @@ export function WelcomeView() {
// Create project directory // Create project directory
const mkdirResult = await api.mkdir(projectPath); const mkdirResult = await api.mkdir(projectPath);
if (!mkdirResult.success) { if (!mkdirResult.success) {
toast.error("Failed to create project directory", { toast.error('Failed to create project directory', {
description: mkdirResult.error || "Unknown error occurred", description: mkdirResult.error || 'Unknown error occurred',
}); });
return; return;
} }
@@ -270,8 +261,8 @@ export function WelcomeView() {
const initResult = await initializeProject(projectPath); const initResult = await initializeProject(projectPath);
if (!initResult.success) { if (!initResult.success) {
toast.error("Failed to initialize project", { toast.error('Failed to initialize project', {
description: initResult.error || "Unknown error occurred", description: initResult.error || 'Unknown error occurred',
}); });
return; return;
} }
@@ -313,7 +304,7 @@ export function WelcomeView() {
setCurrentProject(project); setCurrentProject(project);
setShowNewProjectModal(false); setShowNewProjectModal(false);
toast.success("Project created", { toast.success('Project created', {
description: `Created ${projectName} with .automaker directory`, description: `Created ${projectName} with .automaker directory`,
}); });
@@ -326,9 +317,9 @@ export function WelcomeView() {
}); });
setShowInitDialog(true); setShowInitDialog(true);
} catch (error) { } catch (error) {
console.error("Failed to create project:", error); console.error('Failed to create project:', error);
toast.error("Failed to create project", { toast.error('Failed to create project', {
description: error instanceof Error ? error.message : "Unknown error", description: error instanceof Error ? error.message : 'Unknown error',
}); });
} finally { } finally {
setIsCreating(false); setIsCreating(false);
@@ -356,8 +347,8 @@ export function WelcomeView() {
); );
if (!cloneResult.success || !cloneResult.projectPath) { if (!cloneResult.success || !cloneResult.projectPath) {
toast.error("Failed to clone template", { toast.error('Failed to clone template', {
description: cloneResult.error || "Unknown error occurred", description: cloneResult.error || 'Unknown error occurred',
}); });
return; return;
} }
@@ -368,8 +359,8 @@ export function WelcomeView() {
const initResult = await initializeProject(projectPath); const initResult = await initializeProject(projectPath);
if (!initResult.success) { if (!initResult.success) {
toast.error("Failed to initialize project", { toast.error('Failed to initialize project', {
description: initResult.error || "Unknown error occurred", description: initResult.error || 'Unknown error occurred',
}); });
return; return;
} }
@@ -387,15 +378,11 @@ export function WelcomeView() {
</overview> </overview>
<technology_stack> <technology_stack>
${template.techStack ${template.techStack.map((tech) => `<technology>${tech}</technology>`).join('\n ')}
.map((tech) => `<technology>${tech}</technology>`)
.join("\n ")}
</technology_stack> </technology_stack>
<core_capabilities> <core_capabilities>
${template.features ${template.features.map((feature) => `<capability>${feature}</capability>`).join('\n ')}
.map((feature) => `<capability>${feature}</capability>`)
.join("\n ")}
</core_capabilities> </core_capabilities>
<implemented_features> <implemented_features>
@@ -415,7 +402,7 @@ export function WelcomeView() {
setCurrentProject(project); setCurrentProject(project);
setShowNewProjectModal(false); setShowNewProjectModal(false);
toast.success("Project created from template", { toast.success('Project created from template', {
description: `Created ${projectName} from ${template.name}`, description: `Created ${projectName} from ${template.name}`,
}); });
@@ -431,9 +418,9 @@ export function WelcomeView() {
// Kick off project analysis // Kick off project analysis
analyzeProject(projectPath); analyzeProject(projectPath);
} catch (error) { } catch (error) {
console.error("Failed to create project from template:", error); console.error('Failed to create project from template:', error);
toast.error("Failed to create project", { toast.error('Failed to create project', {
description: error instanceof Error ? error.message : "Unknown error", description: error instanceof Error ? error.message : 'Unknown error',
}); });
} finally { } finally {
setIsCreating(false); setIsCreating(false);
@@ -454,15 +441,11 @@ export function WelcomeView() {
const api = getElectronAPI(); const api = getElectronAPI();
// Clone the repository // Clone the repository
const cloneResult = await httpClient.templates.clone( const cloneResult = await httpClient.templates.clone(repoUrl, projectName, parentDir);
repoUrl,
projectName,
parentDir
);
if (!cloneResult.success || !cloneResult.projectPath) { if (!cloneResult.success || !cloneResult.projectPath) {
toast.error("Failed to clone repository", { toast.error('Failed to clone repository', {
description: cloneResult.error || "Unknown error occurred", description: cloneResult.error || 'Unknown error occurred',
}); });
return; return;
} }
@@ -473,8 +456,8 @@ export function WelcomeView() {
const initResult = await initializeProject(projectPath); const initResult = await initializeProject(projectPath);
if (!initResult.success) { if (!initResult.success) {
toast.error("Failed to initialize project", { toast.error('Failed to initialize project', {
description: initResult.error || "Unknown error occurred", description: initResult.error || 'Unknown error occurred',
}); });
return; return;
} }
@@ -516,7 +499,7 @@ export function WelcomeView() {
setCurrentProject(project); setCurrentProject(project);
setShowNewProjectModal(false); setShowNewProjectModal(false);
toast.success("Project created from repository", { toast.success('Project created from repository', {
description: `Created ${projectName} from ${repoUrl}`, description: `Created ${projectName} from ${repoUrl}`,
}); });
@@ -532,9 +515,9 @@ export function WelcomeView() {
// Kick off project analysis // Kick off project analysis
analyzeProject(projectPath); analyzeProject(projectPath);
} catch (error) { } catch (error) {
console.error("Failed to create project from custom URL:", error); console.error('Failed to create project from custom URL:', error);
toast.error("Failed to create project", { toast.error('Failed to create project', {
description: error instanceof Error ? error.message : "Unknown error", description: error instanceof Error ? error.message : 'Unknown error',
}); });
} finally { } finally {
setIsCreating(false); setIsCreating(false);
@@ -587,12 +570,9 @@ export function WelcomeView() {
<Plus className="w-6 h-6 text-white" /> <Plus className="w-6 h-6 text-white" />
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<h3 className="text-lg font-semibold text-foreground mb-1.5"> <h3 className="text-lg font-semibold text-foreground mb-1.5">New Project</h3>
New Project
</h3>
<p className="text-sm text-muted-foreground leading-relaxed"> <p className="text-sm text-muted-foreground leading-relaxed">
Create a new project from scratch with AI-powered Create a new project from scratch with AI-powered development
development
</p> </p>
</div> </div>
</div> </div>
@@ -608,10 +588,7 @@ export function WelcomeView() {
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56"> <DropdownMenuContent align="end" className="w-56">
<DropdownMenuItem <DropdownMenuItem onClick={handleNewProject} data-testid="quick-setup-option">
onClick={handleNewProject}
data-testid="quick-setup-option"
>
<Plus className="w-4 h-4 mr-2" /> <Plus className="w-4 h-4 mr-2" />
Quick Setup Quick Setup
</DropdownMenuItem> </DropdownMenuItem>
@@ -640,9 +617,7 @@ export function WelcomeView() {
<FolderOpen className="w-6 h-6 text-muted-foreground group-hover:text-blue-500 transition-colors duration-300" /> <FolderOpen className="w-6 h-6 text-muted-foreground group-hover:text-blue-500 transition-colors duration-300" />
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<h3 className="text-lg font-semibold text-foreground mb-1.5"> <h3 className="text-lg font-semibold text-foreground mb-1.5">Open Project</h3>
Open Project
</h3>
<p className="text-sm text-muted-foreground leading-relaxed"> <p className="text-sm text-muted-foreground leading-relaxed">
Open an existing project folder to continue working Open an existing project folder to continue working
</p> </p>
@@ -667,9 +642,7 @@ export function WelcomeView() {
<div className="w-8 h-8 rounded-lg bg-muted/50 flex items-center justify-center"> <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" /> <Clock className="w-4 h-4 text-muted-foreground" />
</div> </div>
<h2 className="text-lg font-semibold text-foreground"> <h2 className="text-lg font-semibold text-foreground">Recent Projects</h2>
Recent Projects
</h2>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{recentProjects.map((project, index) => ( {recentProjects.map((project, index) => (
@@ -695,9 +668,7 @@ export function WelcomeView() {
</p> </p>
{project.lastOpened && ( {project.lastOpened && (
<p className="text-xs text-muted-foreground mt-1.5"> <p className="text-xs text-muted-foreground mt-1.5">
{new Date( {new Date(project.lastOpened).toLocaleDateString()}
project.lastOpened
).toLocaleDateString()}
</p> </p>
)} )}
</div> </div>
@@ -715,9 +686,7 @@ export function WelcomeView() {
<div className="w-20 h-20 rounded-2xl bg-muted/50 border border-border flex items-center justify-center mb-5"> <div className="w-20 h-20 rounded-2xl bg-muted/50 border border-border flex items-center justify-center mb-5">
<Sparkles className="w-10 h-10 text-muted-foreground/50" /> <Sparkles className="w-10 h-10 text-muted-foreground/50" />
</div> </div>
<h3 className="text-xl font-semibold text-foreground mb-2"> <h3 className="text-xl font-semibold text-foreground mb-2">No projects yet</h3>
No projects yet
</h3>
<p className="text-sm text-muted-foreground max-w-md leading-relaxed"> <p className="text-sm text-muted-foreground max-w-md leading-relaxed">
Get started by creating a new project or opening an existing one Get started by creating a new project or opening an existing one
</p> </p>
@@ -747,9 +716,7 @@ export function WelcomeView() {
<div className="w-8 h-8 rounded-lg bg-brand-500/10 flex items-center justify-center"> <div className="w-8 h-8 rounded-lg bg-brand-500/10 flex items-center justify-center">
<Sparkles className="w-4 h-4 text-brand-500" /> <Sparkles className="w-4 h-4 text-brand-500" />
</div> </div>
{initStatus?.isNewProject {initStatus?.isNewProject ? 'Project Initialized' : 'Project Updated'}
? "Project Initialized"
: "Project Updated"}
</DialogTitle> </DialogTitle>
<DialogDescription className="text-muted-foreground mt-1"> <DialogDescription className="text-muted-foreground mt-1">
{initStatus?.isNewProject {initStatus?.isNewProject
@@ -759,9 +726,7 @@ export function WelcomeView() {
</DialogHeader> </DialogHeader>
<div className="py-4"> <div className="py-4">
<div className="space-y-3"> <div className="space-y-3">
<p className="text-sm text-foreground font-medium"> <p className="text-sm text-foreground font-medium">Created files:</p>
Created files:
</p>
<ul className="space-y-2"> <ul className="space-y-2">
{initStatus?.createdFiles.map((file) => ( {initStatus?.createdFiles.map((file) => (
<li <li
@@ -788,12 +753,12 @@ export function WelcomeView() {
</div> </div>
) : ( ) : (
<p className="text-sm text-muted-foreground leading-relaxed"> <p className="text-sm text-muted-foreground leading-relaxed">
<span className="text-brand-500 font-medium">Tip:</span> Edit the{" "} <span className="text-brand-500 font-medium">Tip:</span> Edit the{' '}
<code className="text-xs bg-muted px-1.5 py-0.5 rounded font-mono"> <code className="text-xs bg-muted px-1.5 py-0.5 rounded font-mono">
app_spec.txt app_spec.txt
</code>{" "} </code>{' '}
file to describe your project. The AI agent will use this to file to describe your project. The AI agent will use this to understand your
understand your project structure. project structure.
</p> </p>
)} )}
</div> </div>
@@ -826,9 +791,7 @@ export function WelcomeView() {
> >
<div className="flex flex-col items-center gap-4 p-8 rounded-2xl bg-card border border-border shadow-2xl"> <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" /> <Loader2 className="w-10 h-10 text-brand-500 animate-spin" />
<p className="text-foreground font-medium"> <p className="text-foreground font-medium">Initializing project...</p>
Initializing project...
</p>
</div> </div>
</div> </div>
)} )}