feat: add new themes and improve UI components

- Introduced multiple new themes: retro, dracula, nord, monokai, tokyonight, solarized, gruvbox, catppuccin, onedark, and synthwave.
- Updated global CSS to support new themes and added custom variants for theme-specific styles.
- Enhanced layout and sidebar components with improved styling and responsiveness.
- Refactored button and slider components for better visual consistency and added an animated outline variant.
- Improved various views (e.g., settings, welcome, context) with updated styles and better user experience.

This update enhances the overall aesthetic and usability of the application, providing users with more customization options.
This commit is contained in:
Cody Seibert
2025-12-09 18:51:06 -05:00
parent 66951f2b94
commit 6f3bf2f6b6
19 changed files with 2212 additions and 500 deletions

View File

@@ -0,0 +1,70 @@
You are a very strong reasoner and planner. Use these critical instructions to structure your plans, thoughts, and responses.
Before taking any action (either tool calls or responses to the user), you must proactively, methodically, and independently plan and reason about:
1. Logical dependencies and constraints:
Analyze the intended action against the following factors. Resolve conflicts in order of importance:
1.1) Policy-based rules, mandatory prerequisites, and constraints.
1.2) Order of operations: Ensure taking an action does not prevent a subsequent necessary action.
1.2.1) The user may request actions in a random order, but you may need to reorder operations to maximize successful completion of the task.
1.3) Other prerequisites (information and/or actions needed).
1.4) Explicit user constraints or preferences.
2. Risk assessment:
What are the consequences of taking the action? Will the new state cause any future issues?
2.1) For exploratory tasks (like searches), missing optional parameters is a LOW risk.
Prefer calling the tool with the available information over asking the user, unless your Rule 1 (Logical Dependencies) reasoning determines that optional information is required for a later step in your plan.
3. Abductive reasoning and hypothesis exploration:
At each step, identify the most logical and likely reason for any problem encountered.
3.1) Look beyond immediate or obvious causes. The most likely reason may not be the simplest and may require deeper inference.
3.2) Hypotheses may require additional research. Each hypothesis may take multiple steps to test.
3.3) Prioritize hypotheses based on likelihood, but do not discard less likely ones prematurely. A low-probability event may still be the root cause.
4. Outcome evaluation and adaptability:
Does the previous observation require any changes to your plan?
4.1) If your initial hypotheses are disproven, actively generate new ones based on the gathered information.
5. Information availability:
Incorporate all applicable and alternative sources of information, including:
5.1) Using available tools and their capabilities
5.2) All policies, rules, checklists, and constraints
5.3) Previous observations and conversation history
5.4) Information only available by asking the user
6. Precision and Grounding:
Ensure your reasoning is extremely precise and relevant to each exact ongoing situation.
6.1) Verify your claims by quoting the exact applicable information (including policies) when referring to them.
7. Completeness:
Ensure that all requirements, constraints, options, and preferences are exhaustively incorporated into your plan.
7.1) Resolve conflicts using the order of importance in #1.
7.2) Avoid premature conclusions: There may be multiple relevant options for a given situation.
7.2.1) To check for whether an option is relevant, reason about all information sources from #5.
7.2.2) You may need to consult the user to even know whether something is applicable. Do not assume it is not applicable without checking.
7.3) Review applicable sources of information from #5 to confirm which are relevant to the current state.
8. Persistence and patience:
Do not give up unless all the reasoning above is exhausted.
8.1) Don't be dissuaded by time taken or user frustration.
8.2) This persistence must be intelligent: On transient errors (e.g. please try again), you must retry unless an explicit retry limit (e.g., max x tries) has been reached. If such a limit is hit, you must stop. On other errors, you must change your strategy or arguments, not repeat the same failed call.
9. Inhibit your response:
Only take an action after all the above reasoning is completed. Once you've taken an action, you cannot take it back.

File diff suppressed because it is too large Load Diff

View File

@@ -26,7 +26,7 @@ export default function RootLayout({
return (
<html lang="en" suppressHydrationWarning>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased dark`}
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
<Toaster richColors position="top-right" />

View File

@@ -41,17 +41,52 @@ export default function Home() {
// Apply theme class to document
useEffect(() => {
const root = document.documentElement;
root.classList.remove(
"dark",
"retro",
"light",
"dracula",
"nord",
"monokai",
"tokyonight",
"solarized",
"gruvbox",
"catppuccin",
"onedark",
"synthwave"
);
if (theme === "dark") {
root.classList.add("dark");
} else if (theme === "retro") {
root.classList.add("retro");
} else if (theme === "dracula") {
root.classList.add("dracula");
} else if (theme === "nord") {
root.classList.add("nord");
} else if (theme === "monokai") {
root.classList.add("monokai");
} else if (theme === "tokyonight") {
root.classList.add("tokyonight");
} else if (theme === "solarized") {
root.classList.add("solarized");
} else if (theme === "gruvbox") {
root.classList.add("gruvbox");
} else if (theme === "catppuccin") {
root.classList.add("catppuccin");
} else if (theme === "onedark") {
root.classList.add("onedark");
} else if (theme === "synthwave") {
root.classList.add("synthwave");
} else if (theme === "light") {
root.classList.remove("dark");
} else {
root.classList.add("light");
} else if (theme === "system") {
// System theme
const isDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
if (isDark) {
root.classList.add("dark");
} else {
root.classList.remove("dark");
root.classList.add("light");
}
}
}, [theme]);

View File

@@ -264,7 +264,7 @@ export function Sidebar() {
return (
<aside
className={cn(
"flex-shrink-0 border-r border-white/10 bg-zinc-950/50 backdrop-blur-md flex flex-col z-30 transition-all duration-300 relative",
"flex-shrink-0 border-r border-sidebar-border bg-sidebar backdrop-blur-md flex flex-col z-30 transition-all duration-300 relative",
sidebarOpen ? "w-16 lg:w-60" : "w-16"
)}
data-testid="sidebar"
@@ -272,7 +272,7 @@ export function Sidebar() {
{/* Floating Collapse Toggle Button - Desktop only - At border intersection */}
<button
onClick={toggleSidebar}
className="hidden lg:flex absolute top-[68px] -right-3 z-[9999] group/toggle items-center justify-center w-6 h-6 rounded-full bg-zinc-800 border border-white/10 text-zinc-400 hover:text-white hover:bg-zinc-700 hover:border-white/20 transition-all shadow-lg titlebar-no-drag"
className="hidden lg:flex absolute top-[68px] -right-3 z-9999 group/toggle items-center justify-center w-6 h-6 rounded-full bg-sidebar-accent border border-border text-muted-foreground hover:text-foreground hover:bg-accent hover:border-border transition-all shadow-lg titlebar-no-drag"
data-testid="sidebar-collapse-button"
>
{sidebarOpen ? (
@@ -282,12 +282,12 @@ export function Sidebar() {
)}
{/* Tooltip */}
<div
className="absolute left-full ml-2 px-2 py-1 bg-zinc-800 text-white text-xs rounded opacity-0 group-hover/toggle:opacity-100 transition-opacity whitespace-nowrap z-50 border border-zinc-700 pointer-events-none"
className="absolute left-full ml-2 px-2 py-1 bg-popover text-popover-foreground text-xs rounded opacity-0 group-hover/toggle:opacity-100 transition-opacity whitespace-nowrap z-50 border border-border pointer-events-none"
data-testid="sidebar-toggle-tooltip"
>
{sidebarOpen ? "Collapse sidebar" : "Expand sidebar"}{" "}
<span
className="ml-1 px-1 py-0.5 bg-white/10 rounded text-[10px] font-mono"
className="ml-1 px-1 py-0.5 bg-sidebar-accent/10 rounded text-[10px] font-mono"
data-testid="sidebar-toggle-shortcut"
>
{UI_SHORTCUTS.toggleSidebar}
@@ -299,7 +299,7 @@ export function Sidebar() {
{/* Logo */}
<div
className={cn(
"h-20 pt-8 flex items-center justify-center border-b border-zinc-800 flex-shrink-0 titlebar-drag-region",
"h-20 pt-8 flex items-center justify-center border-b border-sidebar-border shrink-0 titlebar-drag-region",
sidebarOpen ? "px-3 lg:px-6" : "px-3"
)}
>
@@ -308,12 +308,12 @@ export function Sidebar() {
onClick={() => setCurrentView("welcome")}
data-testid="logo-button"
>
<div className="relative flex items-center justify-center w-8 h-8 bg-gradient-to-br from-brand-500 to-purple-600 rounded-lg shadow-lg shadow-brand-500/20 group">
<Cpu className="text-white w-5 h-5 group-hover:rotate-12 transition-transform" />
<div className="relative flex items-center justify-center w-8 h-8 bg-linear-to-br from-brand-500 to-brand-600 rounded-lg shadow-lg shadow-brand-500/20 group">
<Cpu className="text-primary-foreground w-5 h-5 group-hover:rotate-12 transition-transform" />
</div>
<span
className={cn(
"ml-3 font-bold text-white text-base tracking-tight",
"ml-3 font-bold text-sidebar-foreground text-base tracking-tight",
sidebarOpen ? "hidden lg:block" : "hidden"
)}
>
@@ -327,7 +327,7 @@ export function Sidebar() {
<div className="flex items-center gap-2 titlebar-no-drag px-2 mt-3">
<button
onClick={() => setCurrentView("welcome")}
className="group flex items-center justify-center flex-1 px-3 py-2.5 rounded-lg relative overflow-hidden transition-all text-zinc-400 hover:text-white hover:bg-white/5 border border-white/10"
className="group flex items-center justify-center flex-1 px-3 py-2.5 rounded-lg relative overflow-hidden transition-all text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/50 border border-sidebar-border"
title="New Project"
data-testid="new-project-button"
>
@@ -338,11 +338,11 @@ export function Sidebar() {
</button>
<button
onClick={handleOpenFolder}
className="group flex items-center justify-center flex-1 px-3 py-2.5 rounded-lg relative overflow-hidden transition-all text-zinc-400 hover:text-white hover:bg-white/5 border border-white/10"
className="group flex items-center justify-center flex-1 px-3 py-2.5 rounded-lg relative overflow-hidden transition-all text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/50 border border-sidebar-border"
title={`Open Folder (${ACTION_SHORTCUTS.openProject})`}
data-testid="open-project-button"
>
<FolderOpen className="w-4 h-4 flex-shrink-0" />
<FolderOpen className="w-4 h-4 shrink-0" />
<span className="ml-2 text-sm font-medium hidden lg:block whitespace-nowrap">
Open
</span>
@@ -362,28 +362,28 @@ export function Sidebar() {
>
<DropdownMenuTrigger asChild>
<button
className="w-full flex items-center justify-between px-3 py-2.5 rounded-lg bg-white/5 border border-white/10 hover:bg-white/10 transition-all text-white titlebar-no-drag"
className="w-full flex items-center justify-between px-3 py-2.5 rounded-lg bg-sidebar-accent/10 border border-sidebar-border hover:bg-sidebar-accent/20 transition-all text-foreground titlebar-no-drag"
data-testid="project-selector"
>
<div className="flex items-center gap-2 flex-1 min-w-0">
<Folder className="h-4 w-4 text-brand-500 flex-shrink-0" />
<Folder className="h-4 w-4 text-brand-500 shrink-0" />
<span className="text-sm font-medium truncate">
{currentProject?.name || "Select Project"}
</span>
</div>
<div className="flex items-center gap-1">
<span
className="hidden lg:flex items-center justify-center w-5 h-5 text-[10px] font-mono rounded bg-white/5 border border-white/10 text-zinc-500"
className="hidden lg:flex items-center justify-center w-5 h-5 text-[10px] font-mono rounded bg-sidebar-accent/10 border border-sidebar-border text-muted-foreground"
data-testid="project-picker-shortcut"
>
{ACTION_SHORTCUTS.projectPicker}
</span>
<ChevronDown className="h-4 w-4 text-zinc-400 flex-shrink-0" />
<ChevronDown className="h-4 w-4 text-muted-foreground shrink-0" />
</div>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-56 bg-zinc-800 border-zinc-700"
className="w-56 bg-popover border-border"
align="start"
data-testid="project-picker-dropdown"
>
@@ -394,12 +394,12 @@ export function Sidebar() {
setCurrentProject(project);
setIsProjectPickerOpen(false);
}}
className="flex items-center gap-2 cursor-pointer text-zinc-300 hover:text-white hover:bg-zinc-700/50"
className="flex items-center gap-2 cursor-pointer text-muted-foreground hover:text-foreground hover:bg-accent"
data-testid={`project-option-${project.id}`}
>
{index < 9 && (
<span
className="flex items-center justify-center w-5 h-5 text-[10px] font-mono rounded bg-white/5 border border-white/10 text-zinc-400"
className="flex items-center justify-center w-5 h-5 text-[10px] font-mono rounded bg-sidebar-accent/10 border border-sidebar-border text-muted-foreground"
data-testid={`project-hotkey-${index + 1}`}
>
{index + 1}
@@ -422,7 +422,7 @@ export function Sidebar() {
{!currentProject && sidebarOpen ? (
// Placeholder when no project is selected (only in expanded state)
<div className="flex items-center justify-center h-full px-4">
<p className="text-zinc-500 text-sm text-center">
<p className="text-muted-foreground text-sm text-center">
<span className="hidden lg:block">
Select or create a project above
</span>
@@ -435,13 +435,13 @@ export function Sidebar() {
{/* Section Label */}
{section.label && sidebarOpen && (
<div className="hidden lg:block px-4 mb-2">
<span className="text-[10px] font-semibold text-zinc-500 uppercase tracking-wider">
<span className="text-[10px] font-semibold text-muted-foreground uppercase tracking-wider">
{section.label}
</span>
</div>
)}
{section.label && !sidebarOpen && (
<div className="h-px bg-zinc-800 mx-2 mb-2"></div>
<div className="h-px bg-sidebar-border mx-2 mb-2"></div>
)}
{/* Nav Items */}
@@ -457,8 +457,8 @@ export function Sidebar() {
className={cn(
"group flex items-center w-full px-2 lg:px-3 py-2.5 rounded-lg relative overflow-hidden transition-all titlebar-no-drag",
isActive
? "bg-white/5 text-white border border-white/10"
: "text-zinc-400 hover:text-white hover:bg-white/5",
? "bg-sidebar-accent/50 text-foreground border border-sidebar-border"
: "text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/50",
!sidebarOpen && "justify-center"
)}
title={!sidebarOpen ? item.label : undefined}
@@ -469,7 +469,7 @@ export function Sidebar() {
)}
<Icon
className={cn(
"w-4 h-4 flex-shrink-0 transition-colors",
"w-4 h-4 shrink-0 transition-colors",
isActive
? "text-brand-500"
: "group-hover:text-brand-400"
@@ -515,7 +515,7 @@ export function Sidebar() {
</div>
{/* Bottom Section - User / Settings */}
<div className="border-t border-zinc-800 bg-zinc-900/50 flex-shrink-0">
<div className="border-t border-sidebar-border bg-sidebar-accent/10 shrink-0">
{/* Settings Link */}
<div className="p-2">
<button
@@ -523,8 +523,8 @@ export function Sidebar() {
className={cn(
"group flex items-center w-full px-2 lg:px-3 py-2.5 rounded-lg relative overflow-hidden transition-all titlebar-no-drag",
isActiveRoute("settings")
? "bg-white/5 text-white border border-white/10"
: "text-zinc-400 hover:text-white hover:bg-white/5",
? "bg-sidebar-accent/50 text-foreground border border-sidebar-border"
: "text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/50",
sidebarOpen ? "justify-start" : "justify-center"
)}
title={!sidebarOpen ? "Settings" : undefined}
@@ -535,7 +535,7 @@ export function Sidebar() {
)}
<Settings
className={cn(
"w-4 h-4 flex-shrink-0 transition-colors",
"w-4 h-4 shrink-0 transition-colors",
isActiveRoute("settings")
? "text-brand-500"
: "group-hover:text-brand-400"
@@ -562,7 +562,7 @@ export function Sidebar() {
</span>
)}
{!sidebarOpen && (
<span className="absolute left-full ml-2 px-2 py-1 bg-zinc-800 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-50 border border-zinc-700">
<span className="absolute left-full ml-2 px-2 py-1 bg-popover text-popover-foreground text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-50 border border-border">
Settings
</span>
)}

View File

@@ -1,8 +1,8 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
@@ -19,6 +19,8 @@ const buttonVariants = cva(
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
"animated-outline":
"relative overflow-hidden rounded-xl hover:bg-transparent shadow-none",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
@@ -34,27 +36,60 @@ const buttonVariants = cva(
size: "default",
},
}
)
);
function Button({
className,
variant,
size,
asChild = false,
children,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
asChild?: boolean;
}) {
const Comp = asChild ? Slot : "button"
// Special handling for animated-outline variant
if (variant === "animated-outline" && !asChild) {
return (
<button
className={cn(
buttonVariants({ variant, size }),
"p-[1px]", // Force 1px padding for the gradient border
className
)}
data-slot="button"
{...props}
>
{/* Animated rotating gradient border */}
<span className="absolute inset-[-1000%] animate-[spin_2s_linear_infinite] animated-outline-gradient" />
{/* Inner content container */}
<span
className={cn(
"animated-outline-inner inline-flex h-full w-full cursor-pointer items-center justify-center gap-2 rounded-[10px] px-4 py-1 text-sm font-medium backdrop-blur-3xl transition-all",
size === "sm" && "px-3 text-xs gap-1.5",
size === "lg" && "px-8",
size === "icon" && "p-0 gap-0"
)}
>
{children}
</span>
</button>
);
}
const Comp = asChild ? Slot : "button";
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
>
{children}
</Comp>
);
}
export { Button, buttonVariants }
export { Button, buttonVariants };

View File

@@ -16,10 +16,10 @@ const Slider = React.forwardRef<
)}
{...props}
>
<SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-white/10">
<SliderPrimitive.Range className="absolute h-full bg-gradient-to-r from-purple-600 to-blue-600" />
<SliderPrimitive.Track className="slider-track relative h-1.5 w-full grow overflow-hidden rounded-full bg-muted">
<SliderPrimitive.Range className="slider-range absolute h-full bg-primary" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-4 w-4 rounded-full border border-white/20 bg-zinc-800 shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-white/50 disabled:pointer-events-none disabled:opacity-50 hover:bg-zinc-700" />
<SliderPrimitive.Thumb className="slider-thumb block h-4 w-4 rounded-full border border-border bg-card shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 hover:bg-accent" />
</SliderPrimitive.Root>
));
Slider.displayName = SliderPrimitive.Root.displayName;

View File

@@ -213,7 +213,7 @@ export function AgentToolsView() {
data-testid="agent-tools-view"
>
{/* Header */}
<div className="flex items-center gap-3 p-4 border-b border-white/10 bg-zinc-950/50 backdrop-blur-md">
<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" />
<div>
<h1 className="text-xl font-bold">Agent Tools</h1>

View File

@@ -452,7 +452,7 @@ export function AgentView() {
{/* Chat Area */}
<div className="flex-1 flex flex-col overflow-hidden">
{/* 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 justify-between p-4 border-b border-border bg-glass backdrop-blur-md">
<div className="flex items-center gap-3">
<Button
variant="ghost"
@@ -503,7 +503,10 @@ export function AgentView() {
{/* Messages */}
{!currentSessionId ? (
<div className="flex-1 flex items-center justify-center" data-testid="no-session-placeholder">
<div
className="flex-1 flex items-center justify-center"
data-testid="no-session-placeholder"
>
<div className="text-center">
<Bot className="w-12 h-12 text-muted-foreground mx-auto mb-4 opacity-50" />
<h2 className="text-lg font-semibold mb-2">

View File

@@ -37,11 +37,26 @@ export function AutoModeLog({ onClose }: AutoModeLogProps) {
case "error":
return <AlertCircle className="w-4 h-4 text-red-500" />;
case "planning":
return <ClipboardList className="w-4 h-4 text-cyan-500" data-testid="planning-phase-icon" />;
return (
<ClipboardList
className="w-4 h-4 text-cyan-500"
data-testid="planning-phase-icon"
/>
);
case "action":
return <Zap className="w-4 h-4 text-orange-500" data-testid="action-phase-icon" />;
return (
<Zap
className="w-4 h-4 text-orange-500"
data-testid="action-phase-icon"
/>
);
case "verification":
return <ShieldCheck className="w-4 h-4 text-emerald-500" data-testid="verification-phase-icon" />;
return (
<ShieldCheck
className="w-4 h-4 text-emerald-500"
data-testid="verification-phase-icon"
/>
);
}
};
@@ -80,8 +95,8 @@ export function AutoModeLog({ onClose }: AutoModeLogProps) {
};
return (
<Card className="h-full flex flex-col border-white/10 bg-zinc-950/50 backdrop-blur-sm">
<CardHeader className="p-4 border-b border-white/10 flex-shrink-0">
<Card className="h-full flex flex-col border-border bg-card backdrop-blur-sm">
<CardHeader className="p-4 border-b border-border flex-shrink-0">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Loader2 className="w-5 h-5 text-purple-500 animate-spin" />
@@ -127,12 +142,14 @@ export function AutoModeLog({ onClose }: AutoModeLogProps) {
<div
key={activity.id}
className={cn(
"p-3 rounded-lg bg-zinc-900/50 border-l-4 hover:bg-zinc-900/70 transition-colors",
"p-3 rounded-lg bg-secondary border-l-4 hover:bg-accent transition-colors",
getActivityColor(activity.type)
)}
>
<div className="flex items-start gap-3">
<div className="mt-0.5">{getActivityIcon(activity.type)}</div>
<div className="mt-0.5">
{getActivityIcon(activity.type)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-baseline gap-2 mb-1">
<span className="text-xs text-muted-foreground">

View File

@@ -16,7 +16,12 @@ import {
SortableContext,
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { useAppStore, Feature, FeatureImage, FeatureImagePath } from "@/store/app-store";
import {
useAppStore,
Feature,
FeatureImage,
FeatureImagePath,
} from "@/store/app-store";
import { getElectronAPI } from "@/lib/electron";
import { cn } from "@/lib/utils";
import {
@@ -31,7 +36,10 @@ import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { CategoryAutocomplete } from "@/components/ui/category-autocomplete";
import { FeatureImageUpload } from "@/components/ui/feature-image-upload";
import { DescriptionImageDropZone, FeatureImagePath as DescriptionImagePath } from "@/components/ui/description-image-dropzone";
import {
DescriptionImageDropZone,
FeatureImagePath as DescriptionImagePath,
} from "@/components/ui/description-image-dropzone";
import {
Dialog,
DialogContent,
@@ -44,7 +52,20 @@ import { KanbanColumn } from "./kanban-column";
import { KanbanCard } from "./kanban-card";
import { AutoModeLog } from "./auto-mode-log";
import { AgentOutputModal } from "./agent-output-modal";
import { Plus, RefreshCw, Play, StopCircle, Loader2, ChevronUp, ChevronDown, Users, Trash2, FastForward, FlaskConical, CheckCircle2 } from "lucide-react";
import {
Plus,
RefreshCw,
Play,
StopCircle,
Loader2,
ChevronUp,
ChevronDown,
Users,
Trash2,
FastForward,
FlaskConical,
CheckCircle2,
} from "lucide-react";
import { toast } from "sonner";
import { Slider } from "@/components/ui/slider";
import { Checkbox } from "@/components/ui/checkbox";
@@ -92,8 +113,11 @@ export function BoardView() {
const [showActivityLog, setShowActivityLog] = useState(false);
const [showOutputModal, setShowOutputModal] = useState(false);
const [outputFeature, setOutputFeature] = useState<Feature | null>(null);
const [featuresWithContext, setFeaturesWithContext] = useState<Set<string>>(new Set());
const [showDeleteAllVerifiedDialog, setShowDeleteAllVerifiedDialog] = useState(false);
const [featuresWithContext, setFeaturesWithContext] = useState<Set<string>>(
new Set()
);
const [showDeleteAllVerifiedDialog, setShowDeleteAllVerifiedDialog] =
useState(false);
const [persistedCategories, setPersistedCategories] = useState<string[]>([]);
// Make current project available globally for modal
@@ -125,39 +149,36 @@ export function BoardView() {
const startNextFeaturesRef = useRef<() => void>(() => {});
// Keyboard shortcuts for this view
const boardShortcuts: KeyboardShortcut[] = useMemo(
() => {
const shortcuts: KeyboardShortcut[] = [
{
key: ACTION_SHORTCUTS.addFeature,
action: () => setShowAddDialog(true),
description: "Add new feature",
},
{
key: ACTION_SHORTCUTS.startNext,
action: () => startNextFeaturesRef.current(),
description: "Start next features from backlog",
},
];
const boardShortcuts: KeyboardShortcut[] = useMemo(() => {
const shortcuts: KeyboardShortcut[] = [
{
key: ACTION_SHORTCUTS.addFeature,
action: () => setShowAddDialog(true),
description: "Add new feature",
},
{
key: ACTION_SHORTCUTS.startNext,
action: () => startNextFeaturesRef.current(),
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);
shortcuts.push({
key,
action: () => {
setOutputFeature(feature);
setShowOutputModal(true);
},
description: `View output for in-progress card ${index + 1}`,
});
// 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);
shortcuts.push({
key,
action: () => {
setOutputFeature(feature);
setShowOutputModal(true);
},
description: `View output for in-progress card ${index + 1}`,
});
});
return shortcuts;
},
[inProgressFeaturesForShortcuts]
);
return shortcuts;
}, [inProgressFeaturesForShortcuts]);
useKeyboardShortcuts(boardShortcuts);
// Prevent hydration issues
@@ -207,7 +228,9 @@ export function BoardView() {
// If project switched, clear features first to prevent cross-contamination
if (previousPath !== null && currentPath !== previousPath) {
console.log(`[BoardView] Project switch detected: ${previousPath} -> ${currentPath}, clearing features`);
console.log(
`[BoardView] Project switch detected: ${previousPath} -> ${currentPath}, clearing features`
);
isSwitchingProjectRef.current = true;
setFeatures([]);
setPersistedCategories([]); // Also clear categories
@@ -225,14 +248,12 @@ export function BoardView() {
if (result.success && result.content) {
const parsed = JSON.parse(result.content);
const featuresWithIds = parsed.map(
(f: any, index: number) => ({
...f,
id: f.id || `feature-${index}-${Date.now()}`,
status: f.status || "backlog",
startedAt: f.startedAt, // Preserve startedAt timestamp
})
);
const featuresWithIds = parsed.map((f: any, index: number) => ({
...f,
id: f.id || `feature-${index}-${Date.now()}`,
status: f.status || "backlog",
startedAt: f.startedAt, // Preserve startedAt timestamp
}));
setFeatures(featuresWithIds);
}
} catch (error) {
@@ -270,33 +291,36 @@ export function BoardView() {
}, [currentProject]);
// Save a new category to the persisted categories file
const saveCategory = useCallback(async (category: string) => {
if (!currentProject || !category.trim()) return;
const saveCategory = useCallback(
async (category: string) => {
if (!currentProject || !category.trim()) return;
try {
const api = getElectronAPI();
try {
const api = getElectronAPI();
// Read existing categories
let categories: string[] = [...persistedCategories];
// Read existing categories
let categories: string[] = [...persistedCategories];
// Add new category if it doesn't exist
if (!categories.includes(category)) {
categories.push(category);
categories.sort(); // Keep sorted
// Add new category if it doesn't exist
if (!categories.includes(category)) {
categories.push(category);
categories.sort(); // Keep sorted
// Write back to file
await api.writeFile(
`${currentProject.path}/.automaker/categories.json`,
JSON.stringify(categories, null, 2)
);
// Write back to file
await api.writeFile(
`${currentProject.path}/.automaker/categories.json`,
JSON.stringify(categories, null, 2)
);
// Update state
setPersistedCategories(categories);
// Update state
setPersistedCategories(categories);
}
} catch (error) {
console.error("Failed to save category:", error);
}
} catch (error) {
console.error("Failed to save category:", error);
}
}, [currentProject, persistedCategories]);
},
[currentProject, persistedCategories]
);
// Auto-show activity log when auto mode starts
useEffect(() => {
@@ -339,7 +363,10 @@ export function BoardView() {
const status = await api.autoMode.status();
if (status.success && status.runningFeatures) {
console.log("[Board] Syncing running tasks from backend:", status.runningFeatures);
console.log(
"[Board] Syncing running tasks from backend:",
status.runningFeatures
);
// Clear existing running tasks and add the actual running ones
const { clearRunningTasks, addRunningTask } = useAppStore.getState();
@@ -361,7 +388,9 @@ export function BoardView() {
// Check which features have context files
useEffect(() => {
const checkAllContexts = async () => {
const inProgressFeatures = features.filter((f) => f.status === "in_progress");
const inProgressFeatures = features.filter(
(f) => f.status === "in_progress"
);
const contextChecks = await Promise.all(
inProgressFeatures.map(async (f) => ({
id: f.id,
@@ -447,7 +476,9 @@ export function BoardView() {
if (draggedFeature.status !== "backlog") {
// Only allow dragging in_progress/verified if it's a skipTests feature and not currently running
if (!draggedFeature.skipTests || isRunningTask) {
console.log("[Board] Cannot drag feature - TDD feature or currently running");
console.log(
"[Board] Cannot drag feature - TDD feature or currently running"
);
return;
}
}
@@ -472,10 +503,16 @@ export function BoardView() {
if (targetStatus === draggedFeature.status) return;
// Check concurrency limit before moving to in_progress (only for backlog -> in_progress and if running agent)
if (targetStatus === "in_progress" && draggedFeature.status === "backlog" && !autoMode.canStartNewTask) {
if (
targetStatus === "in_progress" &&
draggedFeature.status === "backlog" &&
!autoMode.canStartNewTask
) {
console.log("[Board] Cannot start new task - at max concurrency limit");
toast.error("Concurrency limit reached", {
description: `You can only have ${autoMode.maxConcurrency} task${autoMode.maxConcurrency > 1 ? "s" : ""} running at a time. Wait for a task to complete or increase the limit.`,
description: `You can only have ${autoMode.maxConcurrency} task${
autoMode.maxConcurrency > 1 ? "s" : ""
} running at a time. Wait for a task to complete or increase the limit.`,
});
return;
}
@@ -485,7 +522,10 @@ export function BoardView() {
// From backlog
if (targetStatus === "in_progress") {
// Update with startedAt timestamp
updateFeature(featureId, { status: targetStatus, startedAt: new Date().toISOString() });
updateFeature(featureId, {
status: targetStatus,
startedAt: new Date().toISOString(),
});
console.log("[Board] Feature moved to in_progress, starting agent...");
await handleRunFeature(draggedFeature);
} else {
@@ -493,23 +533,41 @@ export function BoardView() {
}
} else if (draggedFeature.skipTests) {
// skipTests feature being moved between in_progress and verified
if (targetStatus === "verified" && draggedFeature.status === "in_progress") {
if (
targetStatus === "verified" &&
draggedFeature.status === "in_progress"
) {
// Manual verify via drag
moveFeature(featureId, "verified");
toast.success("Feature verified", {
description: `Marked as verified: ${draggedFeature.description.slice(0, 50)}${draggedFeature.description.length > 50 ? "..." : ""}`,
description: `Marked as verified: ${draggedFeature.description.slice(
0,
50
)}${draggedFeature.description.length > 50 ? "..." : ""}`,
});
} else if (targetStatus === "in_progress" && draggedFeature.status === "verified") {
} else if (
targetStatus === "in_progress" &&
draggedFeature.status === "verified"
) {
// Move back to in_progress
updateFeature(featureId, { status: "in_progress", startedAt: new Date().toISOString() });
updateFeature(featureId, {
status: "in_progress",
startedAt: new Date().toISOString(),
});
toast.info("Feature moved back", {
description: `Moved back to In Progress: ${draggedFeature.description.slice(0, 50)}${draggedFeature.description.length > 50 ? "..." : ""}`,
description: `Moved back to In Progress: ${draggedFeature.description.slice(
0,
50
)}${draggedFeature.description.length > 50 ? "..." : ""}`,
});
} else if (targetStatus === "backlog") {
// Allow moving skipTests cards back to backlog
moveFeature(featureId, "backlog");
toast.info("Feature moved to backlog", {
description: `Moved to Backlog: ${draggedFeature.description.slice(0, 50)}${draggedFeature.description.length > 50 ? "..." : ""}`,
description: `Moved to Backlog: ${draggedFeature.description.slice(
0,
50
)}${draggedFeature.description.length > 50 ? "..." : ""}`,
});
}
}
@@ -528,7 +586,14 @@ export function BoardView() {
});
// Persist the category
saveCategory(category);
setNewFeature({ category: "", description: "", steps: [""], images: [], imagePaths: [], skipTests: false });
setNewFeature({
category: "",
description: "",
steps: [""],
images: [],
imagePaths: [],
skipTests: false,
});
setShowAddDialog(false);
};
@@ -560,7 +625,10 @@ export function BoardView() {
try {
await autoMode.stopFeature(featureId);
toast.success("Agent stopped", {
description: `Stopped and deleted: ${feature.description.slice(0, 50)}${feature.description.length > 50 ? "..." : ""}`,
description: `Stopped and deleted: ${feature.description.slice(
0,
50
)}${feature.description.length > 50 ? "..." : ""}`,
});
} catch (error) {
console.error("[Board] Error stopping feature before delete:", error);
@@ -609,7 +677,10 @@ export function BoardView() {
const handleVerifyFeature = async (feature: Feature) => {
if (!currentProject) return;
console.log("[Board] Verifying feature:", { id: feature.id, description: feature.description });
console.log("[Board] Verifying feature:", {
id: feature.id,
description: feature.description,
});
try {
const api = getElectronAPI();
@@ -641,7 +712,10 @@ export function BoardView() {
const handleResumeFeature = async (feature: Feature) => {
if (!currentProject) return;
console.log("[Board] Resuming feature:", { id: feature.id, description: feature.description });
console.log("[Board] Resuming feature:", {
id: feature.id,
description: feature.description,
});
try {
const api = getElectronAPI();
@@ -672,19 +746,33 @@ export function BoardView() {
// Manual verification handler for skipTests features
const handleManualVerify = (feature: Feature) => {
console.log("[Board] Manually verifying feature:", { id: feature.id, description: feature.description });
console.log("[Board] Manually verifying feature:", {
id: feature.id,
description: feature.description,
});
moveFeature(feature.id, "verified");
toast.success("Feature verified", {
description: `Marked as verified: ${feature.description.slice(0, 50)}${feature.description.length > 50 ? "..." : ""}`,
description: `Marked as verified: ${feature.description.slice(0, 50)}${
feature.description.length > 50 ? "..." : ""
}`,
});
};
// Move feature back to in_progress from verified (for skipTests features)
const handleMoveBackToInProgress = (feature: Feature) => {
console.log("[Board] Moving feature back to in_progress:", { id: feature.id, description: feature.description });
updateFeature(feature.id, { status: "in_progress", startedAt: new Date().toISOString() });
console.log("[Board] Moving feature back to in_progress:", {
id: feature.id,
description: feature.description,
});
updateFeature(feature.id, {
status: "in_progress",
startedAt: new Date().toISOString(),
});
toast.info("Feature moved back", {
description: `Moved back to In Progress: ${feature.description.slice(0, 50)}${feature.description.length > 50 ? "..." : ""}`,
description: `Moved back to In Progress: ${feature.description.slice(
0,
50
)}${feature.description.length > 50 ? "..." : ""}`,
});
};
@@ -727,28 +815,31 @@ export function BoardView() {
};
// Handle number key press when output modal is open
const handleOutputModalNumberKeyPress = useCallback((key: string) => {
// Convert key to index: 1-9 -> 0-8, 0 -> 9
const index = key === "0" ? 9 : parseInt(key, 10) - 1;
const handleOutputModalNumberKeyPress = useCallback(
(key: string) => {
// Convert key to index: 1-9 -> 0-8, 0 -> 9
const index = key === "0" ? 9 : parseInt(key, 10) - 1;
// Get the feature at that index from in-progress features
const targetFeature = inProgressFeaturesForShortcuts[index];
// Get the feature at that index from in-progress features
const targetFeature = inProgressFeaturesForShortcuts[index];
if (!targetFeature) {
// No feature at this index, do nothing
return;
}
if (!targetFeature) {
// No feature at this index, do nothing
return;
}
// If pressing the same number key as the currently open feature, close the modal
if (targetFeature.id === outputFeature?.id) {
setShowOutputModal(false);
}
// If pressing a different number key, switch to that feature's output
else {
setOutputFeature(targetFeature);
// Modal stays open, just showing different content
}
}, [inProgressFeaturesForShortcuts, outputFeature?.id]);
// If pressing the same number key as the currently open feature, close the modal
if (targetFeature.id === outputFeature?.id) {
setShowOutputModal(false);
}
// If pressing a different number key, switch to that feature's output
else {
setOutputFeature(targetFeature);
// Modal stays open, just showing different content
}
},
[inProgressFeaturesForShortcuts, outputFeature?.id]
);
const handleForceStopFeature = async (feature: Feature) => {
try {
@@ -756,12 +847,15 @@ export function BoardView() {
// Move the feature back to backlog status after stopping
moveFeature(feature.id, "backlog");
toast.success("Agent stopped", {
description: `Stopped working on: ${feature.description.slice(0, 50)}${feature.description.length > 50 ? "..." : ""}`,
description: `Stopped working on: ${feature.description.slice(0, 50)}${
feature.description.length > 50 ? "..." : ""
}`,
});
} catch (error) {
console.error("[Board] Error stopping feature:", error);
toast.error("Failed to stop agent", {
description: error instanceof Error ? error.message : "An error occurred",
description:
error instanceof Error ? error.message : "An error occurred",
});
}
};
@@ -773,7 +867,9 @@ export function BoardView() {
if (availableSlots <= 0) {
toast.error("Concurrency limit reached", {
description: `You can only have ${maxConcurrency} task${maxConcurrency > 1 ? "s" : ""} running at a time. Wait for a task to complete or increase the limit.`,
description: `You can only have ${maxConcurrency} task${
maxConcurrency > 1 ? "s" : ""
} running at a time. Wait for a task to complete or increase the limit.`,
});
return;
}
@@ -789,14 +885,28 @@ export function BoardView() {
for (const feature of featuresToStart) {
// Update the feature status with startedAt timestamp
updateFeature(feature.id, { status: "in_progress", startedAt: new Date().toISOString() });
updateFeature(feature.id, {
status: "in_progress",
startedAt: new Date().toISOString(),
});
// Start the agent for this feature
await handleRunFeature(feature);
}
toast.success(`Started ${featuresToStart.length} feature${featuresToStart.length > 1 ? "s" : ""}`, {
description: featuresToStart.map((f) => f.description.slice(0, 30) + (f.description.length > 30 ? "..." : "")).join(", "),
});
toast.success(
`Started ${featuresToStart.length} feature${
featuresToStart.length > 1 ? "s" : ""
}`,
{
description: featuresToStart
.map(
(f) =>
f.description.slice(0, 30) +
(f.description.length > 30 ? "..." : "")
)
.join(", "),
}
);
}, [features, maxConcurrency, runningAutoTasks.length, updateFeature]);
// Update ref when handleStartNextFeatures changes
@@ -832,7 +942,7 @@ export function BoardView() {
data-testid="board-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 justify-between p-4 border-b border-border bg-glass backdrop-blur-md">
<div>
<h1 className="text-xl font-bold">Kanban Board</h1>
<p className="text-sm text-muted-foreground">{currentProject.name}</p>
@@ -841,10 +951,10 @@ export function BoardView() {
{/* Concurrency Slider - only show after mount to prevent hydration issues */}
{isMounted && (
<div
className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-white/5 border border-white/10"
className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-secondary border border-border"
data-testid="concurrency-slider-container"
>
<Users className="w-4 h-4 text-zinc-400" />
<Users className="w-4 h-4 text-muted-foreground" />
<Slider
value={[maxConcurrency]}
onValueChange={(value) => setMaxConcurrency(value[0])}
@@ -855,7 +965,7 @@ export function BoardView() {
data-testid="concurrency-slider"
/>
<span
className="text-sm text-zinc-400 min-w-[2ch] text-center"
className="text-sm text-muted-foreground min-w-[2ch] text-center"
data-testid="concurrency-value"
>
{maxConcurrency}
@@ -878,11 +988,10 @@ export function BoardView() {
</Button>
) : (
<Button
variant="default"
variant="secondary"
size="sm"
onClick={() => autoMode.start()}
data-testid="start-auto-mode"
className="bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700"
>
<Play className="w-4 h-4 mr-2" />
Auto Mode
@@ -928,110 +1037,117 @@ export function BoardView() {
{/* Main Content Area */}
<div className="flex-1 flex overflow-hidden">
{/* Kanban Columns */}
<div className={cn(
"flex-1 overflow-x-auto p-4",
showActivityLog && "transition-all"
)}>
<DndContext
sensors={sensors}
collisionDetection={collisionDetectionStrategy}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
<div
className={cn(
"flex-1 overflow-x-auto p-4",
showActivityLog && "transition-all"
)}
>
<div className="flex gap-4 h-full min-w-max">
{COLUMNS.map((column) => {
const columnFeatures = getColumnFeatures(column.id);
return (
<KanbanColumn
key={column.id}
id={column.id}
title={column.title}
color={column.color}
count={columnFeatures.length}
isDoubleWidth={column.id === "in_progress"}
headerAction={
column.id === "verified" && columnFeatures.length > 0 ? (
<Button
variant="ghost"
size="sm"
className="h-6 px-2 text-xs text-destructive hover:text-destructive hover:bg-destructive/10"
onClick={() => setShowDeleteAllVerifiedDialog(true)}
data-testid="delete-all-verified-button"
>
<Trash2 className="w-3 h-3 mr-1" />
Delete All
</Button>
) : column.id === "backlog" && columnFeatures.length > 0 ? (
<Button
variant="ghost"
size="sm"
className="h-6 px-2 text-xs text-primary hover:text-primary hover:bg-primary/10"
onClick={handleStartNextFeatures}
data-testid="start-next-button"
>
<FastForward className="w-3 h-3 mr-1" />
Start Next
<span className="ml-1 px-1 py-0.5 text-[9px] font-mono rounded bg-white/10 border border-white/20">
{ACTION_SHORTCUTS.startNext}
</span>
</Button>
) : undefined
}
>
<SortableContext
items={columnFeatures.map((f) => f.id)}
strategy={verticalListSortingStrategy}
<DndContext
sensors={sensors}
collisionDetection={collisionDetectionStrategy}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<div className="flex gap-4 h-full min-w-max">
{COLUMNS.map((column) => {
const columnFeatures = getColumnFeatures(column.id);
return (
<KanbanColumn
key={column.id}
id={column.id}
title={column.title}
color={column.color}
count={columnFeatures.length}
isDoubleWidth={column.id === "in_progress"}
headerAction={
column.id === "verified" && columnFeatures.length > 0 ? (
<Button
variant="ghost"
size="sm"
className="h-6 px-2 text-xs text-destructive hover:text-destructive hover:bg-destructive/10"
onClick={() => setShowDeleteAllVerifiedDialog(true)}
data-testid="delete-all-verified-button"
>
<Trash2 className="w-3 h-3 mr-1" />
Delete All
</Button>
) : column.id === "backlog" &&
columnFeatures.length > 0 ? (
<Button
variant="ghost"
size="sm"
className="h-6 px-2 text-xs text-primary hover:text-primary hover:bg-primary/10"
onClick={handleStartNextFeatures}
data-testid="start-next-button"
>
<FastForward className="w-3 h-3 mr-1" />
Start Next
<span className="ml-1 px-1 py-0.5 text-[9px] font-mono rounded bg-white/10 border border-white/20">
{ACTION_SHORTCUTS.startNext}
</span>
</Button>
) : undefined
}
>
{columnFeatures.map((feature, index) => {
// Calculate shortcut key for in-progress cards (first 10 get 1-9, 0)
let shortcutKey: string | undefined;
if (column.id === "in_progress" && index < 10) {
shortcutKey = index === 9 ? "0" : String(index + 1);
}
return (
<KanbanCard
key={feature.id}
feature={feature}
onEdit={() => setEditingFeature(feature)}
onDelete={() => handleDeleteFeature(feature.id)}
onViewOutput={() => handleViewOutput(feature)}
onVerify={() => handleVerifyFeature(feature)}
onResume={() => handleResumeFeature(feature)}
onForceStop={() => handleForceStopFeature(feature)}
onManualVerify={() => handleManualVerify(feature)}
onMoveBackToInProgress={() => handleMoveBackToInProgress(feature)}
hasContext={featuresWithContext.has(feature.id)}
isCurrentAutoTask={runningAutoTasks.includes(feature.id)}
shortcutKey={shortcutKey}
/>
);
})}
</SortableContext>
</KanbanColumn>
);
})}
</div>
<SortableContext
items={columnFeatures.map((f) => f.id)}
strategy={verticalListSortingStrategy}
>
{columnFeatures.map((feature, index) => {
// Calculate shortcut key for in-progress cards (first 10 get 1-9, 0)
let shortcutKey: string | undefined;
if (column.id === "in_progress" && index < 10) {
shortcutKey = index === 9 ? "0" : String(index + 1);
}
return (
<KanbanCard
key={feature.id}
feature={feature}
onEdit={() => setEditingFeature(feature)}
onDelete={() => handleDeleteFeature(feature.id)}
onViewOutput={() => handleViewOutput(feature)}
onVerify={() => handleVerifyFeature(feature)}
onResume={() => handleResumeFeature(feature)}
onForceStop={() => handleForceStopFeature(feature)}
onManualVerify={() => handleManualVerify(feature)}
onMoveBackToInProgress={() =>
handleMoveBackToInProgress(feature)
}
hasContext={featuresWithContext.has(feature.id)}
isCurrentAutoTask={runningAutoTasks.includes(
feature.id
)}
shortcutKey={shortcutKey}
/>
);
})}
</SortableContext>
</KanbanColumn>
);
})}
</div>
<DragOverlay>
{activeFeature && (
<Card className="w-72 opacity-90 rotate-3 shadow-xl">
<CardHeader className="p-3">
<CardTitle className="text-sm">
{activeFeature.description}
</CardTitle>
<CardDescription className="text-xs">
{activeFeature.category}
</CardDescription>
</CardHeader>
</Card>
)}
</DragOverlay>
</DndContext>
<DragOverlay>
{activeFeature && (
<Card className="w-72 opacity-90 rotate-3 shadow-xl">
<CardHeader className="p-3">
<CardTitle className="text-sm">
{activeFeature.description}
</CardTitle>
<CardDescription className="text-xs">
{activeFeature.category}
</CardDescription>
</CardHeader>
</Card>
)}
</DragOverlay>
</DndContext>
</div>
{/* Activity Log Panel */}
{showActivityLog && (
<div className="w-96 border-l border-white/10 flex-shrink-0">
<div className="w-96 border-l border-border flex-shrink-0">
<AutoModeLog onClose={() => setShowActivityLog(false)} />
</div>
)}
@@ -1042,7 +1158,11 @@ export function BoardView() {
<DialogContent
data-testid="add-feature-dialog"
onKeyDown={(e) => {
if ((e.metaKey || e.ctrlKey) && e.key === "Enter" && newFeature.description) {
if (
(e.metaKey || e.ctrlKey) &&
e.key === "Enter" &&
newFeature.description
) {
e.preventDefault();
handleAddFeature();
}
@@ -1128,7 +1248,8 @@ export function BoardView() {
</div>
</div>
<p className="text-xs text-muted-foreground">
When enabled, this feature will require manual verification instead of automated TDD.
When enabled, this feature will require manual verification
instead of automated TDD.
</p>
</div>
<DialogFooter>
@@ -1227,19 +1348,26 @@ export function BoardView() {
id="edit-skip-tests"
checked={editingFeature.skipTests ?? false}
onCheckedChange={(checked) =>
setEditingFeature({ ...editingFeature, skipTests: checked === true })
setEditingFeature({
...editingFeature,
skipTests: checked === true,
})
}
data-testid="edit-skip-tests-checkbox"
/>
<div className="flex items-center gap-2">
<Label htmlFor="edit-skip-tests" className="text-sm cursor-pointer">
<Label
htmlFor="edit-skip-tests"
className="text-sm cursor-pointer"
>
Skip automated testing
</Label>
<FlaskConical className="w-3.5 h-3.5 text-muted-foreground" />
</div>
</div>
<p className="text-xs text-muted-foreground">
When enabled, this feature will require manual verification instead of automated TDD.
When enabled, this feature will require manual verification
instead of automated TDD.
</p>
</div>
)}
@@ -1267,21 +1395,29 @@ export function BoardView() {
/>
{/* Delete All Verified Dialog */}
<Dialog open={showDeleteAllVerifiedDialog} onOpenChange={setShowDeleteAllVerifiedDialog}>
<Dialog
open={showDeleteAllVerifiedDialog}
onOpenChange={setShowDeleteAllVerifiedDialog}
>
<DialogContent data-testid="delete-all-verified-dialog">
<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.
{getColumnFeatures("verified").length > 0 && (
<span className="block mt-2 text-yellow-500">
{getColumnFeatures("verified").length} feature(s) will be deleted.
{getColumnFeatures("verified").length} feature(s) will be
deleted.
</span>
)}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="ghost" onClick={() => setShowDeleteAllVerifiedDialog(false)}>
<Button
variant="ghost"
onClick={() => setShowDeleteAllVerifiedDialog(false)}
>
Cancel
</Button>
<Button
@@ -1297,7 +1433,10 @@ export function BoardView() {
try {
await autoMode.stopFeature(feature.id);
} catch (error) {
console.error("[Board] Error stopping feature before delete:", error);
console.error(
"[Board] Error stopping feature before delete:",
error
);
}
}

View File

@@ -53,7 +53,9 @@ export function ContextView() {
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [newFileName, setNewFileName] = useState("");
const [newFileType, setNewFileType] = useState<"text" | "image">("text");
const [uploadedImageData, setUploadedImageData] = useState<string | null>(null);
const [uploadedImageData, setUploadedImageData] = useState<string | null>(
null
);
const [newFileContent, setNewFileContent] = useState("");
const [isDropHovering, setIsDropHovering] = useState(false);
@@ -78,7 +80,15 @@ export function ContextView() {
// Determine if a file is an image based on extension
const isImageFile = (filename: string): boolean => {
const imageExtensions = [".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".bmp"];
const imageExtensions = [
".png",
".jpg",
".jpeg",
".gif",
".webp",
".svg",
".bmp",
];
const ext = filename.toLowerCase().substring(filename.lastIndexOf("."));
return imageExtensions.includes(ext);
};
@@ -270,7 +280,9 @@ export function ContextView() {
};
// Handle drag and drop for .txt and .md files in the add context dialog textarea
const handleTextAreaDrop = async (e: React.DragEvent<HTMLTextAreaElement>) => {
const handleTextAreaDrop = async (
e: React.DragEvent<HTMLTextAreaElement>
) => {
e.preventDefault();
e.stopPropagation();
setIsDropHovering(false);
@@ -282,8 +294,8 @@ export function ContextView() {
const fileName = file.name.toLowerCase();
// Only accept .txt and .md files
if (!fileName.endsWith('.txt') && !fileName.endsWith('.md')) {
console.warn('Only .txt and .md files are supported for drag and drop');
if (!fileName.endsWith(".txt") && !fileName.endsWith(".md")) {
console.warn("Only .txt and .md files are supported for drag and drop");
return;
}
@@ -340,7 +352,7 @@ export function ContextView() {
data-testid="context-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 justify-between p-4 border-b border-border bg-glass backdrop-blur-md">
<div className="flex items-center gap-3">
<BookOpen className="w-5 h-5 text-muted-foreground" />
<div>
@@ -381,7 +393,10 @@ export function ContextView() {
Context Files ({contextFiles.length})
</h2>
</div>
<div className="flex-1 overflow-y-auto p-2" data-testid="context-file-list">
<div
className="flex-1 overflow-y-auto p-2"
data-testid="context-file-list"
>
{contextFiles.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-center p-4">
<Upload className="w-8 h-8 text-zinc-500 mb-2" />
@@ -430,7 +445,9 @@ export function ContextView() {
) : (
<FileText className="w-4 h-4 text-zinc-400" />
)}
<span className="text-sm font-medium">{selectedFile.name}</span>
<span className="text-sm font-medium">
{selectedFile.name}
</span>
</div>
<div className="flex gap-2">
{selectedFile.type === "text" && (
@@ -487,9 +504,7 @@ export function ContextView() {
<div className="flex-1 flex items-center justify-center">
<div className="text-center">
<File className="w-12 h-12 text-zinc-600 mx-auto mb-3" />
<p className="text-zinc-500">
Select a file to view or edit
</p>
<p className="text-zinc-500">Select a file to view or edit</p>
<p className="text-zinc-600 text-sm mt-1">
Or drop files here to add them
</p>
@@ -536,7 +551,9 @@ export function ContextView() {
id="filename"
value={newFileName}
onChange={(e) => setNewFileName(e.target.value)}
placeholder={newFileType === "text" ? "context.md" : "image.png"}
placeholder={
newFileType === "text" ? "context.md" : "image.png"
}
data-testid="new-file-name"
/>
</div>
@@ -569,7 +586,9 @@ export function ContextView() {
<div className="absolute inset-0 flex items-center justify-center bg-brand-500/20 rounded-lg pointer-events-none">
<div className="flex flex-col items-center text-brand-400">
<Upload className="w-8 h-8 mb-2" />
<span className="text-sm font-medium">Drop .txt or .md file here</span>
<span className="text-sm font-medium">
Drop .txt or .md file here
</span>
</div>
</div>
)}
@@ -606,7 +625,9 @@ export function ContextView() {
<Upload className="w-8 h-8 text-zinc-500 mb-2" />
)}
<span className="text-sm text-zinc-400">
{uploadedImageData ? "Click to change" : "Click to upload"}
{uploadedImageData
? "Click to change"
: "Click to upload"}
</span>
</label>
</div>
@@ -628,7 +649,10 @@ export function ContextView() {
</Button>
<Button
onClick={handleAddFile}
disabled={!newFileName.trim() || (newFileType === "image" && !uploadedImageData)}
disabled={
!newFileName.trim() ||
(newFileType === "image" && !uploadedImageData)
}
data-testid="confirm-add-file"
>
Add File
@@ -643,11 +667,15 @@ export function ContextView() {
<DialogHeader>
<DialogTitle>Delete Context File</DialogTitle>
<DialogDescription>
Are you sure you want to delete "{selectedFile?.name}"? This action cannot be undone.
Are you sure you want to delete "{selectedFile?.name}"? This
action cannot be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setIsDeleteDialogOpen(false)}>
<Button
variant="outline"
onClick={() => setIsDeleteDialogOpen(false)}
>
Cancel
</Button>
<Button

View File

@@ -357,7 +357,7 @@ export function InterviewView() {
data-testid="interview-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 justify-between p-4 border-b border-border bg-glass backdrop-blur-md">
<div className="flex items-center gap-3">
<Button
variant="ghost"
@@ -545,7 +545,7 @@ export function InterviewView() {
<Button
onClick={handleCreateProject}
disabled={!projectName || !projectPath || isGenerating}
className="w-full bg-gradient-to-r from-brand-500 to-purple-600 hover:from-brand-600 hover:to-purple-700 text-white border-0"
className="w-full bg-linear-to-r from-brand-500 to-brand-600 hover:from-brand-600 hover:to-brand-600 text-primary-foreground border-0"
data-testid="interview-create-project"
>
{isGenerating ? (

View File

@@ -87,8 +87,7 @@ export function KanbanCard({
// - skipTests items can be dragged even when in_progress or verified (unless currently running)
// - Non-skipTests (TDD) items in progress or verified cannot be dragged
const isDraggable =
feature.status === "backlog" ||
(feature.skipTests && !isCurrentAutoTask);
feature.status === "backlog" || (feature.skipTests && !isCurrentAutoTask);
const {
attributes,
listeners,
@@ -111,7 +110,7 @@ export function KanbanCard({
ref={setNodeRef}
style={style}
className={cn(
"cursor-grab active:cursor-grabbing transition-all backdrop-blur-sm border-white/10 relative",
"cursor-grab active:cursor-grabbing transition-all backdrop-blur-sm border-border relative",
isDragging && "opacity-50 scale-105 shadow-lg",
isCurrentAutoTask &&
"border-purple-500 border-2 shadow-purple-500/50 shadow-lg animate-pulse"
@@ -122,7 +121,7 @@ export function KanbanCard({
{/* Shortcut key badge for in-progress cards */}
{shortcutKey && (
<div
className="absolute top-2 left-2 px-1.5 py-0.5 text-[10px] font-mono rounded bg-white/10 border border-white/20 text-zinc-300 z-10"
className="absolute top-2 left-2 px-1.5 py-0.5 text-[10px] font-mono rounded bg-muted border border-border text-muted-foreground z-10"
data-testid={`shortcut-key-${feature.id}`}
>
{shortcutKey}
@@ -399,7 +398,8 @@ export function KanbanCard({
<DialogHeader>
<DialogTitle>Delete Feature</DialogTitle>
<DialogDescription>
Are you sure you want to delete this feature? This action cannot be undone.
Are you sure you want to delete this feature? This action cannot
be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>

View File

@@ -29,14 +29,14 @@ export function KanbanColumn({
<div
ref={setNodeRef}
className={cn(
"flex flex-col h-full rounded-lg bg-zinc-900/50 backdrop-blur-sm border border-white/5 transition-colors",
"flex flex-col h-full rounded-lg bg-card backdrop-blur-sm border border-border transition-colors",
isDoubleWidth ? "w-[37rem]" : "w-72",
isOver && "bg-zinc-800/50"
isOver && "bg-accent"
)}
data-testid={`kanban-column-${id}`}
>
{/* Column Header */}
<div className="flex items-center gap-2 p-3 border-b border-white/5">
<div className="flex items-center gap-2 p-3 border-b border-border">
<div className={cn("w-3 h-3 rounded-full", color)} />
<h3 className="font-medium text-sm flex-1">{title}</h3>
{headerAction}

View File

@@ -5,20 +5,55 @@ import { useAppStore } from "@/store/app-store";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Settings, Key, Eye, EyeOff, CheckCircle2, AlertCircle, Loader2, Zap, Sun, Moon, Palette } from "lucide-react";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Settings,
Key,
Eye,
EyeOff,
CheckCircle2,
AlertCircle,
Loader2,
Zap,
Sun,
Moon,
Palette,
Terminal,
Ghost,
Snowflake,
Flame,
Sparkles,
Eclipse,
Trees,
Cat,
Atom,
Radio,
} from "lucide-react";
export function SettingsView() {
const { apiKeys, setApiKeys, setCurrentView, theme, setTheme } = useAppStore();
const { apiKeys, setApiKeys, setCurrentView, theme, setTheme } =
useAppStore();
const [anthropicKey, setAnthropicKey] = useState(apiKeys.anthropic);
const [googleKey, setGoogleKey] = useState(apiKeys.google);
const [showAnthropicKey, setShowAnthropicKey] = useState(false);
const [showGoogleKey, setShowGoogleKey] = useState(false);
const [saved, setSaved] = useState(false);
const [testingConnection, setTestingConnection] = useState(false);
const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null);
const [testResult, setTestResult] = useState<{
success: boolean;
message: string;
} | null>(null);
const [testingGeminiConnection, setTestingGeminiConnection] = useState(false);
const [geminiTestResult, setGeminiTestResult] = useState<{ success: boolean; message: string } | null>(null);
const [geminiTestResult, setGeminiTestResult] = useState<{
success: boolean;
message: string;
} | null>(null);
useEffect(() => {
setAnthropicKey(apiKeys.anthropic);
@@ -41,12 +76,21 @@ export function SettingsView() {
const data = await response.json();
if (response.ok && data.success) {
setTestResult({ success: true, message: data.message || "Connection successful! Claude responded." });
setTestResult({
success: true,
message: data.message || "Connection successful! Claude responded.",
});
} else {
setTestResult({ success: false, message: data.error || "Failed to connect to Claude API." });
setTestResult({
success: false,
message: data.error || "Failed to connect to Claude API.",
});
}
} catch (error) {
setTestResult({ success: false, message: "Network error. Please check your connection." });
setTestResult({
success: false,
message: "Network error. Please check your connection.",
});
} finally {
setTestingConnection(false);
}
@@ -68,12 +112,21 @@ export function SettingsView() {
const data = await response.json();
if (response.ok && data.success) {
setGeminiTestResult({ success: true, message: data.message || "Connection successful! Gemini responded." });
setGeminiTestResult({
success: true,
message: data.message || "Connection successful! Gemini responded.",
});
} else {
setGeminiTestResult({ success: false, message: data.error || "Failed to connect to Gemini API." });
setGeminiTestResult({
success: false,
message: data.error || "Failed to connect to Gemini API.",
});
}
} catch (error) {
setGeminiTestResult({ success: false, message: "Network error. Please check your connection." });
setGeminiTestResult({
success: false,
message: "Network error. Please check your connection.",
});
} finally {
setTestingGeminiConnection(false);
}
@@ -89,17 +142,22 @@ export function SettingsView() {
};
return (
<div className="flex-1 flex flex-col overflow-hidden content-bg" data-testid="settings-view">
<div
className="flex-1 flex flex-col overflow-hidden content-bg"
data-testid="settings-view"
>
{/* Header Section */}
<div className="flex-shrink-0 border-b border-white/10 bg-zinc-950/50 backdrop-blur-md">
<div className="shrink-0 border-b border-border bg-glass backdrop-blur-md">
<div className="px-8 py-6">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-brand-500 to-purple-600 shadow-lg shadow-brand-500/20 flex items-center justify-center">
<Settings className="w-5 h-5 text-white" />
<div className="w-10 h-10 rounded-xl bg-linear-to-br from-brand-500 to-brand-600 shadow-lg shadow-brand-500/20 flex items-center justify-center">
<Settings className="w-5 h-5 text-primary-foreground" />
</div>
<div>
<h1 className="text-2xl font-bold text-white">Settings</h1>
<p className="text-sm text-zinc-400">Configure your API keys and preferences</p>
<h1 className="text-2xl font-bold text-foreground">Settings</h1>
<p className="text-sm text-muted-foreground">
Configure your API keys and preferences
</p>
</div>
</div>
</div>
@@ -109,25 +167,28 @@ export function SettingsView() {
<div className="flex-1 overflow-y-auto p-8">
<div className="max-w-4xl mx-auto space-y-6">
{/* API Keys Section */}
<div className="rounded-xl border border-white/10 bg-zinc-900/50 backdrop-blur-md overflow-hidden">
<div className="p-6 border-b border-white/10">
<div className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden">
<div className="p-6 border-b border-border">
<div className="flex items-center gap-2 mb-2">
<Key className="w-5 h-5 text-brand-500" />
<h2 className="text-lg font-semibold text-white">API Keys</h2>
<h2 className="text-lg font-semibold text-foreground">
API Keys
</h2>
</div>
<p className="text-sm text-zinc-400">
Configure your AI provider API keys. Keys are stored locally in your browser.
<p className="text-sm text-muted-foreground">
Configure your AI provider API keys. Keys are stored locally in
your browser.
</p>
</div>
<div className="p-6 space-y-6">
{/* Claude/Anthropic API Key */}
<div className="space-y-3">
<div className="flex items-center gap-2">
<Label htmlFor="anthropic-key" className="text-zinc-300">
<Label htmlFor="anthropic-key" className="text-foreground">
Anthropic API Key (Claude)
</Label>
{apiKeys.anthropic && (
<CheckCircle2 className="w-4 h-4 text-green-500" />
<CheckCircle2 className="w-4 h-4 text-brand-500" />
)}
</div>
<div className="flex gap-2">
@@ -138,14 +199,14 @@ export function SettingsView() {
value={anthropicKey}
onChange={(e) => setAnthropicKey(e.target.value)}
placeholder="sk-ant-..."
className="pr-10 bg-zinc-950/50 border-white/10 text-white placeholder:text-zinc-500"
className="pr-10 bg-input border-border text-foreground placeholder:text-muted-foreground"
data-testid="anthropic-api-key-input"
/>
<Button
type="button"
variant="ghost"
size="icon"
className="absolute right-0 top-0 h-full px-3 text-zinc-400 hover:text-white hover:bg-transparent"
className="absolute right-0 top-0 h-full px-3 text-muted-foreground hover:text-foreground hover:bg-transparent"
onClick={() => setShowAnthropicKey(!showAnthropicKey)}
data-testid="toggle-anthropic-visibility"
>
@@ -161,7 +222,7 @@ export function SettingsView() {
variant="secondary"
onClick={handleTestConnection}
disabled={!anthropicKey || testingConnection}
className="bg-white/5 hover:bg-white/10 text-white border border-white/10"
className="bg-secondary hover:bg-accent text-secondary-foreground border border-border"
data-testid="test-claude-connection"
>
{testingConnection ? (
@@ -187,14 +248,15 @@ export function SettingsView() {
>
console.anthropic.com
</a>
. Alternatively, the CLAUDE_CODE_OAUTH_TOKEN environment variable can be used.
. Alternatively, the CLAUDE_CODE_OAUTH_TOKEN environment
variable can be used.
</p>
{testResult && (
<div
className={`flex items-center gap-2 p-3 rounded-lg ${
testResult.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="test-connection-result"
>
@@ -203,7 +265,12 @@ export function SettingsView() {
) : (
<AlertCircle className="w-4 h-4" />
)}
<span className="text-sm" data-testid="test-connection-message">{testResult.message}</span>
<span
className="text-sm"
data-testid="test-connection-message"
>
{testResult.message}
</span>
</div>
)}
</div>
@@ -211,11 +278,11 @@ export function SettingsView() {
{/* Google API Key */}
<div className="space-y-3">
<div className="flex items-center gap-2">
<Label htmlFor="google-key" className="text-zinc-300">
<Label htmlFor="google-key" className="text-foreground">
Google API Key (Gemini)
</Label>
{apiKeys.google && (
<CheckCircle2 className="w-4 h-4 text-green-500" />
<CheckCircle2 className="w-4 h-4 text-brand-500" />
)}
</div>
<div className="flex gap-2">
@@ -226,14 +293,14 @@ export function SettingsView() {
value={googleKey}
onChange={(e) => setGoogleKey(e.target.value)}
placeholder="AIza..."
className="pr-10 bg-zinc-950/50 border-white/10 text-white placeholder:text-zinc-500"
className="pr-10 bg-input border-border text-foreground placeholder:text-muted-foreground"
data-testid="google-api-key-input"
/>
<Button
type="button"
variant="ghost"
size="icon"
className="absolute right-0 top-0 h-full px-3 text-zinc-400 hover:text-white hover:bg-transparent"
className="absolute right-0 top-0 h-full px-3 text-muted-foreground hover:text-foreground hover:bg-transparent"
onClick={() => setShowGoogleKey(!showGoogleKey)}
data-testid="toggle-google-visibility"
>
@@ -249,7 +316,7 @@ export function SettingsView() {
variant="secondary"
onClick={handleTestGeminiConnection}
disabled={!googleKey || testingGeminiConnection}
className="bg-white/5 hover:bg-white/10 text-white border border-white/10"
className="bg-secondary hover:bg-accent text-secondary-foreground border border-border"
data-testid="test-gemini-connection"
>
{testingGeminiConnection ? (
@@ -266,7 +333,8 @@ export function SettingsView() {
</Button>
</div>
<p className="text-xs text-zinc-500">
Used for Gemini AI features (including image/design prompts). Get your key at{" "}
Used for Gemini AI features (including image/design prompts).
Get your key at{" "}
<a
href="https://makersuite.google.com/app/apikey"
target="_blank"
@@ -280,8 +348,8 @@ export function SettingsView() {
<div
className={`flex items-center gap-2 p-3 rounded-lg ${
geminiTestResult.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="gemini-test-connection-result"
>
@@ -290,7 +358,12 @@ export function SettingsView() {
) : (
<AlertCircle className="w-4 h-4" />
)}
<span className="text-sm" data-testid="gemini-test-connection-message">{geminiTestResult.message}</span>
<span
className="text-sm"
data-testid="gemini-test-connection-message"
>
{geminiTestResult.message}
</span>
</div>
)}
</div>
@@ -301,8 +374,8 @@ export function SettingsView() {
<div className="text-sm">
<p className="font-medium text-yellow-500">Security Notice</p>
<p className="text-yellow-500/80 text-xs mt-1">
API keys are stored in your browser's local storage. Never share your API keys
or commit them to version control.
API keys are stored in your browser's local storage. Never
share your API keys or commit them to version control.
</p>
</div>
</div>
@@ -310,43 +383,165 @@ export function SettingsView() {
</div>
{/* Appearance Section */}
<div className="rounded-xl border border-white/10 bg-zinc-900/50 backdrop-blur-md overflow-hidden">
<div className="p-6 border-b border-white/10">
<div className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden">
<div className="p-6 border-b border-border">
<div className="flex items-center gap-2 mb-2">
<Palette className="w-5 h-5 text-brand-500" />
<h2 className="text-lg font-semibold text-white">Appearance</h2>
<h2 className="text-lg font-semibold text-foreground">
Appearance
</h2>
</div>
<p className="text-sm text-zinc-400">
<p className="text-sm text-muted-foreground">
Customize the look and feel of your application.
</p>
</div>
<div className="p-6 space-y-4">
<div className="space-y-3">
<Label className="text-zinc-300">Theme</Label>
<div className="flex gap-3">
<Label className="text-foreground">Theme</Label>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<button
onClick={() => setTheme("dark")}
className={`flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-lg border transition-all ${
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
theme === "dark"
? "bg-white/5 border-brand-500 text-white"
: "bg-zinc-950/50 border-white/10 text-zinc-400 hover:text-white hover:bg-white/5"
? "bg-accent border-brand-500 text-foreground"
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
}`}
data-testid="dark-mode-button"
>
<Moon className="w-4 h-4" />
<span className="font-medium text-sm">Dark Mode</span>
<span className="font-medium text-sm">Dark</span>
</button>
<button
onClick={() => setTheme("light")}
className={`flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-lg border transition-all ${
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
theme === "light"
? "bg-white/5 border-brand-500 text-white"
: "bg-zinc-950/50 border-white/10 text-zinc-400 hover:text-white hover:bg-white/5"
? "bg-accent border-brand-500 text-foreground"
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
}`}
data-testid="light-mode-button"
>
<Sun className="w-4 h-4" />
<span className="font-medium text-sm">Light Mode</span>
<span className="font-medium text-sm">Light</span>
</button>
<button
onClick={() => setTheme("retro")}
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
theme === "retro"
? "bg-accent border-brand-500 text-foreground"
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
}`}
data-testid="retro-mode-button"
>
<Terminal className="w-4 h-4" />
<span className="font-medium text-sm">Retro</span>
</button>
<button
onClick={() => setTheme("dracula")}
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
theme === "dracula"
? "bg-accent border-brand-500 text-foreground"
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
}`}
data-testid="dracula-mode-button"
>
<Ghost className="w-4 h-4" />
<span className="font-medium text-sm">Dracula</span>
</button>
<button
onClick={() => setTheme("nord")}
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
theme === "nord"
? "bg-accent border-brand-500 text-foreground"
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
}`}
data-testid="nord-mode-button"
>
<Snowflake className="w-4 h-4" />
<span className="font-medium text-sm">Nord</span>
</button>
<button
onClick={() => setTheme("monokai")}
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
theme === "monokai"
? "bg-accent border-brand-500 text-foreground"
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
}`}
data-testid="monokai-mode-button"
>
<Flame className="w-4 h-4" />
<span className="font-medium text-sm">Monokai</span>
</button>
<button
onClick={() => setTheme("tokyonight")}
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
theme === "tokyonight"
? "bg-accent border-brand-500 text-foreground"
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
}`}
data-testid="tokyonight-mode-button"
>
<Sparkles className="w-4 h-4" />
<span className="font-medium text-sm">Tokyo Night</span>
</button>
<button
onClick={() => setTheme("solarized")}
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
theme === "solarized"
? "bg-accent border-brand-500 text-foreground"
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
}`}
data-testid="solarized-mode-button"
>
<Eclipse className="w-4 h-4" />
<span className="font-medium text-sm">Solarized</span>
</button>
<button
onClick={() => setTheme("gruvbox")}
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
theme === "gruvbox"
? "bg-accent border-brand-500 text-foreground"
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
}`}
data-testid="gruvbox-mode-button"
>
<Trees className="w-4 h-4" />
<span className="font-medium text-sm">Gruvbox</span>
</button>
<button
onClick={() => setTheme("catppuccin")}
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
theme === "catppuccin"
? "bg-accent border-brand-500 text-foreground"
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
}`}
data-testid="catppuccin-mode-button"
>
<Cat className="w-4 h-4" />
<span className="font-medium text-sm">Catppuccin</span>
</button>
<button
onClick={() => setTheme("onedark")}
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
theme === "onedark"
? "bg-accent border-brand-500 text-foreground"
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
}`}
data-testid="onedark-mode-button"
>
<Atom className="w-4 h-4" />
<span className="font-medium text-sm">One Dark</span>
</button>
<button
onClick={() => setTheme("synthwave")}
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
theme === "synthwave"
? "bg-accent border-brand-500 text-foreground"
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
}`}
data-testid="synthwave-mode-button"
>
<Radio className="w-4 h-4" />
<span className="font-medium text-sm">Synthwave</span>
</button>
</div>
</div>
@@ -358,7 +553,7 @@ export function SettingsView() {
<Button
onClick={handleSave}
data-testid="save-settings"
className="min-w-[120px] bg-gradient-to-r from-brand-500 to-purple-600 hover:from-brand-600 hover:to-purple-700 text-white border-0"
className="min-w-[120px] bg-linear-to-r from-brand-500 to-brand-600 hover:from-brand-600 hover:to-brand-600 text-primary-foreground border-0"
>
{saved ? (
<>

View File

@@ -20,7 +20,9 @@ export function SpecView() {
setIsLoading(true);
try {
const api = getElectronAPI();
const result = await api.readFile(`${currentProject.path}/.automaker/app_spec.txt`);
const result = await api.readFile(
`${currentProject.path}/.automaker/app_spec.txt`
);
if (result.success && result.content) {
setAppSpec(result.content);
@@ -44,7 +46,10 @@ export function SpecView() {
setIsSaving(true);
try {
const api = getElectronAPI();
await api.writeFile(`${currentProject.path}/.automaker/app_spec.txt`, appSpec);
await api.writeFile(
`${currentProject.path}/.automaker/app_spec.txt`,
appSpec
);
setHasChanges(false);
} catch (error) {
console.error("Failed to save spec:", error);
@@ -86,7 +91,7 @@ export function SpecView() {
data-testid="spec-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 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>

View File

@@ -91,59 +91,65 @@ export function WelcomeView() {
/**
* Initialize project and optionally kick off project analysis agent
*/
const initializeAndOpenProject = useCallback(async (path: string, name: string) => {
setIsOpening(true);
try {
// Initialize the .automaker directory structure
const initResult = await initializeProject(path);
const initializeAndOpenProject = useCallback(
async (path: string, name: string) => {
setIsOpening(true);
try {
// Initialize the .automaker directory structure
const initResult = await initializeProject(path);
if (!initResult.success) {
toast.error("Failed to initialize project", {
description: initResult.error || "Unknown error occurred",
if (!initResult.success) {
toast.error("Failed to initialize project", {
description: initResult.error || "Unknown error occurred",
});
return;
}
const project = {
id: `project-${Date.now()}`,
name,
path,
lastOpened: new Date().toISOString(),
};
addProject(project);
setCurrentProject(project);
// Show initialization dialog if files were created
if (initResult.createdFiles && initResult.createdFiles.length > 0) {
setInitStatus({
isNewProject: initResult.isNewProject,
createdFiles: initResult.createdFiles,
projectName: name,
projectPath: path,
});
setShowInitDialog(true);
// Kick off agent to analyze the project and update app_spec.txt
console.log(
"[Welcome] Project initialized, created files:",
initResult.createdFiles
);
console.log("[Welcome] Kicking off project analysis agent...");
// Start analysis in background (don't await, let it run async)
analyzeProject(path);
} else {
toast.success("Project opened", {
description: `Opened ${name}`,
});
}
} catch (error) {
console.error("[Welcome] Failed to open project:", error);
toast.error("Failed to open project", {
description: error instanceof Error ? error.message : "Unknown error",
});
return;
} finally {
setIsOpening(false);
}
const project = {
id: `project-${Date.now()}`,
name,
path,
lastOpened: new Date().toISOString(),
};
addProject(project);
setCurrentProject(project);
// Show initialization dialog if files were created
if (initResult.createdFiles && initResult.createdFiles.length > 0) {
setInitStatus({
isNewProject: initResult.isNewProject,
createdFiles: initResult.createdFiles,
projectName: name,
projectPath: path,
});
setShowInitDialog(true);
// Kick off agent to analyze the project and update app_spec.txt
console.log("[Welcome] Project initialized, created files:", initResult.createdFiles);
console.log("[Welcome] Kicking off project analysis agent...");
// Start analysis in background (don't await, let it run async)
analyzeProject(path);
} else {
toast.success("Project opened", {
description: `Opened ${name}`,
});
}
} catch (error) {
console.error("[Welcome] Failed to open project:", error);
toast.error("Failed to open project", {
description: error instanceof Error ? error.message : "Unknown error",
});
} finally {
setIsOpening(false);
}
}, [addProject, setCurrentProject, analyzeProject]);
},
[addProject, setCurrentProject, analyzeProject]
);
const handleOpenProject = useCallback(async () => {
const api = getElectronAPI();
@@ -159,9 +165,12 @@ export function WelcomeView() {
/**
* Handle clicking on a recent project
*/
const handleRecentProjectClick = useCallback(async (project: { id: string; name: string; path: string }) => {
await initializeAndOpenProject(project.path, project.name);
}, [initializeAndOpenProject]);
const handleRecentProjectClick = useCallback(
async (project: { id: string; name: string; path: string }) => {
await initializeAndOpenProject(project.path, project.name);
},
[initializeAndOpenProject]
);
const handleNewProject = () => {
setNewProjectName("");
@@ -272,17 +281,17 @@ export function WelcomeView() {
return (
<div className="flex-1 flex flex-col content-bg" data-testid="welcome-view">
{/* Header Section */}
<div className="flex-shrink-0 border-b border-white/10 bg-zinc-950/50 backdrop-blur-md">
<div className="flex-shrink-0 border-b border-border bg-glass backdrop-blur-md">
<div className="px-8 py-6">
<div className="flex items-center gap-3 mb-2">
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-brand-500 to-purple-600 shadow-lg shadow-brand-500/20 flex items-center justify-center">
<Cpu className="w-5 h-5 text-white" />
<div className="w-10 h-10 rounded-xl bg-linear-to-br from-brand-500 to-brand-600 shadow-lg shadow-brand-500/20 flex items-center justify-center">
<Cpu className="w-5 h-5 text-primary-foreground" />
</div>
<div>
<h1 className="text-2xl font-bold text-white">
<h1 className="text-2xl font-bold text-foreground">
Welcome to Automaker
</h1>
<p className="text-sm text-zinc-400">
<p className="text-sm text-muted-foreground">
Your autonomous AI development studio
</p>
</div>
@@ -296,20 +305,20 @@ export function WelcomeView() {
{/* Quick Actions */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-12">
<div
className="group relative overflow-hidden rounded-xl border border-white/10 bg-zinc-900/50 backdrop-blur-md hover:bg-zinc-900/70 hover:border-white/20 transition-all duration-200"
className="group relative overflow-hidden rounded-xl border border-border bg-card backdrop-blur-md hover:bg-card/70 hover:border-border-glass transition-all duration-200"
data-testid="new-project-card"
>
<div className="absolute inset-0 bg-gradient-to-br from-brand-500/5 to-purple-600/5 opacity-0 group-hover:opacity-100 transition-opacity"></div>
<div className="relative p-6">
<div className="flex items-start gap-4 mb-4">
<div className="w-12 h-12 rounded-lg bg-gradient-to-br from-brand-500 to-purple-600 shadow-lg shadow-brand-500/20 flex items-center justify-center group-hover:scale-110 transition-transform">
<div className="w-12 h-12 rounded-lg bg-linear-to-br from-brand-500 to-brand-600 shadow-lg shadow-brand-500/20 flex items-center justify-center group-hover:scale-110 transition-transform">
<Plus className="w-6 h-6 text-white" />
</div>
<div className="flex-1">
<h3 className="text-lg font-semibold text-white mb-1">
<h3 className="text-lg font-semibold text-foreground mb-1">
New Project
</h3>
<p className="text-sm text-zinc-400">
<p className="text-sm text-muted-foreground">
Create a new project from scratch with AI-powered
development
</p>
@@ -318,7 +327,7 @@ export function WelcomeView() {
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
className="w-full bg-gradient-to-r from-brand-500 to-purple-600 hover:from-brand-600 hover:to-purple-700 text-white border-0"
className="w-full bg-linear-to-r from-brand-500 to-brand-600 hover:from-brand-600 hover:to-brand-600 text-primary-foreground border-0"
data-testid="create-new-project"
>
<Plus className="w-4 h-4 mr-2" />
@@ -347,28 +356,28 @@ export function WelcomeView() {
</div>
<div
className="group relative overflow-hidden rounded-xl border border-white/10 bg-zinc-900/50 backdrop-blur-md hover:bg-zinc-900/70 hover:border-white/20 transition-all duration-200 cursor-pointer"
className="group relative overflow-hidden rounded-xl border border-border bg-card backdrop-blur-md hover:bg-card/70 hover:border-border-glass transition-all duration-200 cursor-pointer"
onClick={handleOpenProject}
data-testid="open-project-card"
>
<div className="absolute inset-0 bg-gradient-to-br from-blue-500/5 to-cyan-600/5 opacity-0 group-hover:opacity-100 transition-opacity"></div>
<div className="relative p-6">
<div className="flex items-start gap-4 mb-4">
<div className="w-12 h-12 rounded-lg bg-zinc-800 border border-white/10 flex items-center justify-center group-hover:scale-110 transition-transform">
<FolderOpen className="w-6 h-6 text-zinc-400 group-hover:text-white transition-colors" />
<div className="w-12 h-12 rounded-lg bg-muted border border-border flex items-center justify-center group-hover:scale-110 transition-transform">
<FolderOpen className="w-6 h-6 text-muted-foreground group-hover:text-foreground transition-colors" />
</div>
<div className="flex-1">
<h3 className="text-lg font-semibold text-white mb-1">
<h3 className="text-lg font-semibold text-foreground mb-1">
Open Project
</h3>
<p className="text-sm text-zinc-400">
<p className="text-sm text-muted-foreground">
Open an existing project folder to continue working
</p>
</div>
</div>
<Button
variant="secondary"
className="w-full bg-white/5 hover:bg-white/10 text-white border border-white/10 hover:border-white/20"
className="w-full bg-secondary hover:bg-secondary/80 text-foreground border border-border hover:border-border-glass"
data-testid="open-existing-project"
>
<FolderOpen className="w-4 h-4 mr-2" />
@@ -382,8 +391,8 @@ export function WelcomeView() {
{recentProjects.length > 0 && (
<div>
<div className="flex items-center gap-2 mb-4">
<Clock className="w-5 h-5 text-zinc-400" />
<h2 className="text-lg font-semibold text-white">
<Clock className="w-5 h-5 text-muted-foreground" />
<h2 className="text-lg font-semibold text-foreground">
Recent Projects
</h2>
</div>
@@ -391,25 +400,25 @@ export function WelcomeView() {
{recentProjects.map((project) => (
<div
key={project.id}
className="group relative overflow-hidden rounded-xl border border-white/10 bg-zinc-900/50 backdrop-blur-md hover:bg-zinc-900/70 hover:border-brand-500/50 transition-all duration-200 cursor-pointer"
className="group relative overflow-hidden rounded-xl border border-border bg-card backdrop-blur-md hover:bg-card/70 hover:border-brand-500/50 transition-all duration-200 cursor-pointer"
onClick={() => handleRecentProjectClick(project)}
data-testid={`recent-project-${project.id}`}
>
<div className="absolute inset-0 bg-gradient-to-br from-brand-500/0 to-purple-600/0 group-hover:from-brand-500/5 group-hover:to-purple-600/5 transition-all"></div>
<div className="relative p-4">
<div className="flex items-start gap-3">
<div className="w-10 h-10 rounded-lg bg-zinc-800 border border-white/10 flex items-center justify-center group-hover:border-brand-500/50 transition-colors">
<Folder className="w-5 h-5 text-zinc-400 group-hover:text-brand-500 transition-colors" />
<div className="w-10 h-10 rounded-lg bg-muted border border-border flex items-center justify-center group-hover:border-brand-500/50 transition-colors">
<Folder className="w-5 h-5 text-muted-foreground group-hover:text-brand-500 transition-colors" />
</div>
<div className="flex-1 min-w-0">
<p className="font-medium text-white truncate group-hover:text-brand-500 transition-colors">
<p className="font-medium text-foreground truncate group-hover:text-brand-500 transition-colors">
{project.name}
</p>
<p className="text-xs text-zinc-500 truncate mt-0.5">
<p className="text-xs text-muted-foreground/70 truncate mt-0.5">
{project.path}
</p>
{project.lastOpened && (
<p className="text-xs text-zinc-600 mt-1">
<p className="text-xs text-muted-foreground mt-1">
{new Date(
project.lastOpened
).toLocaleDateString()}
@@ -427,10 +436,10 @@ export function WelcomeView() {
{/* Empty State for No Projects */}
{recentProjects.length === 0 && (
<div className="flex flex-col items-center justify-center py-12 text-center">
<div className="w-16 h-16 rounded-2xl bg-zinc-900/50 border border-white/10 flex items-center justify-center mb-4">
<Sparkles className="w-8 h-8 text-zinc-600" />
<div className="w-16 h-16 rounded-2xl bg-muted border border-border flex items-center justify-center mb-4">
<Sparkles className="w-8 h-8 text-muted-foreground" />
</div>
<h3 className="text-lg font-semibold text-white mb-2">
<h3 className="text-lg font-semibold text-foreground mb-2">
No projects yet
</h3>
<p className="text-sm text-zinc-400 max-w-md">
@@ -447,18 +456,20 @@ export function WelcomeView() {
onOpenChange={setShowNewProjectDialog}
>
<DialogContent
className="bg-zinc-900 border-white/10"
className="bg-card border-border"
data-testid="new-project-dialog"
>
<DialogHeader>
<DialogTitle className="text-white">Create New Project</DialogTitle>
<DialogDescription className="text-zinc-400">
<DialogTitle className="text-foreground">
Create New Project
</DialogTitle>
<DialogDescription className="text-muted-foreground">
Set up a new project directory with initial configuration files.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="project-name" className="text-zinc-300">
<Label htmlFor="project-name" className="text-foreground">
Project Name
</Label>
<Input
@@ -466,12 +477,12 @@ export function WelcomeView() {
placeholder="my-awesome-project"
value={newProjectName}
onChange={(e) => setNewProjectName(e.target.value)}
className="bg-zinc-950/50 border-white/10 text-white placeholder:text-zinc-500"
className="bg-input border-border text-foreground placeholder:text-muted-foreground"
data-testid="project-name-input"
/>
</div>
<div className="space-y-2">
<Label htmlFor="project-path" className="text-zinc-300">
<Label htmlFor="project-path" className="text-foreground">
Parent Directory
</Label>
<div className="flex gap-2">
@@ -480,13 +491,13 @@ export function WelcomeView() {
placeholder="/path/to/projects"
value={newProjectPath}
onChange={(e) => setNewProjectPath(e.target.value)}
className="flex-1 bg-zinc-950/50 border-white/10 text-white placeholder:text-zinc-500"
className="flex-1 bg-input border-border text-foreground placeholder:text-muted-foreground"
data-testid="project-path-input"
/>
<Button
variant="secondary"
onClick={handleSelectDirectory}
className="bg-white/5 hover:bg-white/10 text-white border border-white/10"
className="bg-secondary hover:bg-secondary/80 text-foreground border border-border"
data-testid="browse-directory"
>
Browse
@@ -498,14 +509,14 @@ export function WelcomeView() {
<Button
variant="ghost"
onClick={() => setShowNewProjectDialog(false)}
className="text-zinc-400 hover:text-white hover:bg-white/5"
className="text-muted-foreground hover:text-foreground hover:bg-accent"
>
Cancel
</Button>
<Button
onClick={handleCreateProject}
disabled={!newProjectName || !newProjectPath || isCreating}
className="bg-gradient-to-r from-brand-500 to-purple-600 hover:from-brand-600 hover:to-purple-700 text-white border-0"
className="bg-gradient-to-r from-brand-500 to-brand-600 hover:from-brand-600 hover:to-brand-600 text-white border-0"
data-testid="confirm-create-project"
>
{isCreating ? "Creating..." : "Create Project"}
@@ -517,15 +528,17 @@ export function WelcomeView() {
{/* Project Initialization Dialog */}
<Dialog open={showInitDialog} onOpenChange={setShowInitDialog}>
<DialogContent
className="bg-zinc-900 border-white/10"
className="bg-card border-border"
data-testid="project-init-dialog"
>
<DialogHeader>
<DialogTitle className="text-white flex items-center gap-2">
<DialogTitle className="text-foreground flex items-center gap-2">
<Sparkles className="w-5 h-5 text-brand-500" />
{initStatus?.isNewProject ? "Project Initialized" : "Project Updated"}
{initStatus?.isNewProject
? "Project Initialized"
: "Project Updated"}
</DialogTitle>
<DialogDescription className="text-zinc-400">
<DialogDescription className="text-muted-foreground">
{initStatus?.isNewProject
? `Created .automaker directory structure for ${initStatus?.projectName}`
: `Updated missing files in .automaker for ${initStatus?.projectName}`}
@@ -533,15 +546,17 @@ export function WelcomeView() {
</DialogHeader>
<div className="py-4">
<div className="space-y-2">
<p className="text-sm text-zinc-300 font-medium">Created files:</p>
<p className="text-sm text-foreground font-medium">
Created files:
</p>
<ul className="space-y-1.5">
{initStatus?.createdFiles.map((file) => (
<li
key={file}
className="flex items-center gap-2 text-sm text-zinc-400"
className="flex items-center gap-2 text-sm text-muted-foreground"
>
<div className="w-1.5 h-1.5 rounded-full bg-green-500" />
<code className="text-xs bg-zinc-800 px-2 py-0.5 rounded">
<code className="text-xs bg-muted px-2 py-0.5 rounded">
{file}
</code>
</li>
@@ -550,7 +565,7 @@ export function WelcomeView() {
</div>
{initStatus?.isNewProject && (
<div className="mt-4 p-3 rounded-lg bg-zinc-800/50 border border-white/5">
<div className="mt-4 p-3 rounded-lg bg-muted/50 border border-border-glass">
{isAnalyzing ? (
<div className="flex items-center gap-2">
<Loader2 className="w-4 h-4 text-brand-500 animate-spin" />
@@ -559,9 +574,9 @@ export function WelcomeView() {
</p>
</div>
) : (
<p className="text-sm text-zinc-400">
<p className="text-sm text-muted-foreground">
<span className="text-brand-400">Tip:</span> Edit the{" "}
<code className="text-xs bg-zinc-800 px-1.5 py-0.5 rounded">
<code className="text-xs bg-muted px-1.5 py-0.5 rounded">
app_spec.txt
</code>{" "}
file to describe your project. The AI agent will use this to
@@ -574,7 +589,7 @@ export function WelcomeView() {
<DialogFooter>
<Button
onClick={() => setShowInitDialog(false)}
className="bg-gradient-to-r from-brand-500 to-purple-600 hover:from-brand-600 hover:to-purple-700 text-white border-0"
className="bg-gradient-to-r from-brand-500 to-brand-600 hover:from-brand-600 hover:to-brand-600 text-white border-0"
data-testid="close-init-dialog"
>
Get Started
@@ -586,12 +601,14 @@ export function WelcomeView() {
{/* Loading overlay when opening project */}
{isOpening && (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm"
className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm"
data-testid="project-opening-overlay"
>
<div className="flex flex-col items-center gap-3 p-6 rounded-xl bg-zinc-900 border border-white/10">
<div className="flex flex-col items-center gap-3 p-6 rounded-xl bg-card border border-border">
<Loader2 className="w-8 h-8 text-brand-500 animate-spin" />
<p className="text-white font-medium">Initializing project...</p>
<p className="text-foreground font-medium">
Initializing project...
</p>
</div>
</div>
)}

View File

@@ -2,8 +2,29 @@ import { create } from "zustand";
import { persist } from "zustand/middleware";
import type { Project } from "@/lib/electron";
export type ViewMode = "welcome" | "spec" | "board" | "agent" | "settings" | "tools" | "interview" | "context";
export type ThemeMode = "light" | "dark" | "system";
export type ViewMode =
| "welcome"
| "spec"
| "board"
| "agent"
| "settings"
| "tools"
| "interview"
| "context";
export type ThemeMode =
| "light"
| "dark"
| "system"
| "retro"
| "dracula"
| "nord"
| "monokai"
| "tokyonight"
| "solarized"
| "gruvbox"
| "catppuccin"
| "onedark"
| "synthwave";
export interface ApiKeys {
anthropic: string;
@@ -103,7 +124,15 @@ export interface AutoModeActivity {
id: string;
featureId: string;
timestamp: Date;
type: "start" | "progress" | "tool" | "complete" | "error" | "planning" | "action" | "verification";
type:
| "start"
| "progress"
| "tool"
| "complete"
| "error"
| "planning"
| "action"
| "verification";
message: string;
tool?: string;
passes?: boolean;
@@ -157,7 +186,9 @@ export interface AppActions {
addRunningTask: (taskId: string) => void;
removeRunningTask: (taskId: string) => void;
clearRunningTasks: () => void;
addAutoModeActivity: (activity: Omit<AutoModeActivity, "id" | "timestamp">) => void;
addAutoModeActivity: (
activity: Omit<AutoModeActivity, "id" | "timestamp">
) => void;
clearAutoModeActivity: () => void;
setMaxConcurrency: (max: number) => void;
@@ -200,11 +231,17 @@ export const useAppStore = create<AppState & AppActions>()(
const existing = projects.findIndex((p) => p.path === project.path);
if (existing >= 0) {
const updated = [...projects];
updated[existing] = { ...project, lastOpened: new Date().toISOString() };
updated[existing] = {
...project,
lastOpened: new Date().toISOString(),
};
set({ projects: updated });
} else {
set({
projects: [...projects, { ...project, lastOpened: new Date().toISOString() }],
projects: [
...projects,
{ ...project, lastOpened: new Date().toISOString() },
],
});
}
},
@@ -242,7 +279,9 @@ export const useAppStore = create<AppState & AppActions>()(
},
addFeature: (feature) => {
const id = `feature-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const id = `feature-${Date.now()}-${Math.random()
.toString(36)
.substr(2, 9)}`;
set({ features: [...get().features, { ...feature, id }] });
},
@@ -277,14 +316,19 @@ export const useAppStore = create<AppState & AppActions>()(
const now = new Date();
const session: ChatSession = {
id: `chat-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
title: title || `Chat ${new Date().toLocaleDateString()} ${new Date().toLocaleTimeString()}`,
title:
title ||
`Chat ${new Date().toLocaleDateString()} ${new Date().toLocaleTimeString()}`,
projectId: currentProject.id,
messages: [{
id: "welcome",
role: "assistant",
content: "Hello! I'm the Automaker Agent. I can help you build software autonomously. What would you like to create today?",
timestamp: now,
}],
messages: [
{
id: "welcome",
role: "assistant",
content:
"Hello! I'm the Automaker Agent. I can help you build software autonomously. What would you like to create today?",
timestamp: now,
},
],
createdAt: now,
updatedAt: now,
archived: false,
@@ -311,14 +355,18 @@ export const useAppStore = create<AppState & AppActions>()(
const currentSession = get().currentChatSession;
if (currentSession && currentSession.id === sessionId) {
set({
currentChatSession: { ...currentSession, ...updates, updatedAt: new Date() }
currentChatSession: {
...currentSession,
...updates,
updatedAt: new Date(),
},
});
}
},
addMessageToSession: (sessionId, message) => {
const sessions = get().chatSessions;
const sessionIndex = sessions.findIndex(s => s.id === sessionId);
const sessionIndex = sessions.findIndex((s) => s.id === sessionId);
if (sessionIndex >= 0) {
const updatedSessions = [...sessions];
@@ -334,7 +382,7 @@ export const useAppStore = create<AppState & AppActions>()(
const currentSession = get().currentChatSession;
if (currentSession && currentSession.id === sessionId) {
set({
currentChatSession: updatedSessions[sessionIndex]
currentChatSession: updatedSessions[sessionIndex],
});
}
}
@@ -356,7 +404,8 @@ export const useAppStore = create<AppState & AppActions>()(
const currentSession = get().currentChatSession;
set({
chatSessions: get().chatSessions.filter((s) => s.id !== sessionId),
currentChatSession: currentSession?.id === sessionId ? null : currentSession,
currentChatSession:
currentSession?.id === sessionId ? null : currentSession,
});
},
@@ -375,13 +424,19 @@ export const useAppStore = create<AppState & AppActions>()(
},
removeRunningTask: (taskId) => {
set({ runningAutoTasks: get().runningAutoTasks.filter(id => id !== taskId) });
set({
runningAutoTasks: get().runningAutoTasks.filter(
(id) => id !== taskId
),
});
},
clearRunningTasks: () => set({ runningAutoTasks: [] }),
addAutoModeActivity: (activity) => {
const id = `activity-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const id = `activity-${Date.now()}-${Math.random()
.toString(36)
.substr(2, 9)}`;
const newActivity: AutoModeActivity = {
...activity,
id,