mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 20:23:36 +00:00
🗂️ 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:
@@ -1,4 +1,3 @@
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -6,9 +5,9 @@ import {
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Trash2 } from "lucide-react";
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Trash2 } from 'lucide-react';
|
||||
|
||||
interface DeleteAllArchivedSessionsDialogProps {
|
||||
open: boolean;
|
||||
@@ -29,8 +28,7 @@ export function DeleteAllArchivedSessionsDialog({
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete All Archived Sessions</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to delete all archived sessions? This action
|
||||
cannot be undone.
|
||||
Are you sure you want to delete all archived sessions? This action cannot be undone.
|
||||
{archivedCount > 0 && (
|
||||
<span className="block mt-2 text-yellow-500">
|
||||
{archivedCount} session(s) will be deleted.
|
||||
@@ -1,6 +1,6 @@
|
||||
import { MessageSquare } from "lucide-react";
|
||||
import { DeleteConfirmDialog } from "@/components/ui/delete-confirm-dialog";
|
||||
import type { SessionListItem } from "@/types/electron";
|
||||
import { MessageSquare } from 'lucide-react';
|
||||
import { DeleteConfirmDialog } from '@/components/ui/delete-confirm-dialog';
|
||||
import type { SessionListItem } from '@/types/electron';
|
||||
|
||||
interface DeleteSessionDialogProps {
|
||||
open: boolean;
|
||||
@@ -38,12 +38,8 @@ export function DeleteSessionDialog({
|
||||
<MessageSquare className="w-5 h-5 text-brand-500" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="font-medium text-foreground truncate">
|
||||
{session.name}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{session.messageCount} messages
|
||||
</p>
|
||||
<p className="font-medium text-foreground truncate">{session.name}</p>
|
||||
<p className="text-xs text-muted-foreground">{session.messageCount} messages</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -1,2 +1,6 @@
|
||||
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 { NewProjectModal } from './new-project-modal';
|
||||
export { WorkspacePickerModal } from './workspace-picker-modal';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -6,13 +6,13 @@ import {
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { HotkeyButton } from "@/components/ui/hotkey-button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { HotkeyButton } from '@/components/ui/hotkey-button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
FolderPlus,
|
||||
FolderOpen,
|
||||
@@ -22,15 +22,12 @@ import {
|
||||
Loader2,
|
||||
Link,
|
||||
Folder,
|
||||
} from "lucide-react";
|
||||
import { starterTemplates, type StarterTemplate } from "@/lib/templates";
|
||||
import { getElectronAPI } from "@/lib/electron";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useFileBrowser } from "@/contexts/file-browser-context";
|
||||
import {
|
||||
getDefaultWorkspaceDirectory,
|
||||
saveLastProjectDirectory,
|
||||
} from "@/lib/workspace-config";
|
||||
} from 'lucide-react';
|
||||
import { starterTemplates, type StarterTemplate } from '@/lib/templates';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useFileBrowser } from '@/contexts/file-browser-context';
|
||||
import { getDefaultWorkspaceDirectory, saveLastProjectDirectory } from '@/lib/workspace-config';
|
||||
|
||||
interface ValidationErrors {
|
||||
projectName?: boolean;
|
||||
@@ -42,20 +39,13 @@ interface ValidationErrors {
|
||||
interface NewProjectModalProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onCreateBlankProject: (
|
||||
projectName: string,
|
||||
parentDir: string
|
||||
) => Promise<void>;
|
||||
onCreateBlankProject: (projectName: string, parentDir: string) => Promise<void>;
|
||||
onCreateFromTemplate: (
|
||||
template: StarterTemplate,
|
||||
projectName: string,
|
||||
parentDir: string
|
||||
) => Promise<void>;
|
||||
onCreateFromCustomUrl: (
|
||||
repoUrl: string,
|
||||
projectName: string,
|
||||
parentDir: string
|
||||
) => Promise<void>;
|
||||
onCreateFromCustomUrl: (repoUrl: string, projectName: string, parentDir: string) => Promise<void>;
|
||||
isCreating: boolean;
|
||||
}
|
||||
|
||||
@@ -67,14 +57,13 @@ export function NewProjectModal({
|
||||
onCreateFromCustomUrl,
|
||||
isCreating,
|
||||
}: NewProjectModalProps) {
|
||||
const [activeTab, setActiveTab] = useState<"blank" | "template">("blank");
|
||||
const [projectName, setProjectName] = useState("");
|
||||
const [workspaceDir, setWorkspaceDir] = useState<string>("");
|
||||
const [activeTab, setActiveTab] = useState<'blank' | 'template'>('blank');
|
||||
const [projectName, setProjectName] = useState('');
|
||||
const [workspaceDir, setWorkspaceDir] = useState<string>('');
|
||||
const [isLoadingWorkspace, setIsLoadingWorkspace] = useState(false);
|
||||
const [selectedTemplate, setSelectedTemplate] =
|
||||
useState<StarterTemplate | null>(null);
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<StarterTemplate | null>(null);
|
||||
const [useCustomUrl, setUseCustomUrl] = useState(false);
|
||||
const [customUrl, setCustomUrl] = useState("");
|
||||
const [customUrl, setCustomUrl] = useState('');
|
||||
const [errors, setErrors] = useState<ValidationErrors>({});
|
||||
const { openFileBrowser } = useFileBrowser();
|
||||
|
||||
@@ -89,7 +78,7 @@ export function NewProjectModal({
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Failed to get default workspace directory:", error);
|
||||
console.error('Failed to get default workspace directory:', error);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoadingWorkspace(false);
|
||||
@@ -100,11 +89,11 @@ export function NewProjectModal({
|
||||
// Reset form when modal closes
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setProjectName("");
|
||||
setProjectName('');
|
||||
setSelectedTemplate(null);
|
||||
setUseCustomUrl(false);
|
||||
setCustomUrl("");
|
||||
setActiveTab("blank");
|
||||
setCustomUrl('');
|
||||
setActiveTab('blank');
|
||||
setErrors({});
|
||||
}
|
||||
}, [open]);
|
||||
@@ -117,10 +106,7 @@ export function NewProjectModal({
|
||||
}, [projectName, errors.projectName]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
(selectedTemplate || (useCustomUrl && customUrl)) &&
|
||||
errors.templateSelection
|
||||
) {
|
||||
if ((selectedTemplate || (useCustomUrl && customUrl)) && errors.templateSelection) {
|
||||
setErrors((prev) => ({ ...prev, templateSelection: false }));
|
||||
}
|
||||
}, [selectedTemplate, useCustomUrl, customUrl, errors.templateSelection]);
|
||||
@@ -145,7 +131,7 @@ export function NewProjectModal({
|
||||
}
|
||||
|
||||
// Check template selection (only for template tab)
|
||||
if (activeTab === "template") {
|
||||
if (activeTab === 'template') {
|
||||
if (useCustomUrl) {
|
||||
if (!customUrl.trim()) {
|
||||
newErrors.customUrl = true;
|
||||
@@ -164,7 +150,7 @@ export function NewProjectModal({
|
||||
// Clear errors and proceed
|
||||
setErrors({});
|
||||
|
||||
if (activeTab === "blank") {
|
||||
if (activeTab === 'blank') {
|
||||
await onCreateBlankProject(projectName, workspaceDir);
|
||||
} else if (useCustomUrl && customUrl) {
|
||||
await onCreateFromCustomUrl(customUrl, projectName, workspaceDir);
|
||||
@@ -181,7 +167,7 @@ export function NewProjectModal({
|
||||
const handleSelectTemplate = (template: StarterTemplate) => {
|
||||
setSelectedTemplate(template);
|
||||
setUseCustomUrl(false);
|
||||
setCustomUrl("");
|
||||
setCustomUrl('');
|
||||
};
|
||||
|
||||
const handleToggleCustomUrl = () => {
|
||||
@@ -193,9 +179,8 @@ export function NewProjectModal({
|
||||
|
||||
const handleBrowseDirectory = async () => {
|
||||
const selectedPath = await openFileBrowser({
|
||||
title: "Select Base Project Directory",
|
||||
description:
|
||||
"Choose the parent directory where your project will be created",
|
||||
title: 'Select Base Project Directory',
|
||||
description: 'Choose the parent directory where your project will be created',
|
||||
initialPath: workspaceDir || undefined,
|
||||
});
|
||||
if (selectedPath) {
|
||||
@@ -211,15 +196,12 @@ export function NewProjectModal({
|
||||
|
||||
// Use platform-specific path separator
|
||||
const pathSep =
|
||||
typeof window !== "undefined" && (window as any).electronAPI
|
||||
? navigator.platform.indexOf("Win") !== -1
|
||||
? "\\"
|
||||
: "/"
|
||||
: "/";
|
||||
const projectPath =
|
||||
workspaceDir && projectName
|
||||
? `${workspaceDir}${pathSep}${projectName}`
|
||||
: "";
|
||||
typeof window !== 'undefined' && (window as any).electronAPI
|
||||
? navigator.platform.indexOf('Win') !== -1
|
||||
? '\\'
|
||||
: '/'
|
||||
: '/';
|
||||
const projectPath = workspaceDir && projectName ? `${workspaceDir}${pathSep}${projectName}` : '';
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
@@ -228,9 +210,7 @@ export function NewProjectModal({
|
||||
data-testid="new-project-modal"
|
||||
>
|
||||
<DialogHeader className="pb-2">
|
||||
<DialogTitle className="text-foreground">
|
||||
Create New Project
|
||||
</DialogTitle>
|
||||
<DialogTitle className="text-foreground">Create New Project</DialogTitle>
|
||||
<DialogDescription className="text-muted-foreground">
|
||||
Start with a blank project or choose from a starter template.
|
||||
</DialogDescription>
|
||||
@@ -241,13 +221,9 @@ export function NewProjectModal({
|
||||
<div className="space-y-2">
|
||||
<Label
|
||||
htmlFor="project-name"
|
||||
className={cn(
|
||||
"text-foreground",
|
||||
errors.projectName && "text-red-500"
|
||||
)}
|
||||
className={cn('text-foreground', errors.projectName && 'text-red-500')}
|
||||
>
|
||||
Project Name{" "}
|
||||
{errors.projectName && <span className="text-red-500">*</span>}
|
||||
Project Name {errors.projectName && <span className="text-red-500">*</span>}
|
||||
</Label>
|
||||
<Input
|
||||
id="project-name"
|
||||
@@ -255,33 +231,31 @@ export function NewProjectModal({
|
||||
value={projectName}
|
||||
onChange={(e) => setProjectName(e.target.value)}
|
||||
className={cn(
|
||||
"bg-input text-foreground placeholder:text-muted-foreground",
|
||||
'bg-input text-foreground placeholder:text-muted-foreground',
|
||||
errors.projectName
|
||||
? "border-red-500 focus:border-red-500 focus:ring-red-500/20"
|
||||
: "border-border"
|
||||
? 'border-red-500 focus:border-red-500 focus:ring-red-500/20'
|
||||
: 'border-border'
|
||||
)}
|
||||
data-testid="project-name-input"
|
||||
autoFocus
|
||||
/>
|
||||
{errors.projectName && (
|
||||
<p className="text-xs text-red-500">Project name is required</p>
|
||||
)}
|
||||
{errors.projectName && <p className="text-xs text-red-500">Project name is required</p>}
|
||||
</div>
|
||||
|
||||
{/* Workspace Directory Display */}
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-sm",
|
||||
errors.workspaceDir ? "text-red-500" : "text-muted-foreground"
|
||||
'flex items-center gap-2 text-sm',
|
||||
errors.workspaceDir ? 'text-red-500' : 'text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
<Folder className="w-4 h-4 shrink-0" />
|
||||
<span className="flex-1 min-w-0">
|
||||
{isLoadingWorkspace ? (
|
||||
"Loading workspace..."
|
||||
'Loading workspace...'
|
||||
) : workspaceDir ? (
|
||||
<>
|
||||
Will be created at:{" "}
|
||||
Will be created at:{' '}
|
||||
<code className="text-xs bg-muted px-1.5 py-0.5 rounded truncate">
|
||||
{projectPath || workspaceDir}
|
||||
</code>
|
||||
@@ -305,7 +279,7 @@ export function NewProjectModal({
|
||||
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={(v) => setActiveTab(v as "blank" | "template")}
|
||||
onValueChange={(v) => setActiveTab(v as 'blank' | 'template')}
|
||||
className="flex-1 flex flex-col overflow-hidden"
|
||||
>
|
||||
<TabsList className="w-full justify-start">
|
||||
@@ -323,9 +297,8 @@ export function NewProjectModal({
|
||||
<TabsContent value="blank" className="mt-0">
|
||||
<div className="p-4 rounded-lg bg-muted/50 border border-border">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Create an empty project with the standard .automaker directory
|
||||
structure. Perfect for starting from scratch or importing an
|
||||
existing codebase.
|
||||
Create an empty project with the standard .automaker directory structure. Perfect
|
||||
for starting from scratch or importing an existing codebase.
|
||||
</p>
|
||||
</div>
|
||||
</TabsContent>
|
||||
@@ -342,18 +315,18 @@ export function NewProjectModal({
|
||||
{/* Preset Templates */}
|
||||
<div
|
||||
className={cn(
|
||||
"space-y-3 rounded-lg p-1 -m-1",
|
||||
errors.templateSelection && "ring-2 ring-red-500/50"
|
||||
'space-y-3 rounded-lg p-1 -m-1',
|
||||
errors.templateSelection && 'ring-2 ring-red-500/50'
|
||||
)}
|
||||
>
|
||||
{starterTemplates.map((template) => (
|
||||
<div
|
||||
key={template.id}
|
||||
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
|
||||
? "border-brand-500 bg-brand-500/10"
|
||||
: "border-border bg-muted/30 hover:border-border-glass hover:bg-muted/50"
|
||||
? 'border-brand-500 bg-brand-500/10'
|
||||
: 'border-border bg-muted/30 hover:border-border-glass hover:bg-muted/50'
|
||||
)}
|
||||
onClick={() => handleSelectTemplate(template)}
|
||||
data-testid={`template-${template.id}`}
|
||||
@@ -361,13 +334,10 @@ export function NewProjectModal({
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h4 className="font-medium text-foreground">
|
||||
{template.name}
|
||||
</h4>
|
||||
{selectedTemplate?.id === template.id &&
|
||||
!useCustomUrl && (
|
||||
<Check className="w-4 h-4 text-brand-500" />
|
||||
)}
|
||||
<h4 className="font-medium text-foreground">{template.name}</h4>
|
||||
{selectedTemplate?.id === template.id && !useCustomUrl && (
|
||||
<Check className="w-4 h-4 text-brand-500" />
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mb-3">
|
||||
{template.description}
|
||||
@@ -376,11 +346,7 @@ export function NewProjectModal({
|
||||
{/* Tech Stack */}
|
||||
<div className="flex flex-wrap gap-1.5 mb-3">
|
||||
{template.techStack.slice(0, 6).map((tech) => (
|
||||
<Badge
|
||||
key={tech}
|
||||
variant="secondary"
|
||||
className="text-xs"
|
||||
>
|
||||
<Badge key={tech} variant="secondary" className="text-xs">
|
||||
{tech}
|
||||
</Badge>
|
||||
))}
|
||||
@@ -394,7 +360,7 @@ export function NewProjectModal({
|
||||
{/* Key Features */}
|
||||
<div className="text-xs text-muted-foreground">
|
||||
<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} more`}
|
||||
</div>
|
||||
@@ -419,47 +385,38 @@ export function NewProjectModal({
|
||||
{/* Custom URL Option */}
|
||||
<div
|
||||
className={cn(
|
||||
"p-4 rounded-lg border cursor-pointer transition-all",
|
||||
'p-4 rounded-lg border cursor-pointer transition-all',
|
||||
useCustomUrl
|
||||
? "border-brand-500 bg-brand-500/10"
|
||||
: "border-border bg-muted/30 hover:border-border-glass hover:bg-muted/50"
|
||||
? 'border-brand-500 bg-brand-500/10'
|
||||
: 'border-border bg-muted/30 hover:border-border-glass hover:bg-muted/50'
|
||||
)}
|
||||
onClick={handleToggleCustomUrl}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Link className="w-4 h-4 text-muted-foreground" />
|
||||
<h4 className="font-medium text-foreground">
|
||||
Custom GitHub URL
|
||||
</h4>
|
||||
{useCustomUrl && (
|
||||
<Check className="w-4 h-4 text-brand-500" />
|
||||
)}
|
||||
<h4 className="font-medium text-foreground">Custom GitHub URL</h4>
|
||||
{useCustomUrl && <Check className="w-4 h-4 text-brand-500" />}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mb-3">
|
||||
Clone any public GitHub repository as a starting point.
|
||||
</p>
|
||||
|
||||
{useCustomUrl && (
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="space-y-1"
|
||||
>
|
||||
<div onClick={(e) => e.stopPropagation()} className="space-y-1">
|
||||
<Input
|
||||
placeholder="https://github.com/username/repository"
|
||||
value={customUrl}
|
||||
onChange={(e) => setCustomUrl(e.target.value)}
|
||||
className={cn(
|
||||
"bg-input text-foreground placeholder:text-muted-foreground",
|
||||
'bg-input text-foreground placeholder:text-muted-foreground',
|
||||
errors.customUrl
|
||||
? "border-red-500 focus:border-red-500 focus:ring-red-500/20"
|
||||
: "border-border"
|
||||
? 'border-red-500 focus:border-red-500 focus:ring-red-500/20'
|
||||
: 'border-border'
|
||||
)}
|
||||
data-testid="custom-url-input"
|
||||
/>
|
||||
{errors.customUrl && (
|
||||
<p className="text-xs text-red-500">
|
||||
GitHub URL is required
|
||||
</p>
|
||||
<p className="text-xs text-red-500">GitHub URL is required</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -482,14 +439,14 @@ export function NewProjectModal({
|
||||
onClick={validateAndCreate}
|
||||
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"
|
||||
hotkey={{ key: "Enter", cmdCtrl: true }}
|
||||
hotkey={{ key: 'Enter', cmdCtrl: true }}
|
||||
hotkeyActive={open}
|
||||
data-testid="confirm-create-project"
|
||||
>
|
||||
{isCreating ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
{activeTab === "template" ? "Cloning..." : "Creating..."}
|
||||
{activeTab === 'template' ? 'Cloning...' : 'Creating...'}
|
||||
</>
|
||||
) : (
|
||||
<>Create Project</>
|
||||
@@ -1,5 +1,4 @@
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -7,10 +6,10 @@ import {
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Folder, Loader2, FolderOpen, AlertCircle } from "lucide-react";
|
||||
import { getHttpApiClient } from "@/lib/http-api-client";
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Folder, Loader2, FolderOpen, AlertCircle } from 'lucide-react';
|
||||
import { getHttpApiClient } from '@/lib/http-api-client';
|
||||
|
||||
interface WorkspaceDirectory {
|
||||
name: string;
|
||||
@@ -23,11 +22,7 @@ interface WorkspacePickerModalProps {
|
||||
onSelect: (path: string, name: string) => void;
|
||||
}
|
||||
|
||||
export function WorkspacePickerModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
onSelect,
|
||||
}: WorkspacePickerModalProps) {
|
||||
export function WorkspacePickerModal({ open, onOpenChange, onSelect }: WorkspacePickerModalProps) {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [directories, setDirectories] = useState<WorkspaceDirectory[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -43,10 +38,10 @@ export function WorkspacePickerModal({
|
||||
if (result.success && result.directories) {
|
||||
setDirectories(result.directories);
|
||||
} else {
|
||||
setError(result.error || "Failed to load directories");
|
||||
setError(result.error || 'Failed to load directories');
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to load directories");
|
||||
setError(err instanceof Error ? err.message : 'Failed to load directories');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -90,12 +85,7 @@ export function WorkspacePickerModal({
|
||||
<AlertCircle className="w-6 h-6 text-destructive" />
|
||||
</div>
|
||||
<p className="text-sm text-destructive">{error}</p>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={loadDirectories}
|
||||
className="mt-2"
|
||||
>
|
||||
<Button variant="secondary" size="sm" onClick={loadDirectories} className="mt-2">
|
||||
Try Again
|
||||
</Button>
|
||||
</div>
|
||||
@@ -128,9 +118,7 @@ export function WorkspacePickerModal({
|
||||
<p className="font-medium text-foreground truncate group-hover:text-brand-500 transition-colors">
|
||||
{dir.name}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground/70 truncate">
|
||||
{dir.path}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground/70 truncate">{dir.path}</p>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
@@ -72,7 +72,7 @@ import { toast } from 'sonner';
|
||||
import { themeOptions } from '@/config/theme-options';
|
||||
import type { SpecRegenerationEvent } from '@/types/electron';
|
||||
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 type { FeatureCount } from '@/components/views/spec-view/types';
|
||||
import {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { HotkeyButton } from "@/components/ui/hotkey-button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { HotkeyButton } from '@/components/ui/hotkey-button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import {
|
||||
Plus,
|
||||
MessageSquare,
|
||||
@@ -15,66 +14,66 @@ import {
|
||||
X,
|
||||
ArchiveRestore,
|
||||
Loader2,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { SessionListItem } from "@/types/electron";
|
||||
import { useKeyboardShortcutsConfig } from "@/hooks/use-keyboard-shortcuts";
|
||||
import { getElectronAPI } from "@/lib/electron";
|
||||
import { DeleteSessionDialog } from "@/components/delete-session-dialog";
|
||||
import { DeleteAllArchivedSessionsDialog } from "@/components/delete-all-archived-sessions-dialog";
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { SessionListItem } from '@/types/electron';
|
||||
import { useKeyboardShortcutsConfig } from '@/hooks/use-keyboard-shortcuts';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
import { DeleteSessionDialog } from '@/components/dialogs/delete-session-dialog';
|
||||
import { DeleteAllArchivedSessionsDialog } from '@/components/dialogs/delete-all-archived-sessions-dialog';
|
||||
|
||||
// Random session name generator
|
||||
const adjectives = [
|
||||
"Swift",
|
||||
"Bright",
|
||||
"Clever",
|
||||
"Dynamic",
|
||||
"Eager",
|
||||
"Focused",
|
||||
"Gentle",
|
||||
"Happy",
|
||||
"Inventive",
|
||||
"Jolly",
|
||||
"Keen",
|
||||
"Lively",
|
||||
"Mighty",
|
||||
"Noble",
|
||||
"Optimal",
|
||||
"Peaceful",
|
||||
"Quick",
|
||||
"Radiant",
|
||||
"Smart",
|
||||
"Tranquil",
|
||||
"Unique",
|
||||
"Vibrant",
|
||||
"Wise",
|
||||
"Zealous",
|
||||
'Swift',
|
||||
'Bright',
|
||||
'Clever',
|
||||
'Dynamic',
|
||||
'Eager',
|
||||
'Focused',
|
||||
'Gentle',
|
||||
'Happy',
|
||||
'Inventive',
|
||||
'Jolly',
|
||||
'Keen',
|
||||
'Lively',
|
||||
'Mighty',
|
||||
'Noble',
|
||||
'Optimal',
|
||||
'Peaceful',
|
||||
'Quick',
|
||||
'Radiant',
|
||||
'Smart',
|
||||
'Tranquil',
|
||||
'Unique',
|
||||
'Vibrant',
|
||||
'Wise',
|
||||
'Zealous',
|
||||
];
|
||||
|
||||
const nouns = [
|
||||
"Agent",
|
||||
"Builder",
|
||||
"Coder",
|
||||
"Developer",
|
||||
"Explorer",
|
||||
"Forge",
|
||||
"Garden",
|
||||
"Helper",
|
||||
"Innovator",
|
||||
"Journey",
|
||||
"Kernel",
|
||||
"Lighthouse",
|
||||
"Mission",
|
||||
"Navigator",
|
||||
"Oracle",
|
||||
"Project",
|
||||
"Quest",
|
||||
"Runner",
|
||||
"Spark",
|
||||
"Task",
|
||||
"Unicorn",
|
||||
"Voyage",
|
||||
"Workshop",
|
||||
'Agent',
|
||||
'Builder',
|
||||
'Coder',
|
||||
'Developer',
|
||||
'Explorer',
|
||||
'Forge',
|
||||
'Garden',
|
||||
'Helper',
|
||||
'Innovator',
|
||||
'Journey',
|
||||
'Kernel',
|
||||
'Lighthouse',
|
||||
'Mission',
|
||||
'Navigator',
|
||||
'Oracle',
|
||||
'Project',
|
||||
'Quest',
|
||||
'Runner',
|
||||
'Spark',
|
||||
'Task',
|
||||
'Unicorn',
|
||||
'Voyage',
|
||||
'Workshop',
|
||||
];
|
||||
|
||||
function generateRandomSessionName(): string {
|
||||
@@ -101,19 +100,15 @@ export function SessionManager({
|
||||
}: SessionManagerProps) {
|
||||
const shortcuts = useKeyboardShortcutsConfig();
|
||||
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 [editingName, setEditingName] = useState("");
|
||||
const [editingName, setEditingName] = useState('');
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [newSessionName, setNewSessionName] = useState("");
|
||||
const [runningSessions, setRunningSessions] = useState<Set<string>>(
|
||||
new Set()
|
||||
);
|
||||
const [newSessionName, setNewSessionName] = useState('');
|
||||
const [runningSessions, setRunningSessions] = useState<Set<string>>(new Set());
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||
const [sessionToDelete, setSessionToDelete] =
|
||||
useState<SessionListItem | null>(null);
|
||||
const [isDeleteAllArchivedDialogOpen, setIsDeleteAllArchivedDialogOpen] =
|
||||
useState(false);
|
||||
const [sessionToDelete, setSessionToDelete] = useState<SessionListItem | null>(null);
|
||||
const [isDeleteAllArchivedDialogOpen, setIsDeleteAllArchivedDialogOpen] = useState(false);
|
||||
|
||||
// Check running state for all sessions
|
||||
const checkRunningSessions = async (sessionList: SessionListItem[]) => {
|
||||
@@ -131,10 +126,7 @@ export function SessionManager({
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignore errors for individual session checks
|
||||
console.warn(
|
||||
`[SessionManager] Failed to check running state for ${session.id}:`,
|
||||
err
|
||||
);
|
||||
console.warn(`[SessionManager] Failed to check running state for ${session.id}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,14 +172,10 @@ export function SessionManager({
|
||||
|
||||
const sessionName = newSessionName.trim() || generateRandomSessionName();
|
||||
|
||||
const result = await api.sessions.create(
|
||||
sessionName,
|
||||
projectPath,
|
||||
projectPath
|
||||
);
|
||||
const result = await api.sessions.create(sessionName, projectPath, projectPath);
|
||||
|
||||
if (result.success && result.session?.id) {
|
||||
setNewSessionName("");
|
||||
setNewSessionName('');
|
||||
setIsCreating(false);
|
||||
await loadSessions();
|
||||
onSelectSession(result.session.id);
|
||||
@@ -201,11 +189,7 @@ export function SessionManager({
|
||||
|
||||
const sessionName = generateRandomSessionName();
|
||||
|
||||
const result = await api.sessions.create(
|
||||
sessionName,
|
||||
projectPath,
|
||||
projectPath
|
||||
);
|
||||
const result = await api.sessions.create(sessionName, projectPath, projectPath);
|
||||
|
||||
if (result.success && result.session?.id) {
|
||||
await loadSessions();
|
||||
@@ -234,7 +218,7 @@ export function SessionManager({
|
||||
|
||||
if (result.success) {
|
||||
setEditingSessionId(null);
|
||||
setEditingName("");
|
||||
setEditingName('');
|
||||
await loadSessions();
|
||||
}
|
||||
};
|
||||
@@ -243,7 +227,7 @@ export function SessionManager({
|
||||
const handleArchiveSession = async (sessionId: string) => {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.sessions) {
|
||||
console.error("[SessionManager] Sessions API not available");
|
||||
console.error('[SessionManager] Sessions API not available');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -256,10 +240,10 @@ export function SessionManager({
|
||||
}
|
||||
await loadSessions();
|
||||
} else {
|
||||
console.error("[SessionManager] Archive failed:", result.error);
|
||||
console.error('[SessionManager] Archive failed:', result.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 api = getElectronAPI();
|
||||
if (!api?.sessions) {
|
||||
console.error("[SessionManager] Sessions API not available");
|
||||
console.error('[SessionManager] Sessions API not available');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -276,10 +260,10 @@ export function SessionManager({
|
||||
if (result.success) {
|
||||
await loadSessions();
|
||||
} else {
|
||||
console.error("[SessionManager] Unarchive failed:", result.error);
|
||||
console.error('[SessionManager] Unarchive failed:', result.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 archivedSessions = sessions.filter((s) => s.isArchived);
|
||||
const displayedSessions =
|
||||
activeTab === "active" ? activeSessions : archivedSessions;
|
||||
const displayedSessions = activeTab === 'active' ? activeSessions : archivedSessions;
|
||||
|
||||
return (
|
||||
<Card className="h-full flex flex-col rounded-none">
|
||||
@@ -337,8 +320,8 @@ export function SessionManager({
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
// Switch to active tab if on archived tab
|
||||
if (activeTab === "archived") {
|
||||
setActiveTab("active");
|
||||
if (activeTab === 'archived') {
|
||||
setActiveTab('active');
|
||||
}
|
||||
handleQuickCreateSession();
|
||||
}}
|
||||
@@ -354,9 +337,7 @@ export function SessionManager({
|
||||
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={(value) =>
|
||||
setActiveTab(value as "active" | "archived")
|
||||
}
|
||||
onValueChange={(value) => setActiveTab(value as 'active' | 'archived')}
|
||||
className="w-full"
|
||||
>
|
||||
<TabsList className="w-full">
|
||||
@@ -372,10 +353,7 @@ export function SessionManager({
|
||||
</Tabs>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent
|
||||
className="flex-1 overflow-y-auto space-y-2"
|
||||
data-testid="session-list"
|
||||
>
|
||||
<CardContent className="flex-1 overflow-y-auto space-y-2" data-testid="session-list">
|
||||
{/* Create new session */}
|
||||
{isCreating && (
|
||||
<div className="p-3 border rounded-lg bg-muted/50">
|
||||
@@ -385,10 +363,10 @@ export function SessionManager({
|
||||
value={newSessionName}
|
||||
onChange={(e) => setNewSessionName(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") handleCreateSession();
|
||||
if (e.key === "Escape") {
|
||||
if (e.key === 'Enter') handleCreateSession();
|
||||
if (e.key === 'Escape') {
|
||||
setIsCreating(false);
|
||||
setNewSessionName("");
|
||||
setNewSessionName('');
|
||||
}
|
||||
}}
|
||||
autoFocus
|
||||
@@ -401,7 +379,7 @@ export function SessionManager({
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setIsCreating(false);
|
||||
setNewSessionName("");
|
||||
setNewSessionName('');
|
||||
}}
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
@@ -411,7 +389,7 @@ export function SessionManager({
|
||||
)}
|
||||
|
||||
{/* 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">
|
||||
<Button
|
||||
variant="destructive"
|
||||
@@ -431,9 +409,9 @@ export function SessionManager({
|
||||
<div
|
||||
key={session.id}
|
||||
className={cn(
|
||||
"p-3 border rounded-lg cursor-pointer transition-colors hover:bg-accent/50",
|
||||
currentSessionId === session.id && "bg-primary/10 border-primary",
|
||||
session.isArchived && "opacity-60"
|
||||
'p-3 border rounded-lg cursor-pointer transition-colors hover:bg-accent/50',
|
||||
currentSessionId === session.id && 'bg-primary/10 border-primary',
|
||||
session.isArchived && 'opacity-60'
|
||||
)}
|
||||
onClick={() => !session.isArchived && onSelectSession(session.id)}
|
||||
data-testid={`session-item-${session.id}`}
|
||||
@@ -446,10 +424,10 @@ export function SessionManager({
|
||||
value={editingName}
|
||||
onChange={(e) => setEditingName(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") handleRenameSession(session.id);
|
||||
if (e.key === "Escape") {
|
||||
if (e.key === 'Enter') handleRenameSession(session.id);
|
||||
if (e.key === 'Escape') {
|
||||
setEditingSessionId(null);
|
||||
setEditingName("");
|
||||
setEditingName('');
|
||||
}
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
@@ -472,7 +450,7 @@ export function SessionManager({
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setEditingSessionId(null);
|
||||
setEditingName("");
|
||||
setEditingName('');
|
||||
}}
|
||||
className="h-7"
|
||||
>
|
||||
@@ -483,16 +461,14 @@ export function SessionManager({
|
||||
<>
|
||||
<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) */}
|
||||
{(currentSessionId === session.id &&
|
||||
isCurrentSessionThinking) ||
|
||||
{(currentSessionId === session.id && isCurrentSessionThinking) ||
|
||||
runningSessions.has(session.id) ? (
|
||||
<Loader2 className="w-4 h-4 text-primary animate-spin shrink-0" />
|
||||
) : (
|
||||
<MessageSquare className="w-4 h-4 text-muted-foreground shrink-0" />
|
||||
)}
|
||||
<h3 className="font-medium truncate">{session.name}</h3>
|
||||
{((currentSessionId === session.id &&
|
||||
isCurrentSessionThinking) ||
|
||||
{((currentSessionId === session.id && isCurrentSessionThinking) ||
|
||||
runningSessions.has(session.id)) && (
|
||||
<span className="text-xs text-primary bg-primary/10 px-2 py-0.5 rounded-full">
|
||||
thinking...
|
||||
@@ -500,9 +476,7 @@ export function SessionManager({
|
||||
)}
|
||||
</div>
|
||||
{session.preview && (
|
||||
<p className="text-xs text-muted-foreground truncate">
|
||||
{session.preview}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground truncate">{session.preview}</p>
|
||||
)}
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
@@ -519,10 +493,7 @@ export function SessionManager({
|
||||
|
||||
{/* Actions */}
|
||||
{!session.isArchived && (
|
||||
<div
|
||||
className="flex gap-1"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex gap-1" onClick={(e) => e.stopPropagation()}>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
@@ -547,10 +518,7 @@ export function SessionManager({
|
||||
)}
|
||||
|
||||
{session.isArchived && (
|
||||
<div
|
||||
className="flex gap-1"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex gap-1" onClick={(e) => e.stopPropagation()}>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
@@ -578,14 +546,12 @@ export function SessionManager({
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<MessageSquare className="w-12 h-12 mx-auto mb-2 opacity-50" />
|
||||
<p className="text-sm">
|
||||
{activeTab === "active"
|
||||
? "No active sessions"
|
||||
: "No archived sessions"}
|
||||
{activeTab === 'active' ? 'No active sessions' : 'No archived sessions'}
|
||||
</p>
|
||||
<p className="text-xs">
|
||||
{activeTab === "active"
|
||||
? "Create your first session to get started"
|
||||
: "Archive sessions to see them here"}
|
||||
{activeTab === 'active'
|
||||
? 'Create your first session to get started'
|
||||
: 'Archive sessions to see them here'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -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';
|
||||
@@ -0,0 +1 @@
|
||||
export { useApiKeyManagement } from './use-api-key-management';
|
||||
@@ -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';
|
||||
@@ -0,0 +1 @@
|
||||
export { AppearanceSection } from './appearance-section';
|
||||
@@ -0,0 +1 @@
|
||||
export { AudioSection } from './audio-section';
|
||||
@@ -0,0 +1 @@
|
||||
export { ClaudeCliStatus } from './claude-cli-status';
|
||||
@@ -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';
|
||||
@@ -0,0 +1,2 @@
|
||||
export { NAV_ITEMS } from './navigation';
|
||||
export type { NavigationItem } from './navigation';
|
||||
@@ -0,0 +1 @@
|
||||
export { DangerZoneSection } from './danger-zone-section';
|
||||
@@ -0,0 +1 @@
|
||||
export { FeatureDefaultsSection } from './feature-defaults-section';
|
||||
@@ -0,0 +1 @@
|
||||
export { KeyboardShortcutsSection } from './keyboard-shortcuts-section';
|
||||
@@ -0,0 +1,2 @@
|
||||
export type { Theme } from '@/config/theme-options';
|
||||
export type { CliStatus, KanbanDetailLevel, Project, ApiKeys } from './types';
|
||||
@@ -1,6 +1,5 @@
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useState, useCallback } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -8,10 +7,10 @@ import {
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { useAppStore, type ThemeMode } from "@/store/app-store";
|
||||
import { getElectronAPI, type Project } from "@/lib/electron";
|
||||
import { initializeProject } from "@/lib/project-init";
|
||||
} from '@/components/ui/dialog';
|
||||
import { useAppStore, type ThemeMode } from '@/store/app-store';
|
||||
import { getElectronAPI, type Project } from '@/lib/electron';
|
||||
import { initializeProject } from '@/lib/project-init';
|
||||
import {
|
||||
FolderOpen,
|
||||
Plus,
|
||||
@@ -21,19 +20,19 @@ import {
|
||||
MessageSquare,
|
||||
ChevronDown,
|
||||
Loader2,
|
||||
} from "lucide-react";
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { toast } from "sonner";
|
||||
import { WorkspacePickerModal } from "@/components/workspace-picker-modal";
|
||||
import { NewProjectModal } from "@/components/new-project-modal";
|
||||
import { getHttpApiClient } from "@/lib/http-api-client";
|
||||
import type { StarterTemplate } from "@/lib/templates";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { toast } from 'sonner';
|
||||
import { WorkspacePickerModal } from '@/components/dialogs/workspace-picker-modal';
|
||||
import { NewProjectModal } from '@/components/dialogs/new-project-modal';
|
||||
import { getHttpApiClient } from '@/lib/http-api-client';
|
||||
import type { StarterTemplate } from '@/lib/templates';
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
|
||||
export function WelcomeView() {
|
||||
const {
|
||||
@@ -66,24 +65,24 @@ export function WelcomeView() {
|
||||
const api = getElectronAPI();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
setIsAnalyzing(true);
|
||||
try {
|
||||
console.log("[Welcome] Starting project analysis for:", projectPath);
|
||||
console.log('[Welcome] Starting project analysis for:', projectPath);
|
||||
const result = await api.autoMode.analyzeProject(projectPath);
|
||||
|
||||
if (result.success) {
|
||||
toast.success("Project analyzed", {
|
||||
description: "AI agent has analyzed your project structure",
|
||||
toast.success('Project analyzed', {
|
||||
description: 'AI agent has analyzed your project structure',
|
||||
});
|
||||
} else {
|
||||
console.error("[Welcome] Project analysis failed:", result.error);
|
||||
console.error('[Welcome] Project analysis failed:', result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[Welcome] Failed to analyze project:", error);
|
||||
console.error('[Welcome] Failed to analyze project:', error);
|
||||
} finally {
|
||||
setIsAnalyzing(false);
|
||||
}
|
||||
@@ -100,8 +99,8 @@ export function WelcomeView() {
|
||||
const initResult = await initializeProject(path);
|
||||
|
||||
if (!initResult.success) {
|
||||
toast.error("Failed to initialize project", {
|
||||
description: initResult.error || "Unknown error occurred",
|
||||
toast.error('Failed to initialize project', {
|
||||
description: initResult.error || 'Unknown error occurred',
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -126,26 +125,23 @@ export function WelcomeView() {
|
||||
setShowInitDialog(true);
|
||||
|
||||
// Kick off agent to analyze the project and update app_spec.txt
|
||||
console.log(
|
||||
"[Welcome] Project initialized, created files:",
|
||||
initResult.createdFiles
|
||||
);
|
||||
console.log("[Welcome] Kicking off project analysis agent...");
|
||||
console.log('[Welcome] Project initialized, created files:', initResult.createdFiles);
|
||||
console.log('[Welcome] Kicking off project analysis agent...');
|
||||
|
||||
// Start analysis in background (don't await, let it run async)
|
||||
analyzeProject(path);
|
||||
} else {
|
||||
toast.success("Project opened", {
|
||||
toast.success('Project opened', {
|
||||
description: `Opened ${name}`,
|
||||
});
|
||||
}
|
||||
|
||||
// Navigate to the board view
|
||||
navigate({ to: "/board" });
|
||||
navigate({ to: '/board' });
|
||||
} catch (error) {
|
||||
console.error("[Welcome] Failed to open project:", error);
|
||||
toast.error("Failed to open project", {
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
console.error('[Welcome] Failed to open project:', error);
|
||||
toast.error('Failed to open project', {
|
||||
description: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
} finally {
|
||||
setIsOpening(false);
|
||||
@@ -178,21 +174,19 @@ export function WelcomeView() {
|
||||
if (!result.canceled && result.filePaths[0]) {
|
||||
const path = result.filePaths[0];
|
||||
// Extract folder name from path (works on both Windows and Mac/Linux)
|
||||
const name =
|
||||
path.split(/[/\\]/).filter(Boolean).pop() || "Untitled Project";
|
||||
const name = path.split(/[/\\]/).filter(Boolean).pop() || 'Untitled Project';
|
||||
await initializeAndOpenProject(path, name);
|
||||
}
|
||||
}
|
||||
} 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
|
||||
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";
|
||||
const name = path.split(/[/\\]/).filter(Boolean).pop() || 'Untitled Project';
|
||||
await initializeAndOpenProject(path, name);
|
||||
}
|
||||
}
|
||||
@@ -224,16 +218,13 @@ export function WelcomeView() {
|
||||
};
|
||||
|
||||
const handleInteractiveMode = () => {
|
||||
navigate({ to: "/interview" });
|
||||
navigate({ to: '/interview' });
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a blank project with just .automaker directory structure
|
||||
*/
|
||||
const handleCreateBlankProject = async (
|
||||
projectName: string,
|
||||
parentDir: string
|
||||
) => {
|
||||
const handleCreateBlankProject = async (projectName: string, parentDir: string) => {
|
||||
setIsCreating(true);
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
@@ -242,7 +233,7 @@ export function WelcomeView() {
|
||||
// Validate that parent directory exists
|
||||
const parentExists = await api.exists(parentDir);
|
||||
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}`,
|
||||
});
|
||||
return;
|
||||
@@ -251,7 +242,7 @@ export function WelcomeView() {
|
||||
// Verify parent is actually a directory
|
||||
const parentStat = await api.stat(parentDir);
|
||||
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`,
|
||||
});
|
||||
return;
|
||||
@@ -260,8 +251,8 @@ export function WelcomeView() {
|
||||
// Create project directory
|
||||
const mkdirResult = await api.mkdir(projectPath);
|
||||
if (!mkdirResult.success) {
|
||||
toast.error("Failed to create project directory", {
|
||||
description: mkdirResult.error || "Unknown error occurred",
|
||||
toast.error('Failed to create project directory', {
|
||||
description: mkdirResult.error || 'Unknown error occurred',
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -270,8 +261,8 @@ export function WelcomeView() {
|
||||
const initResult = await initializeProject(projectPath);
|
||||
|
||||
if (!initResult.success) {
|
||||
toast.error("Failed to initialize project", {
|
||||
description: initResult.error || "Unknown error occurred",
|
||||
toast.error('Failed to initialize project', {
|
||||
description: initResult.error || 'Unknown error occurred',
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -313,7 +304,7 @@ export function WelcomeView() {
|
||||
setCurrentProject(project);
|
||||
setShowNewProjectModal(false);
|
||||
|
||||
toast.success("Project created", {
|
||||
toast.success('Project created', {
|
||||
description: `Created ${projectName} with .automaker directory`,
|
||||
});
|
||||
|
||||
@@ -326,9 +317,9 @@ export function WelcomeView() {
|
||||
});
|
||||
setShowInitDialog(true);
|
||||
} catch (error) {
|
||||
console.error("Failed to create project:", error);
|
||||
toast.error("Failed to create project", {
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
console.error('Failed to create project:', error);
|
||||
toast.error('Failed to create project', {
|
||||
description: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
@@ -356,8 +347,8 @@ export function WelcomeView() {
|
||||
);
|
||||
|
||||
if (!cloneResult.success || !cloneResult.projectPath) {
|
||||
toast.error("Failed to clone template", {
|
||||
description: cloneResult.error || "Unknown error occurred",
|
||||
toast.error('Failed to clone template', {
|
||||
description: cloneResult.error || 'Unknown error occurred',
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -368,8 +359,8 @@ export function WelcomeView() {
|
||||
const initResult = await initializeProject(projectPath);
|
||||
|
||||
if (!initResult.success) {
|
||||
toast.error("Failed to initialize project", {
|
||||
description: initResult.error || "Unknown error occurred",
|
||||
toast.error('Failed to initialize project', {
|
||||
description: initResult.error || 'Unknown error occurred',
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -387,15 +378,11 @@ export function WelcomeView() {
|
||||
</overview>
|
||||
|
||||
<technology_stack>
|
||||
${template.techStack
|
||||
.map((tech) => `<technology>${tech}</technology>`)
|
||||
.join("\n ")}
|
||||
${template.techStack.map((tech) => `<technology>${tech}</technology>`).join('\n ')}
|
||||
</technology_stack>
|
||||
|
||||
<core_capabilities>
|
||||
${template.features
|
||||
.map((feature) => `<capability>${feature}</capability>`)
|
||||
.join("\n ")}
|
||||
${template.features.map((feature) => `<capability>${feature}</capability>`).join('\n ')}
|
||||
</core_capabilities>
|
||||
|
||||
<implemented_features>
|
||||
@@ -415,7 +402,7 @@ export function WelcomeView() {
|
||||
setCurrentProject(project);
|
||||
setShowNewProjectModal(false);
|
||||
|
||||
toast.success("Project created from template", {
|
||||
toast.success('Project created from template', {
|
||||
description: `Created ${projectName} from ${template.name}`,
|
||||
});
|
||||
|
||||
@@ -431,9 +418,9 @@ export function WelcomeView() {
|
||||
// Kick off project analysis
|
||||
analyzeProject(projectPath);
|
||||
} catch (error) {
|
||||
console.error("Failed to create project from template:", error);
|
||||
toast.error("Failed to create project", {
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
console.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);
|
||||
@@ -454,15 +441,11 @@ export function WelcomeView() {
|
||||
const api = getElectronAPI();
|
||||
|
||||
// Clone the repository
|
||||
const cloneResult = await httpClient.templates.clone(
|
||||
repoUrl,
|
||||
projectName,
|
||||
parentDir
|
||||
);
|
||||
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",
|
||||
toast.error('Failed to clone repository', {
|
||||
description: cloneResult.error || 'Unknown error occurred',
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -473,8 +456,8 @@ export function WelcomeView() {
|
||||
const initResult = await initializeProject(projectPath);
|
||||
|
||||
if (!initResult.success) {
|
||||
toast.error("Failed to initialize project", {
|
||||
description: initResult.error || "Unknown error occurred",
|
||||
toast.error('Failed to initialize project', {
|
||||
description: initResult.error || 'Unknown error occurred',
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -516,7 +499,7 @@ export function WelcomeView() {
|
||||
setCurrentProject(project);
|
||||
setShowNewProjectModal(false);
|
||||
|
||||
toast.success("Project created from repository", {
|
||||
toast.success('Project created from repository', {
|
||||
description: `Created ${projectName} from ${repoUrl}`,
|
||||
});
|
||||
|
||||
@@ -532,9 +515,9 @@ export function WelcomeView() {
|
||||
// Kick off project analysis
|
||||
analyzeProject(projectPath);
|
||||
} catch (error) {
|
||||
console.error("Failed to create project from custom URL:", error);
|
||||
toast.error("Failed to create project", {
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
console.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);
|
||||
@@ -587,12 +570,9 @@ export function WelcomeView() {
|
||||
<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>
|
||||
<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
|
||||
Create a new project from scratch with AI-powered development
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -608,10 +588,7 @@ export function WelcomeView() {
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-56">
|
||||
<DropdownMenuItem
|
||||
onClick={handleNewProject}
|
||||
data-testid="quick-setup-option"
|
||||
>
|
||||
<DropdownMenuItem onClick={handleNewProject} data-testid="quick-setup-option">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Quick Setup
|
||||
</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" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-lg font-semibold text-foreground mb-1.5">
|
||||
Open Project
|
||||
</h3>
|
||||
<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>
|
||||
@@ -667,9 +642,7 @@ export function WelcomeView() {
|
||||
<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>
|
||||
<h2 className="text-lg font-semibold text-foreground">
|
||||
Recent Projects
|
||||
</h2>
|
||||
<h2 className="text-lg font-semibold text-foreground">Recent Projects</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{recentProjects.map((project, index) => (
|
||||
@@ -695,9 +668,7 @@ export function WelcomeView() {
|
||||
</p>
|
||||
{project.lastOpened && (
|
||||
<p className="text-xs text-muted-foreground mt-1.5">
|
||||
{new Date(
|
||||
project.lastOpened
|
||||
).toLocaleDateString()}
|
||||
{new Date(project.lastOpened).toLocaleDateString()}
|
||||
</p>
|
||||
)}
|
||||
</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">
|
||||
<Sparkles className="w-10 h-10 text-muted-foreground/50" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold text-foreground mb-2">
|
||||
No projects yet
|
||||
</h3>
|
||||
<h3 className="text-xl font-semibold text-foreground mb-2">No projects yet</h3>
|
||||
<p className="text-sm text-muted-foreground max-w-md leading-relaxed">
|
||||
Get started by creating a new project or opening an existing one
|
||||
</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">
|
||||
<Sparkles className="w-4 h-4 text-brand-500" />
|
||||
</div>
|
||||
{initStatus?.isNewProject
|
||||
? "Project Initialized"
|
||||
: "Project Updated"}
|
||||
{initStatus?.isNewProject ? 'Project Initialized' : 'Project Updated'}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-muted-foreground mt-1">
|
||||
{initStatus?.isNewProject
|
||||
@@ -759,9 +726,7 @@ export function WelcomeView() {
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm text-foreground font-medium">
|
||||
Created files:
|
||||
</p>
|
||||
<p className="text-sm text-foreground font-medium">Created files:</p>
|
||||
<ul className="space-y-2">
|
||||
{initStatus?.createdFiles.map((file) => (
|
||||
<li
|
||||
@@ -788,12 +753,12 @@ export function WelcomeView() {
|
||||
</div>
|
||||
) : (
|
||||
<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">
|
||||
app_spec.txt
|
||||
</code>{" "}
|
||||
file to describe your project. The AI agent will use this to
|
||||
understand your project structure.
|
||||
</code>{' '}
|
||||
file to describe your project. The AI agent will use this to understand your
|
||||
project structure.
|
||||
</p>
|
||||
)}
|
||||
</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">
|
||||
<Loader2 className="w-10 h-10 text-brand-500 animate-spin" />
|
||||
<p className="text-foreground font-medium">
|
||||
Initializing project...
|
||||
</p>
|
||||
<p className="text-foreground font-medium">Initializing project...</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user