mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 08:13:37 +00:00
Merge remote-tracking branch 'origin/main' into feat/extend-models-support
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
1119
app/src/components/views/analysis-view.tsx
Normal file
1119
app/src/components/views/analysis-view.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
301
app/src/components/views/code-view.tsx
Normal file
301
app/src/components/views/code-view.tsx
Normal file
@@ -0,0 +1,301 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { useAppStore } from "@/store/app-store";
|
||||
import { getElectronAPI } from "@/lib/electron";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
File,
|
||||
Folder,
|
||||
FolderOpen,
|
||||
ChevronRight,
|
||||
ChevronDown,
|
||||
RefreshCw,
|
||||
Code,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface FileTreeNode {
|
||||
name: string;
|
||||
path: string;
|
||||
isDirectory: boolean;
|
||||
children?: FileTreeNode[];
|
||||
isExpanded?: boolean;
|
||||
}
|
||||
|
||||
const IGNORE_PATTERNS = [
|
||||
"node_modules",
|
||||
".git",
|
||||
".next",
|
||||
"dist",
|
||||
"build",
|
||||
".DS_Store",
|
||||
"*.log",
|
||||
];
|
||||
|
||||
const shouldIgnore = (name: string) => {
|
||||
return IGNORE_PATTERNS.some((pattern) => {
|
||||
if (pattern.startsWith("*")) {
|
||||
return name.endsWith(pattern.slice(1));
|
||||
}
|
||||
return name === pattern;
|
||||
});
|
||||
};
|
||||
|
||||
export function CodeView() {
|
||||
const { currentProject } = useAppStore();
|
||||
const [fileTree, setFileTree] = useState<FileTreeNode[]>([]);
|
||||
const [selectedFile, setSelectedFile] = useState<string | null>(null);
|
||||
const [fileContent, setFileContent] = useState<string>("");
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(
|
||||
new Set()
|
||||
);
|
||||
|
||||
// Load directory tree
|
||||
const loadTree = useCallback(async () => {
|
||||
if (!currentProject) return;
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.readdir(currentProject.path);
|
||||
|
||||
if (result.success && result.entries) {
|
||||
const entries = result.entries
|
||||
.filter((e) => !shouldIgnore(e.name))
|
||||
.sort((a, b) => {
|
||||
// Directories first
|
||||
if (a.isDirectory && !b.isDirectory) return -1;
|
||||
if (!a.isDirectory && b.isDirectory) return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
})
|
||||
.map((e) => ({
|
||||
name: e.name,
|
||||
path: `${currentProject.path}/${e.name}`,
|
||||
isDirectory: e.isDirectory,
|
||||
}));
|
||||
|
||||
setFileTree(entries);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load file tree:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [currentProject]);
|
||||
|
||||
useEffect(() => {
|
||||
loadTree();
|
||||
}, [loadTree]);
|
||||
|
||||
// Load subdirectory
|
||||
const loadSubdirectory = async (path: string): Promise<FileTreeNode[]> => {
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.readdir(path);
|
||||
|
||||
if (result.success && result.entries) {
|
||||
return result.entries
|
||||
.filter((e) => !shouldIgnore(e.name))
|
||||
.sort((a, b) => {
|
||||
if (a.isDirectory && !b.isDirectory) return -1;
|
||||
if (!a.isDirectory && b.isDirectory) return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
})
|
||||
.map((e) => ({
|
||||
name: e.name,
|
||||
path: `${path}/${e.name}`,
|
||||
isDirectory: e.isDirectory,
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load subdirectory:", error);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
// Load file content
|
||||
const loadFileContent = async (path: string) => {
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.readFile(path);
|
||||
|
||||
if (result.success && result.content) {
|
||||
setFileContent(result.content);
|
||||
setSelectedFile(path);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load file:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Toggle folder expansion
|
||||
const toggleFolder = async (node: FileTreeNode) => {
|
||||
const newExpanded = new Set(expandedFolders);
|
||||
|
||||
if (expandedFolders.has(node.path)) {
|
||||
newExpanded.delete(node.path);
|
||||
} else {
|
||||
newExpanded.add(node.path);
|
||||
|
||||
// Load children if not already loaded
|
||||
if (!node.children) {
|
||||
const children = await loadSubdirectory(node.path);
|
||||
// Update the tree with children
|
||||
const updateTree = (nodes: FileTreeNode[]): FileTreeNode[] => {
|
||||
return nodes.map((n) => {
|
||||
if (n.path === node.path) {
|
||||
return { ...n, children };
|
||||
}
|
||||
if (n.children) {
|
||||
return { ...n, children: updateTree(n.children) };
|
||||
}
|
||||
return n;
|
||||
});
|
||||
};
|
||||
setFileTree(updateTree(fileTree));
|
||||
}
|
||||
}
|
||||
|
||||
setExpandedFolders(newExpanded);
|
||||
};
|
||||
|
||||
// Render file tree node
|
||||
const renderNode = (node: FileTreeNode, depth: number = 0) => {
|
||||
const isExpanded = expandedFolders.has(node.path);
|
||||
const isSelected = selectedFile === node.path;
|
||||
|
||||
return (
|
||||
<div key={node.path}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-2 py-1 px-2 rounded cursor-pointer hover:bg-muted/50",
|
||||
isSelected && "bg-muted"
|
||||
)}
|
||||
style={{ paddingLeft: `${depth * 16 + 8}px` }}
|
||||
onClick={() => {
|
||||
if (node.isDirectory) {
|
||||
toggleFolder(node);
|
||||
} else {
|
||||
loadFileContent(node.path);
|
||||
}
|
||||
}}
|
||||
data-testid={`file-tree-item-${node.name}`}
|
||||
>
|
||||
{node.isDirectory ? (
|
||||
<>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="w-4 h-4 text-muted-foreground shrink-0" />
|
||||
) : (
|
||||
<ChevronRight className="w-4 h-4 text-muted-foreground shrink-0" />
|
||||
)}
|
||||
{isExpanded ? (
|
||||
<FolderOpen className="w-4 h-4 text-primary shrink-0" />
|
||||
) : (
|
||||
<Folder className="w-4 h-4 text-primary shrink-0" />
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="w-4" />
|
||||
<File className="w-4 h-4 text-muted-foreground shrink-0" />
|
||||
</>
|
||||
)}
|
||||
<span className="text-sm truncate">{node.name}</span>
|
||||
</div>
|
||||
{node.isDirectory && isExpanded && node.children && (
|
||||
<div>
|
||||
{node.children.map((child) => renderNode(child, depth + 1))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (!currentProject) {
|
||||
return (
|
||||
<div
|
||||
className="flex-1 flex items-center justify-center"
|
||||
data-testid="code-view-no-project"
|
||||
>
|
||||
<p className="text-muted-foreground">No project selected</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div
|
||||
className="flex-1 flex items-center justify-center"
|
||||
data-testid="code-view-loading"
|
||||
>
|
||||
<RefreshCw className="w-6 h-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex-1 flex flex-col overflow-hidden content-bg"
|
||||
data-testid="code-view"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-white/10 bg-zinc-950/50 backdrop-blur-md">
|
||||
<div className="flex items-center gap-3">
|
||||
<Code className="w-5 h-5 text-muted-foreground" />
|
||||
<div>
|
||||
<h1 className="text-xl font-bold">Code Explorer</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{currentProject.name}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={loadTree}
|
||||
data-testid="refresh-tree"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Split View */}
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
{/* File Tree */}
|
||||
<div className="w-64 border-r overflow-y-auto" data-testid="file-tree">
|
||||
<div className="p-2">{fileTree.map((node) => renderNode(node))}</div>
|
||||
</div>
|
||||
|
||||
{/* Code Preview */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{selectedFile ? (
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="px-4 py-2 border-b bg-muted/30">
|
||||
<p className="text-sm font-mono text-muted-foreground truncate">
|
||||
{selectedFile.replace(currentProject.path, "")}
|
||||
</p>
|
||||
</div>
|
||||
<Card className="flex-1 m-4 overflow-hidden">
|
||||
<CardContent className="p-0 h-full">
|
||||
<pre className="p-4 h-full overflow-auto text-sm font-mono whitespace-pre-wrap">
|
||||
<code data-testid="code-content">{fileContent}</code>
|
||||
</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<p className="text-muted-foreground">
|
||||
Select a file to view its contents
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -97,9 +97,13 @@ export function KanbanCard({
|
||||
const { kanbanCardDetailLevel } = useAppStore();
|
||||
|
||||
// Helper functions to check what should be shown based on detail level
|
||||
const showSteps = kanbanCardDetailLevel === "standard" || kanbanCardDetailLevel === "detailed";
|
||||
const showSteps =
|
||||
kanbanCardDetailLevel === "standard" ||
|
||||
kanbanCardDetailLevel === "detailed";
|
||||
const showAgentInfo = kanbanCardDetailLevel === "detailed";
|
||||
const showProgressBar = kanbanCardDetailLevel === "standard" || kanbanCardDetailLevel === "detailed";
|
||||
const showProgressBar =
|
||||
kanbanCardDetailLevel === "standard" ||
|
||||
kanbanCardDetailLevel === "detailed";
|
||||
|
||||
// Load context file for in_progress, waiting_approval, and verified features
|
||||
useEffect(() => {
|
||||
@@ -164,8 +168,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,
|
||||
@@ -188,7 +191,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"
|
||||
@@ -199,7 +202,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}
|
||||
@@ -293,19 +296,27 @@ export function KanbanCard({
|
||||
|
||||
{/* Agent Info Panel - shows for in_progress, waiting_approval, verified */}
|
||||
{/* Standard mode: Only show progress bar */}
|
||||
{showProgressBar && !showAgentInfo && feature.status !== "backlog" && agentInfo && (isCurrentAutoTask || feature.status === "in_progress") && (
|
||||
<div className="mb-3 space-y-1">
|
||||
<div className="w-full h-1.5 bg-zinc-800 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="w-full h-full bg-primary transition-transform duration-500 ease-out origin-left"
|
||||
style={{ transform: `translateX(${agentInfo.progressPercentage - 100}%)` }}
|
||||
/>
|
||||
{showProgressBar &&
|
||||
!showAgentInfo &&
|
||||
feature.status !== "backlog" &&
|
||||
agentInfo &&
|
||||
(isCurrentAutoTask || feature.status === "in_progress") && (
|
||||
<div className="mb-3 space-y-1">
|
||||
<div className="w-full h-1.5 bg-zinc-800 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="w-full h-full bg-primary transition-transform duration-500 ease-out origin-left"
|
||||
style={{
|
||||
transform: `translateX(${
|
||||
agentInfo.progressPercentage - 100
|
||||
}%)`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-[10px] text-muted-foreground">
|
||||
<span>{Math.round(agentInfo.progressPercentage)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-[10px] text-muted-foreground">
|
||||
<span>{Math.round(agentInfo.progressPercentage)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{/* Detailed mode: Show all agent info */}
|
||||
{showAgentInfo && feature.status !== "backlog" && agentInfo && (
|
||||
@@ -314,15 +325,22 @@ export function KanbanCard({
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<div className="flex items-center gap-1 text-cyan-400">
|
||||
<Cpu className="w-3 h-3" />
|
||||
<span className="font-medium">{formatModelName(DEFAULT_MODEL)}</span>
|
||||
<span className="font-medium">
|
||||
{formatModelName(DEFAULT_MODEL)}
|
||||
</span>
|
||||
</div>
|
||||
{agentInfo.currentPhase && (
|
||||
<div className={cn(
|
||||
"px-1.5 py-0.5 rounded text-[10px] font-medium",
|
||||
agentInfo.currentPhase === "planning" && "bg-blue-500/20 text-blue-400",
|
||||
agentInfo.currentPhase === "action" && "bg-amber-500/20 text-amber-400",
|
||||
agentInfo.currentPhase === "verification" && "bg-green-500/20 text-green-400"
|
||||
)}>
|
||||
<div
|
||||
className={cn(
|
||||
"px-1.5 py-0.5 rounded text-[10px] font-medium",
|
||||
agentInfo.currentPhase === "planning" &&
|
||||
"bg-blue-500/20 text-blue-400",
|
||||
agentInfo.currentPhase === "action" &&
|
||||
"bg-amber-500/20 text-amber-400",
|
||||
agentInfo.currentPhase === "verification" &&
|
||||
"bg-green-500/20 text-green-400"
|
||||
)}
|
||||
>
|
||||
{agentInfo.currentPhase}
|
||||
</div>
|
||||
)}
|
||||
@@ -334,7 +352,11 @@ export function KanbanCard({
|
||||
<div className="w-full h-1.5 bg-zinc-800 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="w-full h-full bg-primary transition-transform duration-500 ease-out origin-left"
|
||||
style={{ transform: `translateX(${agentInfo.progressPercentage - 100}%)` }}
|
||||
style={{
|
||||
transform: `translateX(${
|
||||
agentInfo.progressPercentage - 100
|
||||
}%)`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-[10px] text-muted-foreground">
|
||||
@@ -344,7 +366,10 @@ export function KanbanCard({
|
||||
{agentInfo.toolCallCount} tools
|
||||
</span>
|
||||
{agentInfo.lastToolUsed && (
|
||||
<span className="text-zinc-500 truncate max-w-[80px]" title={agentInfo.lastToolUsed}>
|
||||
<span
|
||||
className="text-zinc-500 truncate max-w-[80px]"
|
||||
title={agentInfo.lastToolUsed}
|
||||
>
|
||||
{agentInfo.lastToolUsed}
|
||||
</span>
|
||||
)}
|
||||
@@ -360,7 +385,11 @@ export function KanbanCard({
|
||||
<div className="flex items-center gap-1 text-[10px] text-muted-foreground">
|
||||
<ListTodo className="w-3 h-3" />
|
||||
<span>
|
||||
{agentInfo.todos.filter(t => t.status === "completed").length}/{agentInfo.todos.length} tasks
|
||||
{
|
||||
agentInfo.todos.filter((t) => t.status === "completed")
|
||||
.length
|
||||
}
|
||||
/{agentInfo.todos.length} tasks
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-0.5 max-h-16 overflow-y-auto">
|
||||
@@ -376,12 +405,15 @@ export function KanbanCard({
|
||||
) : (
|
||||
<Circle className="w-2.5 h-2.5 text-zinc-500 shrink-0" />
|
||||
)}
|
||||
<span className={cn(
|
||||
"truncate",
|
||||
todo.status === "completed" && "text-zinc-500 line-through",
|
||||
todo.status === "in_progress" && "text-amber-400",
|
||||
todo.status === "pending" && "text-zinc-400"
|
||||
)}>
|
||||
<span
|
||||
className={cn(
|
||||
"truncate",
|
||||
todo.status === "completed" &&
|
||||
"text-zinc-500 line-through",
|
||||
todo.status === "in_progress" && "text-amber-400",
|
||||
todo.status === "pending" && "text-zinc-400"
|
||||
)}
|
||||
>
|
||||
{todo.content}
|
||||
</span>
|
||||
</div>
|
||||
@@ -396,7 +428,8 @@ export function KanbanCard({
|
||||
)}
|
||||
|
||||
{/* Summary for waiting_approval and verified - prioritize feature.summary from UpdateFeatureStatus */}
|
||||
{(feature.status === "waiting_approval" || feature.status === "verified") && (
|
||||
{(feature.status === "waiting_approval" ||
|
||||
feature.status === "verified") && (
|
||||
<>
|
||||
{(feature.summary || summary || agentInfo.summary) && (
|
||||
<div className="space-y-1 pt-1 border-t border-white/5">
|
||||
@@ -423,20 +456,28 @@ export function KanbanCard({
|
||||
</div>
|
||||
)}
|
||||
{/* Show tool count even without summary */}
|
||||
{!feature.summary && !summary && !agentInfo.summary && agentInfo.toolCallCount > 0 && (
|
||||
<div className="flex items-center gap-2 text-[10px] text-muted-foreground pt-1 border-t border-white/5">
|
||||
<span className="flex items-center gap-1">
|
||||
<Wrench className="w-2.5 h-2.5" />
|
||||
{agentInfo.toolCallCount} tool calls
|
||||
</span>
|
||||
{agentInfo.todos.length > 0 && (
|
||||
{!feature.summary &&
|
||||
!summary &&
|
||||
!agentInfo.summary &&
|
||||
agentInfo.toolCallCount > 0 && (
|
||||
<div className="flex items-center gap-2 text-[10px] text-muted-foreground pt-1 border-t border-white/5">
|
||||
<span className="flex items-center gap-1">
|
||||
<CheckCircle2 className="w-2.5 h-2.5 text-green-500" />
|
||||
{agentInfo.todos.filter(t => t.status === "completed").length} tasks done
|
||||
<Wrench className="w-2.5 h-2.5" />
|
||||
{agentInfo.toolCallCount} tool calls
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{agentInfo.todos.length > 0 && (
|
||||
<span className="flex items-center gap-1">
|
||||
<CheckCircle2 className="w-2.5 h-2.5 text-green-500" />
|
||||
{
|
||||
agentInfo.todos.filter(
|
||||
(t) => t.status === "completed"
|
||||
).length
|
||||
}{" "}
|
||||
tasks done
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -672,7 +713,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>
|
||||
@@ -713,7 +755,10 @@ export function KanbanCard({
|
||||
</DialogHeader>
|
||||
<div className="flex-1 overflow-y-auto p-4 bg-zinc-900/50 rounded-lg border border-white/10">
|
||||
<Markdown>
|
||||
{feature.summary || summary || agentInfo?.summary || "No summary available"}
|
||||
{feature.summary ||
|
||||
summary ||
|
||||
agentInfo?.summary ||
|
||||
"No summary available"}
|
||||
</Markdown>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -5,21 +5,60 @@ 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, LayoutGrid, Minimize2, Square, Maximize2, Terminal } from "lucide-react";
|
||||
import {
|
||||
Settings,
|
||||
Key,
|
||||
Eye,
|
||||
EyeOff,
|
||||
CheckCircle2,
|
||||
AlertCircle,
|
||||
Loader2,
|
||||
Zap,
|
||||
Sun,
|
||||
Moon,
|
||||
Palette,
|
||||
Terminal,
|
||||
Ghost,
|
||||
Snowflake,
|
||||
Flame,
|
||||
Sparkles,
|
||||
Eclipse,
|
||||
Trees,
|
||||
Cat,
|
||||
Atom,
|
||||
Radio,
|
||||
LayoutGrid,
|
||||
Minimize2,
|
||||
Square,
|
||||
Maximize2,
|
||||
} from "lucide-react";
|
||||
import { getElectronAPI } from "@/lib/electron";
|
||||
|
||||
export function SettingsView() {
|
||||
const { apiKeys, setApiKeys, setCurrentView, theme, setTheme, kanbanCardDetailLevel, setKanbanCardDetailLevel } = useAppStore();
|
||||
const {
|
||||
apiKeys,
|
||||
setApiKeys,
|
||||
setCurrentView,
|
||||
theme,
|
||||
setTheme,
|
||||
kanbanCardDetailLevel,
|
||||
setKanbanCardDetailLevel,
|
||||
} = 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);
|
||||
const [claudeCliStatus, setClaudeCliStatus] = useState<{
|
||||
success: boolean;
|
||||
status?: string;
|
||||
@@ -72,12 +111,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);
|
||||
}
|
||||
@@ -99,12 +147,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);
|
||||
}
|
||||
@@ -120,17 +177,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>
|
||||
@@ -140,25 +202,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">
|
||||
@@ -169,14 +234,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"
|
||||
>
|
||||
@@ -192,7 +257,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 ? (
|
||||
@@ -218,14 +283,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"
|
||||
>
|
||||
@@ -234,7 +300,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>
|
||||
@@ -242,11 +313,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">
|
||||
@@ -257,14 +328,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"
|
||||
>
|
||||
@@ -280,7 +351,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 ? (
|
||||
@@ -297,7 +368,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"
|
||||
@@ -311,8 +383,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"
|
||||
>
|
||||
@@ -321,7 +393,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>
|
||||
@@ -332,8 +409,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>
|
||||
@@ -421,43 +498,165 @@ export function SettingsView() {
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
@@ -469,7 +668,9 @@ export function SettingsView() {
|
||||
<div className="p-6 border-b border-white/10">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<LayoutGrid className="w-5 h-5 text-brand-500" />
|
||||
<h2 className="text-lg font-semibold text-white">Kanban Card Display</h2>
|
||||
<h2 className="text-lg font-semibold text-white">
|
||||
Kanban Card Display
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-sm text-zinc-400">
|
||||
Control how much information is displayed on Kanban cards.
|
||||
@@ -490,7 +691,9 @@ export function SettingsView() {
|
||||
>
|
||||
<Minimize2 className="w-5 h-5" />
|
||||
<span className="font-medium text-sm">Minimal</span>
|
||||
<span className="text-xs text-zinc-500 text-center">Title & category only</span>
|
||||
<span className="text-xs text-zinc-500 text-center">
|
||||
Title & category only
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setKanbanCardDetailLevel("standard")}
|
||||
@@ -503,7 +706,9 @@ export function SettingsView() {
|
||||
>
|
||||
<Square className="w-5 h-5" />
|
||||
<span className="font-medium text-sm">Standard</span>
|
||||
<span className="text-xs text-zinc-500 text-center">Steps & progress</span>
|
||||
<span className="text-xs text-zinc-500 text-center">
|
||||
Steps & progress
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setKanbanCardDetailLevel("detailed")}
|
||||
@@ -516,13 +721,18 @@ export function SettingsView() {
|
||||
>
|
||||
<Maximize2 className="w-5 h-5" />
|
||||
<span className="font-medium text-sm">Detailed</span>
|
||||
<span className="text-xs text-zinc-500 text-center">Model, tools & tasks</span>
|
||||
<span className="text-xs text-zinc-500 text-center">
|
||||
Model, tools & tasks
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-zinc-500">
|
||||
<strong>Minimal:</strong> Shows only title and category<br />
|
||||
<strong>Standard:</strong> Adds steps preview and progress bar<br />
|
||||
<strong>Detailed:</strong> Shows all info including model, tool calls, task list, and summaries
|
||||
<strong>Minimal:</strong> Shows only title and category
|
||||
<br />
|
||||
<strong>Standard:</strong> Adds steps preview and progress bar
|
||||
<br />
|
||||
<strong>Detailed:</strong> Shows all info including model,
|
||||
tool calls, task list, and summaries
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -533,7 +743,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 ? (
|
||||
<>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user