mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 20:23:36 +00:00
Merge branch 'main' into ui-tweaks
This commit is contained in:
@@ -2,7 +2,6 @@
|
||||
|
||||
import { useState, useCallback, useRef, useEffect, useMemo } from "react";
|
||||
import { useAppStore } from "@/store/app-store";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { ImageDropZone } from "@/components/ui/image-drop-zone";
|
||||
@@ -222,11 +221,6 @@ export function AgentView() {
|
||||
e.stopPropagation();
|
||||
if (isProcessing || !isConnected) return;
|
||||
|
||||
console.log(
|
||||
"[agent-view] Drag enter types:",
|
||||
Array.from(e.dataTransfer.types)
|
||||
);
|
||||
|
||||
// Check if dragged items contain files
|
||||
if (e.dataTransfer.types.includes("Files")) {
|
||||
setIsDragOver(true);
|
||||
@@ -262,39 +256,21 @@ export function AgentView() {
|
||||
|
||||
if (isProcessing || !isConnected) return;
|
||||
|
||||
console.log("[agent-view] Drop event:", {
|
||||
filesCount: e.dataTransfer.files.length,
|
||||
itemsCount: e.dataTransfer.items.length,
|
||||
types: Array.from(e.dataTransfer.types),
|
||||
});
|
||||
|
||||
// Check if we have files
|
||||
const files = e.dataTransfer.files;
|
||||
if (files && files.length > 0) {
|
||||
console.log("[agent-view] Processing files from dataTransfer.files");
|
||||
processDroppedFiles(files);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle file paths (from screenshots or other sources)
|
||||
// This is common on macOS when dragging screenshots
|
||||
const items = e.dataTransfer.items;
|
||||
if (items && items.length > 0) {
|
||||
console.log("[agent-view] Processing items");
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
console.log(`[agent-view] Item ${i}:`, {
|
||||
kind: item.kind,
|
||||
type: item.type,
|
||||
});
|
||||
if (item.kind === "file") {
|
||||
const file = item.getAsFile();
|
||||
if (file) {
|
||||
console.log("[agent-view] Got file from item:", {
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
size: file.size,
|
||||
});
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(file);
|
||||
processDroppedFiles(dataTransfer.files);
|
||||
@@ -315,10 +291,6 @@ export function AgentView() {
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
console.log("[agent-view] Paste item:", {
|
||||
kind: item.kind,
|
||||
type: item.type,
|
||||
});
|
||||
|
||||
if (item.kind === "file") {
|
||||
const file = item.getAsFile();
|
||||
@@ -330,10 +302,6 @@ export function AgentView() {
|
||||
}
|
||||
|
||||
if (files.length > 0) {
|
||||
console.log(
|
||||
"[agent-view] Processing pasted image files:",
|
||||
files.length
|
||||
);
|
||||
const dataTransfer = new DataTransfer();
|
||||
files.forEach((file) => dataTransfer.items.add(file));
|
||||
await processDroppedFiles(dataTransfer.files);
|
||||
@@ -442,13 +410,15 @@ export function AgentView() {
|
||||
if (!currentProject) {
|
||||
return (
|
||||
<div
|
||||
className="flex-1 flex items-center justify-center"
|
||||
className="flex-1 flex items-center justify-center bg-background"
|
||||
data-testid="agent-view-no-project"
|
||||
>
|
||||
<div className="text-center">
|
||||
<Sparkles className="w-12 h-12 text-muted-foreground mx-auto mb-4" />
|
||||
<h2 className="text-xl font-semibold mb-2">No Project Selected</h2>
|
||||
<p className="text-muted-foreground">
|
||||
<div className="text-center max-w-md">
|
||||
<div className="w-16 h-16 rounded-2xl bg-primary/10 flex items-center justify-center mx-auto mb-6">
|
||||
<Sparkles className="w-8 h-8 text-primary" />
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold mb-3 text-foreground">No Project Selected</h2>
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
Open or create a project to start working with the AI agent.
|
||||
</p>
|
||||
</div>
|
||||
@@ -472,12 +442,12 @@ export function AgentView() {
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex-1 flex overflow-hidden content-bg"
|
||||
className="flex-1 flex overflow-hidden bg-background"
|
||||
data-testid="agent-view"
|
||||
>
|
||||
{/* Session Manager Sidebar */}
|
||||
{showSessionManager && currentProject && (
|
||||
<div className="w-80 border-r flex-shrink-0">
|
||||
<div className="w-80 border-r border-border flex-shrink-0 bg-card/50">
|
||||
<SessionManager
|
||||
currentSessionId={currentSessionId}
|
||||
onSelectSession={handleSelectSession}
|
||||
@@ -491,13 +461,13 @@ 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-border bg-glass backdrop-blur-md">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-border bg-card/50 backdrop-blur-sm">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowSessionManager(!showSessionManager)}
|
||||
className="h-8 w-8 p-0"
|
||||
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{showSessionManager ? (
|
||||
<PanelLeftClose className="w-4 h-4" />
|
||||
@@ -505,26 +475,28 @@ export function AgentView() {
|
||||
<PanelLeft className="w-4 h-4" />
|
||||
)}
|
||||
</Button>
|
||||
<Bot className="w-5 h-5 text-primary" />
|
||||
<div className="w-9 h-9 rounded-xl bg-primary/10 flex items-center justify-center">
|
||||
<Bot className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold">AI Agent</h1>
|
||||
<h1 className="text-lg font-semibold text-foreground">AI Agent</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{currentProject.name}
|
||||
{currentSessionId && !isConnected && " · Connecting..."}
|
||||
{currentSessionId && !isConnected && " - Connecting..."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status indicators & actions */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-3">
|
||||
{currentTool && (
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground bg-muted px-2 py-1 rounded">
|
||||
<Wrench className="w-3 h-3" />
|
||||
<span>{currentTool}</span>
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground bg-muted/50 px-3 py-1.5 rounded-full border border-border">
|
||||
<Wrench className="w-3 h-3 text-primary" />
|
||||
<span className="font-medium">{currentTool}</span>
|
||||
</div>
|
||||
)}
|
||||
{agentError && (
|
||||
<span className="text-xs text-destructive">{agentError}</span>
|
||||
<span className="text-xs text-destructive font-medium">{agentError}</span>
|
||||
)}
|
||||
{currentSessionId && messages.length > 0 && (
|
||||
<Button
|
||||
@@ -532,8 +504,9 @@ export function AgentView() {
|
||||
size="sm"
|
||||
onClick={handleClearChat}
|
||||
disabled={isProcessing}
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-1" />
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
Clear
|
||||
</Button>
|
||||
)}
|
||||
@@ -543,22 +516,25 @@ export function AgentView() {
|
||||
{/* Messages */}
|
||||
{!currentSessionId ? (
|
||||
<div
|
||||
className="flex-1 flex items-center justify-center"
|
||||
className="flex-1 flex items-center justify-center bg-background"
|
||||
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">
|
||||
<div className="text-center max-w-md">
|
||||
<div className="w-16 h-16 rounded-2xl bg-muted/50 flex items-center justify-center mx-auto mb-6">
|
||||
<Bot className="w-8 h-8 text-muted-foreground" />
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold mb-3 text-foreground">
|
||||
No Session Selected
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
Create or select a session to start chatting
|
||||
<p className="text-sm text-muted-foreground mb-6 leading-relaxed">
|
||||
Create or select a session to start chatting with the AI agent
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => setShowSessionManager(true)}
|
||||
variant="outline"
|
||||
className="gap-2"
|
||||
>
|
||||
<PanelLeft className="w-4 h-4 mr-2" />
|
||||
<PanelLeft className="w-4 h-4" />
|
||||
{showSessionManager ? "View" : "Show"} Sessions
|
||||
</Button>
|
||||
</div>
|
||||
@@ -566,7 +542,7 @@ export function AgentView() {
|
||||
) : (
|
||||
<div
|
||||
ref={messagesContainerRef}
|
||||
className="flex-1 overflow-y-auto p-4 space-y-4"
|
||||
className="flex-1 overflow-y-auto px-6 py-6 space-y-6 scroll-smooth"
|
||||
data-testid="message-list"
|
||||
onScroll={handleScroll}
|
||||
>
|
||||
@@ -574,95 +550,156 @@ export function AgentView() {
|
||||
<div
|
||||
key={message.id}
|
||||
className={cn(
|
||||
"flex gap-3",
|
||||
message.role === "user" && "flex-row-reverse"
|
||||
"flex gap-4 max-w-4xl",
|
||||
message.role === "user" ? "flex-row-reverse ml-auto" : ""
|
||||
)}
|
||||
>
|
||||
{/* Avatar */}
|
||||
<div
|
||||
className={cn(
|
||||
"w-8 h-8 rounded-full flex items-center justify-center shrink-0",
|
||||
message.role === "assistant" ? "bg-primary/10" : "bg-muted"
|
||||
"w-9 h-9 rounded-xl flex items-center justify-center shrink-0 shadow-sm",
|
||||
message.role === "assistant"
|
||||
? "bg-primary/10 ring-1 ring-primary/20"
|
||||
: "bg-muted ring-1 ring-border"
|
||||
)}
|
||||
>
|
||||
{message.role === "assistant" ? (
|
||||
<Bot className="w-4 h-4 text-primary" />
|
||||
) : (
|
||||
<User className="w-4 h-4" />
|
||||
<User className="w-4 h-4 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
<Card
|
||||
|
||||
{/* Message Bubble */}
|
||||
<div
|
||||
className={cn(
|
||||
"max-w-[80%] py-0",
|
||||
"flex-1 max-w-[85%] rounded-2xl px-4 py-3 shadow-sm",
|
||||
message.role === "user"
|
||||
? "bg-transparent border border-primary text-foreground"
|
||||
: "border-l-4 border-primary bg-card"
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-card border border-border"
|
||||
)}
|
||||
>
|
||||
<CardContent className="px-3 py-2">
|
||||
{message.role === "assistant" ? (
|
||||
<Markdown className="text-sm text-primary prose-headings:text-primary prose-strong:text-primary prose-code:text-primary">
|
||||
{message.content}
|
||||
</Markdown>
|
||||
) : (
|
||||
<p className="text-sm whitespace-pre-wrap">
|
||||
{message.content}
|
||||
</p>
|
||||
)}
|
||||
<p
|
||||
className={cn(
|
||||
"text-xs mt-1",
|
||||
message.role === "user"
|
||||
? "text-muted-foreground"
|
||||
: "text-primary/70"
|
||||
)}
|
||||
>
|
||||
{new Date(message.timestamp).toLocaleTimeString()}
|
||||
{message.role === "assistant" ? (
|
||||
<Markdown className="text-sm text-foreground prose-p:leading-relaxed prose-headings:text-foreground prose-strong:text-foreground prose-code:text-primary prose-code:bg-muted prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded">
|
||||
{message.content}
|
||||
</Markdown>
|
||||
) : (
|
||||
<p className="text-sm whitespace-pre-wrap leading-relaxed">
|
||||
{message.content}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
<p
|
||||
className={cn(
|
||||
"text-[11px] mt-2 font-medium",
|
||||
message.role === "user"
|
||||
? "text-primary-foreground/70"
|
||||
: "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{new Date(message.timestamp).toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Thinking Indicator */}
|
||||
{isProcessing && (
|
||||
<div className="flex gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<div className="flex gap-4 max-w-4xl">
|
||||
<div className="w-9 h-9 rounded-xl bg-primary/10 ring-1 ring-primary/20 flex items-center justify-center shrink-0 shadow-sm">
|
||||
<Bot className="w-4 h-4 text-primary" />
|
||||
</div>
|
||||
<Card className="border-l-4 border-primary bg-card py-0">
|
||||
<CardContent className="p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader2 className="w-4 h-4 animate-spin text-primary" />
|
||||
<span className="text-sm text-primary">
|
||||
Thinking...
|
||||
</span>
|
||||
<div className="bg-card border border-border rounded-2xl px-4 py-3 shadow-sm">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="w-2 h-2 rounded-full bg-primary animate-pulse" style={{ animationDelay: "0ms" }} />
|
||||
<span className="w-2 h-2 rounded-full bg-primary animate-pulse" style={{ animationDelay: "150ms" }} />
|
||||
<span className="w-2 h-2 rounded-full bg-primary animate-pulse" style={{ animationDelay: "300ms" }} />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Thinking...
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Input */}
|
||||
{/* Input Area */}
|
||||
{currentSessionId && (
|
||||
<div className="border-t border-border p-4 space-y-3 bg-background">
|
||||
<div className="border-t border-border p-4 bg-card/50 backdrop-blur-sm">
|
||||
{/* Image Drop Zone (when visible) */}
|
||||
{showImageDropZone && (
|
||||
<ImageDropZone
|
||||
onImagesSelected={handleImagesSelected}
|
||||
images={selectedImages}
|
||||
maxFiles={5}
|
||||
className="mb-3"
|
||||
className="mb-4"
|
||||
disabled={isProcessing || !isConnected}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Text Input and Controls - with drag and drop support */}
|
||||
{/* Selected Images Preview */}
|
||||
{selectedImages.length > 0 && (
|
||||
<div className="mb-4 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs font-medium text-foreground">
|
||||
{selectedImages.length} image
|
||||
{selectedImages.length > 1 ? "s" : ""} attached
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setSelectedImages([])}
|
||||
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||
disabled={isProcessing}
|
||||
>
|
||||
Clear all
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedImages.map((image) => (
|
||||
<div
|
||||
key={image.id}
|
||||
className="group relative rounded-lg border border-border bg-muted/30 p-2 flex items-center gap-2 hover:border-primary/30 transition-colors"
|
||||
>
|
||||
{/* Image thumbnail */}
|
||||
<div className="w-8 h-8 rounded-md overflow-hidden bg-muted flex-shrink-0">
|
||||
<img
|
||||
src={image.data}
|
||||
alt={image.filename}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
{/* Image info */}
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-xs font-medium text-foreground truncate max-w-24">
|
||||
{image.filename}
|
||||
</p>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
{formatFileSize(image.size)}
|
||||
</p>
|
||||
</div>
|
||||
{/* Remove button */}
|
||||
<button
|
||||
onClick={() => removeImage(image.id)}
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 rounded-full hover:bg-destructive/10 text-muted-foreground hover:text-destructive"
|
||||
disabled={isProcessing}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Text Input and Controls */}
|
||||
<div
|
||||
className={cn(
|
||||
"flex gap-2 transition-all duration-200 rounded-lg",
|
||||
isDragOver &&
|
||||
"bg-primary/10 ring-2 ring-primary ring-offset-2 ring-offset-background"
|
||||
"flex gap-2 transition-all duration-200 rounded-xl p-1",
|
||||
isDragOver && "bg-primary/5 ring-2 ring-primary/30"
|
||||
)}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragLeave={handleDragLeave}
|
||||
@@ -684,21 +721,19 @@ export function AgentView() {
|
||||
disabled={isProcessing || !isConnected}
|
||||
data-testid="agent-input"
|
||||
className={cn(
|
||||
"bg-input border-border",
|
||||
selectedImages.length > 0 &&
|
||||
"border-primary/50 bg-primary/5",
|
||||
isDragOver &&
|
||||
"border-primary bg-primary/10"
|
||||
"h-11 bg-background border-border rounded-xl pl-4 pr-20 text-sm transition-all",
|
||||
"focus:ring-2 focus:ring-primary/20 focus:border-primary/50",
|
||||
selectedImages.length > 0 && "border-primary/30",
|
||||
isDragOver && "border-primary bg-primary/5"
|
||||
)}
|
||||
/>
|
||||
{selectedImages.length > 0 && !isDragOver && (
|
||||
<div className="absolute right-2 top-1/2 transform -translate-y-1/2 text-xs text-primary-foreground bg-primary px-2 py-1 rounded">
|
||||
{selectedImages.length} image
|
||||
{selectedImages.length > 1 ? "s" : ""}
|
||||
<div className="absolute right-3 top-1/2 -translate-y-1/2 text-xs bg-primary text-primary-foreground px-2 py-0.5 rounded-full font-medium">
|
||||
{selectedImages.length} image{selectedImages.length > 1 ? "s" : ""}
|
||||
</div>
|
||||
)}
|
||||
{isDragOver && (
|
||||
<div className="absolute right-2 top-1/2 transform -translate-y-1/2 text-xs text-primary-foreground bg-primary px-2 py-1 rounded flex items-center gap-1">
|
||||
<div className="absolute right-3 top-1/2 -translate-y-1/2 flex items-center gap-1.5 text-xs text-primary font-medium">
|
||||
<Paperclip className="w-3 h-3" />
|
||||
Drop here
|
||||
</div>
|
||||
@@ -708,13 +743,13 @@ export function AgentView() {
|
||||
{/* Image Attachment Button */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="default"
|
||||
size="icon"
|
||||
onClick={toggleImageDropZone}
|
||||
disabled={isProcessing || !isConnected}
|
||||
className={cn(
|
||||
showImageDropZone &&
|
||||
"bg-primary/20 text-primary border-primary",
|
||||
selectedImages.length > 0 && "border-primary"
|
||||
"h-11 w-11 rounded-xl border-border",
|
||||
showImageDropZone && "bg-primary/10 text-primary border-primary/30",
|
||||
selectedImages.length > 0 && "border-primary/30 text-primary"
|
||||
)}
|
||||
title="Attach images"
|
||||
>
|
||||
@@ -729,64 +764,17 @@ export function AgentView() {
|
||||
isProcessing ||
|
||||
!isConnected
|
||||
}
|
||||
className="h-11 px-4 rounded-xl"
|
||||
data-testid="send-message"
|
||||
>
|
||||
<Send className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Selected Images Preview */}
|
||||
{selectedImages.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs font-medium text-foreground">
|
||||
{selectedImages.length} image
|
||||
{selectedImages.length > 1 ? "s" : ""} attached
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setSelectedImages([])}
|
||||
className="text-xs text-muted-foreground hover:text-foreground"
|
||||
disabled={isProcessing}
|
||||
>
|
||||
Clear all
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedImages.map((image) => (
|
||||
<div
|
||||
key={image.id}
|
||||
className="relative group rounded-md border border-muted bg-muted/50 p-2 flex items-center space-x-2"
|
||||
>
|
||||
{/* Image thumbnail */}
|
||||
<div className="w-8 h-8 rounded overflow-hidden bg-muted flex-shrink-0">
|
||||
<img
|
||||
src={image.data}
|
||||
alt={image.filename}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
{/* Image info */}
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-xs font-medium text-foreground truncate">
|
||||
{image.filename}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatFileSize(image.size)}
|
||||
</p>
|
||||
</div>
|
||||
{/* Remove button */}
|
||||
<button
|
||||
onClick={() => removeImage(image.id)}
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 rounded-full hover:bg-destructive hover:text-destructive-foreground text-muted-foreground"
|
||||
disabled={isProcessing}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Keyboard hint */}
|
||||
<p className="text-[11px] text-muted-foreground mt-2 text-center">
|
||||
Press <kbd className="px-1.5 py-0.5 bg-muted rounded text-[10px] font-medium">Enter</kbd> to send
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -112,11 +112,11 @@ import { useWindowState } from "@/hooks/use-window-state";
|
||||
|
||||
type ColumnId = Feature["status"];
|
||||
|
||||
const COLUMNS: { id: ColumnId; title: string; color: string }[] = [
|
||||
{ id: "backlog", title: "Backlog", color: "bg-zinc-500" },
|
||||
{ id: "in_progress", title: "In Progress", color: "bg-yellow-500" },
|
||||
{ id: "waiting_approval", title: "Waiting Approval", color: "bg-orange-500" },
|
||||
{ id: "verified", title: "Verified", color: "bg-green-500" },
|
||||
const COLUMNS: { id: ColumnId; title: string; colorClass: string }[] = [
|
||||
{ id: "backlog", title: "Backlog", colorClass: "bg-[var(--status-backlog)]" },
|
||||
{ id: "in_progress", title: "In Progress", colorClass: "bg-[var(--status-in-progress)]" },
|
||||
{ id: "waiting_approval", title: "Waiting Approval", colorClass: "bg-[var(--status-waiting)]" },
|
||||
{ id: "verified", title: "Verified", colorClass: "bg-[var(--status-success)]" },
|
||||
];
|
||||
|
||||
type ModelOption = {
|
||||
@@ -2109,7 +2109,7 @@ export function BoardView() {
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<div className="flex gap-4 h-full min-w-max">
|
||||
<div className="flex gap-5 h-full min-w-max py-1">
|
||||
{COLUMNS.map((column) => {
|
||||
const columnFeatures = getColumnFeatures(column.id);
|
||||
return (
|
||||
@@ -2117,7 +2117,7 @@ export function BoardView() {
|
||||
key={column.id}
|
||||
id={column.id}
|
||||
title={column.title}
|
||||
color={column.color}
|
||||
colorClass={column.colorClass}
|
||||
count={columnFeatures.length}
|
||||
opacity={backgroundSettings.columnOpacity}
|
||||
showBorder={backgroundSettings.columnBorderEnabled}
|
||||
@@ -2238,14 +2238,17 @@ export function BoardView() {
|
||||
})}
|
||||
</div>
|
||||
|
||||
<DragOverlay>
|
||||
<DragOverlay dropAnimation={{
|
||||
duration: 200,
|
||||
easing: 'cubic-bezier(0.18, 0.67, 0.6, 1.22)',
|
||||
}}>
|
||||
{activeFeature && (
|
||||
<Card className="w-72 opacity-90 rotate-3 shadow-xl">
|
||||
<Card className="w-72 rotate-2 shadow-2xl shadow-black/25 border-primary/50 bg-card/95 backdrop-blur-sm transition-transform">
|
||||
<CardHeader className="p-3">
|
||||
<CardTitle className="text-sm">
|
||||
<CardTitle className="text-sm font-medium line-clamp-2">
|
||||
{activeFeature.description}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs">
|
||||
<CardDescription className="text-xs text-muted-foreground">
|
||||
{activeFeature.category}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
@@ -393,6 +393,7 @@ export function ContextView() {
|
||||
className="flex-1 flex overflow-hidden"
|
||||
onDrop={handleDrop}
|
||||
onDragOver={handleDragOver}
|
||||
data-testid="context-drop-zone"
|
||||
>
|
||||
{/* Left Panel - File List */}
|
||||
<div className="w-64 border-r border-border flex flex-col overflow-hidden">
|
||||
|
||||
@@ -57,7 +57,6 @@ import {
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Brain,
|
||||
Flag,
|
||||
Wand2,
|
||||
Archive,
|
||||
} from "lucide-react";
|
||||
@@ -92,33 +91,6 @@ function formatThinkingLevel(level: ThinkingLevel | undefined): string {
|
||||
return labels[level];
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats priority for display
|
||||
*/
|
||||
function formatPriority(priority: number | undefined): string | null {
|
||||
if (!priority) return null;
|
||||
const labels: Record<number, string> = {
|
||||
1: "High",
|
||||
2: "Medium",
|
||||
3: "Low",
|
||||
};
|
||||
return labels[priority] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets priority badge color classes
|
||||
*/
|
||||
function getPriorityBadgeClasses(priority: number | undefined): string {
|
||||
if (priority === 1) {
|
||||
return "bg-red-500/20 border border-red-500/50 text-red-400";
|
||||
} else if (priority === 2) {
|
||||
return "bg-yellow-500/20 border border-yellow-500/50 text-yellow-400";
|
||||
} else if (priority === 3) {
|
||||
return "bg-blue-500/20 border border-blue-500/50 text-blue-400";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
interface KanbanCardProps {
|
||||
feature: Feature;
|
||||
onEdit: () => void;
|
||||
@@ -138,17 +110,11 @@ interface KanbanCardProps {
|
||||
hasContext?: boolean;
|
||||
isCurrentAutoTask?: boolean;
|
||||
shortcutKey?: string;
|
||||
/** Context content for extracting progress info */
|
||||
contextContent?: string;
|
||||
/** Feature summary from agent completion */
|
||||
summary?: string;
|
||||
/** Opacity percentage (0-100) */
|
||||
opacity?: number;
|
||||
/** Whether to use glassmorphism (backdrop-blur) effect */
|
||||
glassmorphism?: boolean;
|
||||
/** Whether to show card borders */
|
||||
cardBorderEnabled?: boolean;
|
||||
/** Card border opacity percentage (0-100) */
|
||||
cardBorderOpacity?: number;
|
||||
}
|
||||
|
||||
@@ -186,16 +152,13 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
const [currentTime, setCurrentTime] = useState(() => Date.now());
|
||||
const { kanbanCardDetailLevel } = useAppStore();
|
||||
|
||||
// Check if feature has worktree
|
||||
const hasWorktree = !!feature.branchName;
|
||||
|
||||
// Helper functions to check what should be shown based on detail level
|
||||
const showSteps =
|
||||
kanbanCardDetailLevel === "standard" ||
|
||||
kanbanCardDetailLevel === "detailed";
|
||||
const showAgentInfo = kanbanCardDetailLevel === "detailed";
|
||||
|
||||
// Helper to check if "just finished" badge should be shown (within 2 minutes)
|
||||
const isJustFinished = useMemo(() => {
|
||||
if (
|
||||
!feature.justFinishedAt ||
|
||||
@@ -205,26 +168,23 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
return false;
|
||||
}
|
||||
const finishedTime = new Date(feature.justFinishedAt).getTime();
|
||||
const twoMinutes = 2 * 60 * 1000; // 2 minutes in milliseconds
|
||||
const twoMinutes = 2 * 60 * 1000;
|
||||
return currentTime - finishedTime < twoMinutes;
|
||||
}, [feature.justFinishedAt, feature.status, feature.error, currentTime]);
|
||||
|
||||
// Update current time periodically to check if badge should be hidden
|
||||
useEffect(() => {
|
||||
if (!feature.justFinishedAt || feature.status !== "waiting_approval") {
|
||||
return;
|
||||
}
|
||||
|
||||
const finishedTime = new Date(feature.justFinishedAt).getTime();
|
||||
const twoMinutes = 2 * 60 * 1000; // 2 minutes in milliseconds
|
||||
const twoMinutes = 2 * 60 * 1000;
|
||||
const timeRemaining = twoMinutes - (currentTime - finishedTime);
|
||||
|
||||
if (timeRemaining <= 0) {
|
||||
// Already past 2 minutes
|
||||
return;
|
||||
}
|
||||
|
||||
// Update time every second to check if 2 minutes have passed
|
||||
const interval = setInterval(() => {
|
||||
setCurrentTime(Date.now());
|
||||
}, 1000);
|
||||
@@ -232,45 +192,14 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
return () => clearInterval(interval);
|
||||
}, [feature.justFinishedAt, feature.status, currentTime]);
|
||||
|
||||
// Calculate priority badge position
|
||||
const priorityLabel = formatPriority(feature.priority);
|
||||
const hasPriority = !!priorityLabel;
|
||||
|
||||
// Calculate top position for badges (stacking vertically)
|
||||
const getBadgeTopPosition = (badgeIndex: number) => {
|
||||
return badgeIndex === 0
|
||||
? "top-2"
|
||||
: badgeIndex === 1
|
||||
? "top-8"
|
||||
: badgeIndex === 2
|
||||
? "top-14"
|
||||
: "top-20";
|
||||
};
|
||||
|
||||
// Determine badge positions (must be after isJustFinished is defined)
|
||||
let badgeIndex = 0;
|
||||
const priorityBadgeIndex = hasPriority ? badgeIndex++ : -1;
|
||||
const skipTestsBadgeIndex =
|
||||
feature.skipTests && !feature.error ? badgeIndex++ : -1;
|
||||
const errorBadgeIndex = feature.error ? badgeIndex++ : -1;
|
||||
const justFinishedBadgeIndex = isJustFinished ? badgeIndex++ : -1;
|
||||
const branchBadgeIndex =
|
||||
hasWorktree && !isCurrentAutoTask ? badgeIndex++ : -1;
|
||||
|
||||
// Total number of badges displayed
|
||||
const totalBadgeCount = badgeIndex;
|
||||
|
||||
// Load context file for in_progress, waiting_approval, and verified features
|
||||
useEffect(() => {
|
||||
const loadContext = async () => {
|
||||
// Use provided context or load from file
|
||||
if (contextContent) {
|
||||
const info = parseAgentContext(contextContent);
|
||||
setAgentInfo(info);
|
||||
return;
|
||||
}
|
||||
|
||||
// Only load for non-backlog features
|
||||
if (feature.status === "backlog") {
|
||||
setAgentInfo(null);
|
||||
return;
|
||||
@@ -282,7 +211,6 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
const currentProject = (window as any).__currentProject;
|
||||
if (!currentProject?.path) return;
|
||||
|
||||
// Use features API to get agent output
|
||||
if (api.features) {
|
||||
const result = await api.features.getAgentOutput(
|
||||
currentProject.path,
|
||||
@@ -294,7 +222,6 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
setAgentInfo(info);
|
||||
}
|
||||
} else {
|
||||
// Fallback to direct file read for backward compatibility
|
||||
const contextPath = `${currentProject.path}/.automaker/features/${feature.id}/agent-output.md`;
|
||||
const result = await api.readFile(contextPath);
|
||||
|
||||
@@ -304,14 +231,12 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Context file might not exist
|
||||
console.debug("[KanbanCard] No context file for feature:", feature.id);
|
||||
}
|
||||
};
|
||||
|
||||
loadContext();
|
||||
|
||||
// Reload context periodically while feature is running
|
||||
if (isCurrentAutoTask) {
|
||||
const interval = setInterval(loadContext, 3000);
|
||||
return () => clearInterval(interval);
|
||||
@@ -327,12 +252,6 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
onDelete();
|
||||
};
|
||||
|
||||
// Dragging logic:
|
||||
// - Backlog items can always be dragged
|
||||
// - skipTests items can be dragged even when in_progress or verified (unless currently running)
|
||||
// - waiting_approval items can always be dragged (to allow manual verification via drag)
|
||||
// - verified items can always be dragged (to allow moving back to waiting_approval or backlog)
|
||||
// - Non-skipTests (TDD) items in progress cannot be dragged (they are running)
|
||||
const isDraggable =
|
||||
feature.status === "backlog" ||
|
||||
feature.status === "waiting_approval" ||
|
||||
@@ -356,15 +275,11 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
opacity: isDragging ? 0.5 : undefined,
|
||||
};
|
||||
|
||||
// Calculate border style based on enabled state and opacity
|
||||
const borderStyle: React.CSSProperties = { ...style };
|
||||
if (!cardBorderEnabled) {
|
||||
(borderStyle as Record<string, string>).borderWidth = "0px";
|
||||
(borderStyle as Record<string, string>).borderColor = "transparent";
|
||||
} else if (cardBorderOpacity !== 100) {
|
||||
// Apply border opacity using color-mix to blend the border color with transparent
|
||||
// The --border variable uses oklch format, so we use color-mix in oklch space
|
||||
// Ensure border width is set (1px is the default Tailwind border width)
|
||||
(borderStyle as Record<string, string>).borderWidth = "1px";
|
||||
(
|
||||
borderStyle as Record<string, string>
|
||||
@@ -376,28 +291,27 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
ref={setNodeRef}
|
||||
style={isCurrentAutoTask ? style : borderStyle}
|
||||
className={cn(
|
||||
"cursor-grab active:cursor-grabbing transition-all relative kanban-card-content select-none",
|
||||
// Apply border class when border is enabled and opacity is 100%
|
||||
// When opacity is not 100%, we use inline styles for border color
|
||||
// Skip border classes when animated border is active (isCurrentAutoTask)
|
||||
"cursor-grab active:cursor-grabbing relative kanban-card-content select-none",
|
||||
"transition-all duration-200 ease-out",
|
||||
// Premium shadow system
|
||||
"shadow-sm hover:shadow-md hover:shadow-black/10",
|
||||
// Subtle lift on hover
|
||||
"hover:-translate-y-0.5",
|
||||
!isCurrentAutoTask &&
|
||||
cardBorderEnabled &&
|
||||
cardBorderOpacity === 100 &&
|
||||
"border-border",
|
||||
// When border is enabled but opacity is not 100%, we still need border width
|
||||
"border-border/50",
|
||||
!isCurrentAutoTask &&
|
||||
cardBorderEnabled &&
|
||||
cardBorderOpacity !== 100 &&
|
||||
"border",
|
||||
// Remove default background when using opacity overlay
|
||||
!isDragging && "bg-transparent",
|
||||
// Remove default backdrop-blur-sm from Card component when glassmorphism is disabled
|
||||
!glassmorphism && "backdrop-blur-[0px]!",
|
||||
isDragging && "scale-105 shadow-lg",
|
||||
// Error state border (only when not in progress)
|
||||
isDragging && "scale-105 shadow-xl shadow-black/20 rotate-1",
|
||||
// Error state - using CSS variable
|
||||
feature.error &&
|
||||
!isCurrentAutoTask &&
|
||||
"border-red-500 border-2 shadow-red-500/30 shadow-lg",
|
||||
"border-[var(--status-error)] border-2 shadow-[var(--status-error-bg)] shadow-lg",
|
||||
!isDraggable && "cursor-default"
|
||||
)}
|
||||
data-testid={`kanban-card-${feature.id}`}
|
||||
@@ -405,7 +319,7 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
{...attributes}
|
||||
{...(isDraggable ? listeners : {})}
|
||||
>
|
||||
{/* Background overlay with opacity - only affects background, not content */}
|
||||
{/* Background overlay with opacity */}
|
||||
{!isDragging && (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -415,88 +329,85 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
style={{ opacity: opacity / 100 }}
|
||||
/>
|
||||
)}
|
||||
{/* Priority badge */}
|
||||
{hasPriority && (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute px-1.5 py-0.5 text-[10px] font-medium rounded flex items-center gap-1 z-10",
|
||||
getBadgeTopPosition(priorityBadgeIndex),
|
||||
"left-2",
|
||||
getPriorityBadgeClasses(feature.priority)
|
||||
)}
|
||||
data-testid={`priority-badge-${feature.id}`}
|
||||
title={`Priority: ${priorityLabel}`}
|
||||
>
|
||||
<Flag className="w-3 h-3" />
|
||||
<span>{priorityLabel}</span>
|
||||
</div>
|
||||
)}
|
||||
{/* Skip Tests indicator badge */}
|
||||
|
||||
{/* Skip Tests (Manual) indicator badge */}
|
||||
{feature.skipTests && !feature.error && (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute px-1.5 py-0.5 text-[10px] font-medium rounded flex items-center gap-1 z-10",
|
||||
getBadgeTopPosition(skipTestsBadgeIndex),
|
||||
"left-2",
|
||||
"bg-orange-500/20 border border-orange-500/50 text-orange-400"
|
||||
)}
|
||||
data-testid={`skip-tests-badge-${feature.id}`}
|
||||
title="Manual verification required"
|
||||
>
|
||||
<Hand className="w-3 h-3" />
|
||||
<span>Manual</span>
|
||||
</div>
|
||||
<TooltipProvider delayDuration={200}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
className={cn(
|
||||
"absolute px-1.5 py-0.5 text-[10px] font-medium rounded-md flex items-center gap-1 z-10",
|
||||
"top-2 left-2",
|
||||
"bg-[var(--status-warning-bg)] border border-[var(--status-warning)]/40 text-[var(--status-warning)]"
|
||||
)}
|
||||
data-testid={`skip-tests-badge-${feature.id}`}
|
||||
>
|
||||
<Hand className="w-3 h-3" />
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" className="text-xs">
|
||||
<p>Manual verification required</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
|
||||
{/* Error indicator badge */}
|
||||
{feature.error && (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute px-1.5 py-0.5 text-[10px] font-medium rounded flex items-center gap-1 z-10",
|
||||
getBadgeTopPosition(errorBadgeIndex),
|
||||
"left-2",
|
||||
"bg-red-500/20 border border-red-500/50 text-red-400"
|
||||
)}
|
||||
data-testid={`error-badge-${feature.id}`}
|
||||
title={feature.error}
|
||||
>
|
||||
<AlertCircle className="w-3 h-3" />
|
||||
<span>Errored</span>
|
||||
</div>
|
||||
<TooltipProvider delayDuration={200}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
className={cn(
|
||||
"absolute px-1.5 py-0.5 text-[10px] font-medium rounded-md flex items-center gap-1 z-10",
|
||||
"top-2 left-2",
|
||||
"bg-[var(--status-error-bg)] border border-[var(--status-error)]/40 text-[var(--status-error)]"
|
||||
)}
|
||||
data-testid={`error-badge-${feature.id}`}
|
||||
>
|
||||
<AlertCircle className="w-3 h-3" />
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" className="text-xs max-w-[250px]">
|
||||
<p>{feature.error}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
{/* Just Finished indicator badge - shows when agent just completed work (for 2 minutes) */}
|
||||
|
||||
{/* Just Finished indicator badge */}
|
||||
{isJustFinished && (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute px-1.5 py-0.5 text-[10px] font-medium rounded flex items-center gap-1 z-10",
|
||||
getBadgeTopPosition(justFinishedBadgeIndex),
|
||||
"left-2",
|
||||
"bg-green-500/20 border border-green-500/50 text-green-400 animate-pulse"
|
||||
"absolute px-1.5 py-0.5 text-[10px] font-medium rounded-md flex items-center gap-1 z-10",
|
||||
feature.skipTests ? "top-8 left-2" : "top-2 left-2",
|
||||
"bg-[var(--status-success-bg)] border border-[var(--status-success)]/40 text-[var(--status-success)]",
|
||||
"animate-pulse"
|
||||
)}
|
||||
data-testid={`just-finished-badge-${feature.id}`}
|
||||
title="Agent just finished working on this feature"
|
||||
>
|
||||
<Sparkles className="w-3 h-3" />
|
||||
<span>Fresh Baked</span>
|
||||
</div>
|
||||
)}
|
||||
{/* Branch badge - show when feature has a worktree */}
|
||||
|
||||
{/* Branch badge */}
|
||||
{hasWorktree && !isCurrentAutoTask && (
|
||||
<TooltipProvider delayDuration={300}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
className={cn(
|
||||
"absolute px-1.5 py-0.5 text-[10px] font-medium rounded flex items-center gap-1 z-10 cursor-default",
|
||||
"bg-purple-500/20 border border-purple-500/50 text-purple-400",
|
||||
getBadgeTopPosition(branchBadgeIndex),
|
||||
"left-2"
|
||||
"absolute px-1.5 py-0.5 text-[10px] font-medium rounded-md flex items-center gap-1 z-10 cursor-default",
|
||||
"bg-[var(--status-info-bg)] border border-[var(--status-info)]/40 text-[var(--status-info)]",
|
||||
feature.error || feature.skipTests || isJustFinished
|
||||
? "top-8 left-2"
|
||||
: "top-2 left-2"
|
||||
)}
|
||||
data-testid={`branch-badge-${feature.id}`}
|
||||
>
|
||||
<GitBranch className="w-3 h-3 shrink-0" />
|
||||
<span className="truncate max-w-[80px]">
|
||||
{feature.branchName?.replace("feature/", "")}
|
||||
</span>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="max-w-[300px]">
|
||||
@@ -507,27 +418,26 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
|
||||
<CardHeader
|
||||
className={cn(
|
||||
"p-3 pb-2 block", // Reset grid layout to block for custom kanban card layout
|
||||
// Add extra top padding when badges are present to prevent text overlap
|
||||
// Calculate padding based on number of badges
|
||||
totalBadgeCount === 1 && "pt-10",
|
||||
totalBadgeCount === 2 && "pt-14",
|
||||
totalBadgeCount === 3 && "pt-20",
|
||||
totalBadgeCount >= 4 && "pt-24"
|
||||
"p-3 pb-2 block",
|
||||
(feature.skipTests || feature.error || isJustFinished) && "pt-10",
|
||||
hasWorktree &&
|
||||
(feature.skipTests || feature.error || isJustFinished) &&
|
||||
"pt-14"
|
||||
)}
|
||||
>
|
||||
{isCurrentAutoTask && (
|
||||
<div className="absolute top-2 right-2 flex items-center justify-center gap-2 bg-running-indicator/20 border border-running-indicator rounded px-2 py-0.5">
|
||||
<Loader2 className="w-4 h-4 text-running-indicator animate-spin" />
|
||||
<span className="text-xs text-running-indicator font-medium">
|
||||
<div className="absolute top-2 right-2 flex items-center justify-center gap-2 bg-[var(--status-in-progress)]/15 border border-[var(--status-in-progress)]/50 rounded-md px-2 py-0.5">
|
||||
<Loader2 className="w-3.5 h-3.5 text-[var(--status-in-progress)] animate-spin" />
|
||||
<span className="text-[10px] text-[var(--status-in-progress)] font-medium">
|
||||
{formatModelName(feature.model ?? DEFAULT_MODEL)}
|
||||
</span>
|
||||
{feature.startedAt && (
|
||||
<CountUpTimer
|
||||
startedAt={feature.startedAt}
|
||||
className="text-running-indicator"
|
||||
className="text-[var(--status-in-progress)] text-[10px]"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -614,21 +524,22 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0 hover:bg-white/10"
|
||||
className="h-6 w-6 p-0 hover:bg-muted/80 rounded-md"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
data-testid={`menu-${feature.id}`}
|
||||
>
|
||||
<MoreVertical className="w-4 h-4" />
|
||||
<MoreVertical className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuContent align="end" className="w-36">
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEdit();
|
||||
}}
|
||||
data-testid={`edit-feature-${feature.id}`}
|
||||
className="text-xs"
|
||||
>
|
||||
<Edit className="w-3 h-3 mr-2" />
|
||||
Edit
|
||||
@@ -640,13 +551,14 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
onViewOutput();
|
||||
}}
|
||||
data-testid={`view-logs-${feature.id}`}
|
||||
className="text-xs"
|
||||
>
|
||||
<FileText className="w-3 h-3 mr-2" />
|
||||
Logs
|
||||
View Logs
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
className="text-destructive focus:text-destructive"
|
||||
className="text-xs text-destructive focus:text-destructive"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteClick(e as unknown as React.MouseEvent);
|
||||
@@ -663,22 +575,21 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
<div className="flex items-start gap-2">
|
||||
{isDraggable && (
|
||||
<div
|
||||
className="-ml-2 -mt-1 p-2 touch-none"
|
||||
className="-ml-2 -mt-1 p-2 touch-none opacity-40 hover:opacity-70 transition-opacity"
|
||||
data-testid={`drag-handle-${feature.id}`}
|
||||
>
|
||||
<GripVertical className="w-4 h-4 text-muted-foreground" />
|
||||
<GripVertical className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1 min-w-0 overflow-hidden">
|
||||
<CardTitle
|
||||
className={cn(
|
||||
"text-sm leading-tight break-words hyphens-auto overflow-hidden",
|
||||
"text-sm leading-snug break-words hyphens-auto overflow-hidden font-medium text-foreground/90",
|
||||
!isDescriptionExpanded && "line-clamp-3"
|
||||
)}
|
||||
>
|
||||
{feature.description || feature.summary || feature.id}
|
||||
</CardTitle>
|
||||
{/* Show More/Less toggle - only show when description is likely truncated */}
|
||||
{(feature.description || feature.summary || "").length > 100 && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
@@ -686,41 +597,42 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
setIsDescriptionExpanded(!isDescriptionExpanded);
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
className="flex items-center gap-0.5 text-[10px] text-muted-foreground hover:text-foreground mt-1 transition-colors"
|
||||
className="flex items-center gap-0.5 text-[10px] text-muted-foreground/70 hover:text-muted-foreground mt-1.5 transition-colors"
|
||||
data-testid={`toggle-description-${feature.id}`}
|
||||
>
|
||||
{isDescriptionExpanded ? (
|
||||
<>
|
||||
<ChevronUp className="w-3 h-3" />
|
||||
<span>Show Less</span>
|
||||
<span>Less</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ChevronDown className="w-3 h-3" />
|
||||
<span>Show More</span>
|
||||
<span>More</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
<CardDescription className="text-xs mt-1 truncate">
|
||||
<CardDescription className="text-[11px] mt-1.5 truncate text-muted-foreground/70">
|
||||
{feature.category}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="p-3 pt-0">
|
||||
{/* Steps Preview - Show in Standard and Detailed modes */}
|
||||
{/* Steps Preview */}
|
||||
{showSteps && feature.steps && feature.steps.length > 0 && (
|
||||
<div className="mb-3 space-y-1">
|
||||
<div className="mb-3 space-y-1.5">
|
||||
{feature.steps.slice(0, 3).map((step, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-start gap-2 text-xs text-muted-foreground"
|
||||
className="flex items-start gap-2 text-[11px] text-muted-foreground/80"
|
||||
>
|
||||
{feature.status === "verified" ? (
|
||||
<CheckCircle2 className="w-3 h-3 mt-0.5 text-green-500 shrink-0" />
|
||||
<CheckCircle2 className="w-3 h-3 mt-0.5 text-[var(--status-success)] shrink-0" />
|
||||
) : (
|
||||
<Circle className="w-3 h-3 mt-0.5 shrink-0" />
|
||||
<Circle className="w-3 h-3 mt-0.5 shrink-0 text-muted-foreground/50" />
|
||||
)}
|
||||
<span className="break-words hyphens-auto line-clamp-2 leading-relaxed">
|
||||
{step}
|
||||
@@ -728,18 +640,18 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
</div>
|
||||
))}
|
||||
{feature.steps.length > 3 && (
|
||||
<p className="text-xs text-muted-foreground pl-5">
|
||||
+{feature.steps.length - 3} more steps
|
||||
<p className="text-[10px] text-muted-foreground/60 pl-5">
|
||||
+{feature.steps.length - 3} more
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Model/Preset Info for Backlog Cards - Show in Detailed mode */}
|
||||
{/* Model/Preset Info for Backlog Cards */}
|
||||
{showAgentInfo && feature.status === "backlog" && (
|
||||
<div className="mb-3 space-y-2 overflow-hidden">
|
||||
<div className="flex items-center gap-2 text-xs flex-wrap">
|
||||
<div className="flex items-center gap-1 text-cyan-400">
|
||||
<div className="flex items-center gap-2 text-[11px] flex-wrap">
|
||||
<div className="flex items-center gap-1 text-[var(--status-info)]">
|
||||
<Cpu className="w-3 h-3" />
|
||||
<span className="font-medium">
|
||||
{formatModelName(feature.model ?? DEFAULT_MODEL)}
|
||||
@@ -757,13 +669,12 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Agent Info Panel - shows for in_progress, waiting_approval, verified */}
|
||||
{/* Detailed mode: Show all agent info */}
|
||||
{/* Agent Info Panel */}
|
||||
{showAgentInfo && feature.status !== "backlog" && agentInfo && (
|
||||
<div className="mb-3 space-y-2 overflow-hidden">
|
||||
{/* Model & Phase */}
|
||||
<div className="flex items-center gap-2 text-xs flex-wrap">
|
||||
<div className="flex items-center gap-1 text-cyan-400">
|
||||
<div className="flex items-center gap-2 text-[11px] flex-wrap">
|
||||
<div className="flex items-center gap-1 text-[var(--status-info)]">
|
||||
<Cpu className="w-3 h-3" />
|
||||
<span className="font-medium">
|
||||
{formatModelName(feature.model ?? DEFAULT_MODEL)}
|
||||
@@ -772,13 +683,13 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
{agentInfo.currentPhase && (
|
||||
<div
|
||||
className={cn(
|
||||
"px-1.5 py-0.5 rounded text-[10px] font-medium",
|
||||
"px-1.5 py-0.5 rounded-md text-[10px] font-medium",
|
||||
agentInfo.currentPhase === "planning" &&
|
||||
"bg-blue-500/20 text-blue-400",
|
||||
"bg-[var(--status-info-bg)] text-[var(--status-info)]",
|
||||
agentInfo.currentPhase === "action" &&
|
||||
"bg-amber-500/20 text-amber-400",
|
||||
"bg-[var(--status-warning-bg)] text-[var(--status-warning)]",
|
||||
agentInfo.currentPhase === "verification" &&
|
||||
"bg-green-500/20 text-green-400"
|
||||
"bg-[var(--status-success-bg)] text-[var(--status-success)]"
|
||||
)}
|
||||
>
|
||||
{agentInfo.currentPhase}
|
||||
@@ -786,10 +697,10 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Task List Progress (if todos found) */}
|
||||
{/* Task List Progress */}
|
||||
{agentInfo.todos.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-1 text-[10px] text-muted-foreground">
|
||||
<div className="flex items-center gap-1 text-[10px] text-muted-foreground/70">
|
||||
<ListTodo className="w-3 h-3" />
|
||||
<span>
|
||||
{
|
||||
@@ -806,20 +717,21 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
className="flex items-center gap-1.5 text-[10px]"
|
||||
>
|
||||
{todo.status === "completed" ? (
|
||||
<CheckCircle2 className="w-2.5 h-2.5 text-green-500 shrink-0" />
|
||||
<CheckCircle2 className="w-2.5 h-2.5 text-[var(--status-success)] shrink-0" />
|
||||
) : todo.status === "in_progress" ? (
|
||||
<Loader2 className="w-2.5 h-2.5 text-amber-400 animate-spin shrink-0" />
|
||||
<Loader2 className="w-2.5 h-2.5 text-[var(--status-warning)] animate-spin shrink-0" />
|
||||
) : (
|
||||
<Circle className="w-2.5 h-2.5 text-muted-foreground shrink-0" />
|
||||
<Circle className="w-2.5 h-2.5 text-muted-foreground/50 shrink-0" />
|
||||
)}
|
||||
<span
|
||||
className={cn(
|
||||
"break-words hyphens-auto line-clamp-2 leading-relaxed",
|
||||
todo.status === "completed" &&
|
||||
"text-muted-foreground line-through",
|
||||
todo.status === "in_progress" && "text-amber-400",
|
||||
"text-muted-foreground/60 line-through",
|
||||
todo.status === "in_progress" &&
|
||||
"text-[var(--status-warning)]",
|
||||
todo.status === "pending" &&
|
||||
"text-foreground-secondary"
|
||||
"text-muted-foreground/80"
|
||||
)}
|
||||
>
|
||||
{todo.content}
|
||||
@@ -827,7 +739,7 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
</div>
|
||||
))}
|
||||
{agentInfo.todos.length > 3 && (
|
||||
<p className="text-[10px] text-muted-foreground pl-4">
|
||||
<p className="text-[10px] text-muted-foreground/60 pl-4">
|
||||
+{agentInfo.todos.length - 3} more
|
||||
</p>
|
||||
)}
|
||||
@@ -835,16 +747,16 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Summary for waiting_approval and verified - prioritize feature.summary from UpdateFeatureStatus */}
|
||||
{/* Summary for waiting_approval and verified */}
|
||||
{(feature.status === "waiting_approval" ||
|
||||
feature.status === "verified") && (
|
||||
<>
|
||||
{(feature.summary || summary || agentInfo.summary) && (
|
||||
<div className="space-y-1 pt-1 border-t border-border-glass overflow-hidden">
|
||||
<div className="space-y-1.5 pt-2 border-t border-border/30 overflow-hidden">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-1 text-[10px] text-green-400 min-w-0">
|
||||
<div className="flex items-center gap-1 text-[10px] text-[var(--status-success)] min-w-0">
|
||||
<Sparkles className="w-3 h-3 shrink-0" />
|
||||
<span className="truncate">Summary</span>
|
||||
<span className="truncate font-medium">Summary</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
@@ -852,31 +764,30 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
setIsSummaryDialogOpen(true);
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
className="p-0.5 rounded hover:bg-accent transition-colors text-muted-foreground hover:text-foreground shrink-0"
|
||||
className="p-0.5 rounded-md hover:bg-muted/80 transition-colors text-muted-foreground/60 hover:text-muted-foreground shrink-0"
|
||||
title="View full summary"
|
||||
data-testid={`expand-summary-${feature.id}`}
|
||||
>
|
||||
<Expand className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-[10px] text-foreground-secondary line-clamp-3 break-words hyphens-auto leading-relaxed overflow-hidden">
|
||||
<p className="text-[10px] text-muted-foreground/70 line-clamp-3 break-words hyphens-auto leading-relaxed overflow-hidden">
|
||||
{feature.summary || summary || agentInfo.summary}
|
||||
</p>
|
||||
</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-border-glass">
|
||||
<div className="flex items-center gap-2 text-[10px] text-muted-foreground/60 pt-2 border-t border-border/30">
|
||||
<span className="flex items-center gap-1">
|
||||
<Wrench className="w-2.5 h-2.5" />
|
||||
{agentInfo.toolCallCount} tool calls
|
||||
</span>
|
||||
{agentInfo.todos.length > 0 && (
|
||||
<span className="flex items-center gap-1">
|
||||
<CheckCircle2 className="w-2.5 h-2.5 text-green-500" />
|
||||
<CheckCircle2 className="w-2.5 h-2.5 text-[var(--status-success)]" />
|
||||
{
|
||||
agentInfo.todos.filter(
|
||||
(t) => t.status === "completed"
|
||||
@@ -893,14 +804,14 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-2">
|
||||
<div className="flex gap-1.5">
|
||||
{isCurrentAutoTask && (
|
||||
<>
|
||||
{onViewOutput && (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="flex-1 h-7 text-xs bg-action-view hover:bg-action-view-hover"
|
||||
className="flex-1 h-7 text-[11px] bg-[var(--status-info)] hover:bg-[var(--status-info)]/90"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onViewOutput();
|
||||
@@ -912,7 +823,7 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
Logs
|
||||
{shortcutKey && (
|
||||
<span
|
||||
className="ml-2 px-1.5 py-0.5 text-[10px] font-mono rounded bg-primary-foreground/10 border border-primary-foreground/20"
|
||||
className="ml-1.5 px-1 py-0.5 text-[9px] font-mono rounded bg-white/20"
|
||||
data-testid={`shortcut-key-${feature.id}`}
|
||||
>
|
||||
{shortcutKey}
|
||||
@@ -924,7 +835,7 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="h-7 text-xs"
|
||||
className="h-7 text-[11px] px-2"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onForceStop();
|
||||
@@ -932,20 +843,18 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
data-testid={`force-stop-${feature.id}`}
|
||||
>
|
||||
<StopCircle className="w-3 h-3 mr-1" />
|
||||
Stop
|
||||
<StopCircle className="w-3 h-3" />
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!isCurrentAutoTask && feature.status === "in_progress" && (
|
||||
<>
|
||||
{/* skipTests features show manual verify button */}
|
||||
{feature.skipTests && onManualVerify ? (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="flex-1 h-7 text-xs bg-primary hover:bg-primary/90"
|
||||
className="flex-1 h-7 text-[11px]"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onManualVerify();
|
||||
@@ -960,7 +869,7 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="flex-1 h-7 text-xs bg-action-verify hover:bg-action-verify-hover"
|
||||
className="flex-1 h-7 text-[11px] bg-[var(--status-success)] hover:bg-[var(--status-success)]/90"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onResume();
|
||||
@@ -975,7 +884,7 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="flex-1 h-7 text-xs bg-action-verify hover:bg-action-verify-hover"
|
||||
className="flex-1 h-7 text-[11px] bg-[var(--status-success)] hover:bg-[var(--status-success)]/90"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onVerify();
|
||||
@@ -991,7 +900,7 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 text-xs"
|
||||
className="h-7 text-[11px] px-2"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onViewOutput();
|
||||
@@ -999,8 +908,7 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
data-testid={`view-output-inprogress-${feature.id}`}
|
||||
>
|
||||
<FileText className="w-3 h-3 mr-1" />
|
||||
Logs
|
||||
<FileText className="w-3 h-3" />
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
@@ -1045,7 +953,6 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
)}
|
||||
{!isCurrentAutoTask && feature.status === "waiting_approval" && (
|
||||
<>
|
||||
{/* Revert button - only show when worktree exists (icon only to save space) */}
|
||||
{hasWorktree && onRevert && (
|
||||
<TooltipProvider delayDuration={300}>
|
||||
<Tooltip>
|
||||
@@ -1053,7 +960,7 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 w-7 p-0 text-red-400 hover:text-red-300 hover:bg-red-500/20 shrink-0"
|
||||
className="h-7 w-7 p-0 text-[var(--status-error)] hover:text-[var(--status-error)] hover:bg-[var(--status-error-bg)] shrink-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsRevertDialogOpen(true);
|
||||
@@ -1064,7 +971,7 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
<Undo2 className="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
<TooltipContent side="top" className="text-xs">
|
||||
<p>Revert changes</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
@@ -1075,7 +982,7 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="flex-1 h-7 text-xs min-w-0"
|
||||
className="flex-1 h-7 text-[11px] min-w-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onFollowUp();
|
||||
@@ -1087,12 +994,11 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
<span className="truncate">Refine</span>
|
||||
</Button>
|
||||
)}
|
||||
{/* Merge button - only show when worktree exists */}
|
||||
{hasWorktree && onMerge && (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="flex-1 h-7 text-xs bg-purple-600 hover:bg-purple-700 min-w-0"
|
||||
className="flex-1 h-7 text-[11px] bg-[var(--status-info)] hover:bg-[var(--status-info)]/90 min-w-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onMerge();
|
||||
@@ -1105,12 +1011,11 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
<span className="truncate">Merge</span>
|
||||
</Button>
|
||||
)}
|
||||
{/* Commit and verify button - show when no worktree */}
|
||||
{!hasWorktree && onCommit && (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="flex-1 h-7 text-xs"
|
||||
className="flex-1 h-7 text-[11px]"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onCommit();
|
||||
@@ -1180,7 +1085,7 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Sparkles className="w-5 h-5 text-green-400" />
|
||||
<Sparkles className="w-5 h-5 text-[var(--status-success)]" />
|
||||
Implementation Summary
|
||||
</DialogTitle>
|
||||
<DialogDescription
|
||||
@@ -1196,7 +1101,7 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
})()}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex-1 overflow-y-auto p-4 bg-card rounded-lg border border-border">
|
||||
<div className="flex-1 overflow-y-auto p-4 bg-card rounded-lg border border-border/50">
|
||||
<Markdown>
|
||||
{feature.summary ||
|
||||
summary ||
|
||||
@@ -1220,7 +1125,7 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
<Dialog open={isRevertDialogOpen} onOpenChange={setIsRevertDialogOpen}>
|
||||
<DialogContent data-testid="revert-confirmation-dialog">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2 text-red-400">
|
||||
<DialogTitle className="flex items-center gap-2 text-[var(--status-error)]">
|
||||
<Undo2 className="w-5 h-5" />
|
||||
Revert Changes
|
||||
</DialogTitle>
|
||||
@@ -1230,13 +1135,13 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
{feature.branchName && (
|
||||
<span className="block mt-2 font-medium">
|
||||
Branch{" "}
|
||||
<code className="bg-muted px-1 py-0.5 rounded">
|
||||
<code className="bg-muted px-1.5 py-0.5 rounded text-[11px]">
|
||||
{feature.branchName}
|
||||
</code>{" "}
|
||||
will be deleted.
|
||||
</span>
|
||||
)}
|
||||
<span className="block mt-2 text-red-400 font-medium">
|
||||
<span className="block mt-2 text-[var(--status-error)] font-medium">
|
||||
This action cannot be undone.
|
||||
</span>
|
||||
</DialogDescription>
|
||||
|
||||
@@ -8,19 +8,19 @@ import type { ReactNode } from "react";
|
||||
interface KanbanColumnProps {
|
||||
id: string;
|
||||
title: string;
|
||||
color: string;
|
||||
colorClass: string;
|
||||
count: number;
|
||||
children: ReactNode;
|
||||
headerAction?: ReactNode;
|
||||
opacity?: number; // Opacity percentage (0-100) - only affects background
|
||||
showBorder?: boolean; // Whether to show column border
|
||||
hideScrollbar?: boolean; // Whether to hide the column scrollbar
|
||||
opacity?: number;
|
||||
showBorder?: boolean;
|
||||
hideScrollbar?: boolean;
|
||||
}
|
||||
|
||||
export const KanbanColumn = memo(function KanbanColumn({
|
||||
id,
|
||||
title,
|
||||
color,
|
||||
colorClass,
|
||||
count,
|
||||
children,
|
||||
headerAction,
|
||||
@@ -34,45 +34,53 @@ export const KanbanColumn = memo(function KanbanColumn({
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
className={cn(
|
||||
"relative flex flex-col h-full rounded-lg transition-colors w-72",
|
||||
showBorder && "border border-border"
|
||||
"relative flex flex-col h-full rounded-xl transition-all duration-200 w-72",
|
||||
showBorder && "border border-border/60",
|
||||
isOver && "ring-2 ring-primary/30 ring-offset-1 ring-offset-background"
|
||||
)}
|
||||
data-testid={`kanban-column-${id}`}
|
||||
>
|
||||
{/* Background layer with opacity - only this layer is affected by opacity */}
|
||||
{/* Background layer with opacity */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-0 rounded-lg backdrop-blur-sm transition-colors",
|
||||
isOver ? "bg-accent" : "bg-card"
|
||||
"absolute inset-0 rounded-xl backdrop-blur-sm transition-colors duration-200",
|
||||
isOver ? "bg-accent/80" : "bg-card/80"
|
||||
)}
|
||||
style={{ opacity: opacity / 100 }}
|
||||
/>
|
||||
|
||||
{/* Column Header - positioned above the background */}
|
||||
{/* Column Header */}
|
||||
<div
|
||||
className={cn(
|
||||
"relative z-10 flex items-center gap-2 p-3",
|
||||
showBorder && "border-b border-border"
|
||||
"relative z-10 flex items-center gap-3 px-3 py-2.5",
|
||||
showBorder && "border-b border-border/40"
|
||||
)}
|
||||
>
|
||||
<div className={cn("w-3 h-3 rounded-full", color)} />
|
||||
<h3 className="font-medium text-sm flex-1">{title}</h3>
|
||||
<div className={cn("w-2.5 h-2.5 rounded-full shrink-0", colorClass)} />
|
||||
<h3 className="font-semibold text-sm text-foreground/90 flex-1 tracking-tight">{title}</h3>
|
||||
{headerAction}
|
||||
<span className="text-xs text-muted-foreground bg-background px-2 py-0.5 rounded-full">
|
||||
<span className="text-xs font-medium text-muted-foreground/80 bg-muted/50 px-2 py-0.5 rounded-md tabular-nums">
|
||||
{count}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Column Content - positioned above the background */}
|
||||
{/* Column Content */}
|
||||
<div
|
||||
className={cn(
|
||||
"relative z-10 flex-1 overflow-y-auto p-2 space-y-2",
|
||||
"relative z-10 flex-1 overflow-y-auto p-2 space-y-2.5",
|
||||
hideScrollbar &&
|
||||
"[&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]"
|
||||
"[&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]",
|
||||
// Smooth scrolling
|
||||
"scroll-smooth"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{/* Drop zone indicator when dragging over */}
|
||||
{isOver && (
|
||||
<div className="absolute inset-0 rounded-xl bg-primary/5 pointer-events-none z-5 border-2 border-dashed border-primary/20" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -157,47 +157,47 @@ export function SettingsView() {
|
||||
{/* Audio Section */}
|
||||
<div
|
||||
id="audio"
|
||||
className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden scroll-mt-6"
|
||||
className="rounded-2xl border border-border/50 bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl shadow-sm shadow-black/5 overflow-hidden scroll-mt-6"
|
||||
>
|
||||
<div className="p-6 border-b border-border">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Volume2 className="w-5 h-5 text-brand-500" />
|
||||
<h2 className="text-lg font-semibold text-foreground">
|
||||
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 flex items-center justify-center border border-brand-500/20">
|
||||
<Volume2 className="w-5 h-5 text-brand-500" />
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold text-foreground tracking-tight">
|
||||
Audio
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-sm text-muted-foreground/80 ml-12">
|
||||
Configure audio and notification settings.
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 space-y-4">
|
||||
{/* Mute Done Sound Setting */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-start space-x-3">
|
||||
<Checkbox
|
||||
id="mute-done-sound"
|
||||
checked={muteDoneSound}
|
||||
onCheckedChange={(checked) =>
|
||||
setMuteDoneSound(checked === true)
|
||||
}
|
||||
className="mt-0.5"
|
||||
data-testid="mute-done-sound-checkbox"
|
||||
/>
|
||||
<div className="space-y-1">
|
||||
<Label
|
||||
htmlFor="mute-done-sound"
|
||||
className="text-foreground cursor-pointer font-medium flex items-center gap-2"
|
||||
>
|
||||
<VolumeX className="w-4 h-4 text-brand-500" />
|
||||
Mute notification sound when agents complete
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
When enabled, disables the "ding" sound that
|
||||
plays when an agent completes a feature. The feature
|
||||
will still move to the completed column, but without
|
||||
audio notification.
|
||||
</p>
|
||||
</div>
|
||||
<div className="group flex items-start space-x-3 p-3 rounded-xl hover:bg-accent/30 transition-colors duration-200 -mx-3">
|
||||
<Checkbox
|
||||
id="mute-done-sound"
|
||||
checked={muteDoneSound}
|
||||
onCheckedChange={(checked) =>
|
||||
setMuteDoneSound(checked === true)
|
||||
}
|
||||
className="mt-1"
|
||||
data-testid="mute-done-sound-checkbox"
|
||||
/>
|
||||
<div className="space-y-1.5">
|
||||
<Label
|
||||
htmlFor="mute-done-sound"
|
||||
className="text-foreground cursor-pointer font-medium flex items-center gap-2"
|
||||
>
|
||||
<VolumeX className="w-4 h-4 text-brand-500" />
|
||||
Mute notification sound when agents complete
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground/80 leading-relaxed">
|
||||
When enabled, disables the "ding" sound that
|
||||
plays when an agent completes a feature. The feature
|
||||
will still move to the completed column, but without
|
||||
audio notification.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { buildProviderConfigs } from "@/config/api-providers";
|
||||
import { AuthenticationStatusDisplay } from "./authentication-status-display";
|
||||
import { SecurityNotice } from "./security-notice";
|
||||
import { useApiKeyManagement } from "./hooks/use-api-key-management";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function ApiKeysSection() {
|
||||
const { apiKeys } = useAppStore();
|
||||
@@ -20,16 +21,22 @@ export function ApiKeysSection() {
|
||||
return (
|
||||
<div
|
||||
id="api-keys"
|
||||
className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden scroll-mt-6"
|
||||
className={cn(
|
||||
"rounded-2xl overflow-hidden scroll-mt-6",
|
||||
"border border-border/50",
|
||||
"bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl",
|
||||
"shadow-sm shadow-black/5"
|
||||
)}
|
||||
>
|
||||
<div className="p-6 border-b border-border">
|
||||
<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-foreground">API Keys</h2>
|
||||
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 flex items-center justify-center border border-brand-500/20">
|
||||
<Key className="w-5 h-5 text-brand-500" />
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold text-foreground tracking-tight">API Keys</h2>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Configure your AI provider API keys. Keys are stored locally in your
|
||||
browser.
|
||||
<p className="text-sm text-muted-foreground/80 ml-12">
|
||||
Configure your AI provider API keys. Keys are stored locally in your browser.
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 space-y-6">
|
||||
@@ -53,7 +60,15 @@ export function ApiKeysSection() {
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
data-testid="save-settings"
|
||||
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"
|
||||
className={cn(
|
||||
"min-w-[140px] h-10",
|
||||
"bg-gradient-to-r from-brand-500 to-brand-600",
|
||||
"hover:from-brand-600 hover:to-brand-600",
|
||||
"text-white font-medium border-0",
|
||||
"shadow-md shadow-brand-500/20 hover:shadow-lg hover:shadow-brand-500/25",
|
||||
"transition-all duration-200 ease-out",
|
||||
"hover:scale-[1.02] active:scale-[0.98]"
|
||||
)}
|
||||
>
|
||||
{saved ? (
|
||||
<>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Palette } from "lucide-react";
|
||||
import { themeOptions } from "@/config/theme-options";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { Theme, Project } from "../shared/types";
|
||||
|
||||
interface AppearanceSectionProps {
|
||||
@@ -18,39 +19,65 @@ export function AppearanceSection({
|
||||
return (
|
||||
<div
|
||||
id="appearance"
|
||||
className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden scroll-mt-6"
|
||||
className={cn(
|
||||
"rounded-2xl overflow-hidden scroll-mt-6",
|
||||
"border border-border/50",
|
||||
"bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl",
|
||||
"shadow-sm shadow-black/5"
|
||||
)}
|
||||
>
|
||||
<div className="p-6 border-b border-border">
|
||||
<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-foreground">Appearance</h2>
|
||||
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 flex items-center justify-center border border-brand-500/20">
|
||||
<Palette className="w-5 h-5 text-brand-500" />
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold text-foreground tracking-tight">Appearance</h2>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-sm text-muted-foreground/80 ml-12">
|
||||
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-foreground">
|
||||
<div className="space-y-4">
|
||||
<Label className="text-foreground font-medium">
|
||||
Theme{" "}
|
||||
{currentProject ? `(for ${currentProject.name})` : "(Global)"}
|
||||
<span className="text-muted-foreground font-normal">
|
||||
{currentProject ? `(for ${currentProject.name})` : "(Global)"}
|
||||
</span>
|
||||
</Label>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
{themeOptions.map(({ value, label, Icon, testId }) => {
|
||||
const isActive = effectiveTheme === value;
|
||||
return (
|
||||
<Button
|
||||
<button
|
||||
key={value}
|
||||
variant={isActive ? "secondary" : "outline"}
|
||||
onClick={() => onThemeChange(value)}
|
||||
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
|
||||
isActive ? "border-brand-500 ring-1 ring-brand-500/50" : ""
|
||||
}`}
|
||||
className={cn(
|
||||
"group flex items-center justify-center gap-2.5 px-4 py-3.5 rounded-xl",
|
||||
"text-sm font-medium transition-all duration-200 ease-out",
|
||||
isActive
|
||||
? [
|
||||
"bg-gradient-to-br from-brand-500/15 to-brand-600/10",
|
||||
"border-2 border-brand-500/40",
|
||||
"text-foreground",
|
||||
"shadow-md shadow-brand-500/10",
|
||||
]
|
||||
: [
|
||||
"bg-accent/30 hover:bg-accent/50",
|
||||
"border border-border/50 hover:border-border",
|
||||
"text-muted-foreground hover:text-foreground",
|
||||
"hover:shadow-sm",
|
||||
],
|
||||
"hover:scale-[1.02] active:scale-[0.98]"
|
||||
)}
|
||||
data-testid={testId}
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
<span className="font-medium text-sm">{label}</span>
|
||||
</Button>
|
||||
<Icon className={cn(
|
||||
"w-4 h-4 transition-all duration-200",
|
||||
isActive ? "text-brand-500" : "group-hover:text-brand-400"
|
||||
)} />
|
||||
<span>{label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
AlertCircle,
|
||||
RefreshCw,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { CliStatus } from "../shared/types";
|
||||
|
||||
interface CliStatusProps {
|
||||
@@ -23,13 +24,20 @@ export function ClaudeCliStatus({
|
||||
return (
|
||||
<div
|
||||
id="claude"
|
||||
className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden scroll-mt-6"
|
||||
className={cn(
|
||||
"rounded-2xl overflow-hidden scroll-mt-6",
|
||||
"border border-border/50",
|
||||
"bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl",
|
||||
"shadow-sm shadow-black/5"
|
||||
)}
|
||||
>
|
||||
<div className="p-6 border-b border-border">
|
||||
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Terminal className="w-5 h-5 text-brand-500" />
|
||||
<h2 className="text-lg font-semibold text-foreground">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 flex items-center justify-center border border-brand-500/20">
|
||||
<Terminal className="w-5 h-5 text-brand-500" />
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold text-foreground tracking-tight">
|
||||
Claude Code CLI
|
||||
</h2>
|
||||
</div>
|
||||
@@ -40,13 +48,18 @@ export function ClaudeCliStatus({
|
||||
disabled={isChecking}
|
||||
data-testid="refresh-claude-cli"
|
||||
title="Refresh Claude CLI detection"
|
||||
className={cn(
|
||||
"h-9 w-9 rounded-lg",
|
||||
"hover:bg-accent/50 hover:scale-105",
|
||||
"transition-all duration-200"
|
||||
)}
|
||||
>
|
||||
<RefreshCw
|
||||
className={`w-4 h-4 ${isChecking ? "animate-spin" : ""}`}
|
||||
className={cn("w-4 h-4", isChecking && "animate-spin")}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-sm text-muted-foreground/80 ml-12">
|
||||
Claude Code CLI provides better performance for long-running tasks,
|
||||
especially with ultrathink.
|
||||
</p>
|
||||
@@ -54,13 +67,15 @@ export function ClaudeCliStatus({
|
||||
<div className="p-6 space-y-4">
|
||||
{status.success && status.status === "installed" ? (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2 p-3 rounded-lg bg-green-500/10 border border-green-500/20">
|
||||
<CheckCircle2 className="w-5 h-5 text-green-500 shrink-0" />
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-green-400">
|
||||
<div className="flex items-center gap-3 p-4 rounded-xl bg-emerald-500/10 border border-emerald-500/20">
|
||||
<div className="w-10 h-10 rounded-xl bg-emerald-500/15 flex items-center justify-center border border-emerald-500/20 shrink-0">
|
||||
<CheckCircle2 className="w-5 h-5 text-emerald-500" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-emerald-400">
|
||||
Claude Code CLI Installed
|
||||
</p>
|
||||
<div className="text-xs text-green-400/80 mt-1 space-y-1">
|
||||
<div className="text-xs text-emerald-400/70 mt-1.5 space-y-0.5">
|
||||
{status.method && (
|
||||
<p>
|
||||
Method: <span className="font-mono">{status.method}</span>
|
||||
@@ -68,71 +83,65 @@ export function ClaudeCliStatus({
|
||||
)}
|
||||
{status.version && (
|
||||
<p>
|
||||
Version:{" "}
|
||||
<span className="font-mono">{status.version}</span>
|
||||
Version: <span className="font-mono">{status.version}</span>
|
||||
</p>
|
||||
)}
|
||||
{status.path && (
|
||||
<p className="truncate" title={status.path}>
|
||||
Path:{" "}
|
||||
<span className="font-mono text-[10px]">
|
||||
{status.path}
|
||||
</span>
|
||||
Path: <span className="font-mono text-[10px]">{status.path}</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{status.recommendation && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<p className="text-xs text-muted-foreground/70 ml-1">
|
||||
{status.recommendation}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-start gap-3 p-3 rounded-lg bg-yellow-500/10 border border-yellow-500/20">
|
||||
<AlertCircle className="w-5 h-5 text-yellow-500 mt-0.5 shrink-0" />
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start gap-3 p-4 rounded-xl bg-amber-500/10 border border-amber-500/20">
|
||||
<div className="w-10 h-10 rounded-xl bg-amber-500/15 flex items-center justify-center border border-amber-500/20 shrink-0 mt-0.5">
|
||||
<AlertCircle className="w-5 h-5 text-amber-500" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-yellow-400">
|
||||
<p className="text-sm font-medium text-amber-400">
|
||||
Claude Code CLI Not Detected
|
||||
</p>
|
||||
<p className="text-xs text-yellow-400/80 mt-1">
|
||||
<p className="text-xs text-amber-400/70 mt-1">
|
||||
{status.recommendation ||
|
||||
"Consider installing Claude Code CLI for optimal performance with ultrathink."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{status.installCommands && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs font-medium text-foreground-secondary">
|
||||
<div className="space-y-3">
|
||||
<p className="text-xs font-medium text-foreground/80">
|
||||
Installation Commands:
|
||||
</p>
|
||||
<div className="space-y-1">
|
||||
<div className="space-y-2">
|
||||
{status.installCommands.npm && (
|
||||
<div className="p-2 rounded bg-background border border-border-glass">
|
||||
<p className="text-xs text-muted-foreground mb-1">npm:</p>
|
||||
<code className="text-xs text-foreground-secondary font-mono break-all">
|
||||
<div className="p-3 rounded-xl bg-accent/30 border border-border/50">
|
||||
<p className="text-[10px] text-muted-foreground mb-1.5 font-medium uppercase tracking-wider">npm</p>
|
||||
<code className="text-xs text-foreground/80 font-mono break-all">
|
||||
{status.installCommands.npm}
|
||||
</code>
|
||||
</div>
|
||||
)}
|
||||
{status.installCommands.macos && (
|
||||
<div className="p-2 rounded bg-background border border-border-glass">
|
||||
<p className="text-xs text-muted-foreground mb-1">
|
||||
macOS/Linux:
|
||||
</p>
|
||||
<code className="text-xs text-foreground-secondary font-mono break-all">
|
||||
<div className="p-3 rounded-xl bg-accent/30 border border-border/50">
|
||||
<p className="text-[10px] text-muted-foreground mb-1.5 font-medium uppercase tracking-wider">macOS/Linux</p>
|
||||
<code className="text-xs text-foreground/80 font-mono break-all">
|
||||
{status.installCommands.macos}
|
||||
</code>
|
||||
</div>
|
||||
)}
|
||||
{status.installCommands.windows && (
|
||||
<div className="p-2 rounded bg-background border border-border-glass">
|
||||
<p className="text-xs text-muted-foreground mb-1">
|
||||
Windows (PowerShell):
|
||||
</p>
|
||||
<code className="text-xs text-foreground-secondary font-mono break-all">
|
||||
<div className="p-3 rounded-xl bg-accent/30 border border-border/50">
|
||||
<p className="text-[10px] text-muted-foreground mb-1.5 font-medium uppercase tracking-wider">Windows (PowerShell)</p>
|
||||
<code className="text-xs text-foreground/80 font-mono break-all">
|
||||
{status.installCommands.windows}
|
||||
</code>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Settings } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface SettingsHeaderProps {
|
||||
title?: string;
|
||||
@@ -10,15 +11,24 @@ export function SettingsHeader({
|
||||
description = "Configure your API keys and preferences",
|
||||
}: SettingsHeaderProps) {
|
||||
return (
|
||||
<div className="shrink-0 border-b border-border bg-glass backdrop-blur-md">
|
||||
<div className={cn(
|
||||
"shrink-0",
|
||||
"border-b border-border/50",
|
||||
"bg-gradient-to-r from-card/90 via-card/70 to-card/80 backdrop-blur-xl"
|
||||
)}>
|
||||
<div className="px-8 py-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<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 className="flex items-center gap-4">
|
||||
<div className={cn(
|
||||
"w-12 h-12 rounded-2xl flex items-center justify-center",
|
||||
"bg-gradient-to-br from-brand-500 to-brand-600",
|
||||
"shadow-lg shadow-brand-500/25",
|
||||
"ring-1 ring-white/10"
|
||||
)}>
|
||||
<Settings className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-foreground">{title}</h1>
|
||||
<p className="text-sm text-muted-foreground">{description}</p>
|
||||
<h1 className="text-2xl font-bold text-foreground tracking-tight">{title}</h1>
|
||||
<p className="text-sm text-muted-foreground/80 mt-0.5">{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,8 +16,12 @@ export function SettingsNavigation({
|
||||
onNavigate,
|
||||
}: SettingsNavigationProps) {
|
||||
return (
|
||||
<nav className="hidden lg:block w-48 shrink-0 border-r border-border bg-card/50 backdrop-blur-sm">
|
||||
<div className="sticky top-0 p-4 space-y-1">
|
||||
<nav className={cn(
|
||||
"hidden lg:block w-52 shrink-0",
|
||||
"border-r border-border/50",
|
||||
"bg-gradient-to-b from-card/80 via-card/60 to-card/40 backdrop-blur-xl"
|
||||
)}>
|
||||
<div className="sticky top-0 p-4 space-y-1.5">
|
||||
{navItems
|
||||
.filter((item) => item.id !== "danger" || currentProject)
|
||||
.map((item) => {
|
||||
@@ -28,16 +32,32 @@ export function SettingsNavigation({
|
||||
key={item.id}
|
||||
onClick={() => onNavigate(item.id)}
|
||||
className={cn(
|
||||
"w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-all text-left",
|
||||
"group w-full flex items-center gap-2.5 px-3 py-2.5 rounded-xl text-sm font-medium transition-all duration-200 ease-out text-left relative overflow-hidden",
|
||||
isActive
|
||||
? "bg-brand-500/10 text-brand-500 border border-brand-500/20"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-accent"
|
||||
? [
|
||||
"bg-gradient-to-r from-brand-500/15 via-brand-500/10 to-brand-600/5",
|
||||
"text-foreground",
|
||||
"border border-brand-500/25",
|
||||
"shadow-sm shadow-brand-500/5",
|
||||
]
|
||||
: [
|
||||
"text-muted-foreground hover:text-foreground",
|
||||
"hover:bg-accent/50",
|
||||
"border border-transparent hover:border-border/40",
|
||||
],
|
||||
"hover:scale-[1.01] active:scale-[0.98]"
|
||||
)}
|
||||
>
|
||||
{/* Active indicator bar */}
|
||||
{isActive && (
|
||||
<div className="absolute inset-y-0 left-0 w-0.5 bg-gradient-to-b from-brand-400 via-brand-500 to-brand-600 rounded-r-full" />
|
||||
)}
|
||||
<Icon
|
||||
className={cn(
|
||||
"w-4 h-4 shrink-0",
|
||||
isActive ? "text-brand-500" : ""
|
||||
"w-4 h-4 shrink-0 transition-all duration-200",
|
||||
isActive
|
||||
? "text-brand-500"
|
||||
: "group-hover:text-brand-400 group-hover:scale-110"
|
||||
)}
|
||||
/>
|
||||
<span className="truncate">{item.label}</span>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Trash2, Folder } from "lucide-react";
|
||||
import { Trash2, Folder, AlertTriangle } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { Project } from "../shared/types";
|
||||
|
||||
interface DangerZoneSectionProps {
|
||||
@@ -16,28 +17,35 @@ export function DangerZoneSection({
|
||||
return (
|
||||
<div
|
||||
id="danger"
|
||||
className="rounded-xl border border-destructive/30 bg-card backdrop-blur-md overflow-hidden scroll-mt-6"
|
||||
className={cn(
|
||||
"rounded-2xl overflow-hidden scroll-mt-6",
|
||||
"border border-destructive/30",
|
||||
"bg-gradient-to-br from-destructive/5 via-card/70 to-card/80 backdrop-blur-xl",
|
||||
"shadow-sm shadow-destructive/5"
|
||||
)}
|
||||
>
|
||||
<div className="p-6 border-b border-destructive/30">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Trash2 className="w-5 h-5 text-destructive" />
|
||||
<h2 className="text-lg font-semibold text-foreground">Danger Zone</h2>
|
||||
<div className="p-6 border-b border-destructive/20 bg-gradient-to-r from-destructive/5 via-transparent to-transparent">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-destructive/20 to-destructive/10 flex items-center justify-center border border-destructive/20">
|
||||
<AlertTriangle className="w-5 h-5 text-destructive" />
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold text-foreground tracking-tight">Danger Zone</h2>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-sm text-muted-foreground/80 ml-12">
|
||||
Permanently remove this project from Automaker.
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<div className="w-10 h-10 rounded-lg bg-sidebar-accent/20 border border-sidebar-border flex items-center justify-center shrink-0">
|
||||
<div className="flex items-center justify-between gap-4 p-4 rounded-xl bg-destructive/5 border border-destructive/10">
|
||||
<div className="flex items-center gap-3.5 min-w-0">
|
||||
<div className="w-11 h-11 rounded-xl bg-gradient-to-br from-brand-500/15 to-brand-600/10 border border-brand-500/20 flex items-center justify-center shrink-0">
|
||||
<Folder className="w-5 h-5 text-brand-500" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="font-medium text-foreground truncate">
|
||||
{project.name}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground truncate">
|
||||
<p className="text-xs text-muted-foreground/70 truncate mt-0.5">
|
||||
{project.path}
|
||||
</p>
|
||||
</div>
|
||||
@@ -46,6 +54,12 @@ export function DangerZoneSection({
|
||||
variant="destructive"
|
||||
onClick={onDeleteClick}
|
||||
data-testid="delete-project-button"
|
||||
className={cn(
|
||||
"shrink-0",
|
||||
"shadow-md shadow-destructive/20 hover:shadow-lg hover:shadow-destructive/25",
|
||||
"transition-all duration-200 ease-out",
|
||||
"hover:scale-[1.02] active:scale-[0.98]"
|
||||
)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
Delete Project
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { FlaskConical, Settings2, TestTube, GitBranch } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface FeatureDefaultsSectionProps {
|
||||
showProfilesOnly: boolean;
|
||||
@@ -22,111 +23,112 @@ export function FeatureDefaultsSection({
|
||||
return (
|
||||
<div
|
||||
id="defaults"
|
||||
className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden scroll-mt-6"
|
||||
className={cn(
|
||||
"rounded-2xl overflow-hidden scroll-mt-6",
|
||||
"border border-border/50",
|
||||
"bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl",
|
||||
"shadow-sm shadow-black/5"
|
||||
)}
|
||||
>
|
||||
<div className="p-6 border-b border-border">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<FlaskConical className="w-5 h-5 text-brand-500" />
|
||||
<h2 className="text-lg font-semibold text-foreground">
|
||||
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 flex items-center justify-center border border-brand-500/20">
|
||||
<FlaskConical className="w-5 h-5 text-brand-500" />
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold text-foreground tracking-tight">
|
||||
Feature Defaults
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-sm text-muted-foreground/80 ml-12">
|
||||
Configure default settings for new features.
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="p-6 space-y-5">
|
||||
{/* Profiles Only Setting */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-start space-x-3">
|
||||
<Checkbox
|
||||
id="show-profiles-only"
|
||||
checked={showProfilesOnly}
|
||||
onCheckedChange={(checked) =>
|
||||
onShowProfilesOnlyChange(checked === true)
|
||||
}
|
||||
className="mt-0.5"
|
||||
data-testid="show-profiles-only-checkbox"
|
||||
/>
|
||||
<div className="space-y-1">
|
||||
<Label
|
||||
htmlFor="show-profiles-only"
|
||||
className="text-foreground cursor-pointer font-medium flex items-center gap-2"
|
||||
>
|
||||
<Settings2 className="w-4 h-4 text-brand-500" />
|
||||
Show profiles only by default
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
When enabled, the Add Feature dialog will show only AI profiles
|
||||
and hide advanced model tweaking options (Claude SDK, thinking
|
||||
levels). This creates a cleaner, less
|
||||
overwhelming UI. You can always disable this to access advanced
|
||||
settings.
|
||||
</p>
|
||||
</div>
|
||||
<div className="group flex items-start space-x-3 p-3 rounded-xl hover:bg-accent/30 transition-colors duration-200 -mx-3">
|
||||
<Checkbox
|
||||
id="show-profiles-only"
|
||||
checked={showProfilesOnly}
|
||||
onCheckedChange={(checked) =>
|
||||
onShowProfilesOnlyChange(checked === true)
|
||||
}
|
||||
className="mt-1"
|
||||
data-testid="show-profiles-only-checkbox"
|
||||
/>
|
||||
<div className="space-y-1.5">
|
||||
<Label
|
||||
htmlFor="show-profiles-only"
|
||||
className="text-foreground cursor-pointer font-medium flex items-center gap-2"
|
||||
>
|
||||
<Settings2 className="w-4 h-4 text-brand-500" />
|
||||
Show profiles only by default
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground/80 leading-relaxed">
|
||||
When enabled, the Add Feature dialog will show only AI profiles
|
||||
and hide advanced model tweaking options. This creates a cleaner, less
|
||||
overwhelming UI.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Separator */}
|
||||
<div className="border-t border-border" />
|
||||
<div className="border-t border-border/30" />
|
||||
|
||||
{/* Automated Testing Setting */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-start space-x-3">
|
||||
<Checkbox
|
||||
id="default-skip-tests"
|
||||
checked={!defaultSkipTests}
|
||||
onCheckedChange={(checked) =>
|
||||
onDefaultSkipTestsChange(checked !== true)
|
||||
}
|
||||
className="mt-0.5"
|
||||
data-testid="default-skip-tests-checkbox"
|
||||
/>
|
||||
<div className="space-y-1">
|
||||
<Label
|
||||
htmlFor="default-skip-tests"
|
||||
className="text-foreground cursor-pointer font-medium flex items-center gap-2"
|
||||
>
|
||||
<TestTube className="w-4 h-4 text-brand-500" />
|
||||
Enable automated testing by default
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
When enabled, new features will use TDD (test-driven
|
||||
development) with automated tests. When disabled, features will
|
||||
require manual verification. You can still override this for
|
||||
individual features.
|
||||
</p>
|
||||
</div>
|
||||
<div className="group flex items-start space-x-3 p-3 rounded-xl hover:bg-accent/30 transition-colors duration-200 -mx-3">
|
||||
<Checkbox
|
||||
id="default-skip-tests"
|
||||
checked={!defaultSkipTests}
|
||||
onCheckedChange={(checked) =>
|
||||
onDefaultSkipTestsChange(checked !== true)
|
||||
}
|
||||
className="mt-1"
|
||||
data-testid="default-skip-tests-checkbox"
|
||||
/>
|
||||
<div className="space-y-1.5">
|
||||
<Label
|
||||
htmlFor="default-skip-tests"
|
||||
className="text-foreground cursor-pointer font-medium flex items-center gap-2"
|
||||
>
|
||||
<TestTube className="w-4 h-4 text-brand-500" />
|
||||
Enable automated testing by default
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground/80 leading-relaxed">
|
||||
When enabled, new features will use TDD with automated tests. When disabled, features will
|
||||
require manual verification.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Separator */}
|
||||
<div className="border-t border-border/30" />
|
||||
|
||||
{/* Worktree Isolation Setting */}
|
||||
<div className="space-y-3 pt-2 border-t border-border">
|
||||
<div className="flex items-start space-x-3">
|
||||
<Checkbox
|
||||
id="use-worktrees"
|
||||
checked={useWorktrees}
|
||||
onCheckedChange={(checked) =>
|
||||
onUseWorktreesChange(checked === true)
|
||||
}
|
||||
className="mt-0.5"
|
||||
data-testid="use-worktrees-checkbox"
|
||||
/>
|
||||
<div className="space-y-1">
|
||||
<Label
|
||||
htmlFor="use-worktrees"
|
||||
className="text-foreground cursor-pointer font-medium flex items-center gap-2"
|
||||
>
|
||||
<GitBranch className="w-4 h-4 text-brand-500" />
|
||||
Enable Git Worktree Isolation (experimental)
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Creates isolated git branches for each feature. When disabled,
|
||||
agents work directly in the main project directory. This feature
|
||||
is experimental and may require additional setup like branch
|
||||
selection and merge configuration.
|
||||
</p>
|
||||
</div>
|
||||
<div className="group flex items-start space-x-3 p-3 rounded-xl hover:bg-accent/30 transition-colors duration-200 -mx-3">
|
||||
<Checkbox
|
||||
id="use-worktrees"
|
||||
checked={useWorktrees}
|
||||
onCheckedChange={(checked) =>
|
||||
onUseWorktreesChange(checked === true)
|
||||
}
|
||||
className="mt-1"
|
||||
data-testid="use-worktrees-checkbox"
|
||||
/>
|
||||
<div className="space-y-1.5">
|
||||
<Label
|
||||
htmlFor="use-worktrees"
|
||||
className="text-foreground cursor-pointer font-medium flex items-center gap-2"
|
||||
>
|
||||
<GitBranch className="w-4 h-4 text-brand-500" />
|
||||
Enable Git Worktree Isolation
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded-md bg-amber-500/15 text-amber-500 border border-amber-500/20 font-medium">
|
||||
experimental
|
||||
</span>
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground/80 leading-relaxed">
|
||||
Creates isolated git branches for each feature. When disabled,
|
||||
agents work directly in the main project directory.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Settings2, Keyboard } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface KeyboardShortcutsSectionProps {
|
||||
onOpenKeyboardMap: () => void;
|
||||
@@ -11,43 +12,58 @@ export function KeyboardShortcutsSection({
|
||||
return (
|
||||
<div
|
||||
id="keyboard"
|
||||
className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden scroll-mt-6"
|
||||
className={cn(
|
||||
"rounded-2xl overflow-hidden scroll-mt-6",
|
||||
"border border-border/50",
|
||||
"bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl",
|
||||
"shadow-sm shadow-black/5"
|
||||
)}
|
||||
>
|
||||
<div className="p-6 border-b border-border">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Settings2 className="w-5 h-5 text-brand-500" />
|
||||
<h2 className="text-lg font-semibold text-foreground">
|
||||
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 flex items-center justify-center border border-brand-500/20">
|
||||
<Settings2 className="w-5 h-5 text-brand-500" />
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold text-foreground tracking-tight">
|
||||
Keyboard Shortcuts
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-sm text-muted-foreground/80 ml-12">
|
||||
Customize keyboard shortcuts for navigation and actions using the
|
||||
visual keyboard map.
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
{/* Centered message directing to keyboard map */}
|
||||
<div className="flex flex-col items-center justify-center py-16 text-center space-y-4">
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center space-y-5">
|
||||
<div className="relative">
|
||||
<Keyboard className="w-16 h-16 text-brand-500/30" />
|
||||
<div className="absolute inset-0 bg-brand-500/10 blur-xl rounded-full" />
|
||||
<div className="w-20 h-20 rounded-2xl bg-gradient-to-br from-brand-500/10 to-brand-600/5 flex items-center justify-center border border-brand-500/20">
|
||||
<Keyboard className="w-10 h-10 text-brand-500/60" />
|
||||
</div>
|
||||
<div className="absolute inset-0 bg-brand-500/10 blur-2xl rounded-full -z-10" />
|
||||
</div>
|
||||
<div className="space-y-2 max-w-md">
|
||||
<h3 className="text-lg font-semibold text-foreground">
|
||||
Use the Visual Keyboard Map
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Click the "View Keyboard Map" button above to customize
|
||||
your keyboard shortcuts. The visual interface shows all available
|
||||
keys and lets you easily edit shortcuts with single-modifier
|
||||
restrictions.
|
||||
<p className="text-sm text-muted-foreground/80">
|
||||
Click the button below to customize your keyboard shortcuts. The visual
|
||||
interface shows all available keys and lets you easily edit shortcuts.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="default"
|
||||
size="lg"
|
||||
onClick={onOpenKeyboardMap}
|
||||
className="gap-2 mt-4"
|
||||
className={cn(
|
||||
"gap-2.5 mt-2 h-11 px-6",
|
||||
"bg-gradient-to-r from-brand-500 to-brand-600",
|
||||
"hover:from-brand-600 hover:to-brand-600",
|
||||
"text-white font-medium border-0",
|
||||
"shadow-md shadow-brand-500/20 hover:shadow-lg hover:shadow-brand-500/25",
|
||||
"transition-all duration-200 ease-out",
|
||||
"hover:scale-[1.02] active:scale-[0.98]"
|
||||
)}
|
||||
>
|
||||
<Keyboard className="w-5 h-5" />
|
||||
Open Keyboard Map
|
||||
|
||||
@@ -530,17 +530,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-border bg-glass 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 mb-2">
|
||||
<div className="w-10 h-10 rounded-xl flex items-center justify-center">
|
||||
<img src="/logo.png" alt="Automaker Logo" className="w-10 h-10" />
|
||||
<div className="flex items-center gap-4 animate-in fade-in slide-in-from-top-2 duration-500">
|
||||
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 border border-brand-500/20 flex items-center justify-center shadow-lg shadow-brand-500/10">
|
||||
<img src="/logo.png" alt="Automaker Logo" className="w-8 h-8" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
<h1 className="text-2xl font-bold text-foreground tracking-tight">
|
||||
Welcome to Automaker
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-sm text-muted-foreground mt-0.5">
|
||||
Your autonomous AI development studio
|
||||
</p>
|
||||
</div>
|
||||
@@ -550,24 +550,25 @@ export function WelcomeView() {
|
||||
|
||||
{/* Content Area */}
|
||||
<div className="flex-1 overflow-y-auto p-8">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
{/* Quick Actions */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-12">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-12 animate-in fade-in slide-in-from-bottom-4 duration-500 delay-100">
|
||||
{/* New Project Card */}
|
||||
<div
|
||||
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"
|
||||
className="group relative rounded-xl border border-border bg-card/80 backdrop-blur-sm hover:bg-card hover:border-brand-500/30 hover:shadow-xl hover:shadow-brand-500/5 transition-all duration-300 hover:-translate-y-1"
|
||||
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="absolute inset-0 rounded-xl bg-gradient-to-br from-brand-500/5 via-transparent to-purple-600/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
||||
<div className="relative p-6 h-full flex flex-col">
|
||||
<div className="flex items-start gap-4 flex-1">
|
||||
<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 shrink-0">
|
||||
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-brand-500 to-brand-600 shadow-lg shadow-brand-500/25 flex items-center justify-center group-hover:scale-105 group-hover:shadow-brand-500/40 transition-all duration-300 shrink-0">
|
||||
<Plus className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold text-foreground mb-1">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-lg font-semibold text-foreground mb-1.5">
|
||||
New Project
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
Create a new project from scratch with AI-powered
|
||||
development
|
||||
</p>
|
||||
@@ -576,7 +577,7 @@ export function WelcomeView() {
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
className="w-full mt-4 bg-linear-to-r from-brand-500 to-brand-600 hover:from-brand-600 hover:to-brand-600 text-primary-foreground border-0"
|
||||
className="w-full mt-5 bg-gradient-to-r from-brand-500 to-brand-600 hover:from-brand-600 hover:to-brand-700 text-white border-0 shadow-md shadow-brand-500/20 hover:shadow-brand-500/30 transition-all"
|
||||
data-testid="create-new-project"
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
@@ -604,29 +605,30 @@ export function WelcomeView() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Open Project Card */}
|
||||
<div
|
||||
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"
|
||||
className="group relative rounded-xl border border-border bg-card/80 backdrop-blur-sm hover:bg-card hover:border-blue-500/30 hover:shadow-xl hover:shadow-blue-500/5 transition-all duration-300 cursor-pointer hover:-translate-y-1"
|
||||
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="absolute inset-0 rounded-xl bg-gradient-to-br from-blue-500/5 via-transparent to-cyan-600/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
||||
<div className="relative p-6 h-full flex flex-col">
|
||||
<div className="flex items-start gap-4 flex-1">
|
||||
<div className="w-12 h-12 rounded-lg bg-muted border border-border flex items-center justify-center group-hover:scale-110 transition-transform shrink-0">
|
||||
<FolderOpen className="w-6 h-6 text-muted-foreground group-hover:text-foreground transition-colors" />
|
||||
<div className="w-12 h-12 rounded-xl bg-muted border border-border flex items-center justify-center group-hover:bg-blue-500/10 group-hover:border-blue-500/30 group-hover:scale-105 transition-all duration-300 shrink-0">
|
||||
<FolderOpen className="w-6 h-6 text-muted-foreground group-hover:text-blue-500 transition-colors duration-300" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold text-foreground mb-1">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-lg font-semibold text-foreground mb-1.5">
|
||||
Open Project
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
Open an existing project folder to continue working
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="w-full mt-4 bg-secondary hover:bg-secondary/80 text-foreground border border-border hover:border-border-glass"
|
||||
className="w-full mt-5 bg-secondary/80 hover:bg-secondary text-foreground border border-border hover:border-blue-500/30 transition-all"
|
||||
data-testid="open-existing-project"
|
||||
>
|
||||
<FolderOpen className="w-4 h-4 mr-2" />
|
||||
@@ -638,36 +640,39 @@ export function WelcomeView() {
|
||||
|
||||
{/* Recent Projects */}
|
||||
{recentProjects.length > 0 && (
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Clock className="w-5 h-5 text-muted-foreground" />
|
||||
<div className="animate-in fade-in slide-in-from-bottom-4 duration-500 delay-200">
|
||||
<div className="flex items-center gap-2.5 mb-5">
|
||||
<div className="w-8 h-8 rounded-lg bg-muted/50 flex items-center justify-center">
|
||||
<Clock className="w-4 h-4 text-muted-foreground" />
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold text-foreground">
|
||||
Recent Projects
|
||||
</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{recentProjects.map((project) => (
|
||||
{recentProjects.map((project, index) => (
|
||||
<div
|
||||
key={project.id}
|
||||
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"
|
||||
className="group relative rounded-xl border border-border bg-card/60 backdrop-blur-sm hover:bg-card hover:border-brand-500/40 hover:shadow-lg hover:shadow-brand-500/5 transition-all duration-300 cursor-pointer hover:-translate-y-0.5"
|
||||
onClick={() => handleRecentProjectClick(project)}
|
||||
data-testid={`recent-project-${project.id}`}
|
||||
style={{ animationDelay: `${index * 50}ms` }}
|
||||
>
|
||||
<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="absolute inset-0 rounded-xl 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 duration-300" />
|
||||
<div className="relative p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<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 className="w-10 h-10 rounded-lg bg-muted/80 border border-border flex items-center justify-center group-hover:bg-brand-500/10 group-hover:border-brand-500/30 transition-all duration-300 shrink-0">
|
||||
<Folder className="w-5 h-5 text-muted-foreground group-hover:text-brand-500 transition-colors duration-300" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium text-foreground truncate group-hover:text-brand-500 transition-colors">
|
||||
<p className="font-medium text-foreground truncate group-hover:text-brand-500 transition-colors duration-300">
|
||||
{project.name}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground/70 truncate mt-0.5">
|
||||
<p className="text-xs text-muted-foreground/70 truncate mt-1">
|
||||
{project.path}
|
||||
</p>
|
||||
{project.lastOpened && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
<p className="text-xs text-muted-foreground mt-1.5">
|
||||
{new Date(
|
||||
project.lastOpened
|
||||
).toLocaleDateString()}
|
||||
@@ -684,14 +689,14 @@ 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-muted border border-border flex items-center justify-center mb-4">
|
||||
<Sparkles className="w-8 h-8 text-muted-foreground" />
|
||||
<div className="flex flex-col items-center justify-center py-16 text-center animate-in fade-in duration-500 delay-200">
|
||||
<div className="w-20 h-20 rounded-2xl bg-muted/50 border border-border flex items-center justify-center mb-5">
|
||||
<Sparkles className="w-10 h-10 text-muted-foreground/50" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-foreground mb-2">
|
||||
<h3 className="text-xl font-semibold text-foreground mb-2">
|
||||
No projects yet
|
||||
</h3>
|
||||
<p className="text-sm text-zinc-400 max-w-md">
|
||||
<p className="text-sm text-muted-foreground max-w-md leading-relaxed">
|
||||
Get started by creating a new project or opening an existing one
|
||||
</p>
|
||||
</div>
|
||||
@@ -712,35 +717,37 @@ export function WelcomeView() {
|
||||
{/* Project Initialization Dialog */}
|
||||
<Dialog open={showInitDialog} onOpenChange={setShowInitDialog}>
|
||||
<DialogContent
|
||||
className="bg-card border-border"
|
||||
className="bg-card border-border shadow-xl"
|
||||
data-testid="project-init-dialog"
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-foreground flex items-center gap-2">
|
||||
<Sparkles className="w-5 h-5 text-brand-500" />
|
||||
<DialogTitle className="text-foreground flex items-center gap-2.5">
|
||||
<div className="w-8 h-8 rounded-lg bg-brand-500/10 flex items-center justify-center">
|
||||
<Sparkles className="w-4 h-4 text-brand-500" />
|
||||
</div>
|
||||
{initStatus?.isNewProject
|
||||
? "Project Initialized"
|
||||
: "Project Updated"}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-muted-foreground">
|
||||
<DialogDescription className="text-muted-foreground mt-1">
|
||||
{initStatus?.isNewProject
|
||||
? `Created .automaker directory structure for ${initStatus?.projectName}`
|
||||
: `Updated missing files in .automaker for ${initStatus?.projectName}`}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm text-foreground font-medium">
|
||||
Created files:
|
||||
</p>
|
||||
<ul className="space-y-1.5">
|
||||
<ul className="space-y-2">
|
||||
{initStatus?.createdFiles.map((file) => (
|
||||
<li
|
||||
key={file}
|
||||
className="flex items-center gap-2 text-sm text-muted-foreground"
|
||||
className="flex items-center gap-2.5 text-sm text-muted-foreground"
|
||||
>
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-green-500" />
|
||||
<code className="text-xs bg-muted px-2 py-0.5 rounded">
|
||||
<div className="w-2 h-2 rounded-full bg-green-500" />
|
||||
<code className="text-xs bg-muted px-2.5 py-1 rounded-md font-mono">
|
||||
{file}
|
||||
</code>
|
||||
</li>
|
||||
@@ -749,18 +756,18 @@ export function WelcomeView() {
|
||||
</div>
|
||||
|
||||
{initStatus?.isNewProject && (
|
||||
<div className="mt-4 p-3 rounded-lg bg-muted/50 border border-border-glass">
|
||||
<div className="mt-5 p-4 rounded-xl bg-muted/50 border border-border">
|
||||
{isAnalyzing ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<Loader2 className="w-4 h-4 text-brand-500 animate-spin" />
|
||||
<p className="text-sm text-brand-400">
|
||||
<p className="text-sm text-brand-500">
|
||||
AI agent is analyzing your project structure...
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<span className="text-brand-400">Tip:</span> Edit the{" "}
|
||||
<code className="text-xs bg-muted px-1.5 py-0.5 rounded">
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
<span className="text-brand-500 font-medium">Tip:</span> Edit the{" "}
|
||||
<code className="text-xs bg-muted px-1.5 py-0.5 rounded font-mono">
|
||||
app_spec.txt
|
||||
</code>{" "}
|
||||
file to describe your project. The AI agent will use this to
|
||||
@@ -773,7 +780,7 @@ export function WelcomeView() {
|
||||
<DialogFooter>
|
||||
<Button
|
||||
onClick={() => setShowInitDialog(false)}
|
||||
className="bg-gradient-to-r from-brand-500 to-brand-600 hover:from-brand-600 hover:to-brand-600 text-white border-0"
|
||||
className="bg-gradient-to-r from-brand-500 to-brand-600 hover:from-brand-600 hover:to-brand-700 text-white border-0 shadow-md shadow-brand-500/20"
|
||||
data-testid="close-init-dialog"
|
||||
>
|
||||
Get Started
|
||||
@@ -795,8 +802,8 @@ export function WelcomeView() {
|
||||
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-card border border-border">
|
||||
<Loader2 className="w-8 h-8 text-brand-500 animate-spin" />
|
||||
<div className="flex flex-col items-center gap-4 p-8 rounded-2xl bg-card border border-border shadow-2xl">
|
||||
<Loader2 className="w-10 h-10 text-brand-500 animate-spin" />
|
||||
<p className="text-foreground font-medium">
|
||||
Initializing project...
|
||||
</p>
|
||||
|
||||
Reference in New Issue
Block a user