style: fix formatting with Prettier

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
SuperComboGamer
2025-12-21 20:31:57 -05:00
parent 584f5a3426
commit 8d578558ff
295 changed files with 9088 additions and 10546 deletions

View File

@@ -1,16 +1,9 @@
import { useState, useCallback } from "react";
import { useAppStore } from "@/store/app-store";
import {
Card,
CardContent,
CardHeader,
CardTitle,
CardDescription,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useState, useCallback } from 'react';
import { useAppStore } from '@/store/app-store';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
FileText,
FolderOpen,
@@ -22,9 +15,9 @@ import {
File,
Pencil,
Wrench,
} from "lucide-react";
import { cn } from "@/lib/utils";
import { getElectronAPI } from "@/lib/electron";
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { getElectronAPI } from '@/lib/electron';
interface ToolResult {
success: boolean;
@@ -45,20 +38,18 @@ export function AgentToolsView() {
const api = getElectronAPI();
// Read File Tool State
const [readFilePath, setReadFilePath] = useState("");
const [readFilePath, setReadFilePath] = useState('');
const [readFileResult, setReadFileResult] = useState<ToolResult | null>(null);
const [isReadingFile, setIsReadingFile] = useState(false);
// Write File Tool State
const [writeFilePath, setWriteFilePath] = useState("");
const [writeFileContent, setWriteFileContent] = useState("");
const [writeFileResult, setWriteFileResult] = useState<ToolResult | null>(
null
);
const [writeFilePath, setWriteFilePath] = useState('');
const [writeFileContent, setWriteFileContent] = useState('');
const [writeFileResult, setWriteFileResult] = useState<ToolResult | null>(null);
const [isWritingFile, setIsWritingFile] = useState(false);
// Terminal Tool State
const [terminalCommand, setTerminalCommand] = useState("ls");
const [terminalCommand, setTerminalCommand] = useState('ls');
const [terminalResult, setTerminalResult] = useState<ToolResult | null>(null);
const [isRunningCommand, setIsRunningCommand] = useState(false);
@@ -85,7 +76,7 @@ export function AgentToolsView() {
} else {
setReadFileResult({
success: false,
error: result.error || "Failed to read file",
error: result.error || 'Failed to read file',
timestamp: new Date(),
});
console.log(`[Agent Tool] File read failed: ${result.error}`);
@@ -93,7 +84,7 @@ export function AgentToolsView() {
} catch (error) {
setReadFileResult({
success: false,
error: error instanceof Error ? error.message : "Unknown error",
error: error instanceof Error ? error.message : 'Unknown error',
timestamp: new Date(),
});
} finally {
@@ -124,7 +115,7 @@ export function AgentToolsView() {
} else {
setWriteFileResult({
success: false,
error: result.error || "Failed to write file",
error: result.error || 'Failed to write file',
timestamp: new Date(),
});
console.log(`[Agent Tool] File write failed: ${result.error}`);
@@ -132,7 +123,7 @@ export function AgentToolsView() {
} catch (error) {
setWriteFileResult({
success: false,
error: error instanceof Error ? error.message : "Unknown error",
error: error instanceof Error ? error.message : 'Unknown error',
timestamp: new Date(),
});
} finally {
@@ -154,13 +145,12 @@ export function AgentToolsView() {
// Simulated outputs for common commands (preview mode)
// In production, the agent executes commands via Claude SDK
const simulatedOutputs: Record<string, string> = {
ls: "app_spec.txt\nfeatures\nnode_modules\npackage.json\nsrc\ntests\ntsconfig.json",
pwd: currentProject?.path || "/Users/demo/project",
"echo hello": "hello",
whoami: "automaker-agent",
ls: 'app_spec.txt\nfeatures\nnode_modules\npackage.json\nsrc\ntests\ntsconfig.json',
pwd: currentProject?.path || '/Users/demo/project',
'echo hello': 'hello',
whoami: 'automaker-agent',
date: new Date().toString(),
"cat package.json":
'{\n "name": "demo-project",\n "version": "1.0.0"\n}',
'cat package.json': '{\n "name": "demo-project",\n "version": "1.0.0"\n}',
};
// Simulate command execution delay
@@ -175,13 +165,11 @@ export function AgentToolsView() {
output: output,
timestamp: new Date(),
});
console.log(
`[Agent Tool] Command executed successfully: ${terminalCommand}`
);
console.log(`[Agent Tool] Command executed successfully: ${terminalCommand}`);
} catch (error) {
setTerminalResult({
success: false,
error: error instanceof Error ? error.message : "Unknown error",
error: error instanceof Error ? error.message : 'Unknown error',
timestamp: new Date(),
});
} finally {
@@ -191,26 +179,18 @@ export function AgentToolsView() {
if (!currentProject) {
return (
<div
className="flex-1 flex items-center justify-center"
data-testid="agent-tools-no-project"
>
<div className="flex-1 flex items-center justify-center" data-testid="agent-tools-no-project">
<div className="text-center">
<Wrench className="w-12 h-12 text-muted-foreground mx-auto mb-4" />
<h2 className="text-xl font-semibold mb-2">No Project Selected</h2>
<p className="text-muted-foreground">
Open or create a project to test agent tools.
</p>
<p className="text-muted-foreground">Open or create a project to test agent tools.</p>
</div>
</div>
);
}
return (
<div
className="flex-1 flex flex-col overflow-hidden content-bg"
data-testid="agent-tools-view"
>
<div className="flex-1 flex flex-col overflow-hidden content-bg" data-testid="agent-tools-view">
{/* Header */}
<div className="flex items-center gap-3 p-4 border-b border-border bg-glass backdrop-blur-md">
<Wrench className="w-5 h-5 text-primary" />
@@ -232,9 +212,7 @@ export function AgentToolsView() {
<File className="w-5 h-5 text-blue-500" />
<CardTitle className="text-lg">Read File</CardTitle>
</div>
<CardDescription>
Agent requests to read a file from the filesystem
</CardDescription>
<CardDescription>Agent requests to read a file from the filesystem</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
@@ -270,10 +248,10 @@ export function AgentToolsView() {
{readFileResult && (
<div
className={cn(
"p-3 rounded-md border",
'p-3 rounded-md border',
readFileResult.success
? "bg-green-500/10 border-green-500/20"
: "bg-red-500/10 border-red-500/20"
? 'bg-green-500/10 border-green-500/20'
: 'bg-red-500/10 border-red-500/20'
)}
data-testid="read-file-result"
>
@@ -284,13 +262,11 @@ export function AgentToolsView() {
<XCircle className="w-4 h-4 text-red-500" />
)}
<span className="text-sm font-medium">
{readFileResult.success ? "Success" : "Failed"}
{readFileResult.success ? 'Success' : 'Failed'}
</span>
</div>
<pre className="text-xs overflow-auto max-h-40 whitespace-pre-wrap">
{readFileResult.success
? readFileResult.output
: readFileResult.error}
{readFileResult.success ? readFileResult.output : readFileResult.error}
</pre>
</div>
)}
@@ -304,9 +280,7 @@ export function AgentToolsView() {
<Pencil className="w-5 h-5 text-green-500" />
<CardTitle className="text-lg">Write File</CardTitle>
</div>
<CardDescription>
Agent requests to write content to a file
</CardDescription>
<CardDescription>Agent requests to write content to a file</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
@@ -332,11 +306,7 @@ export function AgentToolsView() {
</div>
<Button
onClick={handleWriteFile}
disabled={
isWritingFile ||
!writeFilePath.trim() ||
!writeFileContent.trim()
}
disabled={isWritingFile || !writeFilePath.trim() || !writeFileContent.trim()}
className="w-full"
data-testid="write-file-button"
>
@@ -357,10 +327,10 @@ export function AgentToolsView() {
{writeFileResult && (
<div
className={cn(
"p-3 rounded-md border",
'p-3 rounded-md border',
writeFileResult.success
? "bg-green-500/10 border-green-500/20"
: "bg-red-500/10 border-red-500/20"
? 'bg-green-500/10 border-green-500/20'
: 'bg-red-500/10 border-red-500/20'
)}
data-testid="write-file-result"
>
@@ -371,13 +341,11 @@ export function AgentToolsView() {
<XCircle className="w-4 h-4 text-red-500" />
)}
<span className="text-sm font-medium">
{writeFileResult.success ? "Success" : "Failed"}
{writeFileResult.success ? 'Success' : 'Failed'}
</span>
</div>
<pre className="text-xs overflow-auto max-h-40 whitespace-pre-wrap">
{writeFileResult.success
? writeFileResult.output
: writeFileResult.error}
{writeFileResult.success ? writeFileResult.output : writeFileResult.error}
</pre>
</div>
)}
@@ -391,9 +359,7 @@ export function AgentToolsView() {
<Terminal className="w-5 h-5 text-purple-500" />
<CardTitle className="text-lg">Run Terminal</CardTitle>
</div>
<CardDescription>
Agent requests to execute a terminal command
</CardDescription>
<CardDescription>Agent requests to execute a terminal command</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
@@ -429,10 +395,10 @@ export function AgentToolsView() {
{terminalResult && (
<div
className={cn(
"p-3 rounded-md border",
'p-3 rounded-md border',
terminalResult.success
? "bg-green-500/10 border-green-500/20"
: "bg-red-500/10 border-red-500/20"
? 'bg-green-500/10 border-green-500/20'
: 'bg-red-500/10 border-red-500/20'
)}
data-testid="terminal-result"
>
@@ -443,15 +409,13 @@ export function AgentToolsView() {
<XCircle className="w-4 h-4 text-red-500" />
)}
<span className="text-sm font-medium">
{terminalResult.success ? "Success" : "Failed"}
{terminalResult.success ? 'Success' : 'Failed'}
</span>
</div>
<pre className="text-xs overflow-auto max-h-40 whitespace-pre-wrap font-mono bg-black/50 text-green-400 p-2 rounded">
$ {terminalCommand}
{"\n"}
{terminalResult.success
? terminalResult.output
: terminalResult.error}
{'\n'}
{terminalResult.success ? terminalResult.output : terminalResult.error}
</pre>
</div>
)}
@@ -463,15 +427,12 @@ export function AgentToolsView() {
<Card className="mt-6" data-testid="tool-log">
<CardHeader>
<CardTitle className="text-lg">Tool Execution Log</CardTitle>
<CardDescription>
View agent tool requests and responses
</CardDescription>
<CardDescription>View agent tool requests and responses</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-2 text-sm">
<p className="text-muted-foreground">
Open your browser&apos;s developer console to see detailed agent
tool logs.
Open your browser&apos;s developer console to see detailed agent tool logs.
</p>
<ul className="list-disc list-inside space-y-1 text-muted-foreground">
<li>Read File - Agent requests file content from filesystem</li>

View File

@@ -1,16 +1,15 @@
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { ImageIcon, Archive, Minimize2, Square, Maximize2 } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { ImageIcon, Archive, Minimize2, Square, Maximize2 } from 'lucide-react';
import { cn } from '@/lib/utils';
interface BoardControlsProps {
isMounted: boolean;
onShowBoardBackground: () => void;
onShowCompletedModal: () => void;
completedCount: number;
kanbanCardDetailLevel: "minimal" | "standard" | "detailed";
onDetailLevelChange: (level: "minimal" | "standard" | "detailed") => void;
kanbanCardDetailLevel: 'minimal' | 'standard' | 'detailed';
onDetailLevelChange: (level: 'minimal' | 'standard' | 'detailed') => void;
}
export function BoardControls({
@@ -57,7 +56,7 @@ export function BoardControls({
<Archive className="w-4 h-4" />
{completedCount > 0 && (
<span className="absolute -top-1 -right-1 bg-brand-500 text-white text-[10px] font-bold rounded-full w-4 h-4 flex items-center justify-center">
{completedCount > 99 ? "99+" : completedCount}
{completedCount > 99 ? '99+' : completedCount}
</span>
)}
</Button>
@@ -75,12 +74,12 @@ export function BoardControls({
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={() => onDetailLevelChange("minimal")}
onClick={() => onDetailLevelChange('minimal')}
className={cn(
"p-2 rounded-l-lg transition-colors",
kanbanCardDetailLevel === "minimal"
? "bg-brand-500/20 text-brand-500"
: "text-muted-foreground hover:text-foreground hover:bg-accent"
'p-2 rounded-l-lg transition-colors',
kanbanCardDetailLevel === 'minimal'
? 'bg-brand-500/20 text-brand-500'
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
)}
data-testid="kanban-toggle-minimal"
>
@@ -94,12 +93,12 @@ export function BoardControls({
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={() => onDetailLevelChange("standard")}
onClick={() => onDetailLevelChange('standard')}
className={cn(
"p-2 transition-colors",
kanbanCardDetailLevel === "standard"
? "bg-brand-500/20 text-brand-500"
: "text-muted-foreground hover:text-foreground hover:bg-accent"
'p-2 transition-colors',
kanbanCardDetailLevel === 'standard'
? 'bg-brand-500/20 text-brand-500'
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
)}
data-testid="kanban-toggle-standard"
>
@@ -113,12 +112,12 @@ export function BoardControls({
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={() => onDetailLevelChange("detailed")}
onClick={() => onDetailLevelChange('detailed')}
className={cn(
"p-2 rounded-r-lg transition-colors",
kanbanCardDetailLevel === "detailed"
? "bg-brand-500/20 text-brand-500"
: "text-muted-foreground hover:text-foreground hover:bg-accent"
'p-2 rounded-r-lg transition-colors',
kanbanCardDetailLevel === 'detailed'
? 'bg-brand-500/20 text-brand-500'
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
)}
data-testid="kanban-toggle-detailed"
>

View File

@@ -1,7 +1,6 @@
import { useRef, useEffect } from "react";
import { Input } from "@/components/ui/input";
import { Search, X, Loader2 } from "lucide-react";
import { useRef, useEffect } from 'react';
import { Input } from '@/components/ui/input';
import { Search, X, Loader2 } from 'lucide-react';
interface BoardSearchBarProps {
searchQuery: string;
@@ -25,7 +24,7 @@ export function BoardSearchBar({
const handleKeyDown = (e: KeyboardEvent) => {
// Only focus if not typing in an input/textarea
if (
e.key === "/" &&
e.key === '/' &&
!(e.target instanceof HTMLInputElement) &&
!(e.target instanceof HTMLTextAreaElement)
) {
@@ -34,8 +33,8 @@ export function BoardSearchBar({
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, []);
return (
@@ -53,7 +52,7 @@ export function BoardSearchBar({
/>
{searchQuery ? (
<button
onClick={() => onSearchChange("")}
onClick={() => onSearchChange('')}
className="absolute right-2 top-1/2 -translate-y-1/2 p-1 rounded-sm hover:bg-accent text-muted-foreground hover:text-foreground transition-colors"
data-testid="kanban-search-clear"
aria-label="Clear search"
@@ -70,19 +69,18 @@ export function BoardSearchBar({
)}
</div>
{/* Spec Creation Loading Badge */}
{isCreatingSpec &&
currentProjectPath === creatingSpecProjectPath && (
<div
className="flex items-center gap-1.5 px-2 py-1 rounded-md bg-brand-500/10 border border-brand-500/20 shrink-0"
title="Creating App Specification"
data-testid="spec-creation-badge"
>
<Loader2 className="w-3 h-3 animate-spin text-brand-500 shrink-0" />
<span className="text-xs font-medium text-brand-500 whitespace-nowrap">
Creating spec
</span>
</div>
)}
{isCreatingSpec && currentProjectPath === creatingSpecProjectPath && (
<div
className="flex items-center gap-1.5 px-2 py-1 rounded-md bg-brand-500/10 border border-brand-500/20 shrink-0"
title="Creating App Specification"
data-testid="spec-creation-badge"
>
<Loader2 className="w-3 h-3 animate-spin text-brand-500 shrink-0" />
<span className="text-xs font-medium text-brand-500 whitespace-nowrap">
Creating spec
</span>
</div>
)}
</div>
);
}

View File

@@ -1,8 +1,7 @@
import { memo } from "react";
import { useDroppable } from "@dnd-kit/core";
import { cn } from "@/lib/utils";
import type { ReactNode } from "react";
import { memo } from 'react';
import { useDroppable } from '@dnd-kit/core';
import { cn } from '@/lib/utils';
import type { ReactNode } from 'react';
interface KanbanColumnProps {
id: string;
@@ -39,10 +38,10 @@ export const KanbanColumn = memo(function KanbanColumn({
<div
ref={setNodeRef}
className={cn(
"relative flex flex-col h-full rounded-xl transition-all duration-200",
!width && "w-72", // Only apply w-72 if no custom width
showBorder && "border border-border/60",
isOver && "ring-2 ring-primary/30 ring-offset-1 ring-offset-background"
'relative flex flex-col h-full rounded-xl transition-all duration-200',
!width && 'w-72', // Only apply w-72 if no custom width
showBorder && 'border border-border/60',
isOver && 'ring-2 ring-primary/30 ring-offset-1 ring-offset-background'
)}
style={widthStyle}
data-testid={`kanban-column-${id}`}
@@ -50,8 +49,8 @@ export const KanbanColumn = memo(function KanbanColumn({
{/* Background layer with opacity */}
<div
className={cn(
"absolute inset-0 rounded-xl backdrop-blur-sm transition-colors duration-200",
isOver ? "bg-accent/80" : "bg-card/80"
'absolute inset-0 rounded-xl backdrop-blur-sm transition-colors duration-200',
isOver ? 'bg-accent/80' : 'bg-card/80'
)}
style={{ opacity: opacity / 100 }}
/>
@@ -59,11 +58,11 @@ export const KanbanColumn = memo(function KanbanColumn({
{/* Column Header */}
<div
className={cn(
"relative z-10 flex items-center gap-3 px-3 py-2.5",
showBorder && "border-b border-border/40"
'relative z-10 flex items-center gap-3 px-3 py-2.5',
showBorder && 'border-b border-border/40'
)}
>
<div className={cn("w-2.5 h-2.5 rounded-full shrink-0", colorClass)} />
<div className={cn('w-2.5 h-2.5 rounded-full shrink-0', colorClass)} />
<h3 className="font-semibold text-sm text-foreground/90 flex-1 tracking-tight">{title}</h3>
{headerAction}
<span className="text-xs font-medium text-muted-foreground/80 bg-muted/50 px-2 py-0.5 rounded-md tabular-nums">
@@ -74,11 +73,11 @@ export const KanbanColumn = memo(function KanbanColumn({
{/* Column Content */}
<div
className={cn(
"relative z-10 flex-1 overflow-y-auto p-2 space-y-2.5",
'relative z-10 flex-1 overflow-y-auto p-2 space-y-2.5',
hideScrollbar &&
"[&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]",
'[&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]',
// Smooth scrolling
"scroll-smooth"
'scroll-smooth'
)}
>
{children}

View File

@@ -1,22 +1,22 @@
import { Feature } from "@/store/app-store";
import { Feature } from '@/store/app-store';
export type ColumnId = Feature["status"];
export type ColumnId = Feature['status'];
export const COLUMNS: { id: ColumnId; title: string; colorClass: string }[] = [
{ id: "backlog", title: "Backlog", colorClass: "bg-[var(--status-backlog)]" },
{ id: 'backlog', title: 'Backlog', colorClass: 'bg-[var(--status-backlog)]' },
{
id: "in_progress",
title: "In Progress",
colorClass: "bg-[var(--status-in-progress)]",
id: 'in_progress',
title: 'In Progress',
colorClass: 'bg-[var(--status-in-progress)]',
},
{
id: "waiting_approval",
title: "Waiting Approval",
colorClass: "bg-[var(--status-waiting)]",
id: 'waiting_approval',
title: 'Waiting Approval',
colorClass: 'bg-[var(--status-waiting)]',
},
{
id: "verified",
title: "Verified",
colorClass: "bg-[var(--status-success)]",
id: 'verified',
title: 'Verified',
colorClass: 'bg-[var(--status-success)]',
},
];

View File

@@ -1,5 +1,4 @@
import { useState, useEffect } from "react";
import { useState, useEffect } from 'react';
import {
Dialog,
DialogContent,
@@ -7,18 +6,18 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
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 { CategoryAutocomplete } from "@/components/ui/category-autocomplete";
} from '@/components/ui/dialog';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
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 { CategoryAutocomplete } from '@/components/ui/category-autocomplete';
import {
DescriptionImageDropZone,
FeatureImagePath as DescriptionImagePath,
ImagePreviewMap,
} from "@/components/ui/description-image-dropzone";
} from '@/components/ui/description-image-dropzone';
import {
MessageSquare,
Settings2,
@@ -26,10 +25,10 @@ import {
FlaskConical,
Sparkles,
ChevronDown,
} from "lucide-react";
import { toast } from "sonner";
import { getElectronAPI } from "@/lib/electron";
import { modelSupportsThinking } from "@/lib/utils";
} from 'lucide-react';
import { toast } from 'sonner';
import { getElectronAPI } from '@/lib/electron';
import { modelSupportsThinking } from '@/lib/utils';
import {
useAppStore,
AgentModel,
@@ -37,7 +36,7 @@ import {
FeatureImage,
AIProfile,
PlanningMode,
} from "@/store/app-store";
} from '@/store/app-store';
import {
ModelSelector,
ThinkingLevelSelector,
@@ -46,14 +45,14 @@ import {
PrioritySelector,
BranchSelector,
PlanningModeSelector,
} from "../shared";
} from '../shared';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useNavigate } from "@tanstack/react-router";
} from '@/components/ui/dropdown-menu';
import { useNavigate } from '@tanstack/react-router';
interface AddFeatureDialogProps {
open: boolean;
@@ -92,7 +91,7 @@ export function AddFeatureDialog({
branchSuggestions,
branchCardCounts,
defaultSkipTests,
defaultBranch = "main",
defaultBranch = 'main',
currentBranch,
isMaximized,
showProfilesOnly,
@@ -101,27 +100,28 @@ export function AddFeatureDialog({
const navigate = useNavigate();
const [useCurrentBranch, setUseCurrentBranch] = useState(true);
const [newFeature, setNewFeature] = useState({
title: "",
category: "",
description: "",
steps: [""],
title: '',
category: '',
description: '',
steps: [''],
images: [] as FeatureImage[],
imagePaths: [] as DescriptionImagePath[],
skipTests: false,
model: "opus" as AgentModel,
thinkingLevel: "none" as ThinkingLevel,
branchName: "",
model: 'opus' as AgentModel,
thinkingLevel: 'none' as ThinkingLevel,
branchName: '',
priority: 2 as number, // Default to medium priority
});
const [newFeaturePreviewMap, setNewFeaturePreviewMap] =
useState<ImagePreviewMap>(() => new Map());
const [newFeaturePreviewMap, setNewFeaturePreviewMap] = useState<ImagePreviewMap>(
() => new Map()
);
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
const [descriptionError, setDescriptionError] = useState(false);
const [isEnhancing, setIsEnhancing] = useState(false);
const [enhancementMode, setEnhancementMode] = useState<
"improve" | "technical" | "simplify" | "acceptance"
>("improve");
const [planningMode, setPlanningMode] = useState<PlanningMode>("skip");
'improve' | 'technical' | 'simplify' | 'acceptance'
>('improve');
const [planningMode, setPlanningMode] = useState<PlanningMode>('skip');
const [requirePlanApproval, setRequirePlanApproval] = useState(false);
// Get enhancement model, planning mode defaults, and worktrees setting from store
@@ -144,10 +144,10 @@ export function AddFeatureDialog({
setNewFeature((prev) => ({
...prev,
skipTests: defaultSkipTests,
branchName: defaultBranch || "",
branchName: defaultBranch || '',
// Use default profile's model/thinkingLevel if set, else fallback to defaults
model: defaultProfile?.model ?? "opus",
thinkingLevel: defaultProfile?.thinkingLevel ?? "none",
model: defaultProfile?.model ?? 'opus',
thinkingLevel: defaultProfile?.thinkingLevel ?? 'none',
}));
setUseCurrentBranch(true);
setPlanningMode(defaultPlanningMode);
@@ -171,22 +171,20 @@ export function AddFeatureDialog({
// Validate branch selection when "other branch" is selected
if (useWorktrees && !useCurrentBranch && !newFeature.branchName.trim()) {
toast.error("Please select a branch name");
toast.error('Please select a branch name');
return;
}
const category = newFeature.category || "Uncategorized";
const category = newFeature.category || 'Uncategorized';
const selectedModel = newFeature.model;
const normalizedThinking = modelSupportsThinking(selectedModel)
? newFeature.thinkingLevel
: "none";
: 'none';
// Use current branch if toggle is on
// If currentBranch is provided (non-primary worktree), use it
// Otherwise (primary worktree), use empty string which means "unassigned" (show only on primary)
const finalBranchName = useCurrentBranch
? currentBranch || ""
: newFeature.branchName || "";
const finalBranchName = useCurrentBranch ? currentBranch || '' : newFeature.branchName || '';
onAdd({
title: newFeature.title,
@@ -206,17 +204,17 @@ export function AddFeatureDialog({
// Reset form
setNewFeature({
title: "",
category: "",
description: "",
steps: [""],
title: '',
category: '',
description: '',
steps: [''],
images: [],
imagePaths: [],
skipTests: defaultSkipTests,
model: "opus",
model: 'opus',
priority: 2,
thinkingLevel: "none",
branchName: "",
thinkingLevel: 'none',
branchName: '',
});
setUseCurrentBranch(true);
setPlanningMode(defaultPlanningMode);
@@ -251,13 +249,13 @@ export function AddFeatureDialog({
if (result?.success && result.enhancedText) {
const enhancedText = result.enhancedText;
setNewFeature((prev) => ({ ...prev, description: enhancedText }));
toast.success("Description enhanced!");
toast.success('Description enhanced!');
} else {
toast.error(result?.error || "Failed to enhance description");
toast.error(result?.error || 'Failed to enhance description');
}
} catch (error) {
console.error("Enhancement failed:", error);
toast.error("Failed to enhance description");
console.error('Enhancement failed:', error);
toast.error('Failed to enhance description');
} finally {
setIsEnhancing(false);
}
@@ -267,16 +265,11 @@ export function AddFeatureDialog({
setNewFeature({
...newFeature,
model,
thinkingLevel: modelSupportsThinking(model)
? newFeature.thinkingLevel
: "none",
thinkingLevel: modelSupportsThinking(model) ? newFeature.thinkingLevel : 'none',
});
};
const handleProfileSelect = (
model: AgentModel,
thinkingLevel: ThinkingLevel
) => {
const handleProfileSelect = (model: AgentModel, thinkingLevel: ThinkingLevel) => {
setNewFeature({
...newFeature,
model,
@@ -306,14 +299,9 @@ export function AddFeatureDialog({
>
<DialogHeader>
<DialogTitle>Add New Feature</DialogTitle>
<DialogDescription>
Create a new feature card for the Kanban board.
</DialogDescription>
<DialogDescription>Create a new feature card for the Kanban board.</DialogDescription>
</DialogHeader>
<Tabs
defaultValue="prompt"
className="py-4 flex-1 min-h-0 flex flex-col"
>
<Tabs defaultValue="prompt" className="py-4 flex-1 min-h-0 flex flex-col">
<TabsList className="w-full grid grid-cols-3 mb-4">
<TabsTrigger value="prompt" data-testid="tab-prompt">
<MessageSquare className="w-4 h-4 mr-2" />
@@ -330,10 +318,7 @@ export function AddFeatureDialog({
</TabsList>
{/* Prompt Tab */}
<TabsContent
value="prompt"
className="space-y-4 overflow-y-auto cursor-default"
>
<TabsContent value="prompt" className="space-y-4 overflow-y-auto cursor-default">
<div className="space-y-2">
<Label htmlFor="description">Description</Label>
<DescriptionImageDropZone
@@ -345,9 +330,7 @@ export function AddFeatureDialog({
}
}}
images={newFeature.imagePaths}
onImagesChange={(images) =>
setNewFeature({ ...newFeature, imagePaths: images })
}
onImagesChange={(images) => setNewFeature({ ...newFeature, imagePaths: images })}
placeholder="Describe the feature..."
previewMap={newFeaturePreviewMap}
onPreviewMapChange={setNewFeaturePreviewMap}
@@ -360,47 +343,32 @@ export function AddFeatureDialog({
<Input
id="title"
value={newFeature.title}
onChange={(e) =>
setNewFeature({ ...newFeature, title: e.target.value })
}
onChange={(e) => setNewFeature({ ...newFeature, title: e.target.value })}
placeholder="Leave blank to auto-generate"
/>
</div>
<div className="flex w-fit items-center gap-3 select-none cursor-default">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="sm"
className="w-[200px] justify-between"
>
{enhancementMode === "improve" && "Improve Clarity"}
{enhancementMode === "technical" && "Add Technical Details"}
{enhancementMode === "simplify" && "Simplify"}
{enhancementMode === "acceptance" &&
"Add Acceptance Criteria"}
<Button variant="outline" size="sm" className="w-[200px] justify-between">
{enhancementMode === 'improve' && 'Improve Clarity'}
{enhancementMode === 'technical' && 'Add Technical Details'}
{enhancementMode === 'simplify' && 'Simplify'}
{enhancementMode === 'acceptance' && 'Add Acceptance Criteria'}
<ChevronDown className="w-4 h-4 ml-2" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem
onClick={() => setEnhancementMode("improve")}
>
<DropdownMenuItem onClick={() => setEnhancementMode('improve')}>
Improve Clarity
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setEnhancementMode("technical")}
>
<DropdownMenuItem onClick={() => setEnhancementMode('technical')}>
Add Technical Details
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setEnhancementMode("simplify")}
>
<DropdownMenuItem onClick={() => setEnhancementMode('simplify')}>
Simplify
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setEnhancementMode("acceptance")}
>
<DropdownMenuItem onClick={() => setEnhancementMode('acceptance')}>
Add Acceptance Criteria
</DropdownMenuItem>
</DropdownMenuContent>
@@ -422,9 +390,7 @@ export function AddFeatureDialog({
<Label htmlFor="category">Category (optional)</Label>
<CategoryAutocomplete
value={newFeature.category}
onChange={(value) =>
setNewFeature({ ...newFeature, category: value })
}
onChange={(value) => setNewFeature({ ...newFeature, category: value })}
suggestions={categorySuggestions}
placeholder="e.g., Core, UI, API"
data-testid="feature-category-input"
@@ -435,9 +401,7 @@ export function AddFeatureDialog({
useCurrentBranch={useCurrentBranch}
onUseCurrentBranchChange={setUseCurrentBranch}
branchName={newFeature.branchName}
onBranchNameChange={(value) =>
setNewFeature({ ...newFeature, branchName: value })
}
onBranchNameChange={(value) => setNewFeature({ ...newFeature, branchName: value })}
branchSuggestions={branchSuggestions}
branchCardCounts={branchCardCounts}
currentBranch={currentBranch}
@@ -448,25 +412,18 @@ export function AddFeatureDialog({
{/* Priority Selector */}
<PrioritySelector
selectedPriority={newFeature.priority}
onPrioritySelect={(priority) =>
setNewFeature({ ...newFeature, priority })
}
onPrioritySelect={(priority) => setNewFeature({ ...newFeature, priority })}
testIdPrefix="priority"
/>
</TabsContent>
{/* Model Tab */}
<TabsContent
value="model"
className="space-y-4 overflow-y-auto cursor-default"
>
<TabsContent value="model" className="space-y-4 overflow-y-auto cursor-default">
{/* Show Advanced Options Toggle */}
{showProfilesOnly && (
<div className="flex items-center justify-between p-3 bg-muted/30 rounded-lg border border-border">
<div className="space-y-1">
<p className="text-sm font-medium text-foreground">
Simple Mode Active
</p>
<p className="text-sm font-medium text-foreground">Simple Mode Active</p>
<p className="text-xs text-muted-foreground">
Only showing AI profiles. Advanced model tweaking is hidden.
</p>
@@ -478,7 +435,7 @@ export function AddFeatureDialog({
data-testid="show-advanced-options-toggle"
>
<Settings2 className="w-4 h-4 mr-2" />
{showAdvancedOptions ? "Hide" : "Show"} Advanced
{showAdvancedOptions ? 'Hide' : 'Show'} Advanced
</Button>
</div>
)}
@@ -492,23 +449,19 @@ export function AddFeatureDialog({
showManageLink
onManageLinkClick={() => {
onOpenChange(false);
navigate({ to: "/profiles" });
navigate({ to: '/profiles' });
}}
/>
{/* Separator */}
{aiProfiles.length > 0 &&
(!showProfilesOnly || showAdvancedOptions) && (
<div className="border-t border-border" />
)}
{aiProfiles.length > 0 && (!showProfilesOnly || showAdvancedOptions) && (
<div className="border-t border-border" />
)}
{/* Claude Models Section */}
{(!showProfilesOnly || showAdvancedOptions) && (
<>
<ModelSelector
selectedModel={newFeature.model}
onModelSelect={handleModelSelect}
/>
<ModelSelector selectedModel={newFeature.model} onModelSelect={handleModelSelect} />
{newModelAllowsThinking && (
<ThinkingLevelSelector
selectedLevel={newFeature.thinkingLevel}
@@ -522,10 +475,7 @@ export function AddFeatureDialog({
</TabsContent>
{/* Options Tab */}
<TabsContent
value="options"
className="space-y-4 overflow-y-auto cursor-default"
>
<TabsContent value="options" className="space-y-4 overflow-y-auto cursor-default">
{/* Planning Mode Section */}
<PlanningModeSelector
mode={planningMode}
@@ -542,9 +492,7 @@ export function AddFeatureDialog({
{/* Testing Section */}
<TestingTabContent
skipTests={newFeature.skipTests}
onSkipTestsChange={(skipTests) =>
setNewFeature({ ...newFeature, skipTests })
}
onSkipTestsChange={(skipTests) => setNewFeature({ ...newFeature, skipTests })}
steps={newFeature.steps}
onStepsChange={(steps) => setNewFeature({ ...newFeature, steps })}
/>
@@ -556,12 +504,10 @@ export function AddFeatureDialog({
</Button>
<HotkeyButton
onClick={handleAdd}
hotkey={{ key: "Enter", cmdCtrl: true }}
hotkey={{ key: 'Enter', cmdCtrl: true }}
hotkeyActive={open}
data-testid="confirm-add-feature"
disabled={
useWorktrees && !useCurrentBranch && !newFeature.branchName.trim()
}
disabled={useWorktrees && !useCurrentBranch && !newFeature.branchName.trim()}
>
Add Feature
</HotkeyButton>

View File

@@ -1,4 +1,4 @@
"use client";
'use client';
import {
Dialog,
@@ -7,9 +7,9 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Archive } from "lucide-react";
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Archive } from 'lucide-react';
interface ArchiveAllVerifiedDialogProps {
open: boolean;
@@ -30,8 +30,8 @@ export function ArchiveAllVerifiedDialog({
<DialogHeader>
<DialogTitle>Archive All Verified Features</DialogTitle>
<DialogDescription>
Are you sure you want to archive all verified features? They will be
moved to the archive box.
Are you sure you want to archive all verified features? They will be moved to the
archive box.
{verifiedCount > 0 && (
<span className="block mt-2 text-yellow-500">
{verifiedCount} feature(s) will be archived.
@@ -52,5 +52,3 @@ export function ArchiveAllVerifiedDialog({
</Dialog>
);
}

View File

@@ -1,5 +1,4 @@
import { useState } from "react";
import { useState } from 'react';
import {
Dialog,
DialogContent,
@@ -7,13 +6,13 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label";
import { GitCommit, Loader2 } from "lucide-react";
import { getElectronAPI } from "@/lib/electron";
import { toast } from "sonner";
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';
import { GitCommit, Loader2 } from 'lucide-react';
import { getElectronAPI } from '@/lib/electron';
import { toast } from 'sonner';
interface WorktreeInfo {
path: string;
@@ -36,7 +35,7 @@ export function CommitWorktreeDialog({
worktree,
onCommitted,
}: CommitWorktreeDialogProps) {
const [message, setMessage] = useState("");
const [message, setMessage] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -49,36 +48,36 @@ export function CommitWorktreeDialog({
try {
const api = getElectronAPI();
if (!api?.worktree?.commit) {
setError("Worktree API not available");
setError('Worktree API not available');
return;
}
const result = await api.worktree.commit(worktree.path, message);
if (result.success && result.result) {
if (result.result.committed) {
toast.success("Changes committed", {
toast.success('Changes committed', {
description: `Commit ${result.result.commitHash} on ${result.result.branch}`,
});
onCommitted();
onOpenChange(false);
setMessage("");
setMessage('');
} else {
toast.info("No changes to commit", {
toast.info('No changes to commit', {
description: result.result.message,
});
}
} else {
setError(result.error || "Failed to commit changes");
setError(result.error || 'Failed to commit changes');
}
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to commit");
setError(err instanceof Error ? err.message : 'Failed to commit');
} finally {
setIsLoading(false);
}
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter" && e.metaKey && !isLoading && message.trim()) {
if (e.key === 'Enter' && e.metaKey && !isLoading && message.trim()) {
handleCommit();
}
};
@@ -94,15 +93,12 @@ export function CommitWorktreeDialog({
Commit Changes
</DialogTitle>
<DialogDescription>
Commit changes in the{" "}
<code className="font-mono bg-muted px-1 rounded">
{worktree.branch}
</code>{" "}
worktree.
Commit changes in the{' '}
<code className="font-mono bg-muted px-1 rounded">{worktree.branch}</code> worktree.
{worktree.changedFilesCount && (
<span className="ml-1">
({worktree.changedFilesCount} file
{worktree.changedFilesCount > 1 ? "s" : ""} changed)
{worktree.changedFilesCount > 1 ? 's' : ''} changed)
</span>
)}
</DialogDescription>
@@ -132,17 +128,10 @@ export function CommitWorktreeDialog({
</div>
<DialogFooter>
<Button
variant="ghost"
onClick={() => onOpenChange(false)}
disabled={isLoading}
>
<Button variant="ghost" onClick={() => onOpenChange(false)} disabled={isLoading}>
Cancel
</Button>
<Button
onClick={handleCommit}
disabled={isLoading || !message.trim()}
>
<Button onClick={handleCommit} disabled={isLoading || !message.trim()}>
{isLoading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />

View File

@@ -1,4 +1,3 @@
import {
Dialog,
DialogContent,
@@ -6,11 +5,11 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Card, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { ArchiveRestore, Trash2 } from "lucide-react";
import { Feature } from "@/store/app-store";
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Card, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { ArchiveRestore, Trash2 } from 'lucide-react';
import { Feature } from '@/store/app-store';
interface CompletedFeaturesModalProps {
open: boolean;
@@ -37,9 +36,9 @@ export function CompletedFeaturesModal({
<DialogTitle>Completed Features</DialogTitle>
<DialogDescription>
{completedFeatures.length === 0
? "No completed features yet."
? 'No completed features yet.'
: `${completedFeatures.length} completed feature${
completedFeatures.length > 1 ? "s" : ""
completedFeatures.length > 1 ? 's' : ''
}`}
</DialogDescription>
</DialogHeader>
@@ -62,7 +61,7 @@ export function CompletedFeaturesModal({
{feature.description || feature.summary || feature.id}
</CardTitle>
<CardDescription className="text-xs mt-1 truncate">
{feature.category || "Uncategorized"}
{feature.category || 'Uncategorized'}
</CardDescription>
</CardHeader>
<div className="p-3 pt-0 flex gap-2">

View File

@@ -1,5 +1,4 @@
import { useState, useEffect } from "react";
import { useState, useEffect } from 'react';
import {
Dialog,
DialogContent,
@@ -7,13 +6,13 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { getElectronAPI } from "@/lib/electron";
import { toast } from "sonner";
import { GitBranchPlus, Loader2 } from "lucide-react";
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { getElectronAPI } from '@/lib/electron';
import { toast } from 'sonner';
import { GitBranchPlus, Loader2 } from 'lucide-react';
interface WorktreeInfo {
path: string;
@@ -36,14 +35,14 @@ export function CreateBranchDialog({
worktree,
onCreated,
}: CreateBranchDialogProps) {
const [branchName, setBranchName] = useState("");
const [branchName, setBranchName] = useState('');
const [isCreating, setIsCreating] = useState(false);
const [error, setError] = useState<string | null>(null);
// Reset state when dialog opens/closes
useEffect(() => {
if (open) {
setBranchName("");
setBranchName('');
setError(null);
}
}, [open]);
@@ -54,7 +53,7 @@ export function CreateBranchDialog({
// Basic validation
const invalidChars = /[\s~^:?*[\]\\]/;
if (invalidChars.test(branchName)) {
setError("Branch name contains invalid characters");
setError('Branch name contains invalid characters');
return;
}
@@ -64,7 +63,7 @@ export function CreateBranchDialog({
try {
const api = getElectronAPI();
if (!api?.worktree?.checkoutBranch) {
toast.error("Branch API not available");
toast.error('Branch API not available');
return;
}
@@ -75,11 +74,11 @@ export function CreateBranchDialog({
onCreated();
onOpenChange(false);
} else {
setError(result.error || "Failed to create branch");
setError(result.error || 'Failed to create branch');
}
} catch (err) {
console.error("Create branch failed:", err);
setError("Failed to create branch");
console.error('Create branch failed:', err);
setError('Failed to create branch');
} finally {
setIsCreating(false);
}
@@ -94,7 +93,10 @@ export function CreateBranchDialog({
Create New Branch
</DialogTitle>
<DialogDescription>
Create a new branch from <span className="font-mono text-foreground">{worktree?.branch || "current branch"}</span>
Create a new branch from{' '}
<span className="font-mono text-foreground">
{worktree?.branch || 'current branch'}
</span>
</DialogDescription>
</DialogHeader>
@@ -110,38 +112,29 @@ export function CreateBranchDialog({
setError(null);
}}
onKeyDown={(e) => {
if (e.key === "Enter" && branchName.trim() && !isCreating) {
if (e.key === 'Enter' && branchName.trim() && !isCreating) {
handleCreate();
}
}}
disabled={isCreating}
autoFocus
/>
{error && (
<p className="text-sm text-destructive">{error}</p>
)}
{error && <p className="text-sm text-destructive">{error}</p>}
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => onOpenChange(false)}
disabled={isCreating}
>
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={isCreating}>
Cancel
</Button>
<Button
onClick={handleCreate}
disabled={!branchName.trim() || isCreating}
>
<Button onClick={handleCreate} disabled={!branchName.trim() || isCreating}>
{isCreating ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Creating...
</>
) : (
"Create Branch"
'Create Branch'
)}
</Button>
</DialogFooter>

View File

@@ -1,5 +1,4 @@
import { useState, useEffect, useRef } from "react";
import { useState, useEffect, useRef } from 'react';
import {
Dialog,
DialogContent,
@@ -7,15 +6,15 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import { GitPullRequest, Loader2, ExternalLink } from "lucide-react";
import { getElectronAPI } from "@/lib/electron";
import { toast } from "sonner";
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';
import { Checkbox } from '@/components/ui/checkbox';
import { GitPullRequest, Loader2, ExternalLink } from 'lucide-react';
import { getElectronAPI } from '@/lib/electron';
import { toast } from 'sonner';
interface WorktreeInfo {
path: string;
@@ -40,10 +39,10 @@ export function CreatePRDialog({
projectPath,
onCreated,
}: CreatePRDialogProps) {
const [title, setTitle] = useState("");
const [body, setBody] = useState("");
const [baseBranch, setBaseBranch] = useState("main");
const [commitMessage, setCommitMessage] = useState("");
const [title, setTitle] = useState('');
const [body, setBody] = useState('');
const [baseBranch, setBaseBranch] = useState('main');
const [commitMessage, setCommitMessage] = useState('');
const [isDraft, setIsDraft] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -57,10 +56,10 @@ export function CreatePRDialog({
useEffect(() => {
if (open) {
// Reset form fields
setTitle("");
setBody("");
setCommitMessage("");
setBaseBranch("main");
setTitle('');
setBody('');
setCommitMessage('');
setBaseBranch('main');
setIsDraft(false);
setError(null);
// Also reset result states when opening for a new worktree
@@ -72,10 +71,10 @@ export function CreatePRDialog({
operationCompletedRef.current = false;
} else {
// Reset everything when dialog closes
setTitle("");
setBody("");
setCommitMessage("");
setBaseBranch("main");
setTitle('');
setBody('');
setCommitMessage('');
setBaseBranch('main');
setIsDraft(false);
setError(null);
setPrUrl(null);
@@ -94,7 +93,7 @@ export function CreatePRDialog({
try {
const api = getElectronAPI();
if (!api?.worktree?.createPR) {
setError("Worktree API not available");
setError('Worktree API not available');
return;
}
const result = await api.worktree.createPR(worktree.path, {
@@ -114,19 +113,19 @@ export function CreatePRDialog({
// Show different message based on whether PR already existed
if (result.result.prAlreadyExisted) {
toast.success("Pull request found!", {
toast.success('Pull request found!', {
description: `PR already exists for ${result.result.branch}`,
action: {
label: "View PR",
onClick: () => window.open(result.result!.prUrl!, "_blank"),
label: 'View PR',
onClick: () => window.open(result.result!.prUrl!, '_blank'),
},
});
} else {
toast.success("Pull request created!", {
toast.success('Pull request created!', {
description: `PR created from ${result.result.branch}`,
action: {
label: "View PR",
onClick: () => window.open(result.result!.prUrl!, "_blank"),
label: 'View PR',
onClick: () => window.open(result.result!.prUrl!, '_blank'),
},
});
}
@@ -140,12 +139,12 @@ export function CreatePRDialog({
// Check if we should show browser fallback
if (!result.result.prCreated && hasBrowserUrl) {
// If gh CLI is not available, show browser fallback UI
if (prError === "gh_cli_not_available" || !result.result.ghCliAvailable) {
if (prError === 'gh_cli_not_available' || !result.result.ghCliAvailable) {
setBrowserUrl(result.result.browserUrl ?? null);
setShowBrowserFallback(true);
// Mark operation as completed - branch was pushed successfully
operationCompletedRef.current = true;
toast.success("Branch pushed", {
toast.success('Branch pushed', {
description: result.result.committed
? `Commit ${result.result.commitHash} pushed to ${result.result.branch}`
: `Branch ${result.result.branch} pushed`,
@@ -159,11 +158,12 @@ export function CreatePRDialog({
if (prError) {
// Parse common gh CLI errors for better messages
let errorMessage = prError;
if (prError.includes("No commits between")) {
errorMessage = "No new commits to create PR. Make sure your branch has changes compared to the base branch.";
} else if (prError.includes("already exists")) {
errorMessage = "A pull request already exists for this branch.";
} else if (prError.includes("not logged in") || prError.includes("auth")) {
if (prError.includes('No commits between')) {
errorMessage =
'No new commits to create PR. Make sure your branch has changes compared to the base branch.';
} else if (prError.includes('already exists')) {
errorMessage = 'A pull request already exists for this branch.';
} else if (prError.includes('not logged in') || prError.includes('auth')) {
errorMessage = "GitHub CLI not authenticated. Run 'gh auth login' in terminal.";
}
@@ -172,7 +172,7 @@ export function CreatePRDialog({
setShowBrowserFallback(true);
// Mark operation as completed - branch was pushed even though PR creation failed
operationCompletedRef.current = true;
toast.error("PR creation failed", {
toast.error('PR creation failed', {
description: errorMessage,
duration: 8000,
});
@@ -183,7 +183,7 @@ export function CreatePRDialog({
}
// Show success toast for push
toast.success("Branch pushed", {
toast.success('Branch pushed', {
description: result.result.committed
? `Commit ${result.result.commitHash} pushed to ${result.result.branch}`
: `Branch ${result.result.branch} pushed`,
@@ -192,8 +192,9 @@ export function CreatePRDialog({
// No browser URL available, just close
if (!result.result.prCreated) {
if (!hasBrowserUrl) {
toast.info("PR not created", {
description: "Could not determine repository URL. GitHub CLI (gh) may not be installed or authenticated.",
toast.info('PR not created', {
description:
'Could not determine repository URL. GitHub CLI (gh) may not be installed or authenticated.',
duration: 8000,
});
}
@@ -202,10 +203,10 @@ export function CreatePRDialog({
onOpenChange(false);
}
} else {
setError(result.error || "Failed to create pull request");
setError(result.error || 'Failed to create pull request');
}
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to create PR");
setError(err instanceof Error ? err.message : 'Failed to create PR');
} finally {
setIsLoading(false);
}
@@ -235,10 +236,8 @@ export function CreatePRDialog({
Create Pull Request
</DialogTitle>
<DialogDescription>
Push changes and create a pull request from{" "}
<code className="font-mono bg-muted px-1 rounded">
{worktree.branch}
</code>
Push changes and create a pull request from{' '}
<code className="font-mono bg-muted px-1 rounded">{worktree.branch}</code>
</DialogDescription>
</DialogHeader>
@@ -249,15 +248,10 @@ export function CreatePRDialog({
</div>
<div>
<h3 className="text-lg font-semibold">Pull Request Created!</h3>
<p className="text-sm text-muted-foreground mt-1">
Your PR is ready for review
</p>
<p className="text-sm text-muted-foreground mt-1">Your PR is ready for review</p>
</div>
<div className="flex gap-2 justify-center">
<Button
onClick={() => window.open(prUrl, "_blank")}
className="gap-2"
>
<Button onClick={() => window.open(prUrl, '_blank')} className="gap-2">
<ExternalLink className="w-4 h-4" />
View Pull Request
</Button>
@@ -283,7 +277,7 @@ export function CreatePRDialog({
<Button
onClick={() => {
if (browserUrl) {
window.open(browserUrl, "_blank");
window.open(browserUrl, '_blank');
}
}}
className="gap-2 w-full"
@@ -292,11 +286,10 @@ export function CreatePRDialog({
<ExternalLink className="w-4 h-4" />
Create PR in Browser
</Button>
<div className="p-2 bg-muted rounded text-xs break-all font-mono">
{browserUrl}
</div>
<div className="p-2 bg-muted rounded text-xs break-all font-mono">{browserUrl}</div>
<p className="text-xs text-muted-foreground">
Tip: Install the GitHub CLI (<code className="bg-muted px-1 rounded">gh</code>) to create PRs directly from the app
Tip: Install the GitHub CLI (<code className="bg-muted px-1 rounded">gh</code>) to
create PRs directly from the app
</p>
<DialogFooter className="mt-4">
<Button variant="outline" onClick={handleClose}>
@@ -311,8 +304,7 @@ export function CreatePRDialog({
{worktree.hasChanges && (
<div className="grid gap-2">
<Label htmlFor="commit-message">
Commit Message{" "}
<span className="text-muted-foreground">(optional)</span>
Commit Message <span className="text-muted-foreground">(optional)</span>
</Label>
<Input
id="commit-message"
@@ -322,8 +314,7 @@ export function CreatePRDialog({
className="font-mono text-sm"
/>
<p className="text-xs text-muted-foreground">
{worktree.changedFilesCount} uncommitted file(s) will be
committed
{worktree.changedFilesCount} uncommitted file(s) will be committed
</p>
</div>
)}
@@ -374,9 +365,7 @@ export function CreatePRDialog({
</div>
</div>
{error && (
<p className="text-sm text-destructive">{error}</p>
)}
{error && <p className="text-sm text-destructive">{error}</p>}
</div>
<DialogFooter>

View File

@@ -1,5 +1,4 @@
import { useState } from "react";
import { useState } from 'react';
import {
Dialog,
DialogContent,
@@ -7,13 +6,13 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { GitBranch, Loader2 } from "lucide-react";
import { getElectronAPI } from "@/lib/electron";
import { toast } from "sonner";
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { GitBranch, Loader2 } from 'lucide-react';
import { getElectronAPI } from '@/lib/electron';
import { toast } from 'sonner';
interface CreatedWorktreeInfo {
path: string;
@@ -33,13 +32,13 @@ export function CreateWorktreeDialog({
projectPath,
onCreated,
}: CreateWorktreeDialogProps) {
const [branchName, setBranchName] = useState("");
const [branchName, setBranchName] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleCreate = async () => {
if (!branchName.trim()) {
setError("Branch name is required");
setError('Branch name is required');
return;
}
@@ -47,7 +46,7 @@ export function CreateWorktreeDialog({
const validBranchRegex = /^[a-zA-Z0-9._/-]+$/;
if (!validBranchRegex.test(branchName)) {
setError(
"Invalid branch name. Use only letters, numbers, dots, underscores, hyphens, and slashes."
'Invalid branch name. Use only letters, numbers, dots, underscores, hyphens, and slashes.'
);
return;
}
@@ -58,35 +57,30 @@ export function CreateWorktreeDialog({
try {
const api = getElectronAPI();
if (!api?.worktree?.create) {
setError("Worktree API not available");
setError('Worktree API not available');
return;
}
const result = await api.worktree.create(projectPath, branchName);
if (result.success && result.worktree) {
toast.success(
`Worktree created for branch "${result.worktree.branch}"`,
{
description: result.worktree.isNew
? "New branch created"
: "Using existing branch",
}
);
toast.success(`Worktree created for branch "${result.worktree.branch}"`, {
description: result.worktree.isNew ? 'New branch created' : 'Using existing branch',
});
onCreated({ path: result.worktree.path, branch: result.worktree.branch });
onOpenChange(false);
setBranchName("");
setBranchName('');
} else {
setError(result.error || "Failed to create worktree");
setError(result.error || 'Failed to create worktree');
}
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to create worktree");
setError(err instanceof Error ? err.message : 'Failed to create worktree');
} finally {
setIsLoading(false);
}
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter" && !isLoading && branchName.trim()) {
if (e.key === 'Enter' && !isLoading && branchName.trim()) {
handleCreate();
}
};
@@ -100,8 +94,8 @@ export function CreateWorktreeDialog({
Create New Worktree
</DialogTitle>
<DialogDescription>
Create a new git worktree with its own branch. This allows you to
work on multiple features in parallel.
Create a new git worktree with its own branch. This allows you to work on multiple
features in parallel.
</DialogDescription>
</DialogHeader>
@@ -140,17 +134,10 @@ export function CreateWorktreeDialog({
</div>
<DialogFooter>
<Button
variant="ghost"
onClick={() => onOpenChange(false)}
disabled={isLoading}
>
<Button variant="ghost" onClick={() => onOpenChange(false)} disabled={isLoading}>
Cancel
</Button>
<Button
onClick={handleCreate}
disabled={isLoading || !branchName.trim()}
>
<Button onClick={handleCreate} disabled={isLoading || !branchName.trim()}>
{isLoading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />

View File

@@ -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 DeleteAllVerifiedDialogProps {
open: boolean;
@@ -29,8 +28,7 @@ export function DeleteAllVerifiedDialog({
<DialogHeader>
<DialogTitle>Delete All Verified Features</DialogTitle>
<DialogDescription>
Are you sure you want to delete all verified features? This action
cannot be undone.
Are you sure you want to delete all verified features? This action cannot be undone.
{verifiedCount > 0 && (
<span className="block mt-2 text-yellow-500">
{verifiedCount} feature(s) will be deleted.
@@ -42,7 +40,11 @@ export function DeleteAllVerifiedDialog({
<Button variant="ghost" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button variant="destructive" onClick={onConfirm} data-testid="confirm-delete-all-verified">
<Button
variant="destructive"
onClick={onConfirm}
data-testid="confirm-delete-all-verified"
>
<Trash2 className="w-4 h-4 mr-2" />
Delete All
</Button>

View File

@@ -1,4 +1,3 @@
import {
Dialog,
DialogContent,
@@ -6,10 +5,10 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Trash2 } from "lucide-react";
import { Feature } from "@/store/app-store";
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Trash2 } from 'lucide-react';
import { Feature } from '@/store/app-store';
interface DeleteCompletedFeatureDialogProps {
feature: Feature | null;
@@ -36,7 +35,7 @@ export function DeleteCompletedFeatureDialog({
Are you sure you want to permanently delete this feature?
<span className="block mt-2 font-medium text-foreground">
&quot;{feature.description?.slice(0, 100)}
{(feature.description?.length ?? 0) > 100 ? "..." : ""}&quot;
{(feature.description?.length ?? 0) > 100 ? '...' : ''}&quot;
</span>
<span className="block mt-2 text-destructive font-medium">
This action cannot be undone.
@@ -44,11 +43,7 @@ export function DeleteCompletedFeatureDialog({
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
variant="ghost"
onClick={onClose}
data-testid="cancel-delete-completed-button"
>
<Button variant="ghost" onClick={onClose} data-testid="cancel-delete-completed-button">
Cancel
</Button>
<Button

View File

@@ -1,5 +1,4 @@
import { useState } from "react";
import { useState } from 'react';
import {
Dialog,
DialogContent,
@@ -7,13 +6,13 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
import { Loader2, Trash2, AlertTriangle, FileWarning } from "lucide-react";
import { getElectronAPI } from "@/lib/electron";
import { toast } from "sonner";
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { Label } from '@/components/ui/label';
import { Loader2, Trash2, AlertTriangle, FileWarning } from 'lucide-react';
import { getElectronAPI } from '@/lib/electron';
import { toast } from 'sonner';
interface WorktreeInfo {
path: string;
@@ -51,14 +50,10 @@ export function DeleteWorktreeDialog({
try {
const api = getElectronAPI();
if (!api?.worktree?.delete) {
toast.error("Worktree API not available");
toast.error('Worktree API not available');
return;
}
const result = await api.worktree.delete(
projectPath,
worktree.path,
deleteBranch
);
const result = await api.worktree.delete(projectPath, worktree.path, deleteBranch);
if (result.success) {
toast.success(`Worktree deleted`, {
@@ -70,13 +65,13 @@ export function DeleteWorktreeDialog({
onOpenChange(false);
setDeleteBranch(false);
} else {
toast.error("Failed to delete worktree", {
toast.error('Failed to delete worktree', {
description: result.error,
});
}
} catch (err) {
toast.error("Failed to delete worktree", {
description: err instanceof Error ? err.message : "Unknown error",
toast.error('Failed to delete worktree', {
description: err instanceof Error ? err.message : 'Unknown error',
});
} finally {
setIsLoading(false);
@@ -95,21 +90,18 @@ export function DeleteWorktreeDialog({
</DialogTitle>
<DialogDescription className="space-y-3">
<span>
Are you sure you want to delete the worktree for branch{" "}
<code className="font-mono bg-muted px-1 rounded">
{worktree.branch}
</code>
?
Are you sure you want to delete the worktree for branch{' '}
<code className="font-mono bg-muted px-1 rounded">{worktree.branch}</code>?
</span>
{affectedFeatureCount > 0 && (
<div className="flex items-start gap-2 p-3 rounded-md bg-orange-500/10 border border-orange-500/20 mt-2">
<FileWarning className="w-4 h-4 text-orange-500 mt-0.5 flex-shrink-0" />
<span className="text-orange-500 text-sm">
{affectedFeatureCount} feature{affectedFeatureCount !== 1 ? "s" : ""}{" "}
{affectedFeatureCount !== 1 ? "are" : "is"} assigned to this
branch. {affectedFeatureCount !== 1 ? "They" : "It"} will be
unassigned and moved to the main worktree.
{affectedFeatureCount} feature{affectedFeatureCount !== 1 ? 's' : ''}{' '}
{affectedFeatureCount !== 1 ? 'are' : 'is'} assigned to this branch.{' '}
{affectedFeatureCount !== 1 ? 'They' : 'It'} will be unassigned and moved to the
main worktree.
</span>
</div>
)}
@@ -118,8 +110,8 @@ export function DeleteWorktreeDialog({
<div className="flex items-start gap-2 p-3 rounded-md bg-yellow-500/10 border border-yellow-500/20 mt-2">
<AlertTriangle className="w-4 h-4 text-yellow-500 mt-0.5 flex-shrink-0" />
<span className="text-yellow-500 text-sm">
This worktree has {worktree.changedFilesCount} uncommitted
change(s). These will be lost if you proceed.
This worktree has {worktree.changedFilesCount} uncommitted change(s). These will
be lost if you proceed.
</span>
</div>
)}
@@ -133,26 +125,16 @@ export function DeleteWorktreeDialog({
onCheckedChange={(checked) => setDeleteBranch(checked === true)}
/>
<Label htmlFor="delete-branch" className="text-sm cursor-pointer">
Also delete the branch{" "}
<code className="font-mono bg-muted px-1 rounded">
{worktree.branch}
</code>
Also delete the branch{' '}
<code className="font-mono bg-muted px-1 rounded">{worktree.branch}</code>
</Label>
</div>
<DialogFooter>
<Button
variant="ghost"
onClick={() => onOpenChange(false)}
disabled={isLoading}
>
<Button variant="ghost" onClick={() => onOpenChange(false)} disabled={isLoading}>
Cancel
</Button>
<Button
variant="destructive"
onClick={handleDelete}
disabled={isLoading}
>
<Button variant="destructive" onClick={handleDelete} disabled={isLoading}>
{isLoading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />

View File

@@ -1,14 +1,8 @@
import { useState, useEffect } from "react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Feature } from "@/store/app-store";
import { AlertCircle, CheckCircle2, Circle } from "lucide-react";
import { cn } from "@/lib/utils";
import { useState, useEffect } from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Feature } from '@/store/app-store';
import { AlertCircle, CheckCircle2, Circle } from 'lucide-react';
import { cn } from '@/lib/utils';
interface DependencyTreeDialogProps {
open: boolean;
@@ -37,22 +31,20 @@ export function DependencyTreeDialog({
.filter((f): f is Feature => f !== undefined);
// Find features that depend on this one
const dependents = allFeatures.filter((f) =>
f.dependencies?.includes(feature.id)
);
const dependents = allFeatures.filter((f) => f.dependencies?.includes(feature.id));
setDependencyTree({ dependencies, dependents });
}, [feature, allFeatures]);
if (!feature) return null;
const getStatusIcon = (status: Feature["status"]) => {
const getStatusIcon = (status: Feature['status']) => {
switch (status) {
case "completed":
case "verified":
case 'completed':
case 'verified':
return <CheckCircle2 className="w-4 h-4 text-green-500" />;
case "in_progress":
case "waiting_approval":
case 'in_progress':
case 'waiting_approval':
return <Circle className="w-4 h-4 text-blue-500 fill-blue-500/20" />;
default:
return <Circle className="w-4 h-4 text-muted-foreground/50" />;
@@ -64,10 +56,10 @@ export function DependencyTreeDialog({
return (
<span
className={cn(
"text-xs px-1.5 py-0.5 rounded font-medium",
priority === 1 && "bg-red-500/20 text-red-500",
priority === 2 && "bg-yellow-500/20 text-yellow-500",
priority === 3 && "bg-blue-500/20 text-blue-500"
'text-xs px-1.5 py-0.5 rounded font-medium',
priority === 1 && 'bg-red-500/20 text-red-500',
priority === 2 && 'bg-yellow-500/20 text-yellow-500',
priority === 3 && 'bg-blue-500/20 text-blue-500'
)}
>
P{priority}
@@ -91,9 +83,7 @@ export function DependencyTreeDialog({
{getPriorityBadge(feature.priority)}
</div>
<p className="text-sm text-muted-foreground">{feature.description}</p>
<p className="text-xs text-muted-foreground/70 mt-2">
Category: {feature.category}
</p>
<p className="text-xs text-muted-foreground/70 mt-2">Category: {feature.category}</p>
</div>
{/* Dependencies (what this feature needs) */}
@@ -102,9 +92,7 @@ export function DependencyTreeDialog({
<h3 className="font-semibold text-sm">
Dependencies ({dependencyTree.dependencies.length})
</h3>
<span className="text-xs text-muted-foreground">
This feature requires:
</span>
<span className="text-xs text-muted-foreground">This feature requires:</span>
</div>
{dependencyTree.dependencies.length === 0 ? (
@@ -117,35 +105,33 @@ export function DependencyTreeDialog({
<div
key={dep.id}
className={cn(
"border rounded-lg p-3 transition-colors",
dep.status === "completed" || dep.status === "verified"
? "bg-green-500/5 border-green-500/20"
: "bg-muted/30 border-border"
'border rounded-lg p-3 transition-colors',
dep.status === 'completed' || dep.status === 'verified'
? 'bg-green-500/5 border-green-500/20'
: 'bg-muted/30 border-border'
)}
>
<div className="flex items-center gap-3 mb-1">
{getStatusIcon(dep.status)}
<span className="text-sm font-medium flex-1">
{dep.description.slice(0, 100)}
{dep.description.length > 100 && "..."}
{dep.description.length > 100 && '...'}
</span>
{getPriorityBadge(dep.priority)}
</div>
<div className="flex items-center gap-3 ml-7">
<span className="text-xs text-muted-foreground">
{dep.category}
</span>
<span className="text-xs text-muted-foreground">{dep.category}</span>
<span
className={cn(
"text-xs px-2 py-0.5 rounded-full",
dep.status === "completed" || dep.status === "verified"
? "bg-green-500/20 text-green-600"
: dep.status === "in_progress"
? "bg-blue-500/20 text-blue-600"
: "bg-muted text-muted-foreground"
'text-xs px-2 py-0.5 rounded-full',
dep.status === 'completed' || dep.status === 'verified'
? 'bg-green-500/20 text-green-600'
: dep.status === 'in_progress'
? 'bg-blue-500/20 text-blue-600'
: 'bg-muted text-muted-foreground'
)}
>
{dep.status.replace(/_/g, " ")}
{dep.status.replace(/_/g, ' ')}
</span>
</div>
</div>
@@ -160,9 +146,7 @@ export function DependencyTreeDialog({
<h3 className="font-semibold text-sm">
Dependents ({dependencyTree.dependents.length})
</h3>
<span className="text-xs text-muted-foreground">
Features blocked by this:
</span>
<span className="text-xs text-muted-foreground">Features blocked by this:</span>
</div>
{dependencyTree.dependents.length === 0 ? (
@@ -172,34 +156,28 @@ export function DependencyTreeDialog({
) : (
<div className="space-y-2">
{dependencyTree.dependents.map((dependent) => (
<div
key={dependent.id}
className="border rounded-lg p-3 bg-muted/30"
>
<div key={dependent.id} className="border rounded-lg p-3 bg-muted/30">
<div className="flex items-center gap-3 mb-1">
{getStatusIcon(dependent.status)}
<span className="text-sm font-medium flex-1">
{dependent.description.slice(0, 100)}
{dependent.description.length > 100 && "..."}
{dependent.description.length > 100 && '...'}
</span>
{getPriorityBadge(dependent.priority)}
</div>
<div className="flex items-center gap-3 ml-7">
<span className="text-xs text-muted-foreground">
{dependent.category}
</span>
<span className="text-xs text-muted-foreground">{dependent.category}</span>
<span
className={cn(
"text-xs px-2 py-0.5 rounded-full",
dependent.status === "completed" ||
dependent.status === "verified"
? "bg-green-500/20 text-green-600"
: dependent.status === "in_progress"
? "bg-blue-500/20 text-blue-600"
: "bg-muted text-muted-foreground"
'text-xs px-2 py-0.5 rounded-full',
dependent.status === 'completed' || dependent.status === 'verified'
? 'bg-green-500/20 text-green-600'
: dependent.status === 'in_progress'
? 'bg-blue-500/20 text-blue-600'
: 'bg-muted text-muted-foreground'
)}
>
{dependent.status.replace(/_/g, " ")}
{dependent.status.replace(/_/g, ' ')}
</span>
</div>
</div>
@@ -210,7 +188,7 @@ export function DependencyTreeDialog({
{/* Warning for incomplete dependencies */}
{dependencyTree.dependencies.some(
(d) => d.status !== "completed" && d.status !== "verified"
(d) => d.status !== 'completed' && d.status !== 'verified'
) && (
<div className="flex items-start gap-3 p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-lg">
<AlertCircle className="w-5 h-5 text-yellow-600 shrink-0 mt-0.5" />
@@ -219,8 +197,8 @@ export function DependencyTreeDialog({
Incomplete Dependencies
</p>
<p className="text-yellow-600 dark:text-yellow-400 mt-1">
This feature has dependencies that aren't completed yet.
Consider completing them first for a smoother implementation.
This feature has dependencies that aren't completed yet. Consider completing them
first for a smoother implementation.
</p>
</div>
</div>

View File

@@ -1,5 +1,4 @@
import { useEffect, useRef, useState, useCallback } from "react";
import { useEffect, useRef, useState, useCallback } from 'react';
import {
Dialog,
DialogContent,
@@ -7,11 +6,11 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { HotkeyButton } from "@/components/ui/hotkey-button";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { HotkeyButton } from '@/components/ui/hotkey-button';
import { Checkbox } from '@/components/ui/checkbox';
import { Label } from '@/components/ui/label';
import {
Loader2,
Lightbulb,
@@ -22,10 +21,15 @@ import {
RefreshCw,
Shield,
Zap,
} from "lucide-react";
import { getElectronAPI, FeatureSuggestion, SuggestionsEvent, SuggestionType } from "@/lib/electron";
import { useAppStore, Feature } from "@/store/app-store";
import { toast } from "sonner";
} from 'lucide-react';
import {
getElectronAPI,
FeatureSuggestion,
SuggestionsEvent,
SuggestionType,
} from '@/lib/electron';
import { useAppStore, Feature } from '@/store/app-store';
import { toast } from 'sonner';
interface FeatureSuggestionsDialogProps {
open: boolean;
@@ -39,35 +43,38 @@ interface FeatureSuggestionsDialogProps {
}
// Configuration for each suggestion type
const suggestionTypeConfig: Record<SuggestionType, {
label: string;
icon: React.ComponentType<{ className?: string }>;
description: string;
color: string;
}> = {
const suggestionTypeConfig: Record<
SuggestionType,
{
label: string;
icon: React.ComponentType<{ className?: string }>;
description: string;
color: string;
}
> = {
features: {
label: "Feature Suggestions",
label: 'Feature Suggestions',
icon: Lightbulb,
description: "Discover missing features and improvements",
color: "text-yellow-500",
description: 'Discover missing features and improvements',
color: 'text-yellow-500',
},
refactoring: {
label: "Refactoring Suggestions",
label: 'Refactoring Suggestions',
icon: RefreshCw,
description: "Find code smells and refactoring opportunities",
color: "text-blue-500",
description: 'Find code smells and refactoring opportunities',
color: 'text-blue-500',
},
security: {
label: "Security Suggestions",
label: 'Security Suggestions',
icon: Shield,
description: "Identify security vulnerabilities and issues",
color: "text-red-500",
description: 'Identify security vulnerabilities and issues',
color: 'text-red-500',
},
performance: {
label: "Performance Suggestions",
label: 'Performance Suggestions',
icon: Zap,
description: "Discover performance bottlenecks and optimizations",
color: "text-green-500",
description: 'Discover performance bottlenecks and optimizations',
color: 'text-green-500',
},
};
@@ -112,23 +119,25 @@ export function FeatureSuggestionsDialog({
if (!api?.suggestions) return;
const unsubscribe = api.suggestions.onEvent((event: SuggestionsEvent) => {
if (event.type === "suggestions_progress") {
setProgress((prev) => [...prev, event.content || ""]);
} else if (event.type === "suggestions_tool") {
const toolName = event.tool || "Unknown Tool";
if (event.type === 'suggestions_progress') {
setProgress((prev) => [...prev, event.content || '']);
} else if (event.type === 'suggestions_tool') {
const toolName = event.tool || 'Unknown Tool';
setProgress((prev) => [...prev, `Using tool: ${toolName}\n`]);
} else if (event.type === "suggestions_complete") {
} else if (event.type === 'suggestions_complete') {
setIsGenerating(false);
if (event.suggestions && event.suggestions.length > 0) {
setSuggestions(event.suggestions);
// Select all by default
setSelectedIds(new Set(event.suggestions.map((s) => s.id)));
const typeLabel = currentSuggestionType ? suggestionTypeConfig[currentSuggestionType].label.toLowerCase() : "suggestions";
const typeLabel = currentSuggestionType
? suggestionTypeConfig[currentSuggestionType].label.toLowerCase()
: 'suggestions';
toast.success(`Generated ${event.suggestions.length} ${typeLabel}!`);
} else {
toast.info("No suggestions generated. Try again.");
toast.info('No suggestions generated. Try again.');
}
} else if (event.type === "suggestions_error") {
} else if (event.type === 'suggestions_error') {
setIsGenerating(false);
toast.error(`Error: ${event.error}`);
}
@@ -140,31 +149,34 @@ export function FeatureSuggestionsDialog({
}, [open, setSuggestions, setIsGenerating, currentSuggestionType]);
// Start generating suggestions for a specific type
const handleGenerate = useCallback(async (suggestionType: SuggestionType) => {
const api = getElectronAPI();
if (!api?.suggestions) {
toast.error("Suggestions API not available");
return;
}
const handleGenerate = useCallback(
async (suggestionType: SuggestionType) => {
const api = getElectronAPI();
if (!api?.suggestions) {
toast.error('Suggestions API not available');
return;
}
setIsGenerating(true);
setProgress([]);
setSuggestions([]);
setSelectedIds(new Set());
setCurrentSuggestionType(suggestionType);
setIsGenerating(true);
setProgress([]);
setSuggestions([]);
setSelectedIds(new Set());
setCurrentSuggestionType(suggestionType);
try {
const result = await api.suggestions.generate(projectPath, suggestionType);
if (!result.success) {
toast.error(result.error || "Failed to start generation");
try {
const result = await api.suggestions.generate(projectPath, suggestionType);
if (!result.success) {
toast.error(result.error || 'Failed to start generation');
setIsGenerating(false);
}
} catch (error) {
console.error('Failed to generate suggestions:', error);
toast.error('Failed to start generation');
setIsGenerating(false);
}
} catch (error) {
console.error("Failed to generate suggestions:", error);
toast.error("Failed to start generation");
setIsGenerating(false);
}
}, [projectPath, setIsGenerating, setSuggestions]);
},
[projectPath, setIsGenerating, setSuggestions]
);
// Stop generating
const handleStop = useCallback(async () => {
@@ -174,9 +186,9 @@ export function FeatureSuggestionsDialog({
try {
await api.suggestions.stop();
setIsGenerating(false);
toast.info("Generation stopped");
toast.info('Generation stopped');
} catch (error) {
console.error("Failed to stop generation:", error);
console.error('Failed to stop generation:', error);
}
}, [setIsGenerating]);
@@ -218,7 +230,7 @@ export function FeatureSuggestionsDialog({
// Import selected suggestions as features
const handleImport = useCallback(async () => {
if (selectedIds.size === 0) {
toast.warning("No suggestions selected");
toast.warning('No suggestions selected');
return;
}
@@ -226,9 +238,7 @@ export function FeatureSuggestionsDialog({
try {
const api = getElectronAPI();
const selectedSuggestions = suggestions.filter((s) =>
selectedIds.has(s.id)
);
const selectedSuggestions = suggestions.filter((s) => selectedIds.has(s.id));
// Create new features from selected suggestions
const newFeatures: Feature[] = selectedSuggestions.map((s) => ({
@@ -236,7 +246,7 @@ export function FeatureSuggestionsDialog({
category: s.category,
description: s.description,
steps: s.steps,
status: "backlog" as const,
status: 'backlog' as const,
skipTests: true, // As specified, testing mode true
priority: s.priority, // Preserve priority from suggestion
}));
@@ -264,8 +274,8 @@ export function FeatureSuggestionsDialog({
onClose();
} catch (error) {
console.error("Failed to import features:", error);
toast.error("Failed to import features");
console.error('Failed to import features:', error);
toast.error('Failed to import features');
} finally {
setIsImporting(false);
}
@@ -315,7 +325,7 @@ export function FeatureSuggestionsDialog({
<DialogDescription>
{currentConfig
? currentConfig.description
: "Analyze your project to discover improvements. Choose a suggestion type below."}
: 'Analyze your project to discover improvements. Choose a suggestion type below.'}
</DialogDescription>
</DialogHeader>
@@ -323,32 +333,35 @@ export function FeatureSuggestionsDialog({
// Initial state - show suggestion type buttons
<div className="flex-1 flex flex-col items-center justify-center py-8">
<p className="text-muted-foreground text-center max-w-lg mb-8">
Our AI will analyze your project and generate actionable suggestions.
Choose what type of analysis you want to perform:
Our AI will analyze your project and generate actionable suggestions. Choose what type
of analysis you want to perform:
</p>
<div className="grid grid-cols-2 gap-4 w-full max-w-2xl">
{(Object.entries(suggestionTypeConfig) as [SuggestionType, typeof suggestionTypeConfig[SuggestionType]][]).map(
([type, config]) => {
const Icon = config.icon;
return (
<Button
key={type}
variant="outline"
className="h-auto py-6 px-6 flex flex-col items-center gap-3 hover:border-primary/50 transition-colors"
onClick={() => handleGenerate(type)}
data-testid={`generate-${type}-btn`}
>
<Icon className={`w-8 h-8 ${config.color}`} />
<div className="text-center">
<div className="font-semibold">{config.label.replace(" Suggestions", "")}</div>
<div className="text-xs text-muted-foreground mt-1">
{config.description}
</div>
{(
Object.entries(suggestionTypeConfig) as [
SuggestionType,
(typeof suggestionTypeConfig)[SuggestionType],
][]
).map(([type, config]) => {
const Icon = config.icon;
return (
<Button
key={type}
variant="outline"
className="h-auto py-6 px-6 flex flex-col items-center gap-3 hover:border-primary/50 transition-colors"
onClick={() => handleGenerate(type)}
data-testid={`generate-${type}-btn`}
>
<Icon className={`w-8 h-8 ${config.color}`} />
<div className="text-center">
<div className="font-semibold">
{config.label.replace(' Suggestions', '')}
</div>
</Button>
);
}
)}
<div className="text-xs text-muted-foreground mt-1">{config.description}</div>
</div>
</Button>
);
})}
</div>
</div>
) : isGenerating ? (
@@ -370,7 +383,7 @@ export function FeatureSuggestionsDialog({
className="flex-1 overflow-y-auto bg-zinc-950 rounded-lg p-4 font-mono text-xs min-h-[200px] max-h-[400px]"
>
<div className="whitespace-pre-wrap break-words text-zinc-300">
{progress.join("")}
{progress.join('')}
</div>
</div>
</div>
@@ -383,14 +396,10 @@ export function FeatureSuggestionsDialog({
{suggestions.length} suggestions generated
</span>
<Button variant="ghost" size="sm" onClick={toggleSelectAll}>
{selectedIds.size === suggestions.length
? "Deselect All"
: "Select All"}
{selectedIds.size === suggestions.length ? 'Deselect All' : 'Select All'}
</Button>
</div>
<span className="text-sm font-medium">
{selectedIds.size} selected
</span>
<span className="text-sm font-medium">{selectedIds.size} selected</span>
</div>
<div
@@ -406,8 +415,8 @@ export function FeatureSuggestionsDialog({
key={suggestion.id}
className={`border rounded-lg p-3 transition-colors ${
isSelected
? "border-primary bg-primary/5"
: "border-border hover:border-primary/50"
? 'border-primary bg-primary/5'
: 'border-border hover:border-primary/50'
}`}
data-testid={`suggestion-${suggestion.id}`}
>
@@ -447,9 +456,7 @@ export function FeatureSuggestionsDialog({
{isExpanded && (
<div className="mt-3 space-y-2 text-sm">
{suggestion.reasoning && (
<p className="text-muted-foreground italic">
{suggestion.reasoning}
</p>
<p className="text-muted-foreground italic">{suggestion.reasoning}</p>
)}
{suggestion.steps.length > 0 && (
<div>
@@ -513,7 +520,7 @@ export function FeatureSuggestionsDialog({
<HotkeyButton
onClick={handleImport}
disabled={selectedIds.size === 0 || isImporting}
hotkey={{ key: "Enter", cmdCtrl: true }}
hotkey={{ key: 'Enter', cmdCtrl: true }}
hotkeyActive={open && hasSuggestions}
>
{isImporting ? (
@@ -522,7 +529,7 @@ export function FeatureSuggestionsDialog({
<Download className="w-4 h-4 mr-2" />
)}
Import {selectedIds.size} Feature
{selectedIds.size !== 1 ? "s" : ""}
{selectedIds.size !== 1 ? 's' : ''}
</HotkeyButton>
</div>
</div>

View File

@@ -1,5 +1,4 @@
import { useState } from "react";
import { useState } from 'react';
import {
Dialog,
DialogContent,
@@ -7,17 +6,17 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { HotkeyButton } from "@/components/ui/hotkey-button";
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { HotkeyButton } from '@/components/ui/hotkey-button';
import {
DescriptionImageDropZone,
FeatureImagePath as DescriptionImagePath,
ImagePreviewMap,
} from "@/components/ui/description-image-dropzone";
import { MessageSquare } from "lucide-react";
import { Feature } from "@/store/app-store";
} from '@/components/ui/description-image-dropzone';
import { MessageSquare } from 'lucide-react';
import { Feature } from '@/store/app-store';
interface FollowUpDialogProps {
open: boolean;
@@ -58,7 +57,7 @@ export function FollowUpDialog({
compact={!isMaximized}
data-testid="follow-up-dialog"
onKeyDown={(e: React.KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === "Enter" && prompt.trim()) {
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter' && prompt.trim()) {
e.preventDefault();
onSend();
}
@@ -71,7 +70,7 @@ export function FollowUpDialog({
{feature && (
<span className="block mt-2 text-primary">
Feature: {feature.description.slice(0, 100)}
{feature.description.length > 100 ? "..." : ""}
{feature.description.length > 100 ? '...' : ''}
</span>
)}
</DialogDescription>
@@ -90,8 +89,8 @@ export function FollowUpDialog({
/>
</div>
<p className="text-xs text-muted-foreground">
The agent will continue from where it left off, using the existing
context. You can attach screenshots to help explain the issue.
The agent will continue from where it left off, using the existing context. You can
attach screenshots to help explain the issue.
</p>
</div>
<DialogFooter>
@@ -106,7 +105,7 @@ export function FollowUpDialog({
<HotkeyButton
onClick={onSend}
disabled={!prompt.trim()}
hotkey={{ key: "Enter", cmdCtrl: true }}
hotkey={{ key: 'Enter', cmdCtrl: true }}
hotkeyActive={open}
data-testid="confirm-follow-up"
>

View File

@@ -1,9 +1,9 @@
export { AddFeatureDialog } from "./add-feature-dialog";
export { AgentOutputModal } from "./agent-output-modal";
export { CompletedFeaturesModal } from "./completed-features-modal";
export { ArchiveAllVerifiedDialog } from "./archive-all-verified-dialog";
export { DeleteCompletedFeatureDialog } from "./delete-completed-feature-dialog";
export { EditFeatureDialog } from "./edit-feature-dialog";
export { FeatureSuggestionsDialog } from "./feature-suggestions-dialog";
export { FollowUpDialog } from "./follow-up-dialog";
export { PlanApprovalDialog } from "./plan-approval-dialog";
export { AddFeatureDialog } from './add-feature-dialog';
export { AgentOutputModal } from './agent-output-modal';
export { CompletedFeaturesModal } from './completed-features-modal';
export { ArchiveAllVerifiedDialog } from './archive-all-verified-dialog';
export { DeleteCompletedFeatureDialog } from './delete-completed-feature-dialog';
export { EditFeatureDialog } from './edit-feature-dialog';
export { FeatureSuggestionsDialog } from './feature-suggestions-dialog';
export { FollowUpDialog } from './follow-up-dialog';
export { PlanApprovalDialog } from './plan-approval-dialog';

View File

@@ -1,6 +1,6 @@
"use client";
'use client';
import { useState, useEffect } from "react";
import { useState, useEffect } from 'react';
import {
Dialog,
DialogContent,
@@ -8,13 +8,13 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Markdown } from "@/components/ui/markdown";
import { Label } from "@/components/ui/label";
import { Feature } from "@/store/app-store";
import { Check, RefreshCw, Edit2, Eye, Loader2 } from "lucide-react";
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { Markdown } from '@/components/ui/markdown';
import { Label } from '@/components/ui/label';
import { Feature } from '@/store/app-store';
import { Check, RefreshCw, Edit2, Eye, Loader2 } from 'lucide-react';
interface PlanApprovalDialogProps {
open: boolean;
@@ -40,7 +40,7 @@ export function PlanApprovalDialog({
const [isEditMode, setIsEditMode] = useState(false);
const [editedPlan, setEditedPlan] = useState(planContent);
const [showRejectFeedback, setShowRejectFeedback] = useState(false);
const [rejectFeedback, setRejectFeedback] = useState("");
const [rejectFeedback, setRejectFeedback] = useState('');
// Reset state when dialog opens or plan content changes
useEffect(() => {
@@ -48,7 +48,7 @@ export function PlanApprovalDialog({
setEditedPlan(planContent);
setIsEditMode(false);
setShowRejectFeedback(false);
setRejectFeedback("");
setRejectFeedback('');
}
}, [open, planContent]);
@@ -68,7 +68,7 @@ export function PlanApprovalDialog({
const handleCancelReject = () => {
setShowRejectFeedback(false);
setRejectFeedback("");
setRejectFeedback('');
};
const handleClose = (open: boolean) => {
@@ -79,20 +79,17 @@ export function PlanApprovalDialog({
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent
className="max-w-4xl"
data-testid="plan-approval-dialog"
>
<DialogContent className="max-w-4xl" data-testid="plan-approval-dialog">
<DialogHeader>
<DialogTitle>{viewOnly ? "View Plan" : "Review Plan"}</DialogTitle>
<DialogTitle>{viewOnly ? 'View Plan' : 'Review Plan'}</DialogTitle>
<DialogDescription>
{viewOnly
? "View the generated plan for this feature."
: "Review the generated plan before implementation begins."}
? 'View the generated plan for this feature.'
: 'Review the generated plan before implementation begins.'}
{feature && (
<span className="block mt-2 text-primary">
Feature: {feature.description.slice(0, 150)}
{feature.description.length > 150 ? "..." : ""}
{feature.description.length > 150 ? '...' : ''}
</span>
)}
</DialogDescription>
@@ -103,7 +100,7 @@ export function PlanApprovalDialog({
{!viewOnly && (
<div className="flex items-center justify-between mb-3">
<Label className="text-sm text-muted-foreground">
{isEditMode ? "Edit Mode" : "View Mode"}
{isEditMode ? 'Edit Mode' : 'View Mode'}
</Label>
<Button
variant="outline"
@@ -138,7 +135,7 @@ export function PlanApprovalDialog({
/>
) : (
<div className="p-4 overflow-auto">
<Markdown>{editedPlan || "No plan content available."}</Markdown>
<Markdown>{editedPlan || 'No plan content available.'}</Markdown>
</div>
)}
</div>
@@ -169,33 +166,21 @@ export function PlanApprovalDialog({
</Button>
) : showRejectFeedback ? (
<>
<Button
variant="ghost"
onClick={handleCancelReject}
disabled={isLoading}
>
<Button variant="ghost" onClick={handleCancelReject} disabled={isLoading}>
Back
</Button>
<Button
variant="secondary"
onClick={handleReject}
disabled={isLoading}
>
<Button variant="secondary" onClick={handleReject} disabled={isLoading}>
{isLoading ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<RefreshCw className="w-4 h-4 mr-2" />
)}
{rejectFeedback.trim() ? "Revise Plan" : "Cancel Feature"}
{rejectFeedback.trim() ? 'Revise Plan' : 'Cancel Feature'}
</Button>
</>
) : (
<>
<Button
variant="outline"
onClick={handleReject}
disabled={isLoading}
>
<Button variant="outline" onClick={handleReject} disabled={isLoading}>
<RefreshCw className="w-4 h-4 mr-2" />
Request Changes
</Button>

View File

@@ -1,10 +1,10 @@
export { useBoardFeatures } from "./use-board-features";
export { useBoardDragDrop } from "./use-board-drag-drop";
export { useBoardActions } from "./use-board-actions";
export { useBoardKeyboardShortcuts } from "./use-board-keyboard-shortcuts";
export { useBoardColumnFeatures } from "./use-board-column-features";
export { useBoardEffects } from "./use-board-effects";
export { useBoardBackground } from "./use-board-background";
export { useBoardPersistence } from "./use-board-persistence";
export { useFollowUpState } from "./use-follow-up-state";
export { useSuggestionsState } from "./use-suggestions-state";
export { useBoardFeatures } from './use-board-features';
export { useBoardDragDrop } from './use-board-drag-drop';
export { useBoardActions } from './use-board-actions';
export { useBoardKeyboardShortcuts } from './use-board-keyboard-shortcuts';
export { useBoardColumnFeatures } from './use-board-column-features';
export { useBoardEffects } from './use-board-effects';
export { useBoardBackground } from './use-board-background';
export { useBoardPersistence } from './use-board-persistence';
export { useFollowUpState } from './use-follow-up-state';
export { useSuggestionsState } from './use-suggestions-state';

View File

@@ -1,20 +1,17 @@
import { useMemo } from "react";
import { useAppStore, defaultBackgroundSettings } from "@/store/app-store";
import { useMemo } from 'react';
import { useAppStore, defaultBackgroundSettings } from '@/store/app-store';
interface UseBoardBackgroundProps {
currentProject: { path: string; id: string } | null;
}
export function useBoardBackground({ currentProject }: UseBoardBackgroundProps) {
const boardBackgroundByProject = useAppStore(
(state) => state.boardBackgroundByProject
);
const boardBackgroundByProject = useAppStore((state) => state.boardBackgroundByProject);
// Get background settings for current project
const backgroundSettings = useMemo(() => {
return (
(currentProject && boardBackgroundByProject[currentProject.path]) ||
defaultBackgroundSettings
(currentProject && boardBackgroundByProject[currentProject.path]) || defaultBackgroundSettings
);
}, [currentProject, boardBackgroundByProject]);
@@ -26,17 +23,15 @@ export function useBoardBackground({ currentProject }: UseBoardBackgroundProps)
return {
backgroundImage: `url(${
import.meta.env.VITE_SERVER_URL || "http://localhost:3008"
import.meta.env.VITE_SERVER_URL || 'http://localhost:3008'
}/api/fs/image?path=${encodeURIComponent(
backgroundSettings.imagePath
)}&projectPath=${encodeURIComponent(currentProject.path)}${
backgroundSettings.imageVersion
? `&v=${backgroundSettings.imageVersion}`
: ""
backgroundSettings.imageVersion ? `&v=${backgroundSettings.imageVersion}` : ''
})`,
backgroundSize: "cover",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
} as React.CSSProperties;
}, [backgroundSettings, currentProject]);

View File

@@ -1,18 +1,15 @@
import { useState, useCallback } from "react";
import { DragStartEvent, DragEndEvent } from "@dnd-kit/core";
import { Feature } from "@/store/app-store";
import { useAppStore } from "@/store/app-store";
import { toast } from "sonner";
import { COLUMNS, ColumnId } from "../constants";
import { useState, useCallback } from 'react';
import { DragStartEvent, DragEndEvent } from '@dnd-kit/core';
import { Feature } from '@/store/app-store';
import { useAppStore } from '@/store/app-store';
import { toast } from 'sonner';
import { COLUMNS, ColumnId } from '../constants';
interface UseBoardDragDropProps {
features: Feature[];
currentProject: { path: string; id: string } | null;
runningAutoTasks: string[];
persistFeatureUpdate: (
featureId: string,
updates: Partial<Feature>
) => Promise<void>;
persistFeatureUpdate: (featureId: string, updates: Partial<Feature>) => Promise<void>;
handleStartImplementation: (feature: Feature) => Promise<boolean>;
}
@@ -63,12 +60,10 @@ export function useBoardDragDrop({
// - verified items can always be dragged (to allow moving back to waiting_approval)
// - in_progress items can be dragged (but not if they're currently running)
// - Non-skipTests (TDD) items that are in progress cannot be dragged if they are running
if (draggedFeature.status === "in_progress") {
if (draggedFeature.status === 'in_progress') {
// Only allow dragging in_progress if it's not currently running
if (isRunningTask) {
console.log(
"[Board] Cannot drag feature - currently running"
);
console.log('[Board] Cannot drag feature - currently running');
return;
}
}
@@ -94,9 +89,9 @@ export function useBoardDragDrop({
// Handle different drag scenarios
// Note: Worktrees are created server-side at execution time based on feature.branchName
if (draggedFeature.status === "backlog") {
if (draggedFeature.status === 'backlog') {
// From backlog
if (targetStatus === "in_progress") {
if (targetStatus === 'in_progress') {
// Use helper function to handle concurrency check and start implementation
// Server will derive workDir from feature.branchName
await handleStartImplementation(draggedFeature);
@@ -104,122 +99,110 @@ export function useBoardDragDrop({
moveFeature(featureId, targetStatus);
persistFeatureUpdate(featureId, { status: targetStatus });
}
} else if (draggedFeature.status === "waiting_approval") {
} else if (draggedFeature.status === 'waiting_approval') {
// waiting_approval features can be dragged to verified for manual verification
// NOTE: This check must come BEFORE skipTests check because waiting_approval
// features often have skipTests=true, and we want status-based handling first
if (targetStatus === "verified") {
moveFeature(featureId, "verified");
if (targetStatus === 'verified') {
moveFeature(featureId, 'verified');
// Clear justFinishedAt timestamp when manually verifying via drag
persistFeatureUpdate(featureId, {
status: "verified",
status: 'verified',
justFinishedAt: undefined,
});
toast.success("Feature verified", {
toast.success('Feature verified', {
description: `Manually verified: ${draggedFeature.description.slice(
0,
50
)}${draggedFeature.description.length > 50 ? "..." : ""}`,
)}${draggedFeature.description.length > 50 ? '...' : ''}`,
});
} else if (targetStatus === "backlog") {
} else if (targetStatus === 'backlog') {
// Allow moving waiting_approval cards back to backlog
moveFeature(featureId, "backlog");
moveFeature(featureId, 'backlog');
// Clear justFinishedAt timestamp when moving back to backlog
persistFeatureUpdate(featureId, {
status: "backlog",
status: 'backlog',
justFinishedAt: undefined,
});
toast.info("Feature moved to backlog", {
toast.info('Feature moved to backlog', {
description: `Moved to Backlog: ${draggedFeature.description.slice(
0,
50
)}${draggedFeature.description.length > 50 ? "..." : ""}`,
)}${draggedFeature.description.length > 50 ? '...' : ''}`,
});
}
} else if (draggedFeature.status === "in_progress") {
} else if (draggedFeature.status === 'in_progress') {
// Handle in_progress features being moved
if (targetStatus === "backlog") {
if (targetStatus === 'backlog') {
// Allow moving in_progress cards back to backlog
moveFeature(featureId, "backlog");
persistFeatureUpdate(featureId, { status: "backlog" });
toast.info("Feature moved to backlog", {
moveFeature(featureId, 'backlog');
persistFeatureUpdate(featureId, { status: 'backlog' });
toast.info('Feature moved to backlog', {
description: `Moved to Backlog: ${draggedFeature.description.slice(
0,
50
)}${draggedFeature.description.length > 50 ? "..." : ""}`,
)}${draggedFeature.description.length > 50 ? '...' : ''}`,
});
} else if (
targetStatus === "verified" &&
draggedFeature.skipTests
) {
} else if (targetStatus === 'verified' && draggedFeature.skipTests) {
// Manual verify via drag (only for skipTests features)
moveFeature(featureId, "verified");
persistFeatureUpdate(featureId, { status: "verified" });
toast.success("Feature verified", {
moveFeature(featureId, 'verified');
persistFeatureUpdate(featureId, { status: 'verified' });
toast.success('Feature verified', {
description: `Marked as verified: ${draggedFeature.description.slice(
0,
50
)}${draggedFeature.description.length > 50 ? "..." : ""}`,
)}${draggedFeature.description.length > 50 ? '...' : ''}`,
});
}
} else if (draggedFeature.skipTests) {
// skipTests feature being moved between verified and waiting_approval
if (
targetStatus === "waiting_approval" &&
draggedFeature.status === "verified"
) {
if (targetStatus === 'waiting_approval' && draggedFeature.status === 'verified') {
// Move verified feature back to waiting_approval
moveFeature(featureId, "waiting_approval");
persistFeatureUpdate(featureId, { status: "waiting_approval" });
toast.info("Feature moved back", {
moveFeature(featureId, 'waiting_approval');
persistFeatureUpdate(featureId, { status: 'waiting_approval' });
toast.info('Feature moved back', {
description: `Moved back to Waiting Approval: ${draggedFeature.description.slice(
0,
50
)}${draggedFeature.description.length > 50 ? "..." : ""}`,
)}${draggedFeature.description.length > 50 ? '...' : ''}`,
});
} else if (targetStatus === "backlog") {
} else if (targetStatus === 'backlog') {
// Allow moving skipTests cards back to backlog (from verified)
moveFeature(featureId, "backlog");
persistFeatureUpdate(featureId, { status: "backlog" });
toast.info("Feature moved to backlog", {
moveFeature(featureId, 'backlog');
persistFeatureUpdate(featureId, { status: 'backlog' });
toast.info('Feature moved to backlog', {
description: `Moved to Backlog: ${draggedFeature.description.slice(
0,
50
)}${draggedFeature.description.length > 50 ? "..." : ""}`,
)}${draggedFeature.description.length > 50 ? '...' : ''}`,
});
}
} else if (draggedFeature.status === "verified") {
} else if (draggedFeature.status === 'verified') {
// Handle verified TDD (non-skipTests) features being moved back
if (targetStatus === "waiting_approval") {
if (targetStatus === 'waiting_approval') {
// Move verified feature back to waiting_approval
moveFeature(featureId, "waiting_approval");
persistFeatureUpdate(featureId, { status: "waiting_approval" });
toast.info("Feature moved back", {
moveFeature(featureId, 'waiting_approval');
persistFeatureUpdate(featureId, { status: 'waiting_approval' });
toast.info('Feature moved back', {
description: `Moved back to Waiting Approval: ${draggedFeature.description.slice(
0,
50
)}${draggedFeature.description.length > 50 ? "..." : ""}`,
)}${draggedFeature.description.length > 50 ? '...' : ''}`,
});
} else if (targetStatus === "backlog") {
} else if (targetStatus === 'backlog') {
// Allow moving verified cards back to backlog
moveFeature(featureId, "backlog");
persistFeatureUpdate(featureId, { status: "backlog" });
toast.info("Feature moved to backlog", {
moveFeature(featureId, 'backlog');
persistFeatureUpdate(featureId, { status: 'backlog' });
toast.info('Feature moved to backlog', {
description: `Moved to Backlog: ${draggedFeature.description.slice(
0,
50
)}${draggedFeature.description.length > 50 ? "..." : ""}`,
)}${draggedFeature.description.length > 50 ? '...' : ''}`,
});
}
}
},
[
features,
runningAutoTasks,
moveFeature,
persistFeatureUpdate,
handleStartImplementation,
]
[features, runningAutoTasks, moveFeature, persistFeatureUpdate, handleStartImplementation]
);
return {

View File

@@ -1,6 +1,6 @@
import { useEffect } from "react";
import { getElectronAPI } from "@/lib/electron";
import { useAppStore } from "@/store/app-store";
import { useEffect } from 'react';
import { getElectronAPI } from '@/lib/electron';
import { useAppStore } from '@/store/app-store';
interface UseBoardEffectsProps {
currentProject: { path: string; id: string } | null;
@@ -43,11 +43,11 @@ export function useBoardEffects({
if (!api?.suggestions) return;
const unsubscribe = api.suggestions.onEvent((event) => {
if (event.type === "suggestions_complete" && event.suggestions) {
if (event.type === 'suggestions_complete' && event.suggestions) {
setSuggestionsCount(event.suggestions.length);
setFeatureSuggestions(event.suggestions);
setIsGeneratingSuggestions(false);
} else if (event.type === "suggestions_error") {
} else if (event.type === 'suggestions_error') {
setIsGeneratingSuggestions(false);
}
});
@@ -64,9 +64,9 @@ export function useBoardEffects({
const unsubscribe = api.specRegeneration.onEvent((event) => {
console.log(
"[BoardView] Spec regeneration event:",
'[BoardView] Spec regeneration event:',
event.type,
"for project:",
'for project:',
event.projectPath
);
@@ -74,9 +74,9 @@ export function useBoardEffects({
return;
}
if (event.type === "spec_regeneration_complete") {
if (event.type === 'spec_regeneration_complete') {
setSpecCreatingForProject(null);
} else if (event.type === "spec_regeneration_error") {
} else if (event.type === 'spec_regeneration_error') {
setSpecCreatingForProject(null);
}
});
@@ -101,10 +101,7 @@ export function useBoardEffects({
const { clearRunningTasks, addRunningTask } = useAppStore.getState();
if (status.runningFeatures) {
console.log(
"[Board] Syncing running tasks from backend:",
status.runningFeatures
);
console.log('[Board] Syncing running tasks from backend:', status.runningFeatures);
clearRunningTasks(projectId);
@@ -114,7 +111,7 @@ export function useBoardEffects({
}
}
} catch (error) {
console.error("[Board] Failed to sync running tasks:", error);
console.error('[Board] Failed to sync running tasks:', error);
}
};
@@ -126,9 +123,7 @@ export function useBoardEffects({
const checkAllContexts = async () => {
const featuresWithPotentialContext = features.filter(
(f) =>
f.status === "in_progress" ||
f.status === "waiting_approval" ||
f.status === "verified"
f.status === 'in_progress' || f.status === 'waiting_approval' || f.status === 'verified'
);
const contextChecks = await Promise.all(
featuresWithPotentialContext.map(async (f) => ({

View File

@@ -1,7 +1,7 @@
import { useState, useCallback, useEffect, useRef } from "react";
import { useAppStore, Feature } from "@/store/app-store";
import { getElectronAPI } from "@/lib/electron";
import { toast } from "sonner";
import { useState, useCallback, useEffect, useRef } from 'react';
import { useAppStore, Feature } from '@/store/app-store';
import { getElectronAPI } from '@/lib/electron';
import { toast } from 'sonner';
interface UseBoardFeaturesProps {
currentProject: { path: string; id: string } | null;
@@ -24,8 +24,7 @@ export function useBoardFeatures({ currentProject }: UseBoardFeaturesProps) {
const currentPath = currentProject.path;
const previousPath = prevProjectPathRef.current;
const isProjectSwitch =
previousPath !== null && currentPath !== previousPath;
const isProjectSwitch = previousPath !== null && currentPath !== previousPath;
// Get cached features from store (without adding to dependencies)
const cachedFeatures = useAppStore.getState().features;
@@ -33,9 +32,7 @@ export function useBoardFeatures({ currentProject }: UseBoardFeaturesProps) {
// If project switched, mark it but don't clear features yet
// We'll clear after successful API load to prevent data loss
if (isProjectSwitch) {
console.log(
`[BoardView] Project switch detected: ${previousPath} -> ${currentPath}`
);
console.log(`[BoardView] Project switch detected: ${previousPath} -> ${currentPath}`);
isSwitchingProjectRef.current = true;
isInitialLoadRef.current = true;
}
@@ -51,7 +48,7 @@ export function useBoardFeatures({ currentProject }: UseBoardFeaturesProps) {
try {
const api = getElectronAPI();
if (!api.features) {
console.error("[BoardView] Features API not available");
console.error('[BoardView] Features API not available');
// Keep cached features if API is unavailable
return;
}
@@ -59,17 +56,15 @@ export function useBoardFeatures({ currentProject }: UseBoardFeaturesProps) {
const result = await api.features.getAll(currentProject.path);
if (result.success && result.features) {
const featuresWithIds = result.features.map(
(f: any, index: number) => ({
...f,
id: f.id || `feature-${index}-${Date.now()}`,
status: f.status || "backlog",
startedAt: f.startedAt, // Preserve startedAt timestamp
// Ensure model and thinkingLevel are set for backward compatibility
model: f.model || "opus",
thinkingLevel: f.thinkingLevel || "none",
})
);
const featuresWithIds = result.features.map((f: any, index: number) => ({
...f,
id: f.id || `feature-${index}-${Date.now()}`,
status: f.status || 'backlog',
startedAt: f.startedAt, // Preserve startedAt timestamp
// Ensure model and thinkingLevel are set for backward compatibility
model: f.model || 'opus',
thinkingLevel: f.thinkingLevel || 'none',
}));
// Successfully loaded features - now safe to set them
setFeatures(featuresWithIds);
@@ -78,7 +73,7 @@ export function useBoardFeatures({ currentProject }: UseBoardFeaturesProps) {
setPersistedCategories([]);
}
} else if (!result.success && result.error) {
console.error("[BoardView] API returned error:", result.error);
console.error('[BoardView] API returned error:', result.error);
// If it's a new project or the error indicates no features found,
// that's expected - start with empty array
if (isProjectSwitch) {
@@ -88,7 +83,7 @@ export function useBoardFeatures({ currentProject }: UseBoardFeaturesProps) {
// Otherwise keep cached features
}
} catch (error) {
console.error("Failed to load features:", error);
console.error('Failed to load features:', error);
// On error, keep existing cached features for the current project
// Only clear on project switch if we have no features from server
if (isProjectSwitch && cachedFeatures.length === 0) {
@@ -108,9 +103,7 @@ export function useBoardFeatures({ currentProject }: UseBoardFeaturesProps) {
try {
const api = getElectronAPI();
const result = await api.readFile(
`${currentProject.path}/.automaker/categories.json`
);
const result = await api.readFile(`${currentProject.path}/.automaker/categories.json`);
if (result.success && result.content) {
const parsed = JSON.parse(result.content);
@@ -122,7 +115,7 @@ export function useBoardFeatures({ currentProject }: UseBoardFeaturesProps) {
setPersistedCategories([]);
}
} catch (error) {
console.error("Failed to load categories:", error);
console.error('Failed to load categories:', error);
// If file doesn't exist, ensure categories are cleared
setPersistedCategories([]);
}
@@ -154,7 +147,7 @@ export function useBoardFeatures({ currentProject }: UseBoardFeaturesProps) {
setPersistedCategories(categories);
}
} catch (error) {
console.error("Failed to save category:", error);
console.error('Failed to save category:', error);
}
},
[currentProject, persistedCategories]
@@ -168,13 +161,11 @@ export function useBoardFeatures({ currentProject }: UseBoardFeaturesProps) {
const unsubscribe = api.specRegeneration.onEvent((event) => {
// Refresh the kanban board when spec regeneration completes for the current project
if (
event.type === "spec_regeneration_complete" &&
event.type === 'spec_regeneration_complete' &&
currentProject &&
event.projectPath === currentProject.path
) {
console.log(
"[BoardView] Spec regeneration complete, refreshing features"
);
console.log('[BoardView] Spec regeneration complete, refreshing features');
loadFeatures();
}
});
@@ -195,32 +186,26 @@ export function useBoardFeatures({ currentProject }: UseBoardFeaturesProps) {
const unsubscribe = api.autoMode.onEvent((event) => {
// Use event's projectPath or projectId if available, otherwise use current project
// Board view only reacts to events for the currently selected project
const eventProjectId =
("projectId" in event && event.projectId) || projectId;
const eventProjectId = ('projectId' in event && event.projectId) || projectId;
if (event.type === "auto_mode_feature_complete") {
if (event.type === 'auto_mode_feature_complete') {
// Reload features when a feature is completed
console.log("[Board] Feature completed, reloading features...");
console.log('[Board] Feature completed, reloading features...');
loadFeatures();
// Play ding sound when feature is done (unless muted)
const { muteDoneSound } = useAppStore.getState();
if (!muteDoneSound) {
const audio = new Audio("/sounds/ding.mp3");
audio
.play()
.catch((err) => console.warn("Could not play ding sound:", err));
const audio = new Audio('/sounds/ding.mp3');
audio.play().catch((err) => console.warn('Could not play ding sound:', err));
}
} else if (event.type === "plan_approval_required") {
} else if (event.type === 'plan_approval_required') {
// Reload features when plan is generated and requires approval
// This ensures the feature card shows the "Approve Plan" button
console.log("[Board] Plan approval required, reloading features...");
console.log('[Board] Plan approval required, reloading features...');
loadFeatures();
} else if (event.type === "auto_mode_error") {
} else if (event.type === 'auto_mode_error') {
// Reload features when an error occurs (feature moved to waiting_approval)
console.log(
"[Board] Feature error, reloading features...",
event.error
);
console.log('[Board] Feature error, reloading features...', event.error);
// Remove from running tasks so it moves to the correct column
if (event.featureId) {
@@ -231,20 +216,20 @@ export function useBoardFeatures({ currentProject }: UseBoardFeaturesProps) {
// Check for authentication errors and show a more helpful message
const isAuthError =
event.errorType === "authentication" ||
event.errorType === 'authentication' ||
(event.error &&
(event.error.includes("Authentication failed") ||
event.error.includes("Invalid API key")));
(event.error.includes('Authentication failed') ||
event.error.includes('Invalid API key')));
if (isAuthError) {
toast.error("Authentication Failed", {
toast.error('Authentication Failed', {
description:
"Your API key is invalid or expired. Please check Settings or run 'claude login' in terminal.",
duration: 10000,
});
} else {
toast.error("Agent encountered an error", {
description: event.error || "Check the logs for details",
toast.error('Agent encountered an error', {
description: event.error || 'Check the logs for details',
});
}
}

View File

@@ -1,10 +1,10 @@
import { useMemo, useRef, useEffect } from "react";
import { useMemo, useRef, useEffect } from 'react';
import {
useKeyboardShortcuts,
useKeyboardShortcutsConfig,
KeyboardShortcut,
} from "@/hooks/use-keyboard-shortcuts";
import { Feature } from "@/store/app-store";
} from '@/hooks/use-keyboard-shortcuts';
import { Feature } from '@/store/app-store';
interface UseBoardKeyboardShortcutsProps {
features: Feature[];
@@ -27,7 +27,7 @@ export function useBoardKeyboardShortcuts({
const inProgressFeaturesForShortcuts = useMemo(() => {
return features.filter((f) => {
const isRunning = runningAutoTasks.includes(f.id);
return isRunning || f.status === "in_progress";
return isRunning || f.status === 'in_progress';
});
}, [features, runningAutoTasks]);
@@ -45,19 +45,19 @@ export function useBoardKeyboardShortcuts({
{
key: shortcuts.addFeature,
action: onAddFeature,
description: "Add new feature",
description: 'Add new feature',
},
{
key: shortcuts.startNext,
action: () => startNextFeaturesRef.current(),
description: "Start next features from backlog",
description: 'Start next features from backlog',
},
];
// Add shortcuts for in-progress cards (1-9 and 0 for 10th)
inProgressFeaturesForShortcuts.slice(0, 10).forEach((feature, index) => {
// Keys 1-9 for first 9 cards, 0 for 10th card
const key = index === 9 ? "0" : String(index + 1);
const key = index === 9 ? '0' : String(index + 1);
shortcutsList.push({
key,
action: () => {

View File

@@ -1,15 +1,13 @@
import { useCallback } from "react";
import { Feature } from "@/store/app-store";
import { getElectronAPI } from "@/lib/electron";
import { useAppStore } from "@/store/app-store";
import { useCallback } from 'react';
import { Feature } from '@/store/app-store';
import { getElectronAPI } from '@/lib/electron';
import { useAppStore } from '@/store/app-store';
interface UseBoardPersistenceProps {
currentProject: { path: string; id: string } | null;
}
export function useBoardPersistence({
currentProject,
}: UseBoardPersistenceProps) {
export function useBoardPersistence({ currentProject }: UseBoardPersistenceProps) {
const { updateFeature } = useAppStore();
// Persist feature update to API (replaces saveFeatures)
@@ -20,20 +18,16 @@ export function useBoardPersistence({
try {
const api = getElectronAPI();
if (!api.features) {
console.error("[BoardView] Features API not available");
console.error('[BoardView] Features API not available');
return;
}
const result = await api.features.update(
currentProject.path,
featureId,
updates
);
const result = await api.features.update(currentProject.path, featureId, updates);
if (result.success && result.feature) {
updateFeature(result.feature.id, result.feature);
}
} catch (error) {
console.error("Failed to persist feature update:", error);
console.error('Failed to persist feature update:', error);
}
},
[currentProject, updateFeature]
@@ -47,7 +41,7 @@ export function useBoardPersistence({
try {
const api = getElectronAPI();
if (!api.features) {
console.error("[BoardView] Features API not available");
console.error('[BoardView] Features API not available');
return;
}
@@ -56,7 +50,7 @@ export function useBoardPersistence({
updateFeature(result.feature.id, result.feature);
}
} catch (error) {
console.error("Failed to persist feature creation:", error);
console.error('Failed to persist feature creation:', error);
}
},
[currentProject, updateFeature]
@@ -70,13 +64,13 @@ export function useBoardPersistence({
try {
const api = getElectronAPI();
if (!api.features) {
console.error("[BoardView] Features API not available");
console.error('[BoardView] Features API not available');
return;
}
await api.features.delete(currentProject.path, featureId);
} catch (error) {
console.error("Failed to persist feature deletion:", error);
console.error('Failed to persist feature deletion:', error);
}
},
[currentProject]

View File

@@ -1,32 +1,35 @@
import { useState, useCallback } from "react";
import { Feature } from "@/store/app-store";
import { useState, useCallback } from 'react';
import { Feature } from '@/store/app-store';
import {
FeatureImagePath as DescriptionImagePath,
ImagePreviewMap,
} from "@/components/ui/description-image-dropzone";
} from '@/components/ui/description-image-dropzone';
export function useFollowUpState() {
const [showFollowUpDialog, setShowFollowUpDialog] = useState(false);
const [followUpFeature, setFollowUpFeature] = useState<Feature | null>(null);
const [followUpPrompt, setFollowUpPrompt] = useState("");
const [followUpPrompt, setFollowUpPrompt] = useState('');
const [followUpImagePaths, setFollowUpImagePaths] = useState<DescriptionImagePath[]>([]);
const [followUpPreviewMap, setFollowUpPreviewMap] = useState<ImagePreviewMap>(() => new Map());
const resetFollowUpState = useCallback(() => {
setShowFollowUpDialog(false);
setFollowUpFeature(null);
setFollowUpPrompt("");
setFollowUpPrompt('');
setFollowUpImagePaths([]);
setFollowUpPreviewMap(new Map());
}, []);
const handleFollowUpDialogChange = useCallback((open: boolean) => {
if (!open) {
resetFollowUpState();
} else {
setShowFollowUpDialog(open);
}
}, [resetFollowUpState]);
const handleFollowUpDialogChange = useCallback(
(open: boolean) => {
if (!open) {
resetFollowUpState();
} else {
setShowFollowUpDialog(open);
}
},
[resetFollowUpState]
);
return {
// State

View File

@@ -1,5 +1,5 @@
import { useState, useCallback } from "react";
import type { FeatureSuggestion } from "@/lib/electron";
import { useState, useCallback } from 'react';
import type { FeatureSuggestion } from '@/lib/electron';
export function useSuggestionsState() {
const [showSuggestionsDialog, setShowSuggestionsDialog] = useState(false);

View File

@@ -1,8 +1,8 @@
"use client";
'use client';
import { Label } from "@/components/ui/label";
import { BranchAutocomplete } from "@/components/ui/branch-autocomplete";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Label } from '@/components/ui/label';
import { BranchAutocomplete } from '@/components/ui/branch-autocomplete';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
interface BranchSelectorProps {
useCurrentBranch: boolean;
@@ -25,7 +25,7 @@ export function BranchSelector({
branchCardCounts,
currentBranch,
disabled = false,
testIdPrefix = "branch",
testIdPrefix = 'branch',
}: BranchSelectorProps) {
// Validate: if "other branch" is selected, branch name is required
const isBranchRequired = !useCurrentBranch;
@@ -35,32 +35,22 @@ export function BranchSelector({
<div className="space-y-2">
<Label id={`${testIdPrefix}-label`}>Target Branch</Label>
<RadioGroup
value={useCurrentBranch ? "current" : "other"}
onValueChange={(value: string) => onUseCurrentBranchChange(value === "current")}
value={useCurrentBranch ? 'current' : 'other'}
onValueChange={(value: string) => onUseCurrentBranchChange(value === 'current')}
disabled={disabled}
data-testid={`${testIdPrefix}-radio-group`}
aria-labelledby={`${testIdPrefix}-label`}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="current" id={`${testIdPrefix}-current`} />
<Label
htmlFor={`${testIdPrefix}-current`}
className="font-normal cursor-pointer"
>
<Label htmlFor={`${testIdPrefix}-current`} className="font-normal cursor-pointer">
Use current selected branch
{currentBranch && (
<span className="text-muted-foreground ml-1">
({currentBranch})
</span>
)}
{currentBranch && <span className="text-muted-foreground ml-1">({currentBranch})</span>}
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="other" id={`${testIdPrefix}-other`} />
<Label
htmlFor={`${testIdPrefix}-other`}
className="font-normal cursor-pointer"
>
<Label htmlFor={`${testIdPrefix}-other`} className="font-normal cursor-pointer">
Other branch
</Label>
</div>
@@ -91,11 +81,10 @@ export function BranchSelector({
) : (
<p className="text-xs text-muted-foreground">
{useCurrentBranch
? "Work will be done in the currently selected branch. A worktree will be created if needed."
: "Work will be done in this branch. A worktree will be created if needed."}
? 'Work will be done in the currently selected branch. A worktree will be created if needed.'
: 'Work will be done in this branch. A worktree will be created if needed.'}
</p>
)}
</div>
);
}

View File

@@ -1,8 +1,8 @@
export * from "./model-constants";
export * from "./model-selector";
export * from "./thinking-level-selector";
export * from "./profile-quick-select";
export * from "./testing-tab-content";
export * from "./priority-selector";
export * from "./branch-selector";
export * from "./planning-mode-selector";
export * from './model-constants';
export * from './model-selector';
export * from './thinking-level-selector';
export * from './profile-quick-select';
export * from './testing-tab-content';
export * from './priority-selector';
export * from './branch-selector';
export * from './planning-mode-selector';

View File

@@ -1,9 +1,8 @@
import { Label } from "@/components/ui/label";
import { Brain } from "lucide-react";
import { cn } from "@/lib/utils";
import { AgentModel } from "@/store/app-store";
import { CLAUDE_MODELS, ModelOption } from "./model-constants";
import { Label } from '@/components/ui/label';
import { Brain } from 'lucide-react';
import { cn } from '@/lib/utils';
import { AgentModel } from '@/store/app-store';
import { CLAUDE_MODELS, ModelOption } from './model-constants';
interface ModelSelectorProps {
selectedModel: AgentModel;
@@ -14,7 +13,7 @@ interface ModelSelectorProps {
export function ModelSelector({
selectedModel,
onModelSelect,
testIdPrefix = "model-select",
testIdPrefix = 'model-select',
}: ModelSelectorProps) {
return (
<div className="space-y-3">
@@ -30,7 +29,7 @@ export function ModelSelector({
<div className="flex gap-2 flex-wrap">
{CLAUDE_MODELS.map((option) => {
const isSelected = selectedModel === option.id;
const shortName = option.label.replace("Claude ", "");
const shortName = option.label.replace('Claude ', '');
return (
<button
key={option.id}
@@ -38,10 +37,10 @@ export function ModelSelector({
onClick={() => onModelSelect(option.id)}
title={option.description}
className={cn(
"flex-1 min-w-[80px] px-3 py-2 rounded-md border text-sm font-medium transition-colors",
'flex-1 min-w-[80px] px-3 py-2 rounded-md border text-sm font-medium transition-colors',
isSelected
? "bg-primary text-primary-foreground border-primary"
: "bg-background hover:bg-accent border-input"
? 'bg-primary text-primary-foreground border-primary'
: 'bg-background hover:bg-accent border-input'
)}
data-testid={`${testIdPrefix}-${option.id}`}
>

View File

@@ -1,20 +1,27 @@
"use client";
'use client';
import { useState } from "react";
import { useState } from 'react';
import {
Zap, ClipboardList, FileText, ScrollText,
Loader2, Check, Eye, RefreshCw, Sparkles
} from "lucide-react";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { cn } from "@/lib/utils";
import type { PlanSpec } from "@/store/app-store";
Zap,
ClipboardList,
FileText,
ScrollText,
Loader2,
Check,
Eye,
RefreshCw,
Sparkles,
} from 'lucide-react';
import { Label } from '@/components/ui/label';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { cn } from '@/lib/utils';
import type { PlanSpec } from '@/store/app-store';
export type PlanningMode = 'skip' | 'lite' | 'spec' | 'full';
// Re-export for backwards compatibility
export type { ParsedTask, PlanSpec } from "@/store/app-store";
export type { ParsedTask, PlanSpec } from '@/store/app-store';
interface PlanningModeSelectorProps {
mode: PlanningMode;
@@ -90,7 +97,7 @@ export function PlanningModeSelector({
compact = false,
}: PlanningModeSelectorProps) {
const [showPreview, setShowPreview] = useState(false);
const selectedMode = modes.find(m => m.value === mode);
const selectedMode = modes.find((m) => m.value === mode);
const requiresApproval = mode === 'spec' || mode === 'full';
const canGenerate = requiresApproval && featureDescription?.trim() && !isGenerating;
const hasSpec = planSpec && planSpec.content;
@@ -100,11 +107,13 @@ export function PlanningModeSelector({
{/* Header with icon */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className={cn(
"w-8 h-8 rounded-lg flex items-center justify-center",
selectedMode?.bgColor || "bg-muted"
)}>
{selectedMode && <selectedMode.icon className={cn("h-4 w-4", selectedMode.color)} />}
<div
className={cn(
'w-8 h-8 rounded-lg flex items-center justify-center',
selectedMode?.bgColor || 'bg-muted'
)}
>
{selectedMode && <selectedMode.icon className={cn('h-4 w-4', selectedMode.color)} />}
</div>
<div>
<Label className="text-sm font-medium">Planning Mode</Label>
@@ -117,12 +126,7 @@ export function PlanningModeSelector({
{/* Quick action buttons when spec/full mode */}
{requiresApproval && hasSpec && (
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={onViewSpec}
className="h-7 px-2"
>
<Button variant="ghost" size="sm" onClick={onViewSpec} className="h-7 px-2">
<Eye className="h-3.5 w-3.5 mr-1" />
View
</Button>
@@ -131,12 +135,7 @@ export function PlanningModeSelector({
</div>
{/* Mode Selection Cards */}
<div
className={cn(
"grid gap-2",
compact ? "grid-cols-2" : "grid-cols-2 sm:grid-cols-4"
)}
>
<div className={cn('grid gap-2', compact ? 'grid-cols-2' : 'grid-cols-2 sm:grid-cols-4')}>
{modes.map((m) => {
const isSelected = mode === m.value;
const Icon = m.icon;
@@ -147,37 +146,45 @@ export function PlanningModeSelector({
onClick={() => onModeChange(m.value)}
data-testid={`${testIdPrefix}-mode-${m.value}`}
className={cn(
"flex flex-col items-center gap-2 p-3 rounded-xl cursor-pointer transition-all duration-200",
"border-2 hover:border-primary/50",
'flex flex-col items-center gap-2 p-3 rounded-xl cursor-pointer transition-all duration-200',
'border-2 hover:border-primary/50',
isSelected
? cn("border-primary", m.bgColor)
: "border-border/50 bg-card/50 hover:bg-accent/30"
? cn('border-primary', m.bgColor)
: 'border-border/50 bg-card/50 hover:bg-accent/30'
)}
>
<div className={cn(
"w-10 h-10 rounded-full flex items-center justify-center transition-colors",
isSelected ? m.bgColor : "bg-muted"
)}>
<Icon className={cn(
"h-5 w-5 transition-colors",
isSelected ? m.color : "text-muted-foreground"
)} />
<div
className={cn(
'w-10 h-10 rounded-full flex items-center justify-center transition-colors',
isSelected ? m.bgColor : 'bg-muted'
)}
>
<Icon
className={cn(
'h-5 w-5 transition-colors',
isSelected ? m.color : 'text-muted-foreground'
)}
/>
</div>
<div className="text-center">
<div className="flex items-center justify-center gap-1">
<span className={cn(
"font-medium text-sm",
isSelected ? "text-foreground" : "text-muted-foreground"
)}>
<span
className={cn(
'font-medium text-sm',
isSelected ? 'text-foreground' : 'text-muted-foreground'
)}
>
{m.label}
</span>
{m.badge && (
<span className={cn(
"text-[9px] px-1 py-0.5 rounded font-medium",
m.badge === 'Default'
? "bg-emerald-500/15 text-emerald-500"
: "bg-amber-500/15 text-amber-500"
)}>
<span
className={cn(
'text-[9px] px-1 py-0.5 rounded font-medium',
m.badge === 'Default'
? 'bg-emerald-500/15 text-emerald-500'
: 'bg-amber-500/15 text-amber-500'
)}
>
{m.badge === 'Default' ? 'Default' : 'Review'}
</span>
)}
@@ -213,14 +220,16 @@ export function PlanningModeSelector({
{/* Spec Preview/Actions Panel - Only for spec/full modes */}
{requiresApproval && (
<div className={cn(
"rounded-xl border transition-all duration-300",
planSpec?.status === 'approved'
? "border-emerald-500/30 bg-emerald-500/5"
: planSpec?.status === 'generated'
? "border-amber-500/30 bg-amber-500/5"
: "border-border/50 bg-muted/30"
)}>
<div
className={cn(
'rounded-xl border transition-all duration-300',
planSpec?.status === 'approved'
? 'border-emerald-500/30 bg-emerald-500/5'
: planSpec?.status === 'generated'
? 'border-amber-500/30 bg-amber-500/5'
: 'border-border/50 bg-muted/30'
)}
>
<div className="p-4 space-y-3">
{/* Status indicator */}
<div className="flex items-center justify-between">
@@ -228,7 +237,9 @@ export function PlanningModeSelector({
{isGenerating ? (
<>
<Loader2 className="h-4 w-4 animate-spin text-primary" />
<span className="text-sm text-muted-foreground">Generating {mode === 'full' ? 'comprehensive spec' : 'spec'}...</span>
<span className="text-sm text-muted-foreground">
Generating {mode === 'full' ? 'comprehensive spec' : 'spec'}...
</span>
</>
) : planSpec?.status === 'approved' ? (
<>
@@ -238,7 +249,9 @@ export function PlanningModeSelector({
) : planSpec?.status === 'generated' ? (
<>
<Eye className="h-4 w-4 text-amber-500" />
<span className="text-sm text-amber-500 font-medium">Spec Ready for Review</span>
<span className="text-sm text-amber-500 font-medium">
Spec Ready for Review
</span>
</>
) : (
<>
@@ -293,12 +306,7 @@ export function PlanningModeSelector({
{/* Action buttons when spec is generated */}
{planSpec?.status === 'generated' && (
<div className="flex items-center gap-2 pt-2 border-t border-border/30">
<Button
variant="outline"
size="sm"
onClick={onRejectSpec}
className="flex-1"
>
<Button variant="outline" size="sm" onClick={onRejectSpec} className="flex-1">
Request Changes
</Button>
<Button
@@ -315,12 +323,7 @@ export function PlanningModeSelector({
{/* Regenerate option when approved */}
{planSpec?.status === 'approved' && onGenerateSpec && (
<div className="flex items-center justify-end pt-2 border-t border-border/30">
<Button
variant="ghost"
size="sm"
onClick={onGenerateSpec}
className="h-7"
>
<Button variant="ghost" size="sm" onClick={onGenerateSpec} className="h-7">
<RefreshCw className="h-3.5 w-3.5 mr-1" />
Regenerate
</Button>
@@ -334,7 +337,7 @@ export function PlanningModeSelector({
{!requiresApproval && (
<p className="text-xs text-muted-foreground bg-muted/30 rounded-lg p-3">
{mode === 'skip'
? "The agent will start implementing immediately without creating a plan or spec."
? 'The agent will start implementing immediately without creating a plan or spec.'
: "The agent will create a planning outline before implementing, but won't wait for approval."}
</p>
)}

View File

@@ -1,6 +1,5 @@
import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils";
import { Label } from '@/components/ui/label';
import { cn } from '@/lib/utils';
interface PrioritySelectorProps {
selectedPriority: number;
@@ -11,7 +10,7 @@ interface PrioritySelectorProps {
export function PrioritySelector({
selectedPriority,
onPrioritySelect,
testIdPrefix = "priority",
testIdPrefix = 'priority',
}: PrioritySelectorProps) {
return (
<div className="space-y-2">
@@ -21,10 +20,10 @@ export function PrioritySelector({
type="button"
onClick={() => onPrioritySelect(1)}
className={cn(
"flex-1 px-3 py-2 rounded-md text-sm font-medium transition-colors",
'flex-1 px-3 py-2 rounded-md text-sm font-medium transition-colors',
selectedPriority === 1
? "bg-red-500/20 text-red-500 border-2 border-red-500/50"
: "bg-muted/50 text-muted-foreground border border-border hover:bg-muted"
? 'bg-red-500/20 text-red-500 border-2 border-red-500/50'
: 'bg-muted/50 text-muted-foreground border border-border hover:bg-muted'
)}
data-testid={`${testIdPrefix}-high-button`}
>
@@ -34,10 +33,10 @@ export function PrioritySelector({
type="button"
onClick={() => onPrioritySelect(2)}
className={cn(
"flex-1 px-3 py-2 rounded-md text-sm font-medium transition-colors",
'flex-1 px-3 py-2 rounded-md text-sm font-medium transition-colors',
selectedPriority === 2
? "bg-yellow-500/20 text-yellow-500 border-2 border-yellow-500/50"
: "bg-muted/50 text-muted-foreground border border-border hover:bg-muted"
? 'bg-yellow-500/20 text-yellow-500 border-2 border-yellow-500/50'
: 'bg-muted/50 text-muted-foreground border border-border hover:bg-muted'
)}
data-testid={`${testIdPrefix}-medium-button`}
>
@@ -47,10 +46,10 @@ export function PrioritySelector({
type="button"
onClick={() => onPrioritySelect(3)}
className={cn(
"flex-1 px-3 py-2 rounded-md text-sm font-medium transition-colors",
'flex-1 px-3 py-2 rounded-md text-sm font-medium transition-colors',
selectedPriority === 3
? "bg-blue-500/20 text-blue-500 border-2 border-blue-500/50"
: "bg-muted/50 text-muted-foreground border border-border hover:bg-muted"
? 'bg-blue-500/20 text-blue-500 border-2 border-blue-500/50'
: 'bg-muted/50 text-muted-foreground border border-border hover:bg-muted'
)}
data-testid={`${testIdPrefix}-low-button`}
>

View File

@@ -1,9 +1,8 @@
import { Label } from "@/components/ui/label";
import { Brain, UserCircle } from "lucide-react";
import { cn } from "@/lib/utils";
import { AgentModel, ThinkingLevel, AIProfile } from "@/store/app-store";
import { PROFILE_ICONS } from "./model-constants";
import { Label } from '@/components/ui/label';
import { Brain, UserCircle } from 'lucide-react';
import { cn } from '@/lib/utils';
import { AgentModel, ThinkingLevel, AIProfile } from '@/store/app-store';
import { PROFILE_ICONS } from './model-constants';
interface ProfileQuickSelectProps {
profiles: AIProfile[];
@@ -20,7 +19,7 @@ export function ProfileQuickSelect({
selectedModel,
selectedThinkingLevel,
onSelect,
testIdPrefix = "profile-quick-select",
testIdPrefix = 'profile-quick-select',
showManageLink = false,
onManageLinkClick,
}: ProfileQuickSelectProps) {
@@ -41,36 +40,30 @@ export function ProfileQuickSelect({
</div>
<div className="grid grid-cols-2 gap-2">
{profiles.slice(0, 6).map((profile) => {
const IconComponent = profile.icon
? PROFILE_ICONS[profile.icon]
: Brain;
const IconComponent = profile.icon ? PROFILE_ICONS[profile.icon] : Brain;
const isSelected =
selectedModel === profile.model &&
selectedThinkingLevel === profile.thinkingLevel;
selectedModel === profile.model && selectedThinkingLevel === profile.thinkingLevel;
return (
<button
key={profile.id}
type="button"
onClick={() => onSelect(profile.model, profile.thinkingLevel)}
className={cn(
"flex items-center gap-2 p-2 rounded-lg border text-left transition-all",
'flex items-center gap-2 p-2 rounded-lg border text-left transition-all',
isSelected
? "bg-brand-500/10 border-brand-500 text-foreground"
: "bg-background hover:bg-accent border-input"
? 'bg-brand-500/10 border-brand-500 text-foreground'
: 'bg-background hover:bg-accent border-input'
)}
data-testid={`${testIdPrefix}-${profile.id}`}
>
<div className="w-7 h-7 rounded flex items-center justify-center shrink-0 bg-primary/10">
{IconComponent && (
<IconComponent className="w-4 h-4 text-primary" />
)}
{IconComponent && <IconComponent className="w-4 h-4 text-primary" />}
</div>
<div className="min-w-0 flex-1">
<p className="text-sm font-medium truncate">{profile.name}</p>
<p className="text-[10px] text-muted-foreground truncate">
{profile.model}
{profile.thinkingLevel !== "none" &&
` + ${profile.thinkingLevel}`}
{profile.thinkingLevel !== 'none' && ` + ${profile.thinkingLevel}`}
</p>
</div>
</button>
@@ -81,8 +74,8 @@ export function ProfileQuickSelect({
Or customize below.
{showManageLink && onManageLinkClick && (
<>
{" "}
Manage profiles in{" "}
{' '}
Manage profiles in{' '}
<button
type="button"
onClick={onManageLinkClick}

View File

@@ -1,9 +1,8 @@
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import { FlaskConical, Plus } from "lucide-react";
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Checkbox } from '@/components/ui/checkbox';
import { FlaskConical, Plus } from 'lucide-react';
interface TestingTabContentProps {
skipTests: boolean;
@@ -18,9 +17,9 @@ export function TestingTabContent({
onSkipTestsChange,
steps,
onStepsChange,
testIdPrefix = "",
testIdPrefix = '',
}: TestingTabContentProps) {
const checkboxId = testIdPrefix ? `${testIdPrefix}-skip-tests` : "skip-tests";
const checkboxId = testIdPrefix ? `${testIdPrefix}-skip-tests` : 'skip-tests';
const handleStepChange = (index: number, value: string) => {
const newSteps = [...steps];
@@ -29,7 +28,7 @@ export function TestingTabContent({
};
const handleAddStep = () => {
onStepsChange([...steps, ""]);
onStepsChange([...steps, '']);
};
return (
@@ -39,7 +38,7 @@ export function TestingTabContent({
id={checkboxId}
checked={!skipTests}
onCheckedChange={(checked) => onSkipTestsChange(checked !== true)}
data-testid={`${testIdPrefix ? testIdPrefix + "-" : ""}skip-tests-checkbox`}
data-testid={`${testIdPrefix ? testIdPrefix + '-' : ''}skip-tests-checkbox`}
/>
<div className="flex items-center gap-2">
<Label htmlFor={checkboxId} className="text-sm cursor-pointer">
@@ -49,8 +48,8 @@ export function TestingTabContent({
</div>
</div>
<p className="text-xs text-muted-foreground">
When enabled, this feature will use automated TDD. When disabled, it
will require manual verification.
When enabled, this feature will use automated TDD. When disabled, it will require manual
verification.
</p>
{/* Verification Steps - Only shown when skipTests is enabled */}
@@ -66,14 +65,14 @@ export function TestingTabContent({
value={step}
placeholder={`Verification step ${index + 1}`}
onChange={(e) => handleStepChange(index, e.target.value)}
data-testid={`${testIdPrefix ? testIdPrefix + "-" : ""}feature-step-${index}${testIdPrefix ? "" : "-input"}`}
data-testid={`${testIdPrefix ? testIdPrefix + '-' : ''}feature-step-${index}${testIdPrefix ? '' : '-input'}`}
/>
))}
<Button
variant="outline"
size="sm"
onClick={handleAddStep}
data-testid={`${testIdPrefix ? testIdPrefix + "-" : ""}add-step-button`}
data-testid={`${testIdPrefix ? testIdPrefix + '-' : ''}add-step-button`}
>
<Plus className="w-4 h-4 mr-2" />
Add Verification Step

View File

@@ -1,9 +1,8 @@
import { Label } from "@/components/ui/label";
import { Brain } from "lucide-react";
import { cn } from "@/lib/utils";
import { ThinkingLevel } from "@/store/app-store";
import { THINKING_LEVELS, THINKING_LEVEL_LABELS } from "./model-constants";
import { Label } from '@/components/ui/label';
import { Brain } from 'lucide-react';
import { cn } from '@/lib/utils';
import { ThinkingLevel } from '@/store/app-store';
import { THINKING_LEVELS, THINKING_LEVEL_LABELS } from './model-constants';
interface ThinkingLevelSelectorProps {
selectedLevel: ThinkingLevel;
@@ -14,7 +13,7 @@ interface ThinkingLevelSelectorProps {
export function ThinkingLevelSelector({
selectedLevel,
onLevelSelect,
testIdPrefix = "thinking-level",
testIdPrefix = 'thinking-level',
}: ThinkingLevelSelectorProps) {
return (
<div className="space-y-2 pt-2 border-t border-border">
@@ -29,10 +28,10 @@ export function ThinkingLevelSelector({
type="button"
onClick={() => onLevelSelect(level)}
className={cn(
"flex-1 px-3 py-2 rounded-md border text-sm font-medium transition-colors min-w-[60px]",
'flex-1 px-3 py-2 rounded-md border text-sm font-medium transition-colors min-w-[60px]',
selectedLevel === level
? "bg-primary text-primary-foreground border-primary"
: "bg-background hover:bg-accent border-input"
? 'bg-primary text-primary-foreground border-primary'
: 'bg-background hover:bg-accent border-input'
)}
data-testid={`${testIdPrefix}-${level}`}
>

View File

@@ -1,6 +1,5 @@
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
DropdownMenu,
DropdownMenuContent,
@@ -8,16 +7,10 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
DropdownMenuLabel,
} from "@/components/ui/dropdown-menu";
import {
GitBranch,
RefreshCw,
GitBranchPlus,
Check,
Search,
} from "lucide-react";
import { cn } from "@/lib/utils";
import type { WorktreeInfo, BranchInfo } from "../types";
} from '@/components/ui/dropdown-menu';
import { GitBranch, RefreshCw, GitBranchPlus, Check, Search } from 'lucide-react';
import { cn } from '@/lib/utils';
import type { WorktreeInfo, BranchInfo } from '../types';
interface BranchSwitchDropdownProps {
worktree: WorktreeInfo;
@@ -49,12 +42,12 @@ export function BranchSwitchDropdown({
<DropdownMenu onOpenChange={onOpenChange}>
<DropdownMenuTrigger asChild>
<Button
variant={isSelected ? "default" : "outline"}
variant={isSelected ? 'default' : 'outline'}
size="sm"
className={cn(
"h-7 w-7 p-0 rounded-none border-r-0",
isSelected && "bg-primary text-primary-foreground",
!isSelected && "bg-secondary/50 hover:bg-secondary"
'h-7 w-7 p-0 rounded-none border-r-0',
isSelected && 'bg-primary text-primary-foreground',
!isSelected && 'bg-secondary/50 hover:bg-secondary'
)}
title="Switch branch"
>
@@ -88,7 +81,7 @@ export function BranchSwitchDropdown({
</DropdownMenuItem>
) : filteredBranches.length === 0 ? (
<DropdownMenuItem disabled className="text-xs">
{branchFilter ? "No matching branches" : "No branches found"}
{branchFilter ? 'No matching branches' : 'No branches found'}
</DropdownMenuItem>
) : (
filteredBranches.map((branch) => (
@@ -109,10 +102,7 @@ export function BranchSwitchDropdown({
)}
</div>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => onCreateBranch(worktree)}
className="text-xs"
>
<DropdownMenuItem onClick={() => onCreateBranch(worktree)} className="text-xs">
<GitBranchPlus className="w-3.5 h-3.5 mr-2" />
Create New Branch...
</DropdownMenuItem>

View File

@@ -1,3 +1,3 @@
export { BranchSwitchDropdown } from "./branch-switch-dropdown";
export { WorktreeActionsDropdown } from "./worktree-actions-dropdown";
export { WorktreeTab } from "./worktree-tab";
export { BranchSwitchDropdown } from './branch-switch-dropdown';
export { WorktreeActionsDropdown } from './worktree-actions-dropdown';
export { WorktreeTab } from './worktree-tab';

View File

@@ -1,5 +1,4 @@
import { Button } from "@/components/ui/button";
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
@@ -7,7 +6,7 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
DropdownMenuLabel,
} from "@/components/ui/dropdown-menu";
} from '@/components/ui/dropdown-menu';
import {
Trash2,
MoreHorizontal,
@@ -21,9 +20,9 @@ import {
Globe,
MessageSquare,
GitMerge,
} from "lucide-react";
import { cn } from "@/lib/utils";
import type { WorktreeInfo, DevServerInfo, PRInfo } from "../types";
} from 'lucide-react';
import { cn } from '@/lib/utils';
import type { WorktreeInfo, DevServerInfo, PRInfo } from '../types';
interface WorktreeActionsDropdownProps {
worktree: WorktreeInfo;
@@ -81,12 +80,12 @@ export function WorktreeActionsDropdown({
<DropdownMenu onOpenChange={onOpenChange}>
<DropdownMenuTrigger asChild>
<Button
variant={isSelected ? "default" : "outline"}
variant={isSelected ? 'default' : 'outline'}
size="sm"
className={cn(
"h-7 w-7 p-0 rounded-l-none",
isSelected && "bg-primary text-primary-foreground",
!isSelected && "bg-secondary/50 hover:bg-secondary"
'h-7 w-7 p-0 rounded-l-none',
isSelected && 'bg-primary text-primary-foreground',
!isSelected && 'bg-secondary/50 hover:bg-secondary'
)}
>
<MoreHorizontal className="w-3 h-3" />
@@ -99,10 +98,7 @@ export function WorktreeActionsDropdown({
<span className="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
Dev Server Running (:{devServerInfo?.port})
</DropdownMenuLabel>
<DropdownMenuItem
onClick={() => onOpenDevServerUrl(worktree)}
className="text-xs"
>
<DropdownMenuItem onClick={() => onOpenDevServerUrl(worktree)} className="text-xs">
<Globe className="w-3.5 h-3.5 mr-2" />
Open in Browser
</DropdownMenuItem>
@@ -122,26 +118,15 @@ export function WorktreeActionsDropdown({
disabled={isStartingDevServer}
className="text-xs"
>
<Play
className={cn(
"w-3.5 h-3.5 mr-2",
isStartingDevServer && "animate-pulse"
)}
/>
{isStartingDevServer ? "Starting..." : "Start Dev Server"}
<Play className={cn('w-3.5 h-3.5 mr-2', isStartingDevServer && 'animate-pulse')} />
{isStartingDevServer ? 'Starting...' : 'Start Dev Server'}
</DropdownMenuItem>
<DropdownMenuSeparator />
</>
)}
<DropdownMenuItem
onClick={() => onPull(worktree)}
disabled={isPulling}
className="text-xs"
>
<Download
className={cn("w-3.5 h-3.5 mr-2", isPulling && "animate-pulse")}
/>
{isPulling ? "Pulling..." : "Pull"}
<DropdownMenuItem onClick={() => onPull(worktree)} disabled={isPulling} className="text-xs">
<Download className={cn('w-3.5 h-3.5 mr-2', isPulling && 'animate-pulse')} />
{isPulling ? 'Pulling...' : 'Pull'}
{behindCount > 0 && (
<span className="ml-auto text-[10px] bg-muted px-1.5 py-0.5 rounded">
{behindCount} behind
@@ -153,10 +138,8 @@ export function WorktreeActionsDropdown({
disabled={isPushing || aheadCount === 0}
className="text-xs"
>
<Upload
className={cn("w-3.5 h-3.5 mr-2", isPushing && "animate-pulse")}
/>
{isPushing ? "Pushing..." : "Push"}
<Upload className={cn('w-3.5 h-3.5 mr-2', isPushing && 'animate-pulse')} />
{isPushing ? 'Pushing...' : 'Push'}
{aheadCount > 0 && (
<span className="ml-auto text-[10px] bg-primary/20 text-primary px-1.5 py-0.5 rounded">
{aheadCount} ahead
@@ -173,10 +156,7 @@ export function WorktreeActionsDropdown({
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => onOpenInEditor(worktree)}
className="text-xs"
>
<DropdownMenuItem onClick={() => onOpenInEditor(worktree)} className="text-xs">
<ExternalLink className="w-3.5 h-3.5 mr-2" />
Open in {defaultEditorName}
</DropdownMenuItem>
@@ -199,7 +179,7 @@ export function WorktreeActionsDropdown({
<>
<DropdownMenuItem
onClick={() => {
window.open(worktree.pr!.url, "_blank");
window.open(worktree.pr!.url, '_blank');
}}
className="text-xs"
>
@@ -218,8 +198,8 @@ export function WorktreeActionsDropdown({
title: worktree.pr!.title,
url: worktree.pr!.url,
state: worktree.pr!.state,
author: "", // Will be fetched
body: "", // Will be fetched
author: '', // Will be fetched
body: '', // Will be fetched
comments: [],
reviewComments: [],
};

View File

@@ -1,6 +1,6 @@
export { useWorktrees } from "./use-worktrees";
export { useDevServers } from "./use-dev-servers";
export { useBranches } from "./use-branches";
export { useWorktreeActions } from "./use-worktree-actions";
export { useDefaultEditor } from "./use-default-editor";
export { useRunningFeatures } from "./use-running-features";
export { useWorktrees } from './use-worktrees';
export { useDevServers } from './use-dev-servers';
export { useBranches } from './use-branches';
export { useWorktreeActions } from './use-worktree-actions';
export { useDefaultEditor } from './use-default-editor';
export { useRunningFeatures } from './use-running-features';

View File

@@ -1,21 +1,20 @@
import { useState, useCallback } from "react";
import { getElectronAPI } from "@/lib/electron";
import type { BranchInfo } from "../types";
import { useState, useCallback } from 'react';
import { getElectronAPI } from '@/lib/electron';
import type { BranchInfo } from '../types';
export function useBranches() {
const [branches, setBranches] = useState<BranchInfo[]>([]);
const [aheadCount, setAheadCount] = useState(0);
const [behindCount, setBehindCount] = useState(0);
const [isLoadingBranches, setIsLoadingBranches] = useState(false);
const [branchFilter, setBranchFilter] = useState("");
const [branchFilter, setBranchFilter] = useState('');
const fetchBranches = useCallback(async (worktreePath: string) => {
setIsLoadingBranches(true);
try {
const api = getElectronAPI();
if (!api?.worktree?.listBranches) {
console.warn("List branches API not available");
console.warn('List branches API not available');
return;
}
const result = await api.worktree.listBranches(worktreePath);
@@ -25,14 +24,14 @@ export function useBranches() {
setBehindCount(result.result.behindCount || 0);
}
} catch (error) {
console.error("Failed to fetch branches:", error);
console.error('Failed to fetch branches:', error);
} finally {
setIsLoadingBranches(false);
}
}, []);
const resetBranchFilter = useCallback(() => {
setBranchFilter("");
setBranchFilter('');
}, []);
const filteredBranches = branches.filter((b) =>

View File

@@ -1,9 +1,8 @@
import { useState, useEffect, useCallback } from "react";
import { getElectronAPI } from "@/lib/electron";
import { useState, useEffect, useCallback } from 'react';
import { getElectronAPI } from '@/lib/electron';
export function useDefaultEditor() {
const [defaultEditorName, setDefaultEditorName] = useState<string>("Editor");
const [defaultEditorName, setDefaultEditorName] = useState<string>('Editor');
const fetchDefaultEditor = useCallback(async () => {
try {
@@ -16,7 +15,7 @@ export function useDefaultEditor() {
setDefaultEditorName(result.result.editorName);
}
} catch (error) {
console.error("Failed to fetch default editor:", error);
console.error('Failed to fetch default editor:', error);
}
}, []);

View File

@@ -1,9 +1,8 @@
import { useState, useEffect, useCallback } from "react";
import { getElectronAPI } from "@/lib/electron";
import { normalizePath } from "@/lib/utils";
import { toast } from "sonner";
import type { DevServerInfo, WorktreeInfo } from "../types";
import { useState, useEffect, useCallback } from 'react';
import { getElectronAPI } from '@/lib/electron';
import { normalizePath } from '@/lib/utils';
import { toast } from 'sonner';
import type { DevServerInfo, WorktreeInfo } from '../types';
interface UseDevServersOptions {
projectPath: string;
@@ -11,9 +10,7 @@ interface UseDevServersOptions {
export function useDevServers({ projectPath }: UseDevServersOptions) {
const [isStartingDevServer, setIsStartingDevServer] = useState(false);
const [runningDevServers, setRunningDevServers] = useState<Map<string, DevServerInfo>>(
new Map()
);
const [runningDevServers, setRunningDevServers] = useState<Map<string, DevServerInfo>>(new Map());
const fetchDevServers = useCallback(async () => {
try {
@@ -30,7 +27,7 @@ export function useDevServers({ projectPath }: UseDevServersOptions) {
setRunningDevServers(serversMap);
}
} catch (error) {
console.error("Failed to fetch dev servers:", error);
console.error('Failed to fetch dev servers:', error);
}
}, []);
@@ -54,7 +51,7 @@ export function useDevServers({ projectPath }: UseDevServersOptions) {
try {
const api = getElectronAPI();
if (!api?.worktree?.startDevServer) {
toast.error("Start dev server API not available");
toast.error('Start dev server API not available');
return;
}
@@ -73,11 +70,11 @@ export function useDevServers({ projectPath }: UseDevServersOptions) {
});
toast.success(`Dev server started on port ${result.result.port}`);
} else {
toast.error(result.error || "Failed to start dev server");
toast.error(result.error || 'Failed to start dev server');
}
} catch (error) {
console.error("Start dev server failed:", error);
toast.error("Failed to start dev server");
console.error('Start dev server failed:', error);
toast.error('Failed to start dev server');
} finally {
setIsStartingDevServer(false);
}
@@ -90,7 +87,7 @@ export function useDevServers({ projectPath }: UseDevServersOptions) {
try {
const api = getElectronAPI();
if (!api?.worktree?.stopDevServer) {
toast.error("Stop dev server API not available");
toast.error('Stop dev server API not available');
return;
}
@@ -103,13 +100,13 @@ export function useDevServers({ projectPath }: UseDevServersOptions) {
next.delete(normalizePath(targetPath));
return next;
});
toast.success(result.result?.message || "Dev server stopped");
toast.success(result.result?.message || 'Dev server stopped');
} else {
toast.error(result.error || "Failed to stop dev server");
toast.error(result.error || 'Failed to stop dev server');
}
} catch (error) {
console.error("Stop dev server failed:", error);
toast.error("Failed to stop dev server");
console.error('Stop dev server failed:', error);
toast.error('Failed to stop dev server');
}
},
[projectPath]
@@ -120,7 +117,7 @@ export function useDevServers({ projectPath }: UseDevServersOptions) {
const targetPath = worktree.isMain ? projectPath : worktree.path;
const serverInfo = runningDevServers.get(targetPath);
if (serverInfo) {
window.open(serverInfo.url, "_blank");
window.open(serverInfo.url, '_blank');
}
},
[projectPath, runningDevServers]

View File

@@ -1,16 +1,12 @@
import { useCallback } from "react";
import type { WorktreeInfo, FeatureInfo } from "../types";
import { useCallback } from 'react';
import type { WorktreeInfo, FeatureInfo } from '../types';
interface UseRunningFeaturesOptions {
runningFeatureIds: string[];
features: FeatureInfo[];
}
export function useRunningFeatures({
runningFeatureIds,
features,
}: UseRunningFeaturesOptions) {
export function useRunningFeatures({ runningFeatureIds, features }: UseRunningFeaturesOptions) {
const hasRunningFeatures = useCallback(
(worktree: WorktreeInfo) => {
if (runningFeatureIds.length === 0) return false;

View File

@@ -1,18 +1,14 @@
import { useState, useCallback } from "react";
import { getElectronAPI } from "@/lib/electron";
import { toast } from "sonner";
import type { WorktreeInfo } from "../types";
import { useState, useCallback } from 'react';
import { getElectronAPI } from '@/lib/electron';
import { toast } from 'sonner';
import type { WorktreeInfo } from '../types';
interface UseWorktreeActionsOptions {
fetchWorktrees: () => Promise<Array<{ path: string; branch: string }> | undefined>;
fetchBranches: (worktreePath: string) => Promise<void>;
}
export function useWorktreeActions({
fetchWorktrees,
fetchBranches,
}: UseWorktreeActionsOptions) {
export function useWorktreeActions({ fetchWorktrees, fetchBranches }: UseWorktreeActionsOptions) {
const [isPulling, setIsPulling] = useState(false);
const [isPushing, setIsPushing] = useState(false);
const [isSwitching, setIsSwitching] = useState(false);
@@ -25,7 +21,7 @@ export function useWorktreeActions({
try {
const api = getElectronAPI();
if (!api?.worktree?.switchBranch) {
toast.error("Switch branch API not available");
toast.error('Switch branch API not available');
return;
}
const result = await api.worktree.switchBranch(worktree.path, branchName);
@@ -33,11 +29,11 @@ export function useWorktreeActions({
toast.success(result.result.message);
fetchWorktrees();
} else {
toast.error(result.error || "Failed to switch branch");
toast.error(result.error || 'Failed to switch branch');
}
} catch (error) {
console.error("Switch branch failed:", error);
toast.error("Failed to switch branch");
console.error('Switch branch failed:', error);
toast.error('Failed to switch branch');
} finally {
setIsSwitching(false);
}
@@ -52,7 +48,7 @@ export function useWorktreeActions({
try {
const api = getElectronAPI();
if (!api?.worktree?.pull) {
toast.error("Pull API not available");
toast.error('Pull API not available');
return;
}
const result = await api.worktree.pull(worktree.path);
@@ -60,11 +56,11 @@ export function useWorktreeActions({
toast.success(result.result.message);
fetchWorktrees();
} else {
toast.error(result.error || "Failed to pull latest changes");
toast.error(result.error || 'Failed to pull latest changes');
}
} catch (error) {
console.error("Pull failed:", error);
toast.error("Failed to pull latest changes");
console.error('Pull failed:', error);
toast.error('Failed to pull latest changes');
} finally {
setIsPulling(false);
}
@@ -79,7 +75,7 @@ export function useWorktreeActions({
try {
const api = getElectronAPI();
if (!api?.worktree?.push) {
toast.error("Push API not available");
toast.error('Push API not available');
return;
}
const result = await api.worktree.push(worktree.path);
@@ -88,11 +84,11 @@ export function useWorktreeActions({
fetchBranches(worktree.path);
fetchWorktrees();
} else {
toast.error(result.error || "Failed to push changes");
toast.error(result.error || 'Failed to push changes');
}
} catch (error) {
console.error("Push failed:", error);
toast.error("Failed to push changes");
console.error('Push failed:', error);
toast.error('Failed to push changes');
} finally {
setIsPushing(false);
}
@@ -104,7 +100,7 @@ export function useWorktreeActions({
try {
const api = getElectronAPI();
if (!api?.worktree?.openInEditor) {
console.warn("Open in editor API not available");
console.warn('Open in editor API not available');
return;
}
const result = await api.worktree.openInEditor(worktree.path);
@@ -114,7 +110,7 @@ export function useWorktreeActions({
toast.error(result.error);
}
} catch (error) {
console.error("Open in editor failed:", error);
console.error('Open in editor failed:', error);
}
}, []);

View File

@@ -1,8 +1,8 @@
export { WorktreePanel } from "./worktree-panel";
export { WorktreePanel } from './worktree-panel';
export type {
WorktreeInfo,
BranchInfo,
DevServerInfo,
FeatureInfo,
WorktreePanelProps,
} from "./types";
} from './types';

View File

@@ -1,8 +1,7 @@
import { useState } from "react";
import { useAppStore } from "@/store/app-store";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useState } from 'react';
import { useAppStore } from '@/store/app-store';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
Plus,
MessageSquare,
@@ -12,16 +11,16 @@ import {
Search,
ChevronLeft,
ArchiveRestore,
} from "lucide-react";
import { cn } from "@/lib/utils";
} from 'lucide-react';
import { cn } from '@/lib/utils';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuSeparator,
} from "@/components/ui/dropdown-menu";
import { Badge } from "@/components/ui/badge";
} from '@/components/ui/dropdown-menu';
import { Badge } from '@/components/ui/badge';
export function ChatHistory() {
const {
@@ -37,7 +36,7 @@ export function ChatHistory() {
setChatHistoryOpen,
} = useAppStore();
const [searchQuery, setSearchQuery] = useState("");
const [searchQuery, setSearchQuery] = useState('');
const [showArchived, setShowArchived] = useState(false);
if (!currentProject) {
@@ -45,18 +44,12 @@ export function ChatHistory() {
}
// Filter sessions for current project
const projectSessions = chatSessions.filter(
(session) => session.projectId === currentProject.id
);
const projectSessions = chatSessions.filter((session) => session.projectId === currentProject.id);
// Filter by search query and archived status
const filteredSessions = projectSessions.filter((session) => {
const matchesSearch = session.title
.toLowerCase()
.includes(searchQuery.toLowerCase());
const matchesArchivedStatus = showArchived
? session.archived
: !session.archived;
const matchesSearch = session.title.toLowerCase().includes(searchQuery.toLowerCase());
const matchesArchivedStatus = showArchived ? session.archived : !session.archived;
return matchesSearch && matchesArchivedStatus;
});
@@ -85,7 +78,7 @@ export function ChatHistory() {
const handleDeleteSession = (sessionId: string, e: React.MouseEvent) => {
e.stopPropagation();
if (confirm("Are you sure you want to delete this chat session?")) {
if (confirm('Are you sure you want to delete this chat session?')) {
deleteChatSession(sessionId);
}
};
@@ -93,8 +86,8 @@ export function ChatHistory() {
return (
<div
className={cn(
"flex flex-col h-full bg-zinc-950/50 backdrop-blur-md border-r border-white/10 transition-all duration-200",
chatHistoryOpen ? "w-80" : "w-0 overflow-hidden"
'flex flex-col h-full bg-zinc-950/50 backdrop-blur-md border-r border-white/10 transition-all duration-200',
chatHistoryOpen ? 'w-80' : 'w-0 overflow-hidden'
)}
>
{chatHistoryOpen && (
@@ -105,11 +98,7 @@ export function ChatHistory() {
<MessageSquare className="w-5 h-5" />
<h2 className="font-semibold">Chat History</h2>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => setChatHistoryOpen(false)}
>
<Button variant="ghost" size="sm" onClick={() => setChatHistoryOpen(false)}>
<ChevronLeft className="w-4 h-4" />
</Button>
</div>
@@ -152,7 +141,7 @@ export function ChatHistory() {
) : (
<Archive className="w-4 h-4" />
)}
{showArchived ? "Show Active" : "Show Archived"}
{showArchived ? 'Show Active' : 'Show Archived'}
{showArchived && (
<Badge variant="outline" className="ml-auto">
{projectSessions.filter((s) => s.archived).length}
@@ -179,15 +168,13 @@ export function ChatHistory() {
<div
key={session.id}
className={cn(
"flex items-center gap-2 p-3 rounded-lg cursor-pointer hover:bg-accent transition-colors group",
currentChatSession?.id === session.id && "bg-accent"
'flex items-center gap-2 p-3 rounded-lg cursor-pointer hover:bg-accent transition-colors group',
currentChatSession?.id === session.id && 'bg-accent'
)}
onClick={() => handleSelectSession(session)}
>
<div className="flex-1 min-w-0">
<h3 className="font-medium text-sm truncate">
{session.title}
</h3>
<h3 className="font-medium text-sm truncate">{session.title}</h3>
<p className="text-xs text-muted-foreground truncate">
{session.messages.length} messages
</p>
@@ -199,30 +186,20 @@ export function ChatHistory() {
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0"
>
<Button variant="ghost" size="sm" className="h-6 w-6 p-0">
<MoreVertical className="w-3 h-3" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{session.archived ? (
<DropdownMenuItem
onClick={(e) =>
handleUnarchiveSession(session.id, e)
}
onClick={(e) => handleUnarchiveSession(session.id, e)}
>
<ArchiveRestore className="w-4 h-4 mr-2" />
Unarchive
</DropdownMenuItem>
) : (
<DropdownMenuItem
onClick={(e) =>
handleArchiveSession(session.id, e)
}
>
<DropdownMenuItem onClick={(e) => handleArchiveSession(session.id, e)}>
<Archive className="w-4 h-4 mr-2" />
Archive
</DropdownMenuItem>

View File

@@ -1,19 +1,10 @@
import { useEffect, useState, useCallback } from "react";
import { useAppStore } from "@/store/app-store";
import { getElectronAPI } from "@/lib/electron";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import {
File,
Folder,
FolderOpen,
ChevronRight,
ChevronDown,
RefreshCw,
Code,
} from "lucide-react";
import { cn } from "@/lib/utils";
import { useEffect, useState, useCallback } from 'react';
import { useAppStore } from '@/store/app-store';
import { getElectronAPI } from '@/lib/electron';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { File, Folder, FolderOpen, ChevronRight, ChevronDown, RefreshCw, Code } from 'lucide-react';
import { cn } from '@/lib/utils';
interface FileTreeNode {
name: string;
@@ -23,19 +14,11 @@ interface FileTreeNode {
isExpanded?: boolean;
}
const IGNORE_PATTERNS = [
"node_modules",
".git",
".next",
"dist",
"build",
".DS_Store",
"*.log",
];
const IGNORE_PATTERNS = ['node_modules', '.git', '.next', 'dist', 'build', '.DS_Store', '*.log'];
const shouldIgnore = (name: string) => {
return IGNORE_PATTERNS.some((pattern) => {
if (pattern.startsWith("*")) {
if (pattern.startsWith('*')) {
return name.endsWith(pattern.slice(1));
}
return name === pattern;
@@ -46,11 +29,9 @@ export function CodeView() {
const { currentProject } = useAppStore();
const [fileTree, setFileTree] = useState<FileTreeNode[]>([]);
const [selectedFile, setSelectedFile] = useState<string | null>(null);
const [fileContent, setFileContent] = useState<string>("");
const [fileContent, setFileContent] = useState<string>('');
const [isLoading, setIsLoading] = useState(true);
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(
new Set()
);
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set());
// Load directory tree
const loadTree = useCallback(async () => {
@@ -79,7 +60,7 @@ export function CodeView() {
setFileTree(entries);
}
} catch (error) {
console.error("Failed to load file tree:", error);
console.error('Failed to load file tree:', error);
} finally {
setIsLoading(false);
}
@@ -110,7 +91,7 @@ export function CodeView() {
}));
}
} catch (error) {
console.error("Failed to load subdirectory:", error);
console.error('Failed to load subdirectory:', error);
}
return [];
};
@@ -126,7 +107,7 @@ export function CodeView() {
setSelectedFile(path);
}
} catch (error) {
console.error("Failed to load file:", error);
console.error('Failed to load file:', error);
}
};
@@ -170,8 +151,8 @@ export function CodeView() {
<div key={node.path}>
<div
className={cn(
"flex items-center gap-2 py-1 px-2 rounded cursor-pointer hover:bg-muted/50",
isSelected && "bg-muted"
'flex items-center gap-2 py-1 px-2 rounded cursor-pointer hover:bg-muted/50',
isSelected && 'bg-muted'
)}
style={{ paddingLeft: `${depth * 16 + 8}px` }}
onClick={() => {
@@ -205,9 +186,7 @@ export function CodeView() {
<span className="text-sm truncate">{node.name}</span>
</div>
{node.isDirectory && isExpanded && node.children && (
<div>
{node.children.map((child) => renderNode(child, depth + 1))}
</div>
<div>{node.children.map((child) => renderNode(child, depth + 1))}</div>
)}
</div>
);
@@ -215,10 +194,7 @@ export function CodeView() {
if (!currentProject) {
return (
<div
className="flex-1 flex items-center justify-center"
data-testid="code-view-no-project"
>
<div className="flex-1 flex items-center justify-center" data-testid="code-view-no-project">
<p className="text-muted-foreground">No project selected</p>
</div>
);
@@ -226,37 +202,24 @@ export function CodeView() {
if (isLoading) {
return (
<div
className="flex-1 flex items-center justify-center"
data-testid="code-view-loading"
>
<div className="flex-1 flex items-center justify-center" data-testid="code-view-loading">
<RefreshCw className="w-6 h-6 animate-spin text-muted-foreground" />
</div>
);
}
return (
<div
className="flex-1 flex flex-col overflow-hidden content-bg"
data-testid="code-view"
>
<div className="flex-1 flex flex-col overflow-hidden content-bg" data-testid="code-view">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-white/10 bg-zinc-950/50 backdrop-blur-md">
<div className="flex items-center gap-3">
<Code className="w-5 h-5 text-muted-foreground" />
<div>
<h1 className="text-xl font-bold">Code Explorer</h1>
<p className="text-sm text-muted-foreground">
{currentProject.name}
</p>
<p className="text-sm text-muted-foreground">{currentProject.name}</p>
</div>
</div>
<Button
variant="outline"
size="sm"
onClick={loadTree}
data-testid="refresh-tree"
>
<Button variant="outline" size="sm" onClick={loadTree} data-testid="refresh-tree">
<RefreshCw className="w-4 h-4 mr-2" />
Refresh
</Button>
@@ -275,7 +238,7 @@ export function CodeView() {
<div className="h-full flex flex-col">
<div className="px-4 py-2 border-b bg-muted/30">
<p className="text-sm font-mono text-muted-foreground truncate">
{selectedFile.replace(currentProject.path, "")}
{selectedFile.replace(currentProject.path, '')}
</p>
</div>
<Card className="flex-1 m-4 overflow-hidden">
@@ -288,9 +251,7 @@ export function CodeView() {
</div>
) : (
<div className="flex-1 flex items-center justify-center">
<p className="text-muted-foreground">
Select a file to view its contents
</p>
<p className="text-muted-foreground">Select a file to view its contents</p>
</div>
)}
</div>

View File

@@ -1,24 +1,20 @@
import { useState, useMemo, useCallback } from "react";
import {
useAppStore,
AIProfile,
} from "@/store/app-store";
import { useState, useMemo, useCallback } from 'react';
import { useAppStore, AIProfile } from '@/store/app-store';
import {
useKeyboardShortcuts,
useKeyboardShortcutsConfig,
KeyboardShortcut,
} from "@/hooks/use-keyboard-shortcuts";
} from '@/hooks/use-keyboard-shortcuts';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Sparkles } from "lucide-react";
import { toast } from "sonner";
import { DeleteConfirmDialog } from "@/components/ui/delete-confirm-dialog";
} from '@/components/ui/dialog';
import { Sparkles } from 'lucide-react';
import { toast } from 'sonner';
import { DeleteConfirmDialog } from '@/components/ui/delete-confirm-dialog';
import {
DndContext,
DragEndEvent,
@@ -26,16 +22,9 @@ import {
useSensor,
useSensors,
closestCenter,
} from "@dnd-kit/core";
import {
SortableContext,
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import {
SortableProfileCard,
ProfileForm,
ProfilesHeader,
} from "./profiles-view/components";
} from '@dnd-kit/core';
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
import { SortableProfileCard, ProfileForm, ProfilesHeader } from './profiles-view/components';
export function ProfilesView() {
const {
@@ -62,14 +51,8 @@ export function ProfilesView() {
);
// Separate built-in and custom profiles
const builtInProfiles = useMemo(
() => aiProfiles.filter((p) => p.isBuiltIn),
[aiProfiles]
);
const customProfiles = useMemo(
() => aiProfiles.filter((p) => !p.isBuiltIn),
[aiProfiles]
);
const builtInProfiles = useMemo(() => aiProfiles.filter((p) => p.isBuiltIn), [aiProfiles]);
const customProfiles = useMemo(() => aiProfiles.filter((p) => !p.isBuiltIn), [aiProfiles]);
const handleDragEnd = useCallback(
(event: DragEndEvent) => {
@@ -87,19 +70,19 @@ export function ProfilesView() {
[aiProfiles, reorderAIProfiles]
);
const handleAddProfile = (profile: Omit<AIProfile, "id">) => {
const handleAddProfile = (profile: Omit<AIProfile, 'id'>) => {
addAIProfile(profile);
setShowAddDialog(false);
toast.success("Profile created", {
toast.success('Profile created', {
description: `Created "${profile.name}" profile`,
});
};
const handleUpdateProfile = (profile: Omit<AIProfile, "id">) => {
const handleUpdateProfile = (profile: Omit<AIProfile, 'id'>) => {
if (editingProfile) {
updateAIProfile(editingProfile.id, profile);
setEditingProfile(null);
toast.success("Profile updated", {
toast.success('Profile updated', {
description: `Updated "${profile.name}" profile`,
});
}
@@ -109,7 +92,7 @@ export function ProfilesView() {
if (!profileToDelete) return;
removeAIProfile(profileToDelete.id);
toast.success("Profile deleted", {
toast.success('Profile deleted', {
description: `Deleted "${profileToDelete.name}" profile`,
});
setProfileToDelete(null);
@@ -117,8 +100,8 @@ export function ProfilesView() {
const handleResetProfiles = () => {
resetAIProfiles();
toast.success("Profiles refreshed", {
description: "Default profiles have been updated to the latest version",
toast.success('Profiles refreshed', {
description: 'Default profiles have been updated to the latest version',
});
};
@@ -130,7 +113,7 @@ export function ProfilesView() {
shortcutsList.push({
key: shortcuts.addProfile,
action: () => setShowAddDialog(true),
description: "Create new profile",
description: 'Create new profile',
});
return shortcutsList;
@@ -140,10 +123,7 @@ export function ProfilesView() {
useKeyboardShortcuts(profilesShortcuts);
return (
<div
className="flex-1 flex flex-col overflow-hidden content-bg"
data-testid="profiles-view"
>
<div className="flex-1 flex flex-col overflow-hidden content-bg" data-testid="profiles-view">
{/* Header Section */}
<ProfilesHeader
onResetProfiles={handleResetProfiles}
@@ -157,9 +137,7 @@ export function ProfilesView() {
{/* Custom Profiles Section */}
<div>
<div className="flex items-center gap-2 mb-4">
<h2 className="text-lg font-semibold text-foreground">
Custom Profiles
</h2>
<h2 className="text-lg font-semibold text-foreground">Custom Profiles</h2>
<span className="text-xs px-2 py-0.5 rounded-full bg-primary/10 text-primary">
{customProfiles.length}
</span>
@@ -202,16 +180,13 @@ export function ProfilesView() {
{/* Built-in Profiles Section */}
<div>
<div className="flex items-center gap-2 mb-4">
<h2 className="text-lg font-semibold text-foreground">
Built-in Profiles
</h2>
<h2 className="text-lg font-semibold text-foreground">Built-in Profiles</h2>
<span className="text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground">
{builtInProfiles.length}
</span>
</div>
<p className="text-sm text-muted-foreground mb-4">
Pre-configured profiles for common use cases. These cannot be
edited or deleted.
Pre-configured profiles for common use cases. These cannot be edited or deleted.
</p>
<DndContext
sensors={sensors}
@@ -240,12 +215,13 @@ export function ProfilesView() {
{/* Add Profile Dialog */}
<Dialog open={showAddDialog} onOpenChange={setShowAddDialog}>
<DialogContent data-testid="add-profile-dialog" className="flex flex-col max-h-[calc(100vh-4rem)]">
<DialogContent
data-testid="add-profile-dialog"
className="flex flex-col max-h-[calc(100vh-4rem)]"
>
<DialogHeader className="shrink-0">
<DialogTitle>Create New Profile</DialogTitle>
<DialogDescription>
Define a reusable model configuration preset.
</DialogDescription>
<DialogDescription>Define a reusable model configuration preset.</DialogDescription>
</DialogHeader>
<ProfileForm
profile={{}}
@@ -258,11 +234,11 @@ export function ProfilesView() {
</Dialog>
{/* Edit Profile Dialog */}
<Dialog
open={!!editingProfile}
onOpenChange={() => setEditingProfile(null)}
>
<DialogContent data-testid="edit-profile-dialog" className="flex flex-col max-h-[calc(100vh-4rem)]">
<Dialog open={!!editingProfile} onOpenChange={() => setEditingProfile(null)}>
<DialogContent
data-testid="edit-profile-dialog"
className="flex flex-col max-h-[calc(100vh-4rem)]"
>
<DialogHeader className="shrink-0">
<DialogTitle>Edit Profile</DialogTitle>
<DialogDescription>Modify your profile settings.</DialogDescription>
@@ -288,7 +264,7 @@ export function ProfilesView() {
description={
profileToDelete
? `Are you sure you want to delete "${profileToDelete.name}"? This action cannot be undone.`
: ""
: ''
}
confirmText="Delete Profile"
testId="delete-profile-confirm-dialog"

View File

@@ -1,3 +1,3 @@
export { SortableProfileCard } from "./sortable-profile-card";
export { ProfileForm } from "./profile-form";
export { ProfilesHeader } from "./profiles-header";
export { SortableProfileCard } from './sortable-profile-card';
export { ProfileForm } from './profile-form';
export { ProfilesHeader } from './profiles-header';

View File

@@ -1,21 +1,20 @@
import { useState } from "react";
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 { Textarea } from "@/components/ui/textarea";
import { cn, modelSupportsThinking } from "@/lib/utils";
import { DialogFooter } from "@/components/ui/dialog";
import { Brain } from "lucide-react";
import { toast } from "sonner";
import type { AIProfile, AgentModel, ThinkingLevel } from "@/store/app-store";
import { CLAUDE_MODELS, THINKING_LEVELS, ICON_OPTIONS } from "../constants";
import { getProviderFromModel } from "../utils";
import { useState } from 'react';
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 { Textarea } from '@/components/ui/textarea';
import { cn, modelSupportsThinking } from '@/lib/utils';
import { DialogFooter } from '@/components/ui/dialog';
import { Brain } from 'lucide-react';
import { toast } from 'sonner';
import type { AIProfile, AgentModel, ThinkingLevel } from '@/store/app-store';
import { CLAUDE_MODELS, THINKING_LEVELS, ICON_OPTIONS } from '../constants';
import { getProviderFromModel } from '../utils';
interface ProfileFormProps {
profile: Partial<AIProfile>;
onSave: (profile: Omit<AIProfile, "id">) => void;
onSave: (profile: Omit<AIProfile, 'id'>) => void;
onCancel: () => void;
isEditing: boolean;
hotkeyActive: boolean;
@@ -29,11 +28,11 @@ export function ProfileForm({
hotkeyActive,
}: ProfileFormProps) {
const [formData, setFormData] = useState({
name: profile.name || "",
description: profile.description || "",
model: profile.model || ("opus" as AgentModel),
thinkingLevel: profile.thinkingLevel || ("none" as ThinkingLevel),
icon: profile.icon || "Brain",
name: profile.name || '',
description: profile.description || '',
model: profile.model || ('opus' as AgentModel),
thinkingLevel: profile.thinkingLevel || ('none' as ThinkingLevel),
icon: profile.icon || 'Brain',
});
const provider = getProviderFromModel(formData.model);
@@ -48,7 +47,7 @@ export function ProfileForm({
const handleSubmit = () => {
if (!formData.name.trim()) {
toast.error("Please enter a profile name");
toast.error('Please enter a profile name');
return;
}
@@ -56,7 +55,7 @@ export function ProfileForm({
name: formData.name.trim(),
description: formData.description.trim(),
model: formData.model,
thinkingLevel: supportsThinking ? formData.thinkingLevel : "none",
thinkingLevel: supportsThinking ? formData.thinkingLevel : 'none',
provider,
isBuiltIn: false,
icon: formData.icon,
@@ -84,9 +83,7 @@ export function ProfileForm({
<Textarea
id="profile-description"
value={formData.description}
onChange={(e) =>
setFormData({ ...formData, description: e.target.value })
}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder="Describe when to use this profile..."
rows={2}
data-testid="profile-description-input"
@@ -103,10 +100,10 @@ export function ProfileForm({
type="button"
onClick={() => setFormData({ ...formData, icon: name })}
className={cn(
"w-10 h-10 rounded-lg flex items-center justify-center border transition-colors",
'w-10 h-10 rounded-lg flex items-center justify-center border transition-colors',
formData.icon === name
? "bg-primary text-primary-foreground border-primary"
: "bg-background hover:bg-accent border-border"
? 'bg-primary text-primary-foreground border-primary'
: 'bg-background hover:bg-accent border-border'
)}
data-testid={`icon-select-${name}`}
>
@@ -129,14 +126,14 @@ export function ProfileForm({
type="button"
onClick={() => handleModelChange(id)}
className={cn(
"flex-1 min-w-[100px] px-3 py-2 rounded-md border text-sm font-medium transition-colors",
'flex-1 min-w-[100px] px-3 py-2 rounded-md border text-sm font-medium transition-colors',
formData.model === id
? "bg-primary text-primary-foreground border-primary"
: "bg-background hover:bg-accent border-border"
? 'bg-primary text-primary-foreground border-primary'
: 'bg-background hover:bg-accent border-border'
)}
data-testid={`model-select-${id}`}
>
{label.replace("Claude ", "")}
{label.replace('Claude ', '')}
</button>
))}
</div>
@@ -156,19 +153,19 @@ export function ProfileForm({
type="button"
onClick={() => {
setFormData({ ...formData, thinkingLevel: id });
if (id === "ultrathink") {
toast.warning("Ultrathink uses extensive reasoning", {
if (id === 'ultrathink') {
toast.warning('Ultrathink uses extensive reasoning', {
description:
"Best for complex architecture, migrations, or deep debugging (~$0.48/task).",
'Best for complex architecture, migrations, or deep debugging (~$0.48/task).',
duration: 4000,
});
}
}}
className={cn(
"flex-1 min-w-[70px] px-3 py-2 rounded-md border text-sm font-medium transition-colors",
'flex-1 min-w-[70px] px-3 py-2 rounded-md border text-sm font-medium transition-colors',
formData.thinkingLevel === id
? "bg-amber-500 text-white border-amber-400"
: "bg-background hover:bg-accent border-border"
? 'bg-amber-500 text-white border-amber-400'
: 'bg-background hover:bg-accent border-border'
)}
data-testid={`thinking-select-${id}`}
>
@@ -190,14 +187,13 @@ export function ProfileForm({
</Button>
<HotkeyButton
onClick={handleSubmit}
hotkey={{ key: "Enter", cmdCtrl: true }}
hotkey={{ key: 'Enter', cmdCtrl: true }}
hotkeyActive={hotkeyActive}
data-testid="save-profile-button"
>
{isEditing ? "Save Changes" : "Create Profile"}
{isEditing ? 'Save Changes' : 'Create Profile'}
</HotkeyButton>
</DialogFooter>
</>
);
}

View File

@@ -1,6 +1,6 @@
import { Button } from "@/components/ui/button";
import { HotkeyButton } from "@/components/ui/hotkey-button";
import { UserCircle, Plus, RefreshCw } from "lucide-react";
import { Button } from '@/components/ui/button';
import { HotkeyButton } from '@/components/ui/hotkey-button';
import { UserCircle, Plus, RefreshCw } from 'lucide-react';
interface ProfilesHeaderProps {
onResetProfiles: () => void;
@@ -22,9 +22,7 @@ export function ProfilesHeader({
<UserCircle className="w-5 h-5 text-primary-foreground" />
</div>
<div>
<h1 className="text-2xl font-bold text-foreground">
AI Profiles
</h1>
<h1 className="text-2xl font-bold text-foreground">AI Profiles</h1>
<p className="text-sm text-muted-foreground">
Create and manage model configuration presets
</p>
@@ -55,4 +53,3 @@ export function ProfilesHeader({
</div>
);
}

View File

@@ -1,10 +1,10 @@
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { GripVertical, Lock, Pencil, Trash2, Brain } from "lucide-react";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import type { AIProfile } from "@/store/app-store";
import { PROFILE_ICONS } from "../constants";
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { GripVertical, Lock, Pencil, Trash2, Brain } from 'lucide-react';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import type { AIProfile } from '@/store/app-store';
import { PROFILE_ICONS } from '../constants';
interface SortableProfileCardProps {
profile: AIProfile;
@@ -12,19 +12,10 @@ interface SortableProfileCardProps {
onDelete: () => void;
}
export function SortableProfileCard({
profile,
onEdit,
onDelete,
}: SortableProfileCardProps) {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: profile.id });
export function SortableProfileCard({ profile, onEdit, onDelete }: SortableProfileCardProps) {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: profile.id,
});
const style = {
transform: CSS.Transform.toString(transform),
@@ -39,11 +30,11 @@ export function SortableProfileCard({
ref={setNodeRef}
style={style}
className={cn(
"group relative flex items-start gap-4 p-4 rounded-xl border bg-card transition-all",
isDragging && "shadow-lg",
'group relative flex items-start gap-4 p-4 rounded-xl border bg-card transition-all',
isDragging && 'shadow-lg',
profile.isBuiltIn
? "border-border/50"
: "border-border hover:border-primary/50 hover:shadow-sm"
? 'border-border/50'
: 'border-border hover:border-primary/50 hover:shadow-sm'
)}
data-testid={`profile-card-${profile.id}`}
>
@@ -60,12 +51,8 @@ export function SortableProfileCard({
</button>
{/* Icon */}
<div
className="flex-shrink-0 w-10 h-10 rounded-lg flex items-center justify-center bg-primary/10"
>
{IconComponent && (
<IconComponent className="w-5 h-5 text-primary" />
)}
<div className="flex-shrink-0 w-10 h-10 rounded-lg flex items-center justify-center bg-primary/10">
{IconComponent && <IconComponent className="w-5 h-5 text-primary" />}
</div>
{/* Content */}
@@ -79,16 +66,12 @@ export function SortableProfileCard({
</span>
)}
</div>
<p className="text-sm text-muted-foreground mt-0.5 line-clamp-2">
{profile.description}
</p>
<p className="text-sm text-muted-foreground mt-0.5 line-clamp-2">{profile.description}</p>
<div className="flex items-center gap-2 mt-2 flex-wrap">
<span
className="text-xs px-2 py-0.5 rounded-full border border-primary/30 text-primary bg-primary/10"
>
<span className="text-xs px-2 py-0.5 rounded-full border border-primary/30 text-primary bg-primary/10">
{profile.model}
</span>
{profile.thinkingLevel !== "none" && (
{profile.thinkingLevel !== 'none' && (
<span className="text-xs px-2 py-0.5 rounded-full border border-amber-500/30 text-amber-600 dark:text-amber-400 bg-amber-500/10">
{profile.thinkingLevel}
</span>
@@ -124,4 +107,3 @@ export function SortableProfileCard({
</div>
);
}

View File

@@ -1,18 +1,8 @@
import {
Brain,
Zap,
Scale,
Cpu,
Rocket,
Sparkles,
} from "lucide-react";
import type { AgentModel, ThinkingLevel } from "@/store/app-store";
import { Brain, Zap, Scale, Cpu, Rocket, Sparkles } from 'lucide-react';
import type { AgentModel, ThinkingLevel } from '@/store/app-store';
// Icon mapping for profiles
export const PROFILE_ICONS: Record<
string,
React.ComponentType<{ className?: string }>
> = {
export const PROFILE_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {
Brain,
Zap,
Scale,
@@ -23,27 +13,25 @@ export const PROFILE_ICONS: Record<
// Available icons for selection
export const ICON_OPTIONS = [
{ name: "Brain", icon: Brain },
{ name: "Zap", icon: Zap },
{ name: "Scale", icon: Scale },
{ name: "Cpu", icon: Cpu },
{ name: "Rocket", icon: Rocket },
{ name: "Sparkles", icon: Sparkles },
{ name: 'Brain', icon: Brain },
{ name: 'Zap', icon: Zap },
{ name: 'Scale', icon: Scale },
{ name: 'Cpu', icon: Cpu },
{ name: 'Rocket', icon: Rocket },
{ name: 'Sparkles', icon: Sparkles },
];
// Model options for the form
export const CLAUDE_MODELS: { id: AgentModel; label: string }[] = [
{ id: "haiku", label: "Claude Haiku" },
{ id: "sonnet", label: "Claude Sonnet" },
{ id: "opus", label: "Claude Opus" },
{ id: 'haiku', label: 'Claude Haiku' },
{ id: 'sonnet', label: 'Claude Sonnet' },
{ id: 'opus', label: 'Claude Opus' },
];
export const THINKING_LEVELS: { id: ThinkingLevel; label: string }[] = [
{ id: "none", label: "None" },
{ id: "low", label: "Low" },
{ id: "medium", label: "Medium" },
{ id: "high", label: "High" },
{ id: "ultrathink", label: "Ultrathink" },
{ id: 'none', label: 'None' },
{ id: 'low', label: 'Low' },
{ id: 'medium', label: 'Medium' },
{ id: 'high', label: 'High' },
{ id: 'ultrathink', label: 'Ultrathink' },
];

View File

@@ -1,8 +1,6 @@
import type { AgentModel, ModelProvider } from "@/store/app-store";
import type { AgentModel, ModelProvider } from '@/store/app-store';
// Helper to determine provider from model
export function getProviderFromModel(model: AgentModel): ModelProvider {
return "claude";
return 'claude';
}

View File

@@ -1,11 +1,10 @@
import { useState, useEffect, useCallback } from "react";
import { Bot, Folder, Loader2, RefreshCw, Square, Activity } from "lucide-react";
import { getElectronAPI, RunningAgent } from "@/lib/electron";
import { useAppStore } from "@/store/app-store";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { useNavigate } from "@tanstack/react-router";
import { useState, useEffect, useCallback } from 'react';
import { Bot, Folder, Loader2, RefreshCw, Square, Activity } from 'lucide-react';
import { getElectronAPI, RunningAgent } from '@/lib/electron';
import { useAppStore } from '@/store/app-store';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { useNavigate } from '@tanstack/react-router';
export function RunningAgentsView() {
const [runningAgents, setRunningAgents] = useState<RunningAgent[]>([]);
@@ -24,7 +23,7 @@ export function RunningAgentsView() {
}
}
} catch (error) {
console.error("[RunningAgentsView] Error fetching running agents:", error);
console.error('[RunningAgentsView] Error fetching running agents:', error);
} finally {
setLoading(false);
setRefreshing(false);
@@ -52,10 +51,7 @@ export function RunningAgentsView() {
const unsubscribe = api.autoMode.onEvent((event) => {
// When a feature completes or errors, refresh the list
if (
event.type === "auto_mode_feature_complete" ||
event.type === "auto_mode_error"
) {
if (event.type === 'auto_mode_feature_complete' || event.type === 'auto_mode_error') {
fetchRunningAgents();
}
});
@@ -70,27 +66,33 @@ export function RunningAgentsView() {
fetchRunningAgents();
}, [fetchRunningAgents]);
const handleStopAgent = useCallback(async (featureId: string) => {
try {
const api = getElectronAPI();
if (api.autoMode) {
await api.autoMode.stopFeature(featureId);
// Refresh list after stopping
fetchRunningAgents();
const handleStopAgent = useCallback(
async (featureId: string) => {
try {
const api = getElectronAPI();
if (api.autoMode) {
await api.autoMode.stopFeature(featureId);
// Refresh list after stopping
fetchRunningAgents();
}
} catch (error) {
console.error('[RunningAgentsView] Error stopping agent:', error);
}
} catch (error) {
console.error("[RunningAgentsView] Error stopping agent:", error);
}
}, [fetchRunningAgents]);
},
[fetchRunningAgents]
);
const handleNavigateToProject = useCallback((agent: RunningAgent) => {
// Find the project by path
const project = projects.find((p) => p.path === agent.projectPath);
if (project) {
setCurrentProject(project);
navigate({ to: "/board" });
}
}, [projects, setCurrentProject, navigate]);
const handleNavigateToProject = useCallback(
(agent: RunningAgent) => {
// Find the project by path
const project = projects.find((p) => p.path === agent.projectPath);
if (project) {
setCurrentProject(project);
navigate({ to: '/board' });
}
},
[projects, setCurrentProject, navigate]
);
if (loading) {
return (
@@ -112,20 +114,13 @@ export function RunningAgentsView() {
<h1 className="text-2xl font-bold">Running Agents</h1>
<p className="text-sm text-muted-foreground">
{runningAgents.length === 0
? "No agents currently running"
: `${runningAgents.length} agent${runningAgents.length === 1 ? "" : "s"} running across all projects`}
? 'No agents currently running'
: `${runningAgents.length} agent${runningAgents.length === 1 ? '' : 's'} running across all projects`}
</p>
</div>
</div>
<Button
variant="outline"
size="sm"
onClick={handleRefresh}
disabled={refreshing}
>
<RefreshCw
className={cn("h-4 w-4 mr-2", refreshing && "animate-spin")}
/>
<Button variant="outline" size="sm" onClick={handleRefresh} disabled={refreshing}>
<RefreshCw className={cn('h-4 w-4 mr-2', refreshing && 'animate-spin')} />
Refresh
</Button>
</div>
@@ -138,8 +133,8 @@ export function RunningAgentsView() {
</div>
<h2 className="text-lg font-medium mb-2">No Running Agents</h2>
<p className="text-muted-foreground max-w-md">
Agents will appear here when they are actively working on features.
Start an agent from the Kanban board by dragging a feature to "In Progress".
Agents will appear here when they are actively working on features. Start an agent from
the Kanban board by dragging a feature to "In Progress".
</p>
</div>
) : (
@@ -163,9 +158,7 @@ export function RunningAgentsView() {
{/* Agent info */}
<div className="min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium truncate">
{agent.featureId}
</span>
<span className="font-medium truncate">{agent.featureId}</span>
{agent.isAutoMode && (
<span className="px-2 py-0.5 text-[10px] font-medium rounded-full bg-brand-500/10 text-brand-500 border border-brand-500/30">
AUTO

View File

@@ -1,8 +1,8 @@
import { Label } from "@/components/ui/label";
import { Sparkles } from "lucide-react";
import { cn } from "@/lib/utils";
import { useAppStore } from "@/store/app-store";
import { CLAUDE_MODELS } from "@/components/views/board-view/shared/model-constants";
import { Label } from '@/components/ui/label';
import { Sparkles } from 'lucide-react';
import { cn } from '@/lib/utils';
import { useAppStore } from '@/store/app-store';
import { CLAUDE_MODELS } from '@/components/views/board-view/shared/model-constants';
export function AIEnhancementSection() {
const { enhancementModel, setEnhancementModel } = useAppStore();
@@ -10,10 +10,10 @@ export function AIEnhancementSection() {
return (
<div
className={cn(
"rounded-2xl overflow-hidden",
"border border-border/50",
"bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl",
"shadow-sm shadow-black/5"
'rounded-2xl overflow-hidden',
'border border-border/50',
'bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl',
'shadow-sm shadow-black/5'
)}
>
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
@@ -29,9 +29,7 @@ export function AIEnhancementSection() {
</div>
<div className="p-6 space-y-4">
<div className="space-y-4">
<Label className="text-foreground font-medium">
Enhancement Model
</Label>
<Label className="text-foreground font-medium">Enhancement Model</Label>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
{CLAUDE_MODELS.map(({ id, label, description, badge }) => {
const isActive = enhancementModel === id;
@@ -40,46 +38,48 @@ export function AIEnhancementSection() {
key={id}
onClick={() => setEnhancementModel(id)}
className={cn(
"group flex flex-col items-start gap-2 px-4 py-4 rounded-xl text-left",
"transition-all duration-200 ease-out",
'group flex flex-col items-start gap-2 px-4 py-4 rounded-xl text-left',
'transition-all duration-200 ease-out',
isActive
? [
"bg-gradient-to-br from-brand-500/15 to-brand-600/10",
"border-2 border-brand-500/40",
"text-foreground",
"shadow-md shadow-brand-500/10",
'bg-gradient-to-br from-brand-500/15 to-brand-600/10',
'border-2 border-brand-500/40',
'text-foreground',
'shadow-md shadow-brand-500/10',
]
: [
"bg-accent/30 hover:bg-accent/50",
"border border-border/50 hover:border-border",
"text-muted-foreground hover:text-foreground",
"hover:shadow-sm",
'bg-accent/30 hover:bg-accent/50',
'border border-border/50 hover:border-border',
'text-muted-foreground hover:text-foreground',
'hover:shadow-sm',
],
"hover:scale-[1.02] active:scale-[0.98]"
'hover:scale-[1.02] active:scale-[0.98]'
)}
data-testid={`enhancement-model-${id}`}
>
<div className="flex items-center gap-2 w-full">
<span className={cn(
"font-medium text-sm",
isActive ? "text-foreground" : "group-hover:text-foreground"
)}>
<span
className={cn(
'font-medium text-sm',
isActive ? 'text-foreground' : 'group-hover:text-foreground'
)}
>
{label}
</span>
{badge && (
<span className={cn(
"ml-auto text-xs px-2 py-0.5 rounded-full",
isActive
? "bg-brand-500/20 text-brand-500"
: "bg-accent text-muted-foreground"
)}>
<span
className={cn(
'ml-auto text-xs px-2 py-0.5 rounded-full',
isActive
? 'bg-brand-500/20 text-brand-500'
: 'bg-accent text-muted-foreground'
)}
>
{badge}
</span>
)}
</div>
<span className="text-xs text-muted-foreground/80">
{description}
</span>
<span className="text-xs text-muted-foreground/80">{description}</span>
</button>
);
})}

View File

@@ -1 +1 @@
export { AIEnhancementSection } from "./ai-enhancement-section";
export { AIEnhancementSection } from './ai-enhancement-section';

View File

@@ -1,8 +1,8 @@
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { AlertCircle, CheckCircle2, Eye, EyeOff, Loader2, Zap } from "lucide-react";
import type { ProviderConfig } from "@/config/api-providers";
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { AlertCircle, CheckCircle2, Eye, EyeOff, Loader2, Zap } from 'lucide-react';
import type { ProviderConfig } from '@/config/api-providers';
interface ApiKeyFieldProps {
config: ProviderConfig;
@@ -42,7 +42,7 @@ export function ApiKeyField({ config }: ApiKeyFieldProps) {
<div className="relative flex-1">
<Input
id={inputId}
type={showValue ? "text" : "password"}
type={showValue ? 'text' : 'password'}
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder={placeholder}
@@ -82,7 +82,7 @@ export function ApiKeyField({ config }: ApiKeyFieldProps) {
</Button>
</div>
<p className="text-xs text-muted-foreground">
{descriptionPrefix}{" "}
{descriptionPrefix}{' '}
<a
href={descriptionLinkHref}
target="_blank"
@@ -97,8 +97,8 @@ export function ApiKeyField({ config }: ApiKeyFieldProps) {
<div
className={`flex items-center gap-2 p-3 rounded-lg ${
result.success
? "bg-green-500/10 border border-green-500/20 text-green-400"
: "bg-red-500/10 border border-red-500/20 text-red-400"
? 'bg-green-500/10 border border-green-500/20 text-green-400'
: 'bg-red-500/10 border border-red-500/20 text-red-400'
}`}
data-testid={resultTestId}
>

View File

@@ -1,17 +1,17 @@
import { useAppStore } from "@/store/app-store";
import { useSetupStore } from "@/store/setup-store";
import { Button } from "@/components/ui/button";
import { Key, CheckCircle2, Settings, Trash2, Loader2 } from "lucide-react";
import { ApiKeyField } from "./api-key-field";
import { buildProviderConfigs } from "@/config/api-providers";
import { AuthenticationStatusDisplay } from "./authentication-status-display";
import { SecurityNotice } from "./security-notice";
import { useApiKeyManagement } from "./hooks/use-api-key-management";
import { cn } from "@/lib/utils";
import { useState, useCallback } from "react";
import { getElectronAPI } from "@/lib/electron";
import { toast } from "sonner";
import { useNavigate } from "@tanstack/react-router";
import { useAppStore } from '@/store/app-store';
import { useSetupStore } from '@/store/setup-store';
import { Button } from '@/components/ui/button';
import { Key, CheckCircle2, Settings, Trash2, Loader2 } from 'lucide-react';
import { ApiKeyField } from './api-key-field';
import { buildProviderConfigs } from '@/config/api-providers';
import { AuthenticationStatusDisplay } from './authentication-status-display';
import { SecurityNotice } from './security-notice';
import { useApiKeyManagement } from './hooks/use-api-key-management';
import { cn } from '@/lib/utils';
import { useState, useCallback } from 'react';
import { getElectronAPI } from '@/lib/electron';
import { toast } from 'sonner';
import { useNavigate } from '@tanstack/react-router';
export function ApiKeysSection() {
const { apiKeys, setApiKeys } = useAppStore();
@@ -19,8 +19,7 @@ export function ApiKeysSection() {
const [isDeletingAnthropicKey, setIsDeletingAnthropicKey] = useState(false);
const navigate = useNavigate();
const { providerConfigParams, apiKeyStatus, handleSave, saved } =
useApiKeyManagement();
const { providerConfigParams, apiKeyStatus, handleSave, saved } = useApiKeyManagement();
const providerConfigs = buildProviderConfigs(providerConfigParams);
@@ -30,24 +29,24 @@ export function ApiKeysSection() {
try {
const api = getElectronAPI();
if (!api.setup?.deleteApiKey) {
toast.error("Delete API not available");
toast.error('Delete API not available');
return;
}
const result = await api.setup.deleteApiKey("anthropic");
const result = await api.setup.deleteApiKey('anthropic');
if (result.success) {
setApiKeys({ ...apiKeys, anthropic: "" });
setApiKeys({ ...apiKeys, anthropic: '' });
setClaudeAuthStatus({
authenticated: false,
method: "none",
method: 'none',
hasCredentialsFile: claudeAuthStatus?.hasCredentialsFile || false,
});
toast.success("Anthropic API key deleted");
toast.success('Anthropic API key deleted');
} else {
toast.error(result.error || "Failed to delete API key");
toast.error(result.error || 'Failed to delete API key');
}
} catch (error) {
toast.error("Failed to delete API key");
toast.error('Failed to delete API key');
} finally {
setIsDeletingAnthropicKey(false);
}
@@ -56,16 +55,16 @@ export function ApiKeysSection() {
// Open setup wizard
const openSetupWizard = useCallback(() => {
setSetupComplete(false);
navigate({ to: "/setup" });
navigate({ to: '/setup' });
}, [setSetupComplete, navigate]);
return (
<div
className={cn(
"rounded-2xl overflow-hidden",
"border border-border/50",
"bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl",
"shadow-sm shadow-black/5"
'rounded-2xl overflow-hidden',
'border border-border/50',
'bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl',
'shadow-sm shadow-black/5'
)}
>
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
@@ -101,13 +100,13 @@ export function ApiKeysSection() {
onClick={handleSave}
data-testid="save-settings"
className={cn(
"min-w-[140px] h-10",
"bg-gradient-to-r from-brand-500 to-brand-600",
"hover:from-brand-600 hover:to-brand-600",
"text-white font-medium border-0",
"shadow-md shadow-brand-500/20 hover:shadow-lg hover:shadow-brand-500/25",
"transition-all duration-200 ease-out",
"hover:scale-[1.02] active:scale-[0.98]"
'min-w-[140px] h-10',
'bg-gradient-to-r from-brand-500 to-brand-600',
'hover:from-brand-600 hover:to-brand-600',
'text-white font-medium border-0',
'shadow-md shadow-brand-500/20 hover:shadow-lg hover:shadow-brand-500/25',
'transition-all duration-200 ease-out',
'hover:scale-[1.02] active:scale-[0.98]'
)}
>
{saved ? (
@@ -116,7 +115,7 @@ export function ApiKeysSection() {
Saved!
</>
) : (
"Save API Keys"
'Save API Keys'
)}
</Button>

View File

@@ -1,7 +1,7 @@
import { useState, useEffect } from "react";
import { useAppStore } from "@/store/app-store";
import { getElectronAPI } from "@/lib/electron";
import type { ProviderConfigParams } from "@/config/api-providers";
import { useState, useEffect } from 'react';
import { useAppStore } from '@/store/app-store';
import { getElectronAPI } from '@/lib/electron';
import type { ProviderConfigParams } from '@/config/api-providers';
interface TestResult {
success: boolean;
@@ -32,9 +32,7 @@ export function useApiKeyManagement() {
const [testingConnection, setTestingConnection] = useState(false);
const [testResult, setTestResult] = useState<TestResult | null>(null);
const [testingGeminiConnection, setTestingGeminiConnection] = useState(false);
const [geminiTestResult, setGeminiTestResult] = useState<TestResult | null>(
null
);
const [geminiTestResult, setGeminiTestResult] = useState<TestResult | null>(null);
// API key status from environment
const [apiKeyStatus, setApiKeyStatus] = useState<ApiKeyStatus | null>(null);
@@ -62,7 +60,7 @@ export function useApiKeyManagement() {
});
}
} catch (error) {
console.error("Failed to check API key status:", error);
console.error('Failed to check API key status:', error);
}
}
};
@@ -76,23 +74,23 @@ export function useApiKeyManagement() {
try {
const api = getElectronAPI();
const data = await api.setup.verifyClaudeAuth("api_key");
const data = await api.setup.verifyClaudeAuth('api_key');
if (data.success && data.authenticated) {
setTestResult({
success: true,
message: "Connection successful! Claude responded.",
message: 'Connection successful! Claude responded.',
});
} else {
setTestResult({
success: false,
message: data.error || "Failed to connect to Claude API.",
message: data.error || 'Failed to connect to Claude API.',
});
}
} catch {
setTestResult({
success: false,
message: "Network error. Please check your connection.",
message: 'Network error. Please check your connection.',
});
} finally {
setTestingConnection(false);
@@ -109,7 +107,7 @@ export function useApiKeyManagement() {
if (!googleKey || googleKey.trim().length < 10) {
setGeminiTestResult({
success: false,
message: "Please enter a valid API key.",
message: 'Please enter a valid API key.',
});
setTestingGeminiConnection(false);
return;
@@ -119,7 +117,7 @@ export function useApiKeyManagement() {
// Full verification requires a backend endpoint
setGeminiTestResult({
success: true,
message: "API key saved. Connection test not yet available.",
message: 'API key saved. Connection test not yet available.',
});
setTestingGeminiConnection(false);
};

View File

@@ -1,4 +1,4 @@
import { AlertCircle } from "lucide-react";
import { AlertCircle } from 'lucide-react';
interface SecurityNoticeProps {
title?: string;
@@ -6,7 +6,7 @@ interface SecurityNoticeProps {
}
export function SecurityNotice({
title = "Security Notice",
title = 'Security Notice',
message = "API keys are stored in your browser's local storage. Never share your API keys or commit them to version control.",
}: SecurityNoticeProps) {
return (

View File

@@ -1,24 +1,21 @@
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import { Volume2, VolumeX } from "lucide-react";
import { cn } from "@/lib/utils";
import { Label } from '@/components/ui/label';
import { Checkbox } from '@/components/ui/checkbox';
import { Volume2, VolumeX } from 'lucide-react';
import { cn } from '@/lib/utils';
interface AudioSectionProps {
muteDoneSound: boolean;
onMuteDoneSoundChange: (value: boolean) => void;
}
export function AudioSection({
muteDoneSound,
onMuteDoneSoundChange,
}: AudioSectionProps) {
export function AudioSection({ muteDoneSound, onMuteDoneSoundChange }: AudioSectionProps) {
return (
<div
className={cn(
"rounded-2xl overflow-hidden",
"border border-border/50",
"bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl",
"shadow-sm shadow-black/5"
'rounded-2xl overflow-hidden',
'border border-border/50',
'bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl',
'shadow-sm shadow-black/5'
)}
>
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
@@ -26,9 +23,7 @@ export function AudioSection({
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 flex items-center justify-center border border-brand-500/20">
<Volume2 className="w-5 h-5 text-brand-500" />
</div>
<h2 className="text-lg font-semibold text-foreground tracking-tight">
Audio
</h2>
<h2 className="text-lg font-semibold text-foreground tracking-tight">Audio</h2>
</div>
<p className="text-sm text-muted-foreground/80 ml-12">
Configure audio and notification settings.
@@ -52,9 +47,9 @@ export function AudioSection({
Mute notification sound when agents complete
</Label>
<p className="text-xs text-muted-foreground/80 leading-relaxed">
When enabled, disables the &quot;ding&quot; sound that plays when
an agent completes a feature. The feature will still move to the
completed column, but without audio notification.
When enabled, disables the &quot;ding&quot; sound that plays when an agent completes a
feature. The feature will still move to the completed column, but without audio
notification.
</p>
</div>
</div>

View File

@@ -1,12 +1,7 @@
import { Button } from "@/components/ui/button";
import {
Terminal,
CheckCircle2,
AlertCircle,
RefreshCw,
} from "lucide-react";
import { cn } from "@/lib/utils";
import type { CliStatus } from "../shared/types";
import { Button } from '@/components/ui/button';
import { Terminal, CheckCircle2, AlertCircle, RefreshCw } from 'lucide-react';
import { cn } from '@/lib/utils';
import type { CliStatus } from '../shared/types';
interface CliStatusProps {
status: CliStatus | null;
@@ -14,20 +9,16 @@ interface CliStatusProps {
onRefresh: () => void;
}
export function ClaudeCliStatus({
status,
isChecking,
onRefresh,
}: CliStatusProps) {
export function ClaudeCliStatus({ status, isChecking, onRefresh }: CliStatusProps) {
if (!status) return null;
return (
<div
className={cn(
"rounded-2xl overflow-hidden",
"border border-border/50",
"bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl",
"shadow-sm shadow-black/5"
'rounded-2xl overflow-hidden',
'border border-border/50',
'bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl',
'shadow-sm shadow-black/5'
)}
>
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
@@ -48,32 +39,28 @@ export function ClaudeCliStatus({
data-testid="refresh-claude-cli"
title="Refresh Claude CLI detection"
className={cn(
"h-9 w-9 rounded-lg",
"hover:bg-accent/50 hover:scale-105",
"transition-all duration-200"
'h-9 w-9 rounded-lg',
'hover:bg-accent/50 hover:scale-105',
'transition-all duration-200'
)}
>
<RefreshCw
className={cn("w-4 h-4", isChecking && "animate-spin")}
/>
<RefreshCw className={cn('w-4 h-4', isChecking && 'animate-spin')} />
</Button>
</div>
<p className="text-sm text-muted-foreground/80 ml-12">
Claude Code CLI provides better performance for long-running tasks,
especially with ultrathink.
Claude Code CLI provides better performance for long-running tasks, especially with
ultrathink.
</p>
</div>
<div className="p-6 space-y-4">
{status.success && status.status === "installed" ? (
{status.success && status.status === 'installed' ? (
<div className="space-y-3">
<div className="flex items-center gap-3 p-4 rounded-xl bg-emerald-500/10 border border-emerald-500/20">
<div className="w-10 h-10 rounded-xl bg-emerald-500/15 flex items-center justify-center border border-emerald-500/20 shrink-0">
<CheckCircle2 className="w-5 h-5 text-emerald-500" />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-emerald-400">
Claude Code CLI Installed
</p>
<p className="text-sm font-medium text-emerald-400">Claude Code CLI Installed</p>
<div className="text-xs text-emerald-400/70 mt-1.5 space-y-0.5">
{status.method && (
<p>
@@ -94,9 +81,7 @@ export function ClaudeCliStatus({
</div>
</div>
{status.recommendation && (
<p className="text-xs text-muted-foreground/70 ml-1">
{status.recommendation}
</p>
<p className="text-xs text-muted-foreground/70 ml-1">{status.recommendation}</p>
)}
</div>
) : (
@@ -106,24 +91,22 @@ export function ClaudeCliStatus({
<AlertCircle className="w-5 h-5 text-amber-500" />
</div>
<div className="flex-1">
<p className="text-sm font-medium text-amber-400">
Claude Code CLI Not Detected
</p>
<p className="text-sm font-medium text-amber-400">Claude Code CLI Not Detected</p>
<p className="text-xs text-amber-400/70 mt-1">
{status.recommendation ||
"Consider installing Claude Code CLI for optimal performance with ultrathink."}
'Consider installing Claude Code CLI for optimal performance with ultrathink.'}
</p>
</div>
</div>
{status.installCommands && (
<div className="space-y-3">
<p className="text-xs font-medium text-foreground/80">
Installation Commands:
</p>
<p className="text-xs font-medium text-foreground/80">Installation Commands:</p>
<div className="space-y-2">
{status.installCommands.npm && (
<div className="p-3 rounded-xl bg-accent/30 border border-border/50">
<p className="text-[10px] text-muted-foreground mb-1.5 font-medium uppercase tracking-wider">npm</p>
<p className="text-[10px] text-muted-foreground mb-1.5 font-medium uppercase tracking-wider">
npm
</p>
<code className="text-xs text-foreground/80 font-mono break-all">
{status.installCommands.npm}
</code>
@@ -131,7 +114,9 @@ export function ClaudeCliStatus({
)}
{status.installCommands.macos && (
<div className="p-3 rounded-xl bg-accent/30 border border-border/50">
<p className="text-[10px] text-muted-foreground mb-1.5 font-medium uppercase tracking-wider">macOS/Linux</p>
<p className="text-[10px] text-muted-foreground mb-1.5 font-medium uppercase tracking-wider">
macOS/Linux
</p>
<code className="text-xs text-foreground/80 font-mono break-all">
{status.installCommands.macos}
</code>
@@ -139,7 +124,9 @@ export function ClaudeCliStatus({
)}
{status.installCommands.windows && (
<div className="p-3 rounded-xl bg-accent/30 border border-border/50">
<p className="text-[10px] text-muted-foreground mb-1.5 font-medium uppercase tracking-wider">Windows (PowerShell)</p>
<p className="text-[10px] text-muted-foreground mb-1.5 font-medium uppercase tracking-wider">
Windows (PowerShell)
</p>
<code className="text-xs text-foreground/80 font-mono break-all">
{status.installCommands.windows}
</code>

View File

@@ -1,12 +1,12 @@
import { Keyboard } from "lucide-react";
import { Keyboard } from 'lucide-react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { KeyboardMap, ShortcutReferencePanel } from "@/components/ui/keyboard-map";
} from '@/components/ui/dialog';
import { KeyboardMap, ShortcutReferencePanel } from '@/components/ui/keyboard-map';
interface KeyboardMapDialogProps {
open: boolean;
@@ -23,8 +23,8 @@ export function KeyboardMapDialog({ open, onOpenChange }: KeyboardMapDialogProps
Keyboard Shortcut Map
</DialogTitle>
<DialogDescription className="text-muted-foreground">
Visual overview of all keyboard shortcuts. Keys in color are bound to
shortcuts. Click on any shortcut below to edit it.
Visual overview of all keyboard shortcuts. Keys in color are bound to shortcuts. Click
on any shortcut below to edit it.
</DialogDescription>
</DialogHeader>

View File

@@ -1,5 +1,5 @@
import { Settings } from "lucide-react";
import { cn } from "@/lib/utils";
import { Settings } from 'lucide-react';
import { cn } from '@/lib/utils';
interface SettingsHeaderProps {
title?: string;
@@ -7,23 +7,27 @@ interface SettingsHeaderProps {
}
export function SettingsHeader({
title = "Settings",
description = "Configure your API keys and preferences",
title = 'Settings',
description = 'Configure your API keys and preferences',
}: SettingsHeaderProps) {
return (
<div className={cn(
"shrink-0",
"border-b border-border/50",
"bg-gradient-to-r from-card/90 via-card/70 to-card/80 backdrop-blur-xl"
)}>
<div
className={cn(
'shrink-0',
'border-b border-border/50',
'bg-gradient-to-r from-card/90 via-card/70 to-card/80 backdrop-blur-xl'
)}
>
<div className="px-8 py-6">
<div className="flex items-center gap-4">
<div className={cn(
"w-12 h-12 rounded-2xl flex items-center justify-center",
"bg-gradient-to-br from-brand-500 to-brand-600",
"shadow-lg shadow-brand-500/25",
"ring-1 ring-white/10"
)}>
<div
className={cn(
'w-12 h-12 rounded-2xl flex items-center justify-center',
'bg-gradient-to-br from-brand-500 to-brand-600',
'shadow-lg shadow-brand-500/25',
'ring-1 ring-white/10'
)}
>
<Settings className="w-6 h-6 text-white" />
</div>
<div>

View File

@@ -1,7 +1,7 @@
import { cn } from "@/lib/utils";
import type { Project } from "@/lib/electron";
import type { NavigationItem } from "../config/navigation";
import type { SettingsViewId } from "../hooks/use-settings-view";
import { cn } from '@/lib/utils';
import type { Project } from '@/lib/electron';
import type { NavigationItem } from '../config/navigation';
import type { SettingsViewId } from '../hooks/use-settings-view';
interface SettingsNavigationProps {
navItems: NavigationItem[];
@@ -17,14 +17,16 @@ export function SettingsNavigation({
onNavigate,
}: SettingsNavigationProps) {
return (
<nav className={cn(
"hidden lg:block w-52 shrink-0",
"border-r border-border/50",
"bg-gradient-to-b from-card/80 via-card/60 to-card/40 backdrop-blur-xl"
)}>
<nav
className={cn(
'hidden lg:block w-52 shrink-0',
'border-r border-border/50',
'bg-gradient-to-b from-card/80 via-card/60 to-card/40 backdrop-blur-xl'
)}
>
<div className="sticky top-0 p-4 space-y-1.5">
{navItems
.filter((item) => item.id !== "danger" || currentProject)
.filter((item) => item.id !== 'danger' || currentProject)
.map((item) => {
const Icon = item.icon;
const isActive = activeSection === item.id;
@@ -33,20 +35,20 @@ export function SettingsNavigation({
key={item.id}
onClick={() => onNavigate(item.id)}
className={cn(
"group w-full flex items-center gap-2.5 px-3 py-2.5 rounded-xl text-sm font-medium transition-all duration-200 ease-out text-left relative overflow-hidden",
'group w-full flex items-center gap-2.5 px-3 py-2.5 rounded-xl text-sm font-medium transition-all duration-200 ease-out text-left relative overflow-hidden',
isActive
? [
"bg-gradient-to-r from-brand-500/15 via-brand-500/10 to-brand-600/5",
"text-foreground",
"border border-brand-500/25",
"shadow-sm shadow-brand-500/5",
'bg-gradient-to-r from-brand-500/15 via-brand-500/10 to-brand-600/5',
'text-foreground',
'border border-brand-500/25',
'shadow-sm shadow-brand-500/5',
]
: [
"text-muted-foreground hover:text-foreground",
"hover:bg-accent/50",
"border border-transparent hover:border-border/40",
'text-muted-foreground hover:text-foreground',
'hover:bg-accent/50',
'border border-transparent hover:border-border/40',
],
"hover:scale-[1.01] active:scale-[0.98]"
'hover:scale-[1.01] active:scale-[0.98]'
)}
>
{/* Active indicator bar */}
@@ -55,10 +57,8 @@ export function SettingsNavigation({
)}
<Icon
className={cn(
"w-4 h-4 shrink-0 transition-all duration-200",
isActive
? "text-brand-500"
: "group-hover:text-brand-400 group-hover:scale-110"
'w-4 h-4 shrink-0 transition-all duration-200',
isActive ? 'text-brand-500' : 'group-hover:text-brand-400 group-hover:scale-110'
)}
/>
<span className="truncate">{item.label}</span>

View File

@@ -1,4 +1,4 @@
import type { LucideIcon } from "lucide-react";
import type { LucideIcon } from 'lucide-react';
import {
Key,
Terminal,
@@ -9,8 +9,8 @@ import {
FlaskConical,
Trash2,
Sparkles,
} from "lucide-react";
import type { SettingsViewId } from "../hooks/use-settings-view";
} from 'lucide-react';
import type { SettingsViewId } from '../hooks/use-settings-view';
export interface NavigationItem {
id: SettingsViewId;
@@ -20,13 +20,13 @@ export interface NavigationItem {
// Navigation items for the settings side panel
export const NAV_ITEMS: NavigationItem[] = [
{ id: "api-keys", label: "API Keys", icon: Key },
{ id: "claude", label: "Claude", icon: Terminal },
{ id: "ai-enhancement", label: "AI Enhancement", icon: Sparkles },
{ id: "appearance", label: "Appearance", icon: Palette },
{ id: "terminal", label: "Terminal", icon: SquareTerminal },
{ id: "keyboard", label: "Keyboard Shortcuts", icon: Settings2 },
{ id: "audio", label: "Audio", icon: Volume2 },
{ id: "defaults", label: "Feature Defaults", icon: FlaskConical },
{ id: "danger", label: "Danger Zone", icon: Trash2 },
{ id: 'api-keys', label: 'API Keys', icon: Key },
{ id: 'claude', label: 'Claude', icon: Terminal },
{ id: 'ai-enhancement', label: 'AI Enhancement', icon: Sparkles },
{ id: 'appearance', label: 'Appearance', icon: Palette },
{ id: 'terminal', label: 'Terminal', icon: SquareTerminal },
{ id: 'keyboard', label: 'Keyboard Shortcuts', icon: Settings2 },
{ id: 'audio', label: 'Audio', icon: Volume2 },
{ id: 'defaults', label: 'Feature Defaults', icon: FlaskConical },
{ id: 'danger', label: 'Danger Zone', icon: Trash2 },
];

View File

@@ -1,26 +1,23 @@
import { Button } from "@/components/ui/button";
import { Trash2, Folder, AlertTriangle } from "lucide-react";
import { cn } from "@/lib/utils";
import type { Project } from "../shared/types";
import { Button } from '@/components/ui/button';
import { Trash2, Folder, AlertTriangle } from 'lucide-react';
import { cn } from '@/lib/utils';
import type { Project } from '../shared/types';
interface DangerZoneSectionProps {
project: Project | null;
onDeleteClick: () => void;
}
export function DangerZoneSection({
project,
onDeleteClick,
}: DangerZoneSectionProps) {
export function DangerZoneSection({ project, onDeleteClick }: DangerZoneSectionProps) {
if (!project) return null;
return (
<div
className={cn(
"rounded-2xl overflow-hidden",
"border border-destructive/30",
"bg-gradient-to-br from-destructive/5 via-card/70 to-card/80 backdrop-blur-xl",
"shadow-sm shadow-destructive/5"
'rounded-2xl overflow-hidden',
'border border-destructive/30',
'bg-gradient-to-br from-destructive/5 via-card/70 to-card/80 backdrop-blur-xl',
'shadow-sm shadow-destructive/5'
)}
>
<div className="p-6 border-b border-destructive/20 bg-gradient-to-r from-destructive/5 via-transparent to-transparent">
@@ -41,12 +38,8 @@ export function DangerZoneSection({
<Folder className="w-5 h-5 text-brand-500" />
</div>
<div className="min-w-0">
<p className="font-medium text-foreground truncate">
{project.name}
</p>
<p className="text-xs text-muted-foreground/70 truncate mt-0.5">
{project.path}
</p>
<p className="font-medium text-foreground truncate">{project.name}</p>
<p className="text-xs text-muted-foreground/70 truncate mt-0.5">{project.path}</p>
</div>
</div>
<Button
@@ -54,10 +47,10 @@ export function DangerZoneSection({
onClick={onDeleteClick}
data-testid="delete-project-button"
className={cn(
"shrink-0",
"shadow-md shadow-destructive/20 hover:shadow-lg hover:shadow-destructive/25",
"transition-all duration-200 ease-out",
"hover:scale-[1.02] active:scale-[0.98]"
'shrink-0',
'shadow-md shadow-destructive/20 hover:shadow-lg hover:shadow-destructive/25',
'transition-all duration-200 ease-out',
'hover:scale-[1.02] active:scale-[0.98]'
)}
>
<Trash2 className="w-4 h-4 mr-2" />

View File

@@ -1,18 +1,27 @@
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from '@/components/ui/label';
import { Checkbox } from '@/components/ui/checkbox';
import {
FlaskConical, Settings2, TestTube, GitBranch, AlertCircle,
Zap, ClipboardList, FileText, ScrollText, ShieldCheck, User
} from "lucide-react";
import { cn } from "@/lib/utils";
FlaskConical,
Settings2,
TestTube,
GitBranch,
AlertCircle,
Zap,
ClipboardList,
FileText,
ScrollText,
ShieldCheck,
User,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import type { AIProfile } from "@/store/app-store";
} from '@/components/ui/select';
import type { AIProfile } from '@/store/app-store';
type PlanningMode = 'skip' | 'lite' | 'spec' | 'full';
@@ -58,10 +67,10 @@ export function FeatureDefaultsSection({
return (
<div
className={cn(
"rounded-2xl overflow-hidden",
"border border-border/50",
"bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl",
"shadow-sm shadow-black/5"
'rounded-2xl overflow-hidden',
'border border-border/50',
'bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl',
'shadow-sm shadow-black/5'
)}
>
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
@@ -69,9 +78,7 @@ export function FeatureDefaultsSection({
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 flex items-center justify-center border border-brand-500/20">
<FlaskConical className="w-5 h-5 text-brand-500" />
</div>
<h2 className="text-lg font-semibold text-foreground tracking-tight">
Feature Defaults
</h2>
<h2 className="text-lg font-semibold text-foreground tracking-tight">Feature Defaults</h2>
</div>
<p className="text-sm text-muted-foreground/80 ml-12">
Configure default settings for new features.
@@ -80,13 +87,18 @@ export function FeatureDefaultsSection({
<div className="p-6 space-y-5">
{/* Planning Mode Default */}
<div className="group flex items-start space-x-3 p-3 rounded-xl hover:bg-accent/30 transition-colors duration-200 -mx-3">
<div className={cn(
"w-10 h-10 mt-0.5 rounded-xl flex items-center justify-center shrink-0",
defaultPlanningMode === 'skip' ? "bg-emerald-500/10" :
defaultPlanningMode === 'lite' ? "bg-blue-500/10" :
defaultPlanningMode === 'spec' ? "bg-purple-500/10" :
"bg-amber-500/10"
)}>
<div
className={cn(
'w-10 h-10 mt-0.5 rounded-xl flex items-center justify-center shrink-0',
defaultPlanningMode === 'skip'
? 'bg-emerald-500/10'
: defaultPlanningMode === 'lite'
? 'bg-blue-500/10'
: defaultPlanningMode === 'spec'
? 'bg-purple-500/10'
: 'bg-amber-500/10'
)}
>
{defaultPlanningMode === 'skip' && <Zap className="w-5 h-5 text-emerald-500" />}
{defaultPlanningMode === 'lite' && <ClipboardList className="w-5 h-5 text-blue-500" />}
{defaultPlanningMode === 'spec' && <FileText className="w-5 h-5 text-purple-500" />}
@@ -94,17 +106,12 @@ export function FeatureDefaultsSection({
</div>
<div className="flex-1 space-y-2">
<div className="flex items-center justify-between">
<Label className="text-foreground font-medium">
Default Planning Mode
</Label>
<Label className="text-foreground font-medium">Default Planning Mode</Label>
<Select
value={defaultPlanningMode}
onValueChange={(v: string) => onDefaultPlanningModeChange(v as PlanningMode)}
>
<SelectTrigger
className="w-[160px] h-8"
data-testid="default-planning-mode-select"
>
<SelectTrigger className="w-[160px] h-8" data-testid="default-planning-mode-select">
<SelectValue />
</SelectTrigger>
<SelectContent>
@@ -137,10 +144,14 @@ export function FeatureDefaultsSection({
</Select>
</div>
<p className="text-xs text-muted-foreground/80 leading-relaxed">
{defaultPlanningMode === 'skip' && "Jump straight to implementation without upfront planning."}
{defaultPlanningMode === 'lite' && "Create a quick planning outline with tasks before building."}
{defaultPlanningMode === 'spec' && "Generate a specification with acceptance criteria for approval."}
{defaultPlanningMode === 'full' && "Create comprehensive spec with phased implementation plan."}
{defaultPlanningMode === 'skip' &&
'Jump straight to implementation without upfront planning.'}
{defaultPlanningMode === 'lite' &&
'Create a quick planning outline with tasks before building.'}
{defaultPlanningMode === 'spec' &&
'Generate a specification with acceptance criteria for approval.'}
{defaultPlanningMode === 'full' &&
'Create comprehensive spec with phased implementation plan.'}
</p>
</div>
</div>
@@ -152,9 +163,7 @@ export function FeatureDefaultsSection({
<Checkbox
id="default-require-plan-approval"
checked={defaultRequirePlanApproval}
onCheckedChange={(checked) =>
onDefaultRequirePlanApprovalChange(checked === true)
}
onCheckedChange={(checked) => onDefaultRequirePlanApprovalChange(checked === true)}
className="mt-1"
data-testid="default-require-plan-approval-checkbox"
/>
@@ -187,17 +196,12 @@ export function FeatureDefaultsSection({
</div>
<div className="flex-1 space-y-2">
<div className="flex items-center justify-between">
<Label className="text-foreground font-medium">
Default AI Profile
</Label>
<Label className="text-foreground font-medium">Default AI Profile</Label>
<Select
value={defaultAIProfileId ?? "none"}
onValueChange={(v: string) => onDefaultAIProfileIdChange(v === "none" ? null : v)}
value={defaultAIProfileId ?? 'none'}
onValueChange={(v: string) => onDefaultAIProfileIdChange(v === 'none' ? null : v)}
>
<SelectTrigger
className="w-[180px] h-8"
data-testid="default-ai-profile-select"
>
<SelectTrigger className="w-[180px] h-8" data-testid="default-ai-profile-select">
<SelectValue />
</SelectTrigger>
<SelectContent>
@@ -215,7 +219,7 @@ export function FeatureDefaultsSection({
<p className="text-xs text-muted-foreground/80 leading-relaxed">
{selectedProfile
? `New features will use the "${selectedProfile.name}" profile (${selectedProfile.model}, ${selectedProfile.thinkingLevel} thinking).`
: "Pre-select an AI profile when creating new features. Choose \"None\" to pick manually each time."}
: 'Pre-select an AI profile when creating new features. Choose "None" to pick manually each time.'}
</p>
</div>
</div>
@@ -228,9 +232,7 @@ export function FeatureDefaultsSection({
<Checkbox
id="show-profiles-only"
checked={showProfilesOnly}
onCheckedChange={(checked) =>
onShowProfilesOnlyChange(checked === true)
}
onCheckedChange={(checked) => onShowProfilesOnlyChange(checked === true)}
className="mt-1"
data-testid="show-profiles-only-checkbox"
/>
@@ -243,9 +245,8 @@ export function FeatureDefaultsSection({
Show profiles only by default
</Label>
<p className="text-xs text-muted-foreground/80 leading-relaxed">
When enabled, the Add Feature dialog will show only AI profiles
and hide advanced model tweaking options. This creates a cleaner, less
overwhelming UI.
When enabled, the Add Feature dialog will show only AI profiles and hide advanced
model tweaking options. This creates a cleaner, less overwhelming UI.
</p>
</div>
</div>
@@ -258,9 +259,7 @@ export function FeatureDefaultsSection({
<Checkbox
id="default-skip-tests"
checked={!defaultSkipTests}
onCheckedChange={(checked) =>
onDefaultSkipTestsChange(checked !== true)
}
onCheckedChange={(checked) => onDefaultSkipTestsChange(checked !== true)}
className="mt-1"
data-testid="default-skip-tests-checkbox"
/>
@@ -273,8 +272,8 @@ export function FeatureDefaultsSection({
Enable automated testing by default
</Label>
<p className="text-xs text-muted-foreground/80 leading-relaxed">
When enabled, new features will use TDD with automated tests. When disabled, features will
require manual verification.
When enabled, new features will use TDD with automated tests. When disabled, features
will require manual verification.
</p>
</div>
</div>
@@ -287,9 +286,7 @@ export function FeatureDefaultsSection({
<Checkbox
id="enable-dependency-blocking"
checked={enableDependencyBlocking}
onCheckedChange={(checked) =>
onEnableDependencyBlockingChange(checked === true)
}
onCheckedChange={(checked) => onEnableDependencyBlockingChange(checked === true)}
className="mt-1"
data-testid="enable-dependency-blocking-checkbox"
/>
@@ -302,9 +299,9 @@ export function FeatureDefaultsSection({
Enable Dependency Blocking
</Label>
<p className="text-xs text-muted-foreground/80 leading-relaxed">
When enabled, features with incomplete dependencies will show blocked badges
and warnings. Auto mode and backlog ordering always respect dependencies
regardless of this setting.
When enabled, features with incomplete dependencies will show blocked badges and
warnings. Auto mode and backlog ordering always respect dependencies regardless of
this setting.
</p>
</div>
</div>
@@ -317,9 +314,7 @@ export function FeatureDefaultsSection({
<Checkbox
id="use-worktrees"
checked={useWorktrees}
onCheckedChange={(checked) =>
onUseWorktreesChange(checked === true)
}
onCheckedChange={(checked) => onUseWorktreesChange(checked === true)}
className="mt-1"
data-testid="use-worktrees-checkbox"
/>
@@ -335,8 +330,8 @@ export function FeatureDefaultsSection({
</span>
</Label>
<p className="text-xs text-muted-foreground/80 leading-relaxed">
Creates isolated git branches for each feature. When disabled,
agents work directly in the main project directory.
Creates isolated git branches for each feature. When disabled, agents work directly in
the main project directory.
</p>
</div>
</div>

View File

@@ -1,2 +1,2 @@
export { useCliStatus } from "./use-cli-status";
export { useSettingsView, type SettingsViewId } from "./use-settings-view";
export { useCliStatus } from './use-cli-status';
export { useSettingsView, type SettingsViewId } from './use-settings-view';

View File

@@ -1,6 +1,6 @@
import { useState, useEffect, useCallback } from "react";
import { useSetupStore } from "@/store/setup-store";
import { getElectronAPI } from "@/lib/electron";
import { useState, useEffect, useCallback } from 'react';
import { useSetupStore } from '@/store/setup-store';
import { getElectronAPI } from '@/lib/electron';
interface CliStatusResult {
success: boolean;
@@ -25,8 +25,7 @@ interface CliStatusResult {
export function useCliStatus() {
const { setClaudeAuthStatus } = useSetupStore();
const [claudeCliStatus, setClaudeCliStatus] =
useState<CliStatusResult | null>(null);
const [claudeCliStatus, setClaudeCliStatus] = useState<CliStatusResult | null>(null);
const [isCheckingClaudeCli, setIsCheckingClaudeCli] = useState(false);
@@ -41,7 +40,7 @@ export function useCliStatus() {
const status = await api.checkClaudeCli();
setClaudeCliStatus(status);
} catch (error) {
console.error("Failed to check Claude CLI status:", error);
console.error('Failed to check Claude CLI status:', error);
}
}
@@ -57,16 +56,27 @@ export function useCliStatus() {
};
// Map server method names to client method types
// Server returns: oauth_token_env, oauth_token, api_key_env, api_key, credentials_file, cli_authenticated, none
const validMethods = ["oauth_token_env", "oauth_token", "api_key", "api_key_env", "credentials_file", "cli_authenticated", "none"] as const;
type AuthMethod = typeof validMethods[number];
const validMethods = [
'oauth_token_env',
'oauth_token',
'api_key',
'api_key_env',
'credentials_file',
'cli_authenticated',
'none',
] as const;
type AuthMethod = (typeof validMethods)[number];
const method: AuthMethod = validMethods.includes(auth.method as AuthMethod)
? (auth.method as AuthMethod)
: auth.authenticated ? "api_key" : "none"; // Default authenticated to api_key, not none
: auth.authenticated
? 'api_key'
: 'none'; // Default authenticated to api_key, not none
const authStatus = {
authenticated: auth.authenticated,
method,
hasCredentialsFile: auth.hasCredentialsFile ?? false,
oauthTokenValid: auth.oauthTokenValid || auth.hasStoredOAuthToken || auth.hasEnvOAuthToken,
oauthTokenValid:
auth.oauthTokenValid || auth.hasStoredOAuthToken || auth.hasEnvOAuthToken,
apiKeyValid: auth.apiKeyValid || auth.hasStoredApiKey || auth.hasEnvApiKey,
hasEnvOAuthToken: auth.hasEnvOAuthToken,
hasEnvApiKey: auth.hasEnvApiKey,
@@ -74,7 +84,7 @@ export function useCliStatus() {
setClaudeAuthStatus(authStatus);
}
} catch (error) {
console.error("Failed to check Claude auth status:", error);
console.error('Failed to check Claude auth status:', error);
}
}
};
@@ -92,7 +102,7 @@ export function useCliStatus() {
setClaudeCliStatus(status);
}
} catch (error) {
console.error("Failed to refresh Claude CLI status:", error);
console.error('Failed to refresh Claude CLI status:', error);
} finally {
setIsCheckingClaudeCli(false);
}

View File

@@ -1,23 +1,21 @@
import { useState, useCallback } from "react";
import { useState, useCallback } from 'react';
export type SettingsViewId =
| "api-keys"
| "claude"
| "ai-enhancement"
| "appearance"
| "terminal"
| "keyboard"
| "audio"
| "defaults"
| "danger";
| 'api-keys'
| 'claude'
| 'ai-enhancement'
| 'appearance'
| 'terminal'
| 'keyboard'
| 'audio'
| 'defaults'
| 'danger';
interface UseSettingsViewOptions {
initialView?: SettingsViewId;
}
export function useSettingsView({
initialView = "api-keys",
}: UseSettingsViewOptions = {}) {
export function useSettingsView({ initialView = 'api-keys' }: UseSettingsViewOptions = {}) {
const [activeView, setActiveView] = useState<SettingsViewId>(initialView);
const navigateTo = useCallback((viewId: SettingsViewId) => {

View File

@@ -1,21 +1,19 @@
import { Button } from "@/components/ui/button";
import { Settings2, Keyboard } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from '@/components/ui/button';
import { Settings2, Keyboard } from 'lucide-react';
import { cn } from '@/lib/utils';
interface KeyboardShortcutsSectionProps {
onOpenKeyboardMap: () => void;
}
export function KeyboardShortcutsSection({
onOpenKeyboardMap,
}: KeyboardShortcutsSectionProps) {
export function KeyboardShortcutsSection({ onOpenKeyboardMap }: KeyboardShortcutsSectionProps) {
return (
<div
className={cn(
"rounded-2xl overflow-hidden",
"border border-border/50",
"bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl",
"shadow-sm shadow-black/5"
'rounded-2xl overflow-hidden',
'border border-border/50',
'bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl',
'shadow-sm shadow-black/5'
)}
>
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
@@ -28,8 +26,7 @@ export function KeyboardShortcutsSection({
</h2>
</div>
<p className="text-sm text-muted-foreground/80 ml-12">
Customize keyboard shortcuts for navigation and actions using the
visual keyboard map.
Customize keyboard shortcuts for navigation and actions using the visual keyboard map.
</p>
</div>
<div className="p-6">
@@ -42,12 +39,10 @@ export function KeyboardShortcutsSection({
<div className="absolute inset-0 bg-brand-500/10 blur-2xl rounded-full -z-10" />
</div>
<div className="space-y-2 max-w-md">
<h3 className="text-lg font-semibold text-foreground">
Use the Visual Keyboard Map
</h3>
<h3 className="text-lg font-semibold text-foreground">Use the Visual Keyboard Map</h3>
<p className="text-sm text-muted-foreground/80">
Click the button below to customize your keyboard shortcuts. The visual
interface shows all available keys and lets you easily edit shortcuts.
Click the button below to customize your keyboard shortcuts. The visual interface
shows all available keys and lets you easily edit shortcuts.
</p>
</div>
<Button
@@ -55,13 +50,13 @@ export function KeyboardShortcutsSection({
size="lg"
onClick={onOpenKeyboardMap}
className={cn(
"gap-2.5 mt-2 h-11 px-6",
"bg-gradient-to-r from-brand-500 to-brand-600",
"hover:from-brand-600 hover:to-brand-600",
"text-white font-medium border-0",
"shadow-md shadow-brand-500/20 hover:shadow-lg hover:shadow-brand-500/25",
"transition-all duration-200 ease-out",
"hover:scale-[1.02] active:scale-[0.98]"
'gap-2.5 mt-2 h-11 px-6',
'bg-gradient-to-r from-brand-500 to-brand-600',
'hover:from-brand-600 hover:to-brand-600',
'text-white font-medium border-0',
'shadow-md shadow-brand-500/20 hover:shadow-lg hover:shadow-brand-500/25',
'transition-all duration-200 ease-out',
'hover:scale-[1.02] active:scale-[0.98]'
)}
>
<Keyboard className="w-5 h-5" />

View File

@@ -1,12 +1,12 @@
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Slider } from "@/components/ui/slider";
import { Input } from "@/components/ui/input";
import { SquareTerminal } from "lucide-react";
import { cn } from "@/lib/utils";
import { useAppStore } from "@/store/app-store";
import { toast } from "sonner";
import { TERMINAL_FONT_OPTIONS } from "@/config/terminal-themes";
import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
import { Slider } from '@/components/ui/slider';
import { Input } from '@/components/ui/input';
import { SquareTerminal } from 'lucide-react';
import { cn } from '@/lib/utils';
import { useAppStore } from '@/store/app-store';
import { toast } from 'sonner';
import { TERMINAL_FONT_OPTIONS } from '@/config/terminal-themes';
export function TerminalSection() {
const {
@@ -31,10 +31,10 @@ export function TerminalSection() {
return (
<div
className={cn(
"rounded-2xl overflow-hidden",
"border border-border/50",
"bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl",
"shadow-sm shadow-black/5"
'rounded-2xl overflow-hidden',
'border border-border/50',
'bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl',
'shadow-sm shadow-black/5'
)}
>
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
@@ -45,7 +45,8 @@ export function TerminalSection() {
<h2 className="text-lg font-semibold text-foreground tracking-tight">Terminal</h2>
</div>
<p className="text-sm text-muted-foreground/80 ml-12">
Customize terminal appearance and behavior. Theme follows your app theme in Appearance settings.
Customize terminal appearance and behavior. Theme follows your app theme in Appearance
settings.
</p>
</div>
<div className="p-6 space-y-6">
@@ -56,15 +57,15 @@ export function TerminalSection() {
value={fontFamily}
onChange={(e) => {
setTerminalFontFamily(e.target.value);
toast.info("Font family changed", {
description: "Restart terminal for changes to take effect",
toast.info('Font family changed', {
description: 'Restart terminal for changes to take effect',
});
}}
className={cn(
"w-full px-3 py-2 rounded-lg",
"bg-accent/30 border border-border/50",
"text-foreground text-sm",
"focus:outline-none focus:ring-2 focus:ring-green-500/30"
'w-full px-3 py-2 rounded-lg',
'bg-accent/30 border border-border/50',
'text-foreground text-sm',
'focus:outline-none focus:ring-2 focus:ring-green-500/30'
)}
>
{TERMINAL_FONT_OPTIONS.map((font) => (
@@ -106,8 +107,8 @@ export function TerminalSection() {
setTerminalLineHeight(value);
}}
onValueCommit={() => {
toast.info("Line height changed", {
description: "Restart terminal for changes to take effect",
toast.info('Line height changed', {
description: 'Restart terminal for changes to take effect',
});
}}
className="flex-1"
@@ -129,8 +130,8 @@ export function TerminalSection() {
step={1000}
onValueChange={([value]) => setTerminalScrollbackLines(value)}
onValueCommit={() => {
toast.info("Scrollback changed", {
description: "Restart terminal for changes to take effect",
toast.info('Scrollback changed', {
description: 'Restart terminal for changes to take effect',
});
}}
className="flex-1"
@@ -163,9 +164,12 @@ export function TerminalSection() {
checked={screenReaderMode}
onCheckedChange={(checked) => {
setTerminalScreenReaderMode(checked);
toast.success(checked ? "Screen reader mode enabled" : "Screen reader mode disabled", {
description: "Restart terminal for changes to take effect",
});
toast.success(
checked ? 'Screen reader mode enabled' : 'Screen reader mode disabled',
{
description: 'Restart terminal for changes to take effect',
}
);
}}
/>
</div>

View File

@@ -1,4 +1,4 @@
import { ReactNode } from "react";
import { ReactNode } from 'react';
interface AuthMethodOption {
id: string;
@@ -17,39 +17,38 @@ interface AuthMethodSelectorProps {
// Map badge colors to complete Tailwind class names
const getBadgeClasses = (badgeColor: string) => {
const colorMap: Record<string, { border: string; bg: string; text: string }> = {
"brand-500": {
border: "hover:border-brand-500/50",
bg: "hover:bg-brand-500/5",
text: "text-brand-500",
'brand-500': {
border: 'hover:border-brand-500/50',
bg: 'hover:bg-brand-500/5',
text: 'text-brand-500',
},
"green-500": {
border: "hover:border-green-500/50",
bg: "hover:bg-green-500/5",
text: "text-green-500",
'green-500': {
border: 'hover:border-green-500/50',
bg: 'hover:bg-green-500/5',
text: 'text-green-500',
},
"blue-500": {
border: "hover:border-blue-500/50",
bg: "hover:bg-blue-500/5",
text: "text-blue-500",
'blue-500': {
border: 'hover:border-blue-500/50',
bg: 'hover:bg-blue-500/5',
text: 'text-blue-500',
},
"purple-500": {
border: "hover:border-purple-500/50",
bg: "hover:bg-purple-500/5",
text: "text-purple-500",
'purple-500': {
border: 'hover:border-purple-500/50',
bg: 'hover:bg-purple-500/5',
text: 'text-purple-500',
},
};
return colorMap[badgeColor] || {
border: "hover:border-brand-500/50",
bg: "hover:bg-brand-500/5",
text: "text-brand-500",
};
return (
colorMap[badgeColor] || {
border: 'hover:border-brand-500/50',
bg: 'hover:bg-brand-500/5',
text: 'text-brand-500',
}
);
};
export function AuthMethodSelector({
options,
onSelect,
}: AuthMethodSelectorProps) {
export function AuthMethodSelector({ options, onSelect }: AuthMethodSelectorProps) {
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{options.map((option) => {
@@ -65,12 +64,8 @@ export function AuthMethodSelector({
{option.icon}
<div>
<p className="font-medium text-foreground">{option.title}</p>
<p className="text-sm text-muted-foreground mt-1">
{option.description}
</p>
<p className={`text-xs ${badgeClasses.text} mt-2`}>
{option.badge}
</p>
<p className="text-sm text-muted-foreground mt-1">{option.description}</p>
<p className={`text-xs ${badgeClasses.text} mt-2`}>{option.badge}</p>
</div>
</div>
</button>

View File

@@ -1,14 +1,8 @@
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Download, Loader2, AlertCircle } from "lucide-react";
import { CopyableCommandField } from "./copyable-command-field";
import { TerminalOutput } from "./terminal-output";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Download, Loader2, AlertCircle } from 'lucide-react';
import { CopyableCommandField } from './copyable-command-field';
import { TerminalOutput } from './terminal-output';
interface CommandInfo {
label: string; // e.g., "macOS / Linux"
@@ -23,7 +17,7 @@ interface CliInstallationCardProps {
installProgress: { output: string[] };
onInstall: () => void;
warningMessage?: string;
color?: "brand" | "green"; // For different CLI themes
color?: 'brand' | 'green'; // For different CLI themes
}
export function CliInstallationCard({
@@ -34,11 +28,11 @@ export function CliInstallationCard({
installProgress,
onInstall,
warningMessage,
color = "brand",
color = 'brand',
}: CliInstallationCardProps) {
const colorClasses = {
brand: "bg-brand-500 hover:bg-brand-600",
green: "bg-green-500 hover:bg-green-600",
brand: 'bg-brand-500 hover:bg-brand-600',
green: 'bg-green-500 hover:bg-green-600',
};
return (
@@ -52,16 +46,10 @@ export function CliInstallationCard({
</CardHeader>
<CardContent className="space-y-4">
{commands.map((cmd, index) => (
<CopyableCommandField
key={index}
label={cmd.label}
command={cmd.command}
/>
<CopyableCommandField key={index} label={cmd.label} command={cmd.command} />
))}
{isInstalling && (
<TerminalOutput lines={installProgress.output} />
)}
{isInstalling && <TerminalOutput lines={installProgress.output} />}
<Button
onClick={onInstall}
@@ -86,9 +74,7 @@ export function CliInstallationCard({
<div className="p-3 rounded-lg bg-yellow-500/10 border border-yellow-500/20">
<div className="flex items-start gap-2">
<AlertCircle className="w-4 h-4 text-yellow-500 mt-0.5" />
<p className="text-xs text-yellow-600 dark:text-yellow-400">
{warningMessage}
</p>
<p className="text-xs text-yellow-600 dark:text-yellow-400">{warningMessage}</p>
</div>
</div>
)}

View File

@@ -1,26 +1,21 @@
import { Button } from "@/components/ui/button";
import { Copy } from "lucide-react";
import { toast } from "sonner";
import { Button } from '@/components/ui/button';
import { Copy } from 'lucide-react';
import { toast } from 'sonner';
interface CopyableCommandFieldProps {
command: string;
label?: string;
}
export function CopyableCommandField({
command,
label,
}: CopyableCommandFieldProps) {
export function CopyableCommandField({ command, label }: CopyableCommandFieldProps) {
const copyToClipboard = () => {
navigator.clipboard.writeText(command);
toast.success("Command copied to clipboard");
toast.success('Command copied to clipboard');
};
return (
<div className="space-y-2">
{label && (
<span className="text-sm text-muted-foreground">{label}</span>
)}
{label && <span className="text-sm text-muted-foreground">{label}</span>}
<div className="flex items-center gap-2">
<code className="flex-1 bg-muted px-3 py-2 rounded text-sm font-mono text-foreground">
{command}

View File

@@ -1,9 +1,9 @@
// Re-export all setup-view components for easier imports
export { StepIndicator } from "./step-indicator";
export { StatusBadge } from "./status-badge";
export { StatusRow } from "./status-row";
export { TerminalOutput } from "./terminal-output";
export { CopyableCommandField } from "./copyable-command-field";
export { CliInstallationCard } from "./cli-installation-card";
export { ReadyStateCard } from "./ready-state-card";
export { AuthMethodSelector } from "./auth-method-selector";
export { StepIndicator } from './step-indicator';
export { StatusBadge } from './status-badge';
export { StatusRow } from './status-row';
export { TerminalOutput } from './terminal-output';
export { CopyableCommandField } from './copyable-command-field';
export { CliInstallationCard } from './cli-installation-card';
export { ReadyStateCard } from './ready-state-card';
export { AuthMethodSelector } from './auth-method-selector';

View File

@@ -1,25 +1,21 @@
import { Card, CardContent } from "@/components/ui/card";
import { CheckCircle2 } from "lucide-react";
import { Card, CardContent } from '@/components/ui/card';
import { CheckCircle2 } from 'lucide-react';
interface ReadyStateCardProps {
title: string;
description: string;
variant?: "success" | "info";
variant?: 'success' | 'info';
}
export function ReadyStateCard({
title,
description,
variant = "success",
}: ReadyStateCardProps) {
export function ReadyStateCard({ title, description, variant = 'success' }: ReadyStateCardProps) {
const variantClasses = {
success: "bg-green-500/5 border-green-500/20",
info: "bg-blue-500/5 border-blue-500/20",
success: 'bg-green-500/5 border-green-500/20',
info: 'bg-blue-500/5 border-blue-500/20',
};
const iconColorClasses = {
success: "bg-green-500/10 text-green-500",
info: "bg-blue-500/10 text-blue-500",
success: 'bg-green-500/10 text-green-500',
info: 'bg-blue-500/10 text-blue-500',
};
return (

View File

@@ -1,46 +1,46 @@
import { CheckCircle2, XCircle, Loader2, AlertCircle } from "lucide-react";
import { CheckCircle2, XCircle, Loader2, AlertCircle } from 'lucide-react';
interface StatusBadgeProps {
status:
| "installed"
| "not_installed"
| "checking"
| "authenticated"
| "not_authenticated"
| "error"
| "unverified";
| 'installed'
| 'not_installed'
| 'checking'
| 'authenticated'
| 'not_authenticated'
| 'error'
| 'unverified';
label: string;
}
export function StatusBadge({ status, label }: StatusBadgeProps) {
const getStatusConfig = () => {
switch (status) {
case "installed":
case "authenticated":
case 'installed':
case 'authenticated':
return {
icon: <CheckCircle2 className="w-4 h-4" />,
className: "bg-green-500/10 text-green-500 border-green-500/20",
className: 'bg-green-500/10 text-green-500 border-green-500/20',
};
case "not_installed":
case "not_authenticated":
case 'not_installed':
case 'not_authenticated':
return {
icon: <XCircle className="w-4 h-4" />,
className: "bg-red-500/10 text-red-500 border-red-500/20",
className: 'bg-red-500/10 text-red-500 border-red-500/20',
};
case "error":
case 'error':
return {
icon: <XCircle className="w-4 h-4" />,
className: "bg-red-500/10 text-red-500 border-red-500/20",
className: 'bg-red-500/10 text-red-500 border-red-500/20',
};
case "checking":
case 'checking':
return {
icon: <Loader2 className="w-4 h-4 animate-spin" />,
className: "bg-yellow-500/10 text-yellow-500 border-yellow-500/20",
className: 'bg-yellow-500/10 text-yellow-500 border-yellow-500/20',
};
case "unverified":
case 'unverified':
return {
icon: <AlertCircle className="w-4 h-4" />,
className: "bg-yellow-500/10 text-yellow-500 border-yellow-500/20",
className: 'bg-yellow-500/10 text-yellow-500 border-yellow-500/20',
};
}
};

View File

@@ -1,31 +1,19 @@
import { StatusBadge } from "./status-badge";
import { StatusBadge } from './status-badge';
interface StatusRowProps {
label: string;
status:
| "checking"
| "installed"
| "not_installed"
| "authenticated"
| "not_authenticated";
status: 'checking' | 'installed' | 'not_installed' | 'authenticated' | 'not_authenticated';
statusLabel: string;
metadata?: string; // e.g., "(Subscription Token)"
}
export function StatusRow({
label,
status,
statusLabel,
metadata,
}: StatusRowProps) {
export function StatusRow({ label, status, statusLabel, metadata }: StatusRowProps) {
return (
<div className="flex items-center justify-between">
<span className="text-sm text-foreground">{label}</span>
<div className="flex items-center gap-2">
<StatusBadge status={status} label={statusLabel} />
{metadata && (
<span className="text-xs text-muted-foreground">{metadata}</span>
)}
{metadata && <span className="text-xs text-muted-foreground">{metadata}</span>}
</div>
</div>
);

View File

@@ -3,19 +3,14 @@ interface StepIndicatorProps {
totalSteps: number;
}
export function StepIndicator({
currentStep,
totalSteps,
}: StepIndicatorProps) {
export function StepIndicator({ currentStep, totalSteps }: StepIndicatorProps) {
return (
<div className="flex items-center justify-center gap-2 mb-8">
{Array.from({ length: totalSteps }).map((_, index) => (
<div
key={index}
className={`h-2 rounded-full transition-all duration-300 ${
index <= currentStep
? "w-8 bg-brand-500"
: "w-2 bg-muted-foreground/30"
index <= currentStep ? 'w-8 bg-brand-500' : 'w-2 bg-muted-foreground/30'
}`}
/>
))}

View File

@@ -1,4 +1,4 @@
// Re-export all hooks for easier imports
export { useCliStatus } from "./use-cli-status";
export { useCliInstallation } from "./use-cli-installation";
export { useTokenSave } from "./use-token-save";
export { useCliStatus } from './use-cli-status';
export { useCliInstallation } from './use-cli-installation';
export { useTokenSave } from './use-token-save';

View File

@@ -1,8 +1,8 @@
import { useState, useCallback } from "react";
import { toast } from "sonner";
import { useState, useCallback } from 'react';
import { toast } from 'sonner';
interface UseCliInstallationOptions {
cliType: "claude";
cliType: 'claude';
installApi: () => Promise<any>;
onProgressEvent?: (callback: (progress: any) => void) => (() => void) | undefined;
onSuccess?: () => void;
@@ -29,20 +29,22 @@ export function useCliInstallation({
let unsubscribe: (() => void) | undefined;
if (onProgressEvent) {
unsubscribe = onProgressEvent((progress: { cli?: string; data?: string; type?: string }) => {
if (progress.cli === cliType) {
setInstallProgress((prev) => ({
output: [...prev.output, progress.data || progress.type || ""],
}));
unsubscribe = onProgressEvent(
(progress: { cli?: string; data?: string; type?: string }) => {
if (progress.cli === cliType) {
setInstallProgress((prev) => ({
output: [...prev.output, progress.data || progress.type || ''],
}));
}
}
});
);
}
const result = await installApi();
unsubscribe?.();
if (result.success) {
if (cliType === "claude" && onSuccess && getStoreState) {
if (cliType === 'claude' && onSuccess && getStoreState) {
// Claude-specific: retry logic to detect installation
let retries = 5;
let detected = false;
@@ -68,7 +70,7 @@ export function useCliInstallation({
if (!detected) {
toast.success(`${cliType} CLI installation completed`, {
description:
"The CLI was installed but may need a terminal restart to be detected. You can continue with authentication if you have a token.",
'The CLI was installed but may need a terminal restart to be detected. You can continue with authentication if you have a token.',
duration: 7000,
});
}
@@ -77,11 +79,11 @@ export function useCliInstallation({
onSuccess?.();
}
} else {
toast.error("Installation failed", { description: result.error });
toast.error('Installation failed', { description: result.error });
}
} catch (error) {
console.error(`Failed to install ${cliType}:`, error);
toast.error("Installation failed");
toast.error('Installation failed');
} finally {
setIsInstalling(false);
}

View File

@@ -1,7 +1,7 @@
import { useState, useCallback } from "react";
import { useState, useCallback } from 'react';
interface UseCliStatusOptions {
cliType: "claude";
cliType: 'claude';
statusApi: () => Promise<any>;
setCliStatus: (status: any) => void;
setAuthStatus: (status: any) => void;
@@ -24,10 +24,10 @@ export function useCliStatus({
if (result.success) {
const cliStatus = {
installed: result.status === "installed",
installed: result.status === 'installed',
path: result.path || null,
version: result.version || null,
method: result.method || "none",
method: result.method || 'none',
};
console.log(`[${cliType} Setup] CLI Status:`, cliStatus);
setCliStatus(cliStatus);
@@ -35,29 +35,24 @@ export function useCliStatus({
if (result.auth) {
// Validate method is one of the expected values, default to "none"
const validMethods = [
"oauth_token_env",
"oauth_token",
"api_key",
"api_key_env",
"credentials_file",
"cli_authenticated",
"none",
'oauth_token_env',
'oauth_token',
'api_key',
'api_key_env',
'credentials_file',
'cli_authenticated',
'none',
] as const;
type AuthMethod = (typeof validMethods)[number];
const method: AuthMethod = validMethods.includes(
result.auth.method as AuthMethod
)
const method: AuthMethod = validMethods.includes(result.auth.method as AuthMethod)
? (result.auth.method as AuthMethod)
: "none";
: 'none';
const authStatus = {
authenticated: result.auth.authenticated,
method,
hasCredentialsFile: false,
oauthTokenValid:
result.auth.hasStoredOAuthToken ||
result.auth.hasEnvOAuthToken,
apiKeyValid:
result.auth.hasStoredApiKey || result.auth.hasEnvApiKey,
oauthTokenValid: result.auth.hasStoredOAuthToken || result.auth.hasEnvOAuthToken,
apiKeyValid: result.auth.hasStoredApiKey || result.auth.hasEnvApiKey,
hasEnvOAuthToken: result.auth.hasEnvOAuthToken,
hasEnvApiKey: result.auth.hasEnvApiKey,
};

View File

@@ -1,6 +1,6 @@
import { useState, useCallback } from "react";
import { toast } from "sonner";
import { getElectronAPI } from "@/lib/electron";
import { useState, useCallback } from 'react';
import { toast } from 'sonner';
import { getElectronAPI } from '@/lib/electron';
interface UseTokenSaveOptions {
provider: string; // e.g., "anthropic_oauth_token", "anthropic", "openai"
@@ -13,7 +13,7 @@ export function useTokenSave({ provider, onSuccess }: UseTokenSaveOptions) {
const saveToken = useCallback(
async (tokenValue: string) => {
if (!tokenValue.trim()) {
toast.error("Please enter a valid token");
toast.error('Please enter a valid token');
return false;
}
@@ -27,25 +27,23 @@ export function useTokenSave({ provider, onSuccess }: UseTokenSaveOptions) {
console.log(`[Token Save] Store result for ${provider}:`, result);
if (result.success) {
const tokenType = provider.includes("oauth")
? "subscription token"
: "API key";
const tokenType = provider.includes('oauth') ? 'subscription token' : 'API key';
toast.success(`${tokenType} saved successfully`);
onSuccess?.();
return true;
} else {
toast.error("Failed to save token", { description: result.error });
toast.error('Failed to save token', { description: result.error });
return false;
}
} else {
// Web mode fallback - just show success
toast.success("Token saved");
toast.success('Token saved');
onSuccess?.();
return true;
}
} catch (error) {
console.error(`[Token Save] Failed to save ${provider}:`, error);
toast.error("Failed to save token");
toast.error('Failed to save token');
return false;
} finally {
setIsSaving(false);

View File

@@ -1,15 +1,8 @@
import { useState, useEffect, useCallback } from "react";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { useSetupStore } from "@/store/setup-store";
import { getElectronAPI } from "@/lib/electron";
import { useState, useEffect, useCallback } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { useSetupStore } from '@/store/setup-store';
import { getElectronAPI } from '@/lib/electron';
import {
CheckCircle2,
Loader2,
@@ -21,9 +14,9 @@ import {
AlertTriangle,
Github,
XCircle,
} from "lucide-react";
import { toast } from "sonner";
import { StatusBadge } from "../components";
} from 'lucide-react';
import { toast } from 'sonner';
import { StatusBadge } from '../components';
interface GitHubSetupStepProps {
onNext: () => void;
@@ -31,11 +24,7 @@ interface GitHubSetupStepProps {
onSkip: () => void;
}
export function GitHubSetupStep({
onNext,
onBack,
onSkip,
}: GitHubSetupStepProps) {
export function GitHubSetupStep({ onNext, onBack, onSkip }: GitHubSetupStepProps) {
const { ghCliStatus, setGhCliStatus } = useSetupStore();
const [isChecking, setIsChecking] = useState(false);
@@ -57,7 +46,7 @@ export function GitHubSetupStep({
});
}
} catch (error) {
console.error("Failed to check gh status:", error);
console.error('Failed to check gh status:', error);
} finally {
setIsChecking(false);
}
@@ -69,7 +58,7 @@ export function GitHubSetupStep({
const copyCommand = (command: string) => {
navigator.clipboard.writeText(command);
toast.success("Command copied to clipboard");
toast.success('Command copied to clipboard');
};
const isReady = ghCliStatus?.installed && ghCliStatus?.authenticated;
@@ -93,12 +82,8 @@ export function GitHubSetupStep({
<div className="w-16 h-16 rounded-xl bg-zinc-800 flex items-center justify-center mx-auto mb-4">
<Github className="w-8 h-8 text-white" />
</div>
<h2 className="text-2xl font-bold text-foreground mb-2">
GitHub CLI Setup
</h2>
<p className="text-muted-foreground">
Optional - Used for creating pull requests
</p>
<h2 className="text-2xl font-bold text-foreground mb-2">GitHub CLI Setup</h2>
<p className="text-muted-foreground">Optional - Used for creating pull requests</p>
</div>
{/* Info Banner */}
@@ -107,13 +92,10 @@ export function GitHubSetupStep({
<div className="flex items-start gap-3">
<AlertTriangle className="w-5 h-5 text-amber-500 shrink-0 mt-0.5" />
<div>
<p className="font-medium text-foreground">
This step is optional
</p>
<p className="font-medium text-foreground">This step is optional</p>
<p className="text-sm text-muted-foreground mt-1">
The GitHub CLI allows you to create pull requests directly from
the app. Without it, you can still create PRs manually in your
browser.
The GitHub CLI allows you to create pull requests directly from the app. Without it,
you can still create PRs manually in your browser.
</p>
</div>
</div>
@@ -130,24 +112,17 @@ export function GitHubSetupStep({
</CardTitle>
<div className="flex items-center gap-2">
{getStatusBadge()}
<Button
variant="ghost"
size="sm"
onClick={checkStatus}
disabled={isChecking}
>
<RefreshCw
className={`w-4 h-4 ${isChecking ? "animate-spin" : ""}`}
/>
<Button variant="ghost" size="sm" onClick={checkStatus} disabled={isChecking}>
<RefreshCw className={`w-4 h-4 ${isChecking ? 'animate-spin' : ''}`} />
</Button>
</div>
</div>
<CardDescription>
{ghCliStatus?.installed
? ghCliStatus.authenticated
? `Logged in${ghCliStatus.user ? ` as ${ghCliStatus.user}` : ""}`
: "Installed but not logged in"
: "Not installed on your system"}
? `Logged in${ghCliStatus.user ? ` as ${ghCliStatus.user}` : ''}`
: 'Installed but not logged in'
: 'Not installed on your system'}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
@@ -156,9 +131,7 @@ export function GitHubSetupStep({
<div className="flex items-center gap-3 p-4 rounded-lg bg-green-500/10 border border-green-500/20">
<CheckCircle2 className="w-5 h-5 text-green-500" />
<div>
<p className="font-medium text-foreground">
GitHub CLI is ready!
</p>
<p className="font-medium text-foreground">GitHub CLI is ready!</p>
<p className="text-sm text-muted-foreground">
You can create pull requests directly from the app.
{ghCliStatus?.version && (
@@ -175,9 +148,7 @@ export function GitHubSetupStep({
<div className="flex items-start gap-3 p-4 rounded-lg bg-muted/30 border border-border">
<XCircle className="w-5 h-5 text-muted-foreground shrink-0 mt-0.5" />
<div className="flex-1">
<p className="font-medium text-foreground">
GitHub CLI not found
</p>
<p className="font-medium text-foreground">GitHub CLI not found</p>
<p className="text-sm text-muted-foreground mt-1">
Install the GitHub CLI to enable PR creation from the app.
</p>
@@ -185,9 +156,7 @@ export function GitHubSetupStep({
</div>
<div className="space-y-3 p-4 rounded-lg bg-muted/30 border border-border">
<p className="font-medium text-foreground text-sm">
Installation Commands:
</p>
<p className="font-medium text-foreground text-sm">Installation Commands:</p>
<div className="space-y-2">
<p className="text-xs text-muted-foreground">macOS (Homebrew)</p>
@@ -198,7 +167,7 @@ export function GitHubSetupStep({
<Button
variant="ghost"
size="icon"
onClick={() => copyCommand("brew install gh")}
onClick={() => copyCommand('brew install gh')}
>
<Copy className="w-4 h-4" />
</Button>
@@ -214,7 +183,7 @@ export function GitHubSetupStep({
<Button
variant="ghost"
size="icon"
onClick={() => copyCommand("winget install GitHub.cli")}
onClick={() => copyCommand('winget install GitHub.cli')}
>
<Copy className="w-4 h-4" />
</Button>
@@ -230,7 +199,7 @@ export function GitHubSetupStep({
<Button
variant="ghost"
size="icon"
onClick={() => copyCommand("sudo apt install gh")}
onClick={() => copyCommand('sudo apt install gh')}
>
<Copy className="w-4 h-4" />
</Button>
@@ -256,9 +225,7 @@ export function GitHubSetupStep({
<div className="flex items-start gap-3 p-4 rounded-lg bg-amber-500/10 border border-amber-500/20">
<AlertTriangle className="w-5 h-5 text-amber-500 shrink-0 mt-0.5" />
<div className="flex-1">
<p className="font-medium text-foreground">
GitHub CLI not logged in
</p>
<p className="font-medium text-foreground">GitHub CLI not logged in</p>
<p className="text-sm text-muted-foreground mt-1">
Run the login command to authenticate with GitHub.
</p>
@@ -266,18 +233,12 @@ export function GitHubSetupStep({
</div>
<div className="space-y-2 p-4 rounded-lg bg-muted/30 border border-border">
<p className="text-sm text-muted-foreground">
Run this command in your terminal:
</p>
<p className="text-sm text-muted-foreground">Run this command in your terminal:</p>
<div className="flex items-center gap-2">
<code className="flex-1 bg-muted px-3 py-2 rounded text-sm font-mono text-foreground">
gh auth login
</code>
<Button
variant="ghost"
size="icon"
onClick={() => copyCommand("gh auth login")}
>
<Button variant="ghost" size="icon" onClick={() => copyCommand('gh auth login')}>
<Copy className="w-4 h-4" />
</Button>
</div>
@@ -290,9 +251,7 @@ export function GitHubSetupStep({
<div className="flex items-center gap-3 p-4 rounded-lg bg-blue-500/10 border border-blue-500/20">
<Loader2 className="w-5 h-5 text-blue-500 animate-spin" />
<div>
<p className="font-medium text-foreground">
Checking GitHub CLI status...
</p>
<p className="font-medium text-foreground">Checking GitHub CLI status...</p>
</div>
</div>
)}
@@ -301,28 +260,20 @@ export function GitHubSetupStep({
{/* Navigation */}
<div className="flex justify-between pt-4">
<Button
variant="ghost"
onClick={onBack}
className="text-muted-foreground"
>
<Button variant="ghost" onClick={onBack} className="text-muted-foreground">
<ArrowLeft className="w-4 h-4 mr-2" />
Back
</Button>
<div className="flex gap-2">
<Button
variant="ghost"
onClick={onSkip}
className="text-muted-foreground"
>
{isReady ? "Skip" : "Skip for now"}
<Button variant="ghost" onClick={onSkip} className="text-muted-foreground">
{isReady ? 'Skip' : 'Skip for now'}
</Button>
<Button
onClick={onNext}
className="bg-brand-500 hover:bg-brand-600 text-white"
data-testid="github-next-button"
>
{isReady ? "Continue" : "Continue without GitHub CLI"}
{isReady ? 'Continue' : 'Continue without GitHub CLI'}
<ArrowRight className="w-4 h-4 ml-2" />
</Button>
</div>

View File

@@ -1,25 +1,14 @@
import { RefreshCw } from "lucide-react";
import { useAppStore } from "@/store/app-store";
import { RefreshCw } from 'lucide-react';
import { useAppStore } from '@/store/app-store';
// Extracted hooks
import {
useSpecLoading,
useSpecSave,
useSpecGeneration,
} from "./spec-view/hooks";
import { useSpecLoading, useSpecSave, useSpecGeneration } from './spec-view/hooks';
// Extracted components
import {
SpecHeader,
SpecEditor,
SpecEmptyState,
} from "./spec-view/components";
import { SpecHeader, SpecEditor, SpecEmptyState } from './spec-view/components';
// Extracted dialogs
import {
CreateSpecDialog,
RegenerateSpecDialog,
} from "./spec-view/dialogs";
import { CreateSpecDialog, RegenerateSpecDialog } from './spec-view/dialogs';
export function SpecView() {
const { currentProject, appSpec } = useAppStore();
@@ -28,8 +17,7 @@ export function SpecView() {
const { isLoading, specExists, loadSpec } = useSpecLoading();
// Save state
const { isSaving, hasChanges, saveSpec, handleChange, setHasChanges } =
useSpecSave();
const { isSaving, hasChanges, saveSpec, handleChange, setHasChanges } = useSpecSave();
// Generation state and handlers
const {
@@ -79,10 +67,7 @@ export function SpecView() {
// No project selected
if (!currentProject) {
return (
<div
className="flex-1 flex items-center justify-center"
data-testid="spec-view-no-project"
>
<div className="flex-1 flex items-center justify-center" data-testid="spec-view-no-project">
<p className="text-muted-foreground">No project selected</p>
</div>
);
@@ -91,10 +76,7 @@ export function SpecView() {
// Loading state
if (isLoading) {
return (
<div
className="flex-1 flex items-center justify-center"
data-testid="spec-view-loading"
>
<div className="flex-1 flex items-center justify-center" data-testid="spec-view-loading">
<RefreshCw className="w-6 h-6 animate-spin text-muted-foreground" />
</div>
);
@@ -133,10 +115,7 @@ export function SpecView() {
// Main view - spec exists
return (
<div
className="flex-1 flex flex-col overflow-hidden content-bg"
data-testid="spec-view"
>
<div className="flex-1 flex flex-col overflow-hidden content-bg" data-testid="spec-view">
<SpecHeader
projectPath={currentProject.path}
isRegenerating={isRegenerating}

View File

@@ -1,3 +1,3 @@
export { SpecHeader } from "./spec-header";
export { SpecEditor } from "./spec-editor";
export { SpecEmptyState } from "./spec-empty-state";
export { SpecHeader } from './spec-header';
export { SpecEditor } from './spec-editor';
export { SpecEmptyState } from './spec-empty-state';

View File

@@ -1,5 +1,5 @@
import { Card } from "@/components/ui/card";
import { XmlSyntaxEditor } from "@/components/ui/xml-syntax-editor";
import { Card } from '@/components/ui/card';
import { XmlSyntaxEditor } from '@/components/ui/xml-syntax-editor';
interface SpecEditorProps {
value: string;

View File

@@ -1,10 +1,6 @@
import { Button } from "@/components/ui/button";
import {
FileText,
FilePlus2,
Loader2,
} from "lucide-react";
import { PHASE_LABELS } from "../constants";
import { Button } from '@/components/ui/button';
import { FileText, FilePlus2, Loader2 } from 'lucide-react';
import { PHASE_LABELS } from '../constants';
interface SpecEmptyStateProps {
projectPath: string;
@@ -27,19 +23,14 @@ export function SpecEmptyState({
const phaseLabel = PHASE_LABELS[currentPhase] || currentPhase;
return (
<div
className="flex-1 flex flex-col overflow-hidden content-bg"
data-testid="spec-view-empty"
>
<div className="flex-1 flex flex-col overflow-hidden content-bg" data-testid="spec-view-empty">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-border bg-glass backdrop-blur-md">
<div className="flex items-center gap-3">
<FileText className="w-5 h-5 text-muted-foreground" />
<div>
<h1 className="text-xl font-bold">App Specification</h1>
<p className="text-sm text-muted-foreground">
{projectPath}/.automaker/app_spec.txt
</p>
<p className="text-sm text-muted-foreground">{projectPath}/.automaker/app_spec.txt</p>
</div>
</div>
{isProcessing && (
@@ -50,9 +41,7 @@ export function SpecEmptyState({
</div>
<div className="flex flex-col gap-1 min-w-0">
<span className="text-sm font-semibold text-primary leading-tight tracking-tight">
{isCreating
? "Generating Specification"
: "Regenerating Specification"}
{isCreating ? 'Generating Specification' : 'Regenerating Specification'}
</span>
{currentPhase && (
<span className="text-xs text-muted-foreground/90 leading-tight font-medium">
@@ -96,13 +85,13 @@ export function SpecEmptyState({
)}
</>
) : (
"No App Specification Found"
'No App Specification Found'
)}
</h2>
<p className="text-muted-foreground mb-6">
{isCreating
? currentPhase === "feature_generation"
? "The app specification has been created! Now generating features from the implementation roadmap..."
? currentPhase === 'feature_generation'
? 'The app specification has been created! Now generating features from the implementation roadmap...'
: "We're analyzing your project and generating a comprehensive specification. This may take a few moments..."
: "Create an app specification to help our system understand your project. We'll analyze your codebase and generate a comprehensive spec based on your description."}
</p>

View File

@@ -1,12 +1,6 @@
import { Button } from "@/components/ui/button";
import {
Save,
Sparkles,
Loader2,
FileText,
AlertCircle,
} from "lucide-react";
import { PHASE_LABELS } from "../constants";
import { Button } from '@/components/ui/button';
import { Save, Sparkles, Loader2, FileText, AlertCircle } from 'lucide-react';
import { PHASE_LABELS } from '../constants';
interface SpecHeaderProps {
projectPath: string;
@@ -42,9 +36,7 @@ export function SpecHeader({
<FileText className="w-5 h-5 text-muted-foreground" />
<div>
<h1 className="text-xl font-bold">App Specification</h1>
<p className="text-sm text-muted-foreground">
{projectPath}/.automaker/app_spec.txt
</p>
<p className="text-sm text-muted-foreground">{projectPath}/.automaker/app_spec.txt</p>
</div>
</div>
<div className="flex items-center gap-3">
@@ -57,10 +49,10 @@ export function SpecHeader({
<div className="flex flex-col gap-1 min-w-0">
<span className="text-sm font-semibold text-primary leading-tight tracking-tight">
{isGeneratingFeatures
? "Generating Features"
? 'Generating Features'
: isCreating
? "Generating Specification"
: "Regenerating Specification"}
? 'Generating Specification'
: 'Regenerating Specification'}
</span>
{currentPhase && (
<span className="text-xs text-muted-foreground/90 leading-tight font-medium">
@@ -96,7 +88,7 @@ export function SpecHeader({
) : (
<Sparkles className="w-4 h-4 mr-2" />
)}
{isRegenerating ? "Regenerating..." : "Regenerate"}
{isRegenerating ? 'Regenerating...' : 'Regenerate'}
</Button>
<Button
size="sm"
@@ -105,7 +97,7 @@ export function SpecHeader({
data-testid="save-spec"
>
<Save className="w-4 h-4 mr-2" />
{isSaving ? "Saving..." : hasChanges ? "Save Changes" : "Saved"}
{isSaving ? 'Saving...' : hasChanges ? 'Save Changes' : 'Saved'}
</Button>
</div>
</div>

View File

@@ -1,4 +1,4 @@
import type { FeatureCount } from "./types";
import type { FeatureCount } from './types';
// Delay before reloading spec file to ensure it's written to disk
export const SPEC_FILE_WRITE_DELAY = 500;
@@ -12,18 +12,18 @@ export const FEATURE_COUNT_OPTIONS: {
label: string;
warning?: string;
}[] = [
{ value: 20, label: "20" },
{ value: 50, label: "50", warning: "May take up to 5 minutes" },
{ value: 100, label: "100", warning: "May take up to 5 minutes" },
{ value: 20, label: '20' },
{ value: 50, label: '50', warning: 'May take up to 5 minutes' },
{ value: 100, label: '100', warning: 'May take up to 5 minutes' },
];
// Phase display labels for UI
export const PHASE_LABELS: Record<string, string> = {
initialization: "Initializing...",
setup: "Setting up tools...",
analysis: "Analyzing project structure...",
spec_complete: "Spec created! Generating features...",
feature_generation: "Creating features from roadmap...",
complete: "Complete!",
error: "Error occurred",
initialization: 'Initializing...',
setup: 'Setting up tools...',
analysis: 'Analyzing project structure...',
spec_complete: 'Spec created! Generating features...',
feature_generation: 'Creating features from roadmap...',
complete: 'Complete!',
error: 'Error occurred',
};

View File

@@ -1,4 +1,4 @@
import { Sparkles, Clock, Loader2 } from "lucide-react";
import { Sparkles, Clock, Loader2 } from 'lucide-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 { Checkbox } from "@/components/ui/checkbox";
import { cn } from "@/lib/utils";
import { FEATURE_COUNT_OPTIONS } from "../constants";
import type { CreateSpecDialogProps, FeatureCount } from "../types";
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { HotkeyButton } from '@/components/ui/hotkey-button';
import { Checkbox } from '@/components/ui/checkbox';
import { cn } from '@/lib/utils';
import { FEATURE_COUNT_OPTIONS } from '../constants';
import type { CreateSpecDialogProps, FeatureCount } from '../types';
export function CreateSpecDialog({
open,
@@ -29,12 +29,10 @@ export function CreateSpecDialog({
onSkip,
isCreatingSpec,
showSkipButton = false,
title = "Create App Specification",
title = 'Create App Specification',
description = "We didn't find an app_spec.txt file. Let us help you generate your app_spec.txt to help describe your project for our system. We'll analyze your project's tech stack and create a comprehensive specification.",
}: CreateSpecDialogProps) {
const selectedOption = FEATURE_COUNT_OPTIONS.find(
(o) => o.value === featureCount
);
const selectedOption = FEATURE_COUNT_OPTIONS.find((o) => o.value === featureCount);
return (
<Dialog
@@ -49,18 +47,15 @@ export function CreateSpecDialog({
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription className="text-muted-foreground">
{description}
</DialogDescription>
<DialogDescription className="text-muted-foreground">{description}</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<label className="text-sm font-medium">Project Overview</label>
<p className="text-xs text-muted-foreground">
Describe what your project does and what features you want to
build. Be as detailed as you want - this will help us create a
better specification.
Describe what your project does and what features you want to build. Be as detailed as
you want - this will help us create a better specification.
</p>
<textarea
className="w-full h-48 p-3 rounded-md border border-border bg-background font-mono text-sm resize-none focus:outline-none focus:ring-2 focus:ring-ring"
@@ -76,25 +71,20 @@ export function CreateSpecDialog({
<Checkbox
id="create-analyze-project"
checked={analyzeProject}
onCheckedChange={(checked) =>
onAnalyzeProjectChange(checked === true)
}
onCheckedChange={(checked) => onAnalyzeProjectChange(checked === true)}
disabled={isCreatingSpec}
/>
<div className="space-y-1">
<label
htmlFor="create-analyze-project"
className={`text-sm font-medium ${
isCreatingSpec ? "" : "cursor-pointer"
}`}
className={`text-sm font-medium ${isCreatingSpec ? '' : 'cursor-pointer'}`}
>
Analyze current project for additional context
</label>
<p className="text-xs text-muted-foreground">
If checked, the agent will research your existing codebase to
understand the tech stack. If unchecked, defaults to TanStack
Start, Drizzle ORM, PostgreSQL, shadcn/ui, Tailwind CSS, and
React.
If checked, the agent will research your existing codebase to understand the tech
stack. If unchecked, defaults to TanStack Start, Drizzle ORM, PostgreSQL, shadcn/ui,
Tailwind CSS, and React.
</p>
</div>
</div>
@@ -103,23 +93,19 @@ export function CreateSpecDialog({
<Checkbox
id="create-generate-features"
checked={generateFeatures}
onCheckedChange={(checked) =>
onGenerateFeaturesChange(checked === true)
}
onCheckedChange={(checked) => onGenerateFeaturesChange(checked === true)}
disabled={isCreatingSpec}
/>
<div className="space-y-1">
<label
htmlFor="create-generate-features"
className={`text-sm font-medium ${
isCreatingSpec ? "" : "cursor-pointer"
}`}
className={`text-sm font-medium ${isCreatingSpec ? '' : 'cursor-pointer'}`}
>
Generate feature list
</label>
<p className="text-xs text-muted-foreground">
Automatically create features in the features folder from the
implementation roadmap after the spec is generated.
Automatically create features in the features folder from the implementation roadmap
after the spec is generated.
</p>
</div>
</div>
@@ -133,19 +119,15 @@ export function CreateSpecDialog({
<Button
key={option.value}
type="button"
variant={
featureCount === option.value ? "default" : "outline"
}
variant={featureCount === option.value ? 'default' : 'outline'}
size="sm"
onClick={() =>
onFeatureCountChange(option.value as FeatureCount)
}
onClick={() => onFeatureCountChange(option.value as FeatureCount)}
disabled={isCreatingSpec}
className={cn(
"flex-1 transition-all",
'flex-1 transition-all',
featureCount === option.value
? "bg-primary hover:bg-primary/90 text-primary-foreground"
: "bg-muted/30 hover:bg-muted/50 border-border"
? 'bg-primary hover:bg-primary/90 text-primary-foreground'
: 'bg-muted/30 hover:bg-muted/50 border-border'
)}
data-testid={`feature-count-${option.value}`}
>
@@ -169,18 +151,14 @@ export function CreateSpecDialog({
Skip for now
</Button>
) : (
<Button
variant="ghost"
onClick={() => onOpenChange(false)}
disabled={isCreatingSpec}
>
<Button variant="ghost" onClick={() => onOpenChange(false)} disabled={isCreatingSpec}>
Cancel
</Button>
)}
<HotkeyButton
onClick={onCreateSpec}
disabled={!projectOverview.trim() || isCreatingSpec}
hotkey={{ key: "Enter", cmdCtrl: true }}
hotkey={{ key: 'Enter', cmdCtrl: true }}
hotkeyActive={open && !isCreatingSpec}
>
{isCreatingSpec ? (

View File

@@ -1,2 +1,2 @@
export { CreateSpecDialog } from "./create-spec-dialog";
export { RegenerateSpecDialog } from "./regenerate-spec-dialog";
export { CreateSpecDialog } from './create-spec-dialog';
export { RegenerateSpecDialog } from './regenerate-spec-dialog';

View File

@@ -1,4 +1,4 @@
import { Sparkles, Clock, Loader2 } from "lucide-react";
import { Sparkles, Clock, Loader2 } from 'lucide-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 { Checkbox } from "@/components/ui/checkbox";
import { cn } from "@/lib/utils";
import { FEATURE_COUNT_OPTIONS } from "../constants";
import type { RegenerateSpecDialogProps, FeatureCount } from "../types";
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { HotkeyButton } from '@/components/ui/hotkey-button';
import { Checkbox } from '@/components/ui/checkbox';
import { cn } from '@/lib/utils';
import { FEATURE_COUNT_OPTIONS } from '../constants';
import type { RegenerateSpecDialogProps, FeatureCount } from '../types';
export function RegenerateSpecDialog({
open,
@@ -29,9 +29,7 @@ export function RegenerateSpecDialog({
isRegenerating,
isGeneratingFeatures = false,
}: RegenerateSpecDialogProps) {
const selectedOption = FEATURE_COUNT_OPTIONS.find(
(o) => o.value === featureCount
);
const selectedOption = FEATURE_COUNT_OPTIONS.find((o) => o.value === featureCount);
const isDisabled = isRegenerating || isGeneratingFeatures;
return (
@@ -47,10 +45,9 @@ export function RegenerateSpecDialog({
<DialogHeader>
<DialogTitle>Regenerate App Specification</DialogTitle>
<DialogDescription className="text-muted-foreground">
We will regenerate your app spec based on a short project definition
and the current tech stack found in your project. The agent will
analyze your codebase to understand your existing technologies and
create a comprehensive specification.
We will regenerate your app spec based on a short project definition and the current
tech stack found in your project. The agent will analyze your codebase to understand
your existing technologies and create a comprehensive specification.
</DialogDescription>
</DialogHeader>
@@ -58,9 +55,8 @@ export function RegenerateSpecDialog({
<div className="space-y-2">
<label className="text-sm font-medium">Describe your project</label>
<p className="text-xs text-muted-foreground">
Provide a clear description of what your app should do. Be as
detailed as you want - the more context you provide, the more
comprehensive the spec will be.
Provide a clear description of what your app should do. Be as detailed as you want -
the more context you provide, the more comprehensive the spec will be.
</p>
<textarea
className="w-full h-40 p-3 rounded-md border border-border bg-background font-mono text-sm resize-none focus:outline-none focus:ring-2 focus:ring-ring"
@@ -75,25 +71,20 @@ export function RegenerateSpecDialog({
<Checkbox
id="regenerate-analyze-project"
checked={analyzeProject}
onCheckedChange={(checked) =>
onAnalyzeProjectChange(checked === true)
}
onCheckedChange={(checked) => onAnalyzeProjectChange(checked === true)}
disabled={isDisabled}
/>
<div className="space-y-1">
<label
htmlFor="regenerate-analyze-project"
className={`text-sm font-medium ${
isDisabled ? "" : "cursor-pointer"
}`}
className={`text-sm font-medium ${isDisabled ? '' : 'cursor-pointer'}`}
>
Analyze current project for additional context
</label>
<p className="text-xs text-muted-foreground">
If checked, the agent will research your existing codebase to
understand the tech stack. If unchecked, defaults to TanStack
Start, Drizzle ORM, PostgreSQL, shadcn/ui, Tailwind CSS, and
React.
If checked, the agent will research your existing codebase to understand the tech
stack. If unchecked, defaults to TanStack Start, Drizzle ORM, PostgreSQL, shadcn/ui,
Tailwind CSS, and React.
</p>
</div>
</div>
@@ -102,23 +93,19 @@ export function RegenerateSpecDialog({
<Checkbox
id="regenerate-generate-features"
checked={generateFeatures}
onCheckedChange={(checked) =>
onGenerateFeaturesChange(checked === true)
}
onCheckedChange={(checked) => onGenerateFeaturesChange(checked === true)}
disabled={isDisabled}
/>
<div className="space-y-1">
<label
htmlFor="regenerate-generate-features"
className={`text-sm font-medium ${
isDisabled ? "" : "cursor-pointer"
}`}
className={`text-sm font-medium ${isDisabled ? '' : 'cursor-pointer'}`}
>
Generate feature list
</label>
<p className="text-xs text-muted-foreground">
Automatically create features in the features folder from the
implementation roadmap after the spec is regenerated.
Automatically create features in the features folder from the implementation roadmap
after the spec is regenerated.
</p>
</div>
</div>
@@ -132,19 +119,15 @@ export function RegenerateSpecDialog({
<Button
key={option.value}
type="button"
variant={
featureCount === option.value ? "default" : "outline"
}
variant={featureCount === option.value ? 'default' : 'outline'}
size="sm"
onClick={() =>
onFeatureCountChange(option.value as FeatureCount)
}
onClick={() => onFeatureCountChange(option.value as FeatureCount)}
disabled={isDisabled}
className={cn(
"flex-1 transition-all",
'flex-1 transition-all',
featureCount === option.value
? "bg-primary hover:bg-primary/90 text-primary-foreground"
: "bg-muted/30 hover:bg-muted/50 border-border"
? 'bg-primary hover:bg-primary/90 text-primary-foreground'
: 'bg-muted/30 hover:bg-muted/50 border-border'
)}
data-testid={`regenerate-feature-count-${option.value}`}
>
@@ -164,17 +147,13 @@ export function RegenerateSpecDialog({
<DialogFooter>
<div className="flex gap-2">
<Button
variant="ghost"
onClick={() => onOpenChange(false)}
disabled={isDisabled}
>
<Button variant="ghost" onClick={() => onOpenChange(false)} disabled={isDisabled}>
Cancel
</Button>
<HotkeyButton
onClick={onRegenerate}
disabled={!projectDefinition.trim() || isDisabled}
hotkey={{ key: "Enter", cmdCtrl: true }}
hotkey={{ key: 'Enter', cmdCtrl: true }}
hotkeyActive={open && !isDisabled}
>
{isRegenerating ? (

View File

@@ -1,3 +1,3 @@
export { useSpecLoading } from "./use-spec-loading";
export { useSpecSave } from "./use-spec-save";
export { useSpecGeneration } from "./use-spec-generation";
export { useSpecLoading } from './use-spec-loading';
export { useSpecSave } from './use-spec-save';
export { useSpecGeneration } from './use-spec-generation';

View File

@@ -1,12 +1,12 @@
import { useEffect, useState, useCallback, useRef } from "react";
import { useAppStore } from "@/store/app-store";
import { getElectronAPI } from "@/lib/electron";
import { toast } from "sonner";
import { CheckCircle2 } from "lucide-react";
import { createElement } from "react";
import { SPEC_FILE_WRITE_DELAY, STATUS_CHECK_INTERVAL_MS } from "../constants";
import type { FeatureCount } from "../types";
import type { SpecRegenerationEvent } from "@/types/electron";
import { useEffect, useState, useCallback, useRef } from 'react';
import { useAppStore } from '@/store/app-store';
import { getElectronAPI } from '@/lib/electron';
import { toast } from 'sonner';
import { CheckCircle2 } from 'lucide-react';
import { createElement } from 'react';
import { SPEC_FILE_WRITE_DELAY, STATUS_CHECK_INTERVAL_MS } from '../constants';
import type { FeatureCount } from '../types';
import type { SpecRegenerationEvent } from '@/types/electron';
interface UseSpecGenerationOptions {
loadSpec: () => Promise<void>;
@@ -20,33 +20,29 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
const [showRegenerateDialog, setShowRegenerateDialog] = useState(false);
// Create spec state
const [projectOverview, setProjectOverview] = useState("");
const [projectOverview, setProjectOverview] = useState('');
const [isCreating, setIsCreating] = useState(false);
const [generateFeatures, setGenerateFeatures] = useState(true);
const [analyzeProjectOnCreate, setAnalyzeProjectOnCreate] = useState(true);
const [featureCountOnCreate, setFeatureCountOnCreate] =
useState<FeatureCount>(50);
const [featureCountOnCreate, setFeatureCountOnCreate] = useState<FeatureCount>(50);
// Regenerate spec state
const [projectDefinition, setProjectDefinition] = useState("");
const [projectDefinition, setProjectDefinition] = useState('');
const [isRegenerating, setIsRegenerating] = useState(false);
const [generateFeaturesOnRegenerate, setGenerateFeaturesOnRegenerate] =
useState(true);
const [analyzeProjectOnRegenerate, setAnalyzeProjectOnRegenerate] =
useState(true);
const [featureCountOnRegenerate, setFeatureCountOnRegenerate] =
useState<FeatureCount>(50);
const [generateFeaturesOnRegenerate, setGenerateFeaturesOnRegenerate] = useState(true);
const [analyzeProjectOnRegenerate, setAnalyzeProjectOnRegenerate] = useState(true);
const [featureCountOnRegenerate, setFeatureCountOnRegenerate] = useState<FeatureCount>(50);
// Generate features only state
const [isGeneratingFeatures, setIsGeneratingFeatures] = useState(false);
// Logs state (kept for internal tracking)
const [logs, setLogs] = useState<string>("");
const logsRef = useRef<string>("");
const [logs, setLogs] = useState<string>('');
const logsRef = useRef<string>('');
// Phase tracking and status
const [currentPhase, setCurrentPhase] = useState<string>("");
const [errorMessage, setErrorMessage] = useState<string>("");
const [currentPhase, setCurrentPhase] = useState<string>('');
const [errorMessage, setErrorMessage] = useState<string>('');
const statusCheckRef = useRef<boolean>(false);
const stateRestoredRef = useRef<boolean>(false);
const pendingStatusTimeoutRef = useRef<NodeJS.Timeout | null>(null);
@@ -56,10 +52,10 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
setIsCreating(false);
setIsRegenerating(false);
setIsGeneratingFeatures(false);
setCurrentPhase("");
setErrorMessage("");
setLogs("");
logsRef.current = "";
setCurrentPhase('');
setErrorMessage('');
setLogs('');
logsRef.current = '';
stateRestoredRef.current = false;
statusCheckRef.current = false;
@@ -84,15 +80,15 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
const status = await api.specRegeneration.status();
console.log(
"[useSpecGeneration] Status check on mount:",
'[useSpecGeneration] Status check on mount:',
status,
"for project:",
'for project:',
currentProject.path
);
if (status.success && status.isRunning) {
console.log(
"[useSpecGeneration] Spec generation is running globally. Tentatively showing loader."
'[useSpecGeneration] Spec generation is running globally. Tentatively showing loader.'
);
setIsCreating(true);
@@ -100,7 +96,7 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
if (status.currentPhase) {
setCurrentPhase(status.currentPhase);
} else {
setCurrentPhase("initialization");
setCurrentPhase('initialization');
}
if (pendingStatusTimeoutRef.current) {
@@ -108,21 +104,21 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
}
pendingStatusTimeoutRef.current = setTimeout(() => {
console.log(
"[useSpecGeneration] No events received for current project - clearing tentative state"
'[useSpecGeneration] No events received for current project - clearing tentative state'
);
setIsCreating(false);
setIsRegenerating(false);
setCurrentPhase("");
setCurrentPhase('');
pendingStatusTimeoutRef.current = null;
}, 3000);
} else if (status.success && !status.isRunning) {
setIsCreating(false);
setIsRegenerating(false);
setCurrentPhase("");
setCurrentPhase('');
stateRestoredRef.current = false;
}
} catch (error) {
console.error("[useSpecGeneration] Failed to check status:", error);
console.error('[useSpecGeneration] Failed to check status:', error);
} finally {
statusCheckRef.current = false;
}
@@ -145,52 +141,36 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
if (!api.specRegeneration) return;
const status = await api.specRegeneration.status();
console.log(
"[useSpecGeneration] Visibility change - status check:",
status
);
console.log('[useSpecGeneration] Visibility change - status check:', status);
if (!status.isRunning) {
console.log(
"[useSpecGeneration] Visibility change: Backend indicates generation complete - clearing state"
'[useSpecGeneration] Visibility change: Backend indicates generation complete - clearing state'
);
setIsCreating(false);
setIsRegenerating(false);
setIsGeneratingFeatures(false);
setCurrentPhase("");
setCurrentPhase('');
stateRestoredRef.current = false;
loadSpec();
} else if (status.currentPhase) {
setCurrentPhase(status.currentPhase);
}
} catch (error) {
console.error(
"[useSpecGeneration] Failed to check status on visibility change:",
error
);
console.error('[useSpecGeneration] Failed to check status on visibility change:', error);
}
}
};
document.addEventListener("visibilitychange", handleVisibilityChange);
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
document.removeEventListener("visibilitychange", handleVisibilityChange);
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, [
currentProject,
isCreating,
isRegenerating,
isGeneratingFeatures,
loadSpec,
]);
}, [currentProject, isCreating, isRegenerating, isGeneratingFeatures, loadSpec]);
// Periodic status check
useEffect(() => {
if (
!currentProject ||
(!isCreating && !isRegenerating && !isGeneratingFeatures)
)
return;
if (!currentProject || (!isCreating && !isRegenerating && !isGeneratingFeatures)) return;
const intervalId = setInterval(async () => {
try {
@@ -201,46 +181,30 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
if (!status.isRunning) {
console.log(
"[useSpecGeneration] Periodic check: Backend indicates generation complete - clearing state"
'[useSpecGeneration] Periodic check: Backend indicates generation complete - clearing state'
);
setIsCreating(false);
setIsRegenerating(false);
setIsGeneratingFeatures(false);
setCurrentPhase("");
setCurrentPhase('');
stateRestoredRef.current = false;
loadSpec();
} else if (
status.currentPhase &&
status.currentPhase !== currentPhase
) {
console.log(
"[useSpecGeneration] Periodic check: Phase updated from backend",
{
old: currentPhase,
new: status.currentPhase,
}
);
} else if (status.currentPhase && status.currentPhase !== currentPhase) {
console.log('[useSpecGeneration] Periodic check: Phase updated from backend', {
old: currentPhase,
new: status.currentPhase,
});
setCurrentPhase(status.currentPhase);
}
} catch (error) {
console.error(
"[useSpecGeneration] Periodic status check error:",
error
);
console.error('[useSpecGeneration] Periodic status check error:', error);
}
}, STATUS_CHECK_INTERVAL_MS);
return () => {
clearInterval(intervalId);
};
}, [
currentProject,
isCreating,
isRegenerating,
isGeneratingFeatures,
currentPhase,
loadSpec,
]);
}, [currentProject, isCreating, isRegenerating, isGeneratingFeatures, currentPhase, loadSpec]);
// Subscribe to spec regeneration events
useEffect(() => {
@@ -249,198 +213,179 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
const api = getElectronAPI();
if (!api.specRegeneration) return;
const unsubscribe = api.specRegeneration.onEvent(
(event: SpecRegenerationEvent) => {
const unsubscribe = api.specRegeneration.onEvent((event: SpecRegenerationEvent) => {
console.log(
'[useSpecGeneration] Regeneration event:',
event.type,
'for project:',
event.projectPath,
'current project:',
currentProject?.path
);
if (event.projectPath !== currentProject?.path) {
console.log('[useSpecGeneration] Ignoring event - not for current project');
return;
}
if (pendingStatusTimeoutRef.current) {
clearTimeout(pendingStatusTimeoutRef.current);
pendingStatusTimeoutRef.current = null;
console.log(
"[useSpecGeneration] Regeneration event:",
event.type,
"for project:",
event.projectPath,
"current project:",
currentProject?.path
'[useSpecGeneration] Event confirmed this is for current project - clearing timeout'
);
}
if (event.projectPath !== currentProject?.path) {
console.log(
"[useSpecGeneration] Ignoring event - not for current project"
);
return;
}
if (event.type === 'spec_regeneration_progress') {
setIsCreating(true);
setIsRegenerating(true);
if (pendingStatusTimeoutRef.current) {
clearTimeout(pendingStatusTimeoutRef.current);
pendingStatusTimeoutRef.current = null;
console.log(
"[useSpecGeneration] Event confirmed this is for current project - clearing timeout"
);
}
const phaseMatch = event.content.match(/\[Phase:\s*([^\]]+)\]/);
if (phaseMatch) {
const phase = phaseMatch[1];
setCurrentPhase(phase);
console.log(`[useSpecGeneration] Phase updated: ${phase}`);
if (event.type === "spec_regeneration_progress") {
setIsCreating(true);
setIsRegenerating(true);
const phaseMatch = event.content.match(/\[Phase:\s*([^\]]+)\]/);
if (phaseMatch) {
const phase = phaseMatch[1];
setCurrentPhase(phase);
console.log(`[useSpecGeneration] Phase updated: ${phase}`);
if (phase === "complete") {
console.log(
"[useSpecGeneration] Phase is complete - clearing state"
);
setIsCreating(false);
setIsRegenerating(false);
stateRestoredRef.current = false;
setTimeout(() => {
loadSpec();
}, SPEC_FILE_WRITE_DELAY);
}
}
if (
event.content.includes("All tasks completed") ||
event.content.includes("✓ All tasks completed")
) {
console.log(
"[useSpecGeneration] Detected completion in progress message - clearing state"
);
if (phase === 'complete') {
console.log('[useSpecGeneration] Phase is complete - clearing state');
setIsCreating(false);
setIsRegenerating(false);
setCurrentPhase("");
stateRestoredRef.current = false;
setTimeout(() => {
loadSpec();
}, SPEC_FILE_WRITE_DELAY);
}
}
const newLog = logsRef.current + event.content;
logsRef.current = newLog;
setLogs(newLog);
if (
event.content.includes('All tasks completed') ||
event.content.includes('✓ All tasks completed')
) {
console.log(
"[useSpecGeneration] Progress:",
event.content.substring(0, 100)
'[useSpecGeneration] Detected completion in progress message - clearing state'
);
setIsCreating(false);
setIsRegenerating(false);
setCurrentPhase('');
stateRestoredRef.current = false;
setTimeout(() => {
loadSpec();
}, SPEC_FILE_WRITE_DELAY);
}
if (errorMessage) {
setErrorMessage("");
}
} else if (event.type === "spec_regeneration_tool") {
const isFeatureTool =
event.tool === "mcp__automaker-tools__UpdateFeatureStatus" ||
event.tool === "UpdateFeatureStatus" ||
event.tool?.includes("Feature");
const newLog = logsRef.current + event.content;
logsRef.current = newLog;
setLogs(newLog);
console.log('[useSpecGeneration] Progress:', event.content.substring(0, 100));
if (isFeatureTool) {
if (currentPhase !== "feature_generation") {
setCurrentPhase("feature_generation");
setIsCreating(true);
setIsRegenerating(true);
console.log(
"[useSpecGeneration] Detected feature creation tool - setting phase to feature_generation"
);
}
}
if (errorMessage) {
setErrorMessage('');
}
} else if (event.type === 'spec_regeneration_tool') {
const isFeatureTool =
event.tool === 'mcp__automaker-tools__UpdateFeatureStatus' ||
event.tool === 'UpdateFeatureStatus' ||
event.tool?.includes('Feature');
const toolInput = event.input
? ` (${JSON.stringify(event.input).substring(0, 100)}...)`
: "";
const toolLog = `\n[Tool] ${event.tool}${toolInput}\n`;
const newLog = logsRef.current + toolLog;
logsRef.current = newLog;
setLogs(newLog);
console.log("[useSpecGeneration] Tool:", event.tool, event.input);
} else if (event.type === "spec_regeneration_complete") {
const completionLog =
logsRef.current + `\n[Complete] ${event.message}\n`;
logsRef.current = completionLog;
setLogs(completionLog);
const isFinalCompletionMessage =
event.message?.includes("All tasks completed") ||
event.message === "All tasks completed!" ||
event.message === "All tasks completed" ||
event.message === "Spec regeneration complete!" ||
event.message === "Initial spec creation complete!";
const hasCompletePhase =
logsRef.current.includes("[Phase: complete]");
const isIntermediateCompletion =
event.message?.includes("Features are being generated") ||
event.message?.includes("features are being generated");
const shouldComplete =
(isFinalCompletionMessage || hasCompletePhase) &&
!isIntermediateCompletion;
if (shouldComplete) {
console.log(
"[useSpecGeneration] Final completion detected - clearing state",
{
isFinalCompletionMessage,
hasCompletePhase,
message: event.message,
}
);
setIsRegenerating(false);
setIsCreating(false);
setIsGeneratingFeatures(false);
setCurrentPhase("");
setShowRegenerateDialog(false);
setShowCreateDialog(false);
setProjectDefinition("");
setProjectOverview("");
setErrorMessage("");
stateRestoredRef.current = false;
setTimeout(() => {
loadSpec();
}, SPEC_FILE_WRITE_DELAY);
const isRegeneration = event.message?.includes("regeneration");
const isFeatureGeneration =
event.message?.includes("Feature generation");
toast.success(
isFeatureGeneration
? "Feature Generation Complete"
: isRegeneration
? "Spec Regeneration Complete"
: "Spec Creation Complete",
{
description: isFeatureGeneration
? "Features have been created from the app specification."
: "Your app specification has been saved.",
icon: createElement(CheckCircle2, { className: "w-4 h-4" }),
}
);
} else if (isIntermediateCompletion) {
if (isFeatureTool) {
if (currentPhase !== 'feature_generation') {
setCurrentPhase('feature_generation');
setIsCreating(true);
setIsRegenerating(true);
setCurrentPhase("feature_generation");
console.log(
"[useSpecGeneration] Intermediate completion, continuing with feature generation"
'[useSpecGeneration] Detected feature creation tool - setting phase to feature_generation'
);
}
}
console.log(
"[useSpecGeneration] Spec generation event:",
event.message
);
} else if (event.type === "spec_regeneration_error") {
const toolInput = event.input
? ` (${JSON.stringify(event.input).substring(0, 100)}...)`
: '';
const toolLog = `\n[Tool] ${event.tool}${toolInput}\n`;
const newLog = logsRef.current + toolLog;
logsRef.current = newLog;
setLogs(newLog);
console.log('[useSpecGeneration] Tool:', event.tool, event.input);
} else if (event.type === 'spec_regeneration_complete') {
const completionLog = logsRef.current + `\n[Complete] ${event.message}\n`;
logsRef.current = completionLog;
setLogs(completionLog);
const isFinalCompletionMessage =
event.message?.includes('All tasks completed') ||
event.message === 'All tasks completed!' ||
event.message === 'All tasks completed' ||
event.message === 'Spec regeneration complete!' ||
event.message === 'Initial spec creation complete!';
const hasCompletePhase = logsRef.current.includes('[Phase: complete]');
const isIntermediateCompletion =
event.message?.includes('Features are being generated') ||
event.message?.includes('features are being generated');
const shouldComplete =
(isFinalCompletionMessage || hasCompletePhase) && !isIntermediateCompletion;
if (shouldComplete) {
console.log('[useSpecGeneration] Final completion detected - clearing state', {
isFinalCompletionMessage,
hasCompletePhase,
message: event.message,
});
setIsRegenerating(false);
setIsCreating(false);
setIsGeneratingFeatures(false);
setCurrentPhase("error");
setErrorMessage(event.error);
setCurrentPhase('');
setShowRegenerateDialog(false);
setShowCreateDialog(false);
setProjectDefinition('');
setProjectOverview('');
setErrorMessage('');
stateRestoredRef.current = false;
const errorLog = logsRef.current + `\n\n[ERROR] ${event.error}\n`;
logsRef.current = errorLog;
setLogs(errorLog);
console.error("[useSpecGeneration] Regeneration error:", event.error);
setTimeout(() => {
loadSpec();
}, SPEC_FILE_WRITE_DELAY);
const isRegeneration = event.message?.includes('regeneration');
const isFeatureGeneration = event.message?.includes('Feature generation');
toast.success(
isFeatureGeneration
? 'Feature Generation Complete'
: isRegeneration
? 'Spec Regeneration Complete'
: 'Spec Creation Complete',
{
description: isFeatureGeneration
? 'Features have been created from the app specification.'
: 'Your app specification has been saved.',
icon: createElement(CheckCircle2, { className: 'w-4 h-4' }),
}
);
} else if (isIntermediateCompletion) {
setIsCreating(true);
setIsRegenerating(true);
setCurrentPhase('feature_generation');
console.log(
'[useSpecGeneration] Intermediate completion, continuing with feature generation'
);
}
console.log('[useSpecGeneration] Spec generation event:', event.message);
} else if (event.type === 'spec_regeneration_error') {
setIsRegenerating(false);
setIsCreating(false);
setIsGeneratingFeatures(false);
setCurrentPhase('error');
setErrorMessage(event.error);
stateRestoredRef.current = false;
const errorLog = logsRef.current + `\n\n[ERROR] ${event.error}\n`;
logsRef.current = errorLog;
setLogs(errorLog);
console.error('[useSpecGeneration] Regeneration error:', event.error);
}
);
});
return () => {
unsubscribe();
@@ -453,18 +398,15 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
setIsCreating(true);
setShowCreateDialog(false);
setCurrentPhase("initialization");
setErrorMessage("");
logsRef.current = "";
setLogs("");
console.log(
"[useSpecGeneration] Starting spec creation, generateFeatures:",
generateFeatures
);
setCurrentPhase('initialization');
setErrorMessage('');
logsRef.current = '';
setLogs('');
console.log('[useSpecGeneration] Starting spec creation, generateFeatures:', generateFeatures);
try {
const api = getElectronAPI();
if (!api.specRegeneration) {
console.error("[useSpecGeneration] Spec regeneration not available");
console.error('[useSpecGeneration] Spec regeneration not available');
setIsCreating(false);
return;
}
@@ -477,13 +419,10 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
);
if (!result.success) {
const errorMsg = result.error || "Unknown error";
console.error(
"[useSpecGeneration] Failed to start spec creation:",
errorMsg
);
const errorMsg = result.error || 'Unknown error';
console.error('[useSpecGeneration] Failed to start spec creation:', errorMsg);
setIsCreating(false);
setCurrentPhase("error");
setCurrentPhase('error');
setErrorMessage(errorMsg);
const errorLog = `[Error] Failed to start spec creation: ${errorMsg}\n`;
logsRef.current = errorLog;
@@ -491,9 +430,9 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
}
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
console.error("[useSpecGeneration] Failed to create spec:", errorMsg);
console.error('[useSpecGeneration] Failed to create spec:', errorMsg);
setIsCreating(false);
setCurrentPhase("error");
setCurrentPhase('error');
setErrorMessage(errorMsg);
const errorLog = `[Error] Failed to create spec: ${errorMsg}\n`;
logsRef.current = errorLog;
@@ -512,18 +451,18 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
setIsRegenerating(true);
setShowRegenerateDialog(false);
setCurrentPhase("initialization");
setErrorMessage("");
logsRef.current = "";
setLogs("");
setCurrentPhase('initialization');
setErrorMessage('');
logsRef.current = '';
setLogs('');
console.log(
"[useSpecGeneration] Starting spec regeneration, generateFeatures:",
'[useSpecGeneration] Starting spec regeneration, generateFeatures:',
generateFeaturesOnRegenerate
);
try {
const api = getElectronAPI();
if (!api.specRegeneration) {
console.error("[useSpecGeneration] Spec regeneration not available");
console.error('[useSpecGeneration] Spec regeneration not available');
setIsRegenerating(false);
return;
}
@@ -536,13 +475,10 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
);
if (!result.success) {
const errorMsg = result.error || "Unknown error";
console.error(
"[useSpecGeneration] Failed to start regeneration:",
errorMsg
);
const errorMsg = result.error || 'Unknown error';
console.error('[useSpecGeneration] Failed to start regeneration:', errorMsg);
setIsRegenerating(false);
setCurrentPhase("error");
setCurrentPhase('error');
setErrorMessage(errorMsg);
const errorLog = `[Error] Failed to start regeneration: ${errorMsg}\n`;
logsRef.current = errorLog;
@@ -550,9 +486,9 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
}
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
console.error("[useSpecGeneration] Failed to regenerate spec:", errorMsg);
console.error('[useSpecGeneration] Failed to regenerate spec:', errorMsg);
setIsRegenerating(false);
setCurrentPhase("error");
setCurrentPhase('error');
setErrorMessage(errorMsg);
const errorLog = `[Error] Failed to regenerate spec: ${errorMsg}\n`;
logsRef.current = errorLog;
@@ -571,32 +507,25 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
setIsGeneratingFeatures(true);
setShowRegenerateDialog(false);
setCurrentPhase("initialization");
setErrorMessage("");
logsRef.current = "";
setLogs("");
console.log(
"[useSpecGeneration] Starting feature generation from existing spec"
);
setCurrentPhase('initialization');
setErrorMessage('');
logsRef.current = '';
setLogs('');
console.log('[useSpecGeneration] Starting feature generation from existing spec');
try {
const api = getElectronAPI();
if (!api.specRegeneration) {
console.error("[useSpecGeneration] Spec regeneration not available");
console.error('[useSpecGeneration] Spec regeneration not available');
setIsGeneratingFeatures(false);
return;
}
const result = await api.specRegeneration.generateFeatures(
currentProject.path
);
const result = await api.specRegeneration.generateFeatures(currentProject.path);
if (!result.success) {
const errorMsg = result.error || "Unknown error";
console.error(
"[useSpecGeneration] Failed to start feature generation:",
errorMsg
);
const errorMsg = result.error || 'Unknown error';
console.error('[useSpecGeneration] Failed to start feature generation:', errorMsg);
setIsGeneratingFeatures(false);
setCurrentPhase("error");
setCurrentPhase('error');
setErrorMessage(errorMsg);
const errorLog = `[Error] Failed to start feature generation: ${errorMsg}\n`;
logsRef.current = errorLog;
@@ -604,12 +533,9 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
}
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
console.error(
"[useSpecGeneration] Failed to generate features:",
errorMsg
);
console.error('[useSpecGeneration] Failed to generate features:', errorMsg);
setIsGeneratingFeatures(false);
setCurrentPhase("error");
setCurrentPhase('error');
setErrorMessage(errorMsg);
const errorLog = `[Error] Failed to generate features: ${errorMsg}\n`;
logsRef.current = errorLog;

Some files were not shown because too many files have changed in this diff Show More