mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 20:23:36 +00:00
Merge branch 'main' of github.com:webdevcody/automaker
This commit is contained in:
48
app/src/components/ui/markdown.tsx
Normal file
48
app/src/components/ui/markdown.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
"use client";
|
||||
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface MarkdownProps {
|
||||
children: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable Markdown component for rendering markdown content
|
||||
* Styled for dark mode with proper typography
|
||||
*/
|
||||
export function Markdown({ children, className }: MarkdownProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"prose prose-sm prose-invert max-w-none",
|
||||
// Headings
|
||||
"[&_h1]:text-xl [&_h1]:text-zinc-200 [&_h1]:font-semibold [&_h1]:mt-4 [&_h1]:mb-2",
|
||||
"[&_h2]:text-lg [&_h2]:text-zinc-200 [&_h2]:font-semibold [&_h2]:mt-4 [&_h2]:mb-2",
|
||||
"[&_h3]:text-base [&_h3]:text-zinc-200 [&_h3]:font-semibold [&_h3]:mt-3 [&_h3]:mb-2",
|
||||
"[&_h4]:text-sm [&_h4]:text-zinc-200 [&_h4]:font-semibold [&_h4]:mt-2 [&_h4]:mb-1",
|
||||
// Paragraphs
|
||||
"[&_p]:text-zinc-300 [&_p]:leading-relaxed [&_p]:my-2",
|
||||
// Lists
|
||||
"[&_ul]:my-2 [&_ul]:pl-4 [&_ol]:my-2 [&_ol]:pl-4",
|
||||
"[&_li]:text-zinc-300 [&_li]:my-0.5",
|
||||
// Code
|
||||
"[&_code]:text-cyan-400 [&_code]:bg-zinc-800/50 [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:rounded [&_code]:text-sm",
|
||||
"[&_pre]:bg-zinc-900/80 [&_pre]:border [&_pre]:border-white/10 [&_pre]:rounded-lg [&_pre]:my-2 [&_pre]:p-3 [&_pre]:overflow-x-auto",
|
||||
"[&_pre_code]:bg-transparent [&_pre_code]:p-0",
|
||||
// Strong/Bold
|
||||
"[&_strong]:text-zinc-200 [&_strong]:font-semibold",
|
||||
// Links
|
||||
"[&_a]:text-blue-400 [&_a]:no-underline hover:[&_a]:underline",
|
||||
// Blockquotes
|
||||
"[&_blockquote]:border-l-2 [&_blockquote]:border-zinc-600 [&_blockquote]:pl-4 [&_blockquote]:text-zinc-400 [&_blockquote]:italic [&_blockquote]:my-2",
|
||||
// Horizontal rules
|
||||
"[&_hr]:border-zinc-700 [&_hr]:my-4",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<ReactMarkdown>{children}</ReactMarkdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -205,7 +205,7 @@ export function AgentOutputModal({
|
||||
className="max-w-4xl max-h-[80vh] flex flex-col"
|
||||
data-testid="agent-output-modal"
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogHeader className="flex-shrink-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Loader2 className="w-5 h-5 text-purple-500 animate-spin" />
|
||||
@@ -238,7 +238,10 @@ export function AgentOutputModal({
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<DialogDescription className="mt-1">
|
||||
<DialogDescription
|
||||
className="mt-1 max-h-24 overflow-y-auto break-words"
|
||||
data-testid="agent-output-description"
|
||||
>
|
||||
{featureDescription}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
@@ -266,7 +269,7 @@ export function AgentOutputModal({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-muted-foreground text-center">
|
||||
<div className="text-xs text-muted-foreground text-center flex-shrink-0">
|
||||
{autoScrollRef.current
|
||||
? "Auto-scrolling enabled"
|
||||
: "Scroll to bottom to enable auto-scroll"}
|
||||
|
||||
1119
app/src/components/views/analysis-view.tsx
Normal file
1119
app/src/components/views/analysis-view.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -65,6 +65,8 @@ import {
|
||||
FastForward,
|
||||
FlaskConical,
|
||||
CheckCircle2,
|
||||
MessageSquare,
|
||||
GitCommit,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
@@ -81,6 +83,7 @@ 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" },
|
||||
];
|
||||
|
||||
@@ -119,6 +122,12 @@ export function BoardView() {
|
||||
const [showDeleteAllVerifiedDialog, setShowDeleteAllVerifiedDialog] =
|
||||
useState(false);
|
||||
const [persistedCategories, setPersistedCategories] = useState<string[]>([]);
|
||||
const [showFollowUpDialog, setShowFollowUpDialog] = useState(false);
|
||||
const [followUpFeature, setFollowUpFeature] = useState<Feature | null>(null);
|
||||
const [followUpPrompt, setFollowUpPrompt] = useState("");
|
||||
const [followUpImagePaths, setFollowUpImagePaths] = useState<
|
||||
DescriptionImagePath[]
|
||||
>([]);
|
||||
|
||||
// Make current project available globally for modal
|
||||
useEffect(() => {
|
||||
@@ -428,6 +437,7 @@ export function BoardView() {
|
||||
startedAt: f.startedAt,
|
||||
imagePaths: f.imagePaths,
|
||||
skipTests: f.skipTests,
|
||||
summary: f.summary,
|
||||
}));
|
||||
await api.writeFile(
|
||||
`${currentProject.path}/.automaker/feature_list.json`,
|
||||
@@ -776,6 +786,142 @@ export function BoardView() {
|
||||
});
|
||||
};
|
||||
|
||||
// Open follow-up dialog for waiting_approval features
|
||||
const handleOpenFollowUp = (feature: Feature) => {
|
||||
console.log("[Board] Opening follow-up dialog for feature:", {
|
||||
id: feature.id,
|
||||
description: feature.description,
|
||||
});
|
||||
setFollowUpFeature(feature);
|
||||
setFollowUpPrompt("");
|
||||
setFollowUpImagePaths([]);
|
||||
setShowFollowUpDialog(true);
|
||||
};
|
||||
|
||||
// Handle sending follow-up prompt
|
||||
const handleSendFollowUp = async () => {
|
||||
if (!currentProject || !followUpFeature || !followUpPrompt.trim()) return;
|
||||
|
||||
// Save values before clearing state
|
||||
const featureId = followUpFeature.id;
|
||||
const featureDescription = followUpFeature.description;
|
||||
const prompt = followUpPrompt;
|
||||
const imagePaths = followUpImagePaths.map((img) => img.path);
|
||||
|
||||
console.log("[Board] Sending follow-up prompt for feature:", {
|
||||
id: featureId,
|
||||
prompt: prompt,
|
||||
imagePaths: imagePaths,
|
||||
});
|
||||
|
||||
const api = getElectronAPI();
|
||||
if (!api?.autoMode?.followUpFeature) {
|
||||
console.error("Follow-up feature API not available");
|
||||
toast.error("Follow-up not available", {
|
||||
description: "This feature is not available in the current version.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Move feature back to in_progress before sending follow-up
|
||||
updateFeature(featureId, {
|
||||
status: "in_progress",
|
||||
startedAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// Reset follow-up state immediately (close dialog, clear form)
|
||||
setShowFollowUpDialog(false);
|
||||
setFollowUpFeature(null);
|
||||
setFollowUpPrompt("");
|
||||
setFollowUpImagePaths([]);
|
||||
|
||||
// Show success toast immediately
|
||||
toast.success("Follow-up started", {
|
||||
description: `Continuing work on: ${featureDescription.slice(0, 50)}${
|
||||
featureDescription.length > 50 ? "..." : ""
|
||||
}`,
|
||||
});
|
||||
|
||||
// Call the API in the background (don't await - let it run async)
|
||||
api.autoMode
|
||||
.followUpFeature(currentProject.path, featureId, prompt, imagePaths)
|
||||
.catch((error) => {
|
||||
console.error("[Board] Error sending follow-up:", error);
|
||||
toast.error("Failed to send follow-up", {
|
||||
description:
|
||||
error instanceof Error ? error.message : "An error occurred",
|
||||
});
|
||||
// Reload features to revert status if there was an error
|
||||
loadFeatures();
|
||||
});
|
||||
};
|
||||
|
||||
// Handle commit-only for waiting_approval features (marks as verified and commits)
|
||||
const handleCommitFeature = async (feature: Feature) => {
|
||||
if (!currentProject) return;
|
||||
|
||||
console.log("[Board] Committing feature:", {
|
||||
id: feature.id,
|
||||
description: feature.description,
|
||||
});
|
||||
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.autoMode?.commitFeature) {
|
||||
console.error("Commit feature API not available");
|
||||
toast.error("Commit not available", {
|
||||
description: "This feature is not available in the current version.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Call the API to commit this feature
|
||||
const result = await api.autoMode.commitFeature(
|
||||
currentProject.path,
|
||||
feature.id
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
console.log("[Board] Feature committed successfully");
|
||||
// Move to verified status
|
||||
moveFeature(feature.id, "verified");
|
||||
toast.success("Feature committed", {
|
||||
description: `Committed and verified: ${feature.description.slice(
|
||||
0,
|
||||
50
|
||||
)}${feature.description.length > 50 ? "..." : ""}`,
|
||||
});
|
||||
} else {
|
||||
console.error("[Board] Failed to commit feature:", result.error);
|
||||
toast.error("Failed to commit feature", {
|
||||
description: result.error || "An error occurred",
|
||||
});
|
||||
await loadFeatures();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[Board] Error committing feature:", error);
|
||||
toast.error("Failed to commit feature", {
|
||||
description:
|
||||
error instanceof Error ? error.message : "An error occurred",
|
||||
});
|
||||
await loadFeatures();
|
||||
}
|
||||
};
|
||||
|
||||
// Move feature to waiting_approval (for skipTests features when agent completes)
|
||||
const handleMoveToWaitingApproval = (feature: Feature) => {
|
||||
console.log("[Board] Moving feature to waiting_approval:", {
|
||||
id: feature.id,
|
||||
description: feature.description,
|
||||
});
|
||||
updateFeature(feature.id, { status: "waiting_approval" });
|
||||
toast.info("Feature ready for review", {
|
||||
description: `Ready for approval: ${feature.description.slice(0, 50)}${
|
||||
feature.description.length > 50 ? "..." : ""
|
||||
}`,
|
||||
});
|
||||
};
|
||||
|
||||
const checkContextExists = async (featureId: string): Promise<boolean> => {
|
||||
if (!currentProject) return false;
|
||||
|
||||
@@ -844,12 +990,30 @@ export function BoardView() {
|
||||
const handleForceStopFeature = async (feature: Feature) => {
|
||||
try {
|
||||
await autoMode.stopFeature(feature.id);
|
||||
// Move the feature back to backlog status after stopping
|
||||
moveFeature(feature.id, "backlog");
|
||||
|
||||
// Determine where to move the feature after stopping:
|
||||
// - If it's a skipTests feature that was in waiting_approval (i.e., during commit operation),
|
||||
// move it back to waiting_approval so user can try commit again or do follow-up
|
||||
// - Otherwise, move to backlog
|
||||
const targetStatus =
|
||||
feature.skipTests && feature.status === "waiting_approval"
|
||||
? "waiting_approval"
|
||||
: "backlog";
|
||||
|
||||
if (targetStatus !== feature.status) {
|
||||
moveFeature(feature.id, targetStatus);
|
||||
}
|
||||
|
||||
toast.success("Agent stopped", {
|
||||
description: `Stopped working on: ${feature.description.slice(0, 50)}${
|
||||
feature.description.length > 50 ? "..." : ""
|
||||
}`,
|
||||
description:
|
||||
targetStatus === "waiting_approval"
|
||||
? `Stopped commit - returned to waiting approval: ${feature.description.slice(
|
||||
0,
|
||||
50
|
||||
)}${feature.description.length > 50 ? "..." : ""}`
|
||||
: `Stopped working on: ${feature.description.slice(0, 50)}${
|
||||
feature.description.length > 50 ? "..." : ""
|
||||
}`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[Board] Error stopping feature:", error);
|
||||
@@ -1114,6 +1278,8 @@ export function BoardView() {
|
||||
onMoveBackToInProgress={() =>
|
||||
handleMoveBackToInProgress(feature)
|
||||
}
|
||||
onFollowUp={() => handleOpenFollowUp(feature)}
|
||||
onCommit={() => handleCommitFeature(feature)}
|
||||
hasContext={featuresWithContext.has(feature.id)}
|
||||
isCurrentAutoTask={runningAutoTasks.includes(
|
||||
feature.id
|
||||
@@ -1457,6 +1623,86 @@ export function BoardView() {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Follow-Up Prompt Dialog */}
|
||||
<Dialog
|
||||
open={showFollowUpDialog}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setShowFollowUpDialog(false);
|
||||
setFollowUpFeature(null);
|
||||
setFollowUpPrompt("");
|
||||
setFollowUpImagePaths([]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent
|
||||
data-testid="follow-up-dialog"
|
||||
onKeyDown={(e) => {
|
||||
if (
|
||||
(e.metaKey || e.ctrlKey) &&
|
||||
e.key === "Enter" &&
|
||||
followUpPrompt.trim()
|
||||
) {
|
||||
e.preventDefault();
|
||||
handleSendFollowUp();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Follow-Up Prompt</DialogTitle>
|
||||
<DialogDescription>
|
||||
Send additional instructions to continue working on this feature.
|
||||
{followUpFeature && (
|
||||
<span className="block mt-2 text-primary">
|
||||
Feature: {followUpFeature.description.slice(0, 100)}
|
||||
{followUpFeature.description.length > 100 ? "..." : ""}
|
||||
</span>
|
||||
)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="follow-up-prompt">Instructions</Label>
|
||||
<DescriptionImageDropZone
|
||||
value={followUpPrompt}
|
||||
onChange={setFollowUpPrompt}
|
||||
images={followUpImagePaths}
|
||||
onImagesChange={setFollowUpImagePaths}
|
||||
placeholder="Describe what needs to be fixed or changed..."
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
The agent will continue from where it left off, using the existing
|
||||
context. You can attach screenshots to help explain the issue.
|
||||
</p>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setShowFollowUpDialog(false);
|
||||
setFollowUpFeature(null);
|
||||
setFollowUpPrompt("");
|
||||
setFollowUpImagePaths([]);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSendFollowUp}
|
||||
disabled={!followUpPrompt.trim()}
|
||||
data-testid="confirm-follow-up"
|
||||
>
|
||||
<MessageSquare className="w-4 h-4 mr-2" />
|
||||
Send Follow-Up
|
||||
<span className="ml-2 px-1.5 py-0.5 text-[10px] font-mono rounded bg-white/10 border border-white/20">
|
||||
⌘↵
|
||||
</span>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
301
app/src/components/views/code-view.tsx
Normal file
301
app/src/components/views/code-view.tsx
Normal file
@@ -0,0 +1,301 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { useAppStore } from "@/store/app-store";
|
||||
import { getElectronAPI } from "@/lib/electron";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
File,
|
||||
Folder,
|
||||
FolderOpen,
|
||||
ChevronRight,
|
||||
ChevronDown,
|
||||
RefreshCw,
|
||||
Code,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface FileTreeNode {
|
||||
name: string;
|
||||
path: string;
|
||||
isDirectory: boolean;
|
||||
children?: FileTreeNode[];
|
||||
isExpanded?: boolean;
|
||||
}
|
||||
|
||||
const IGNORE_PATTERNS = [
|
||||
"node_modules",
|
||||
".git",
|
||||
".next",
|
||||
"dist",
|
||||
"build",
|
||||
".DS_Store",
|
||||
"*.log",
|
||||
];
|
||||
|
||||
const shouldIgnore = (name: string) => {
|
||||
return IGNORE_PATTERNS.some((pattern) => {
|
||||
if (pattern.startsWith("*")) {
|
||||
return name.endsWith(pattern.slice(1));
|
||||
}
|
||||
return name === pattern;
|
||||
});
|
||||
};
|
||||
|
||||
export function CodeView() {
|
||||
const { currentProject } = useAppStore();
|
||||
const [fileTree, setFileTree] = useState<FileTreeNode[]>([]);
|
||||
const [selectedFile, setSelectedFile] = useState<string | null>(null);
|
||||
const [fileContent, setFileContent] = useState<string>("");
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(
|
||||
new Set()
|
||||
);
|
||||
|
||||
// Load directory tree
|
||||
const loadTree = useCallback(async () => {
|
||||
if (!currentProject) return;
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.readdir(currentProject.path);
|
||||
|
||||
if (result.success && result.entries) {
|
||||
const entries = result.entries
|
||||
.filter((e) => !shouldIgnore(e.name))
|
||||
.sort((a, b) => {
|
||||
// Directories first
|
||||
if (a.isDirectory && !b.isDirectory) return -1;
|
||||
if (!a.isDirectory && b.isDirectory) return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
})
|
||||
.map((e) => ({
|
||||
name: e.name,
|
||||
path: `${currentProject.path}/${e.name}`,
|
||||
isDirectory: e.isDirectory,
|
||||
}));
|
||||
|
||||
setFileTree(entries);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load file tree:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [currentProject]);
|
||||
|
||||
useEffect(() => {
|
||||
loadTree();
|
||||
}, [loadTree]);
|
||||
|
||||
// Load subdirectory
|
||||
const loadSubdirectory = async (path: string): Promise<FileTreeNode[]> => {
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.readdir(path);
|
||||
|
||||
if (result.success && result.entries) {
|
||||
return result.entries
|
||||
.filter((e) => !shouldIgnore(e.name))
|
||||
.sort((a, b) => {
|
||||
if (a.isDirectory && !b.isDirectory) return -1;
|
||||
if (!a.isDirectory && b.isDirectory) return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
})
|
||||
.map((e) => ({
|
||||
name: e.name,
|
||||
path: `${path}/${e.name}`,
|
||||
isDirectory: e.isDirectory,
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load subdirectory:", error);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
// Load file content
|
||||
const loadFileContent = async (path: string) => {
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.readFile(path);
|
||||
|
||||
if (result.success && result.content) {
|
||||
setFileContent(result.content);
|
||||
setSelectedFile(path);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load file:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Toggle folder expansion
|
||||
const toggleFolder = async (node: FileTreeNode) => {
|
||||
const newExpanded = new Set(expandedFolders);
|
||||
|
||||
if (expandedFolders.has(node.path)) {
|
||||
newExpanded.delete(node.path);
|
||||
} else {
|
||||
newExpanded.add(node.path);
|
||||
|
||||
// Load children if not already loaded
|
||||
if (!node.children) {
|
||||
const children = await loadSubdirectory(node.path);
|
||||
// Update the tree with children
|
||||
const updateTree = (nodes: FileTreeNode[]): FileTreeNode[] => {
|
||||
return nodes.map((n) => {
|
||||
if (n.path === node.path) {
|
||||
return { ...n, children };
|
||||
}
|
||||
if (n.children) {
|
||||
return { ...n, children: updateTree(n.children) };
|
||||
}
|
||||
return n;
|
||||
});
|
||||
};
|
||||
setFileTree(updateTree(fileTree));
|
||||
}
|
||||
}
|
||||
|
||||
setExpandedFolders(newExpanded);
|
||||
};
|
||||
|
||||
// Render file tree node
|
||||
const renderNode = (node: FileTreeNode, depth: number = 0) => {
|
||||
const isExpanded = expandedFolders.has(node.path);
|
||||
const isSelected = selectedFile === node.path;
|
||||
|
||||
return (
|
||||
<div key={node.path}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-2 py-1 px-2 rounded cursor-pointer hover:bg-muted/50",
|
||||
isSelected && "bg-muted"
|
||||
)}
|
||||
style={{ paddingLeft: `${depth * 16 + 8}px` }}
|
||||
onClick={() => {
|
||||
if (node.isDirectory) {
|
||||
toggleFolder(node);
|
||||
} else {
|
||||
loadFileContent(node.path);
|
||||
}
|
||||
}}
|
||||
data-testid={`file-tree-item-${node.name}`}
|
||||
>
|
||||
{node.isDirectory ? (
|
||||
<>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="w-4 h-4 text-muted-foreground shrink-0" />
|
||||
) : (
|
||||
<ChevronRight className="w-4 h-4 text-muted-foreground shrink-0" />
|
||||
)}
|
||||
{isExpanded ? (
|
||||
<FolderOpen className="w-4 h-4 text-primary shrink-0" />
|
||||
) : (
|
||||
<Folder className="w-4 h-4 text-primary shrink-0" />
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="w-4" />
|
||||
<File className="w-4 h-4 text-muted-foreground shrink-0" />
|
||||
</>
|
||||
)}
|
||||
<span className="text-sm truncate">{node.name}</span>
|
||||
</div>
|
||||
{node.isDirectory && isExpanded && node.children && (
|
||||
<div>
|
||||
{node.children.map((child) => renderNode(child, depth + 1))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (!currentProject) {
|
||||
return (
|
||||
<div
|
||||
className="flex-1 flex items-center justify-center"
|
||||
data-testid="code-view-no-project"
|
||||
>
|
||||
<p className="text-muted-foreground">No project selected</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div
|
||||
className="flex-1 flex items-center justify-center"
|
||||
data-testid="code-view-loading"
|
||||
>
|
||||
<RefreshCw className="w-6 h-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex-1 flex flex-col overflow-hidden content-bg"
|
||||
data-testid="code-view"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-white/10 bg-zinc-950/50 backdrop-blur-md">
|
||||
<div className="flex items-center gap-3">
|
||||
<Code className="w-5 h-5 text-muted-foreground" />
|
||||
<div>
|
||||
<h1 className="text-xl font-bold">Code Explorer</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{currentProject.name}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={loadTree}
|
||||
data-testid="refresh-tree"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Split View */}
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
{/* File Tree */}
|
||||
<div className="w-64 border-r overflow-y-auto" data-testid="file-tree">
|
||||
<div className="p-2">{fileTree.map((node) => renderNode(node))}</div>
|
||||
</div>
|
||||
|
||||
{/* Code Preview */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{selectedFile ? (
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="px-4 py-2 border-b bg-muted/30">
|
||||
<p className="text-sm font-mono text-muted-foreground truncate">
|
||||
{selectedFile.replace(currentProject.path, "")}
|
||||
</p>
|
||||
</div>
|
||||
<Card className="flex-1 m-4 overflow-hidden">
|
||||
<CardContent className="p-0 h-full">
|
||||
<pre className="p-4 h-full overflow-auto text-sm font-mono whitespace-pre-wrap">
|
||||
<code data-testid="code-content">{fileContent}</code>
|
||||
</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<p className="text-muted-foreground">
|
||||
Select a file to view its contents
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Feature } from "@/store/app-store";
|
||||
import { Feature, useAppStore } from "@/store/app-store";
|
||||
import {
|
||||
GripVertical,
|
||||
Edit,
|
||||
@@ -34,8 +34,23 @@ import {
|
||||
StopCircle,
|
||||
FlaskConical,
|
||||
ArrowLeft,
|
||||
MessageSquare,
|
||||
GitCommit,
|
||||
Cpu,
|
||||
Wrench,
|
||||
ListTodo,
|
||||
Sparkles,
|
||||
Expand,
|
||||
} from "lucide-react";
|
||||
import { CountUpTimer } from "@/components/ui/count-up-timer";
|
||||
import { getElectronAPI } from "@/lib/electron";
|
||||
import {
|
||||
parseAgentContext,
|
||||
AgentTaskInfo,
|
||||
formatModelName,
|
||||
DEFAULT_MODEL,
|
||||
} from "@/lib/agent-context-parser";
|
||||
import { Markdown } from "@/components/ui/markdown";
|
||||
|
||||
interface KanbanCardProps {
|
||||
feature: Feature;
|
||||
@@ -47,9 +62,15 @@ interface KanbanCardProps {
|
||||
onForceStop?: () => void;
|
||||
onManualVerify?: () => void;
|
||||
onMoveBackToInProgress?: () => void;
|
||||
onFollowUp?: () => void;
|
||||
onCommit?: () => void;
|
||||
hasContext?: boolean;
|
||||
isCurrentAutoTask?: boolean;
|
||||
shortcutKey?: string;
|
||||
/** Context content for extracting progress info */
|
||||
contextContent?: string;
|
||||
/** Feature summary from agent completion */
|
||||
summary?: string;
|
||||
}
|
||||
|
||||
export function KanbanCard({
|
||||
@@ -62,11 +83,71 @@ export function KanbanCard({
|
||||
onForceStop,
|
||||
onManualVerify,
|
||||
onMoveBackToInProgress,
|
||||
onFollowUp,
|
||||
onCommit,
|
||||
hasContext,
|
||||
isCurrentAutoTask,
|
||||
shortcutKey,
|
||||
contextContent,
|
||||
summary,
|
||||
}: KanbanCardProps) {
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||
const [isSummaryDialogOpen, setIsSummaryDialogOpen] = useState(false);
|
||||
const [agentInfo, setAgentInfo] = useState<AgentTaskInfo | null>(null);
|
||||
const { kanbanCardDetailLevel } = useAppStore();
|
||||
|
||||
// Helper functions to check what should be shown based on detail level
|
||||
const showSteps =
|
||||
kanbanCardDetailLevel === "standard" ||
|
||||
kanbanCardDetailLevel === "detailed";
|
||||
const showAgentInfo = kanbanCardDetailLevel === "detailed";
|
||||
const showProgressBar =
|
||||
kanbanCardDetailLevel === "standard" ||
|
||||
kanbanCardDetailLevel === "detailed";
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const currentProject = (window as any).__currentProject;
|
||||
if (!currentProject?.path) return;
|
||||
|
||||
const contextPath = `${currentProject.path}/.automaker/agents-context/${feature.id}.md`;
|
||||
const result = await api.readFile(contextPath);
|
||||
|
||||
if (result.success && result.content) {
|
||||
const info = parseAgentContext(result.content);
|
||||
setAgentInfo(info);
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
}, [feature.id, feature.status, contextContent, isCurrentAutoTask]);
|
||||
|
||||
const handleDeleteClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
@@ -189,8 +270,8 @@ export function KanbanCard({
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="p-3 pt-0">
|
||||
{/* Steps Preview */}
|
||||
{feature.steps.length > 0 && (
|
||||
{/* Steps Preview - Show in Standard and Detailed modes */}
|
||||
{showSteps && feature.steps.length > 0 && (
|
||||
<div className="mb-3 space-y-1">
|
||||
{feature.steps.slice(0, 3).map((step, index) => (
|
||||
<div
|
||||
@@ -213,6 +294,195 @@ export function KanbanCard({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Agent Info Panel - shows for in_progress, waiting_approval, verified */}
|
||||
{/* Standard mode: Only show progress bar */}
|
||||
{showProgressBar &&
|
||||
!showAgentInfo &&
|
||||
feature.status !== "backlog" &&
|
||||
agentInfo &&
|
||||
(isCurrentAutoTask || feature.status === "in_progress") && (
|
||||
<div className="mb-3 space-y-1">
|
||||
<div className="w-full h-1.5 bg-zinc-800 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="w-full h-full bg-primary transition-transform duration-500 ease-out origin-left"
|
||||
style={{
|
||||
transform: `translateX(${
|
||||
agentInfo.progressPercentage - 100
|
||||
}%)`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-[10px] text-muted-foreground">
|
||||
<span>{Math.round(agentInfo.progressPercentage)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Detailed mode: Show all agent info */}
|
||||
{showAgentInfo && feature.status !== "backlog" && agentInfo && (
|
||||
<div className="mb-3 space-y-2">
|
||||
{/* Model & Phase */}
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<div className="flex items-center gap-1 text-cyan-400">
|
||||
<Cpu className="w-3 h-3" />
|
||||
<span className="font-medium">
|
||||
{formatModelName(DEFAULT_MODEL)}
|
||||
</span>
|
||||
</div>
|
||||
{agentInfo.currentPhase && (
|
||||
<div
|
||||
className={cn(
|
||||
"px-1.5 py-0.5 rounded text-[10px] font-medium",
|
||||
agentInfo.currentPhase === "planning" &&
|
||||
"bg-blue-500/20 text-blue-400",
|
||||
agentInfo.currentPhase === "action" &&
|
||||
"bg-amber-500/20 text-amber-400",
|
||||
agentInfo.currentPhase === "verification" &&
|
||||
"bg-green-500/20 text-green-400"
|
||||
)}
|
||||
>
|
||||
{agentInfo.currentPhase}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Progress Indicator */}
|
||||
{(isCurrentAutoTask || feature.status === "in_progress") && (
|
||||
<div className="space-y-1">
|
||||
<div className="w-full h-1.5 bg-zinc-800 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="w-full h-full bg-primary transition-transform duration-500 ease-out origin-left"
|
||||
style={{
|
||||
transform: `translateX(${
|
||||
agentInfo.progressPercentage - 100
|
||||
}%)`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-[10px] text-muted-foreground">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="flex items-center gap-1">
|
||||
<Wrench className="w-2.5 h-2.5" />
|
||||
{agentInfo.toolCallCount} tools
|
||||
</span>
|
||||
{agentInfo.lastToolUsed && (
|
||||
<span
|
||||
className="text-zinc-500 truncate max-w-[80px]"
|
||||
title={agentInfo.lastToolUsed}
|
||||
>
|
||||
{agentInfo.lastToolUsed}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span>{Math.round(agentInfo.progressPercentage)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Task List Progress (if todos found) */}
|
||||
{agentInfo.todos.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-1 text-[10px] text-muted-foreground">
|
||||
<ListTodo className="w-3 h-3" />
|
||||
<span>
|
||||
{
|
||||
agentInfo.todos.filter((t) => t.status === "completed")
|
||||
.length
|
||||
}
|
||||
/{agentInfo.todos.length} tasks
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-0.5 max-h-16 overflow-y-auto">
|
||||
{agentInfo.todos.slice(0, 3).map((todo, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
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" />
|
||||
) : todo.status === "in_progress" ? (
|
||||
<Loader2 className="w-2.5 h-2.5 text-amber-400 animate-spin shrink-0" />
|
||||
) : (
|
||||
<Circle className="w-2.5 h-2.5 text-zinc-500 shrink-0" />
|
||||
)}
|
||||
<span
|
||||
className={cn(
|
||||
"truncate",
|
||||
todo.status === "completed" &&
|
||||
"text-zinc-500 line-through",
|
||||
todo.status === "in_progress" && "text-amber-400",
|
||||
todo.status === "pending" && "text-zinc-400"
|
||||
)}
|
||||
>
|
||||
{todo.content}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{agentInfo.todos.length > 3 && (
|
||||
<p className="text-[10px] text-muted-foreground pl-4">
|
||||
+{agentInfo.todos.length - 3} more
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Summary for waiting_approval and verified - prioritize feature.summary from UpdateFeatureStatus */}
|
||||
{(feature.status === "waiting_approval" ||
|
||||
feature.status === "verified") && (
|
||||
<>
|
||||
{(feature.summary || summary || agentInfo.summary) && (
|
||||
<div className="space-y-1 pt-1 border-t border-white/5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1 text-[10px] text-green-400">
|
||||
<Sparkles className="w-3 h-3" />
|
||||
<span>Summary</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsSummaryDialogOpen(true);
|
||||
}}
|
||||
className="p-0.5 rounded hover:bg-white/10 transition-colors text-zinc-500 hover:text-zinc-300"
|
||||
title="View full summary"
|
||||
data-testid={`expand-summary-${feature.id}`}
|
||||
>
|
||||
<Expand className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-[10px] text-zinc-400 line-clamp-3">
|
||||
{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-white/5">
|
||||
<span className="flex items-center gap-1">
|
||||
<Wrench className="w-2.5 h-2.5" />
|
||||
{agentInfo.toolCallCount} tool calls
|
||||
</span>
|
||||
{agentInfo.todos.length > 0 && (
|
||||
<span className="flex items-center gap-1">
|
||||
<CheckCircle2 className="w-2.5 h-2.5 text-green-500" />
|
||||
{
|
||||
agentInfo.todos.filter(
|
||||
(t) => t.status === "completed"
|
||||
).length
|
||||
}{" "}
|
||||
tasks done
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-2">
|
||||
{isCurrentAutoTask && (
|
||||
@@ -363,6 +633,51 @@ export function KanbanCard({
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{!isCurrentAutoTask && feature.status === "waiting_approval" && (
|
||||
<>
|
||||
{/* Follow-up prompt button */}
|
||||
{onFollowUp && (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="flex-1 h-7 text-xs bg-blue-600 hover:bg-blue-700"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onFollowUp();
|
||||
}}
|
||||
data-testid={`follow-up-${feature.id}`}
|
||||
>
|
||||
<MessageSquare className="w-3 h-3 mr-1" />
|
||||
Follow-up
|
||||
</Button>
|
||||
)}
|
||||
{/* Commit and verify button */}
|
||||
{onCommit && (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="flex-1 h-7 text-xs bg-green-600 hover:bg-green-700"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onCommit();
|
||||
}}
|
||||
data-testid={`commit-${feature.id}`}
|
||||
>
|
||||
<GitCommit className="w-3 h-3 mr-1" />
|
||||
Commit
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 text-xs text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||
onClick={handleDeleteClick}
|
||||
data-testid={`delete-waiting-feature-${feature.id}`}
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{!isCurrentAutoTask && feature.status === "backlog" && (
|
||||
<>
|
||||
<Button
|
||||
@@ -420,6 +735,43 @@ export function KanbanCard({
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Summary Modal */}
|
||||
<Dialog open={isSummaryDialogOpen} onOpenChange={setIsSummaryDialogOpen}>
|
||||
<DialogContent
|
||||
className="max-w-4xl max-h-[80vh] overflow-hidden flex flex-col"
|
||||
data-testid={`summary-dialog-${feature.id}`}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Sparkles className="w-5 h-5 text-green-400" />
|
||||
Implementation Summary
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-sm" title={feature.description}>
|
||||
{feature.description.length > 100
|
||||
? `${feature.description.slice(0, 100)}...`
|
||||
: feature.description}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex-1 overflow-y-auto p-4 bg-zinc-900/50 rounded-lg border border-white/10">
|
||||
<Markdown>
|
||||
{feature.summary ||
|
||||
summary ||
|
||||
agentInfo?.summary ||
|
||||
"No summary available"}
|
||||
</Markdown>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => setIsSummaryDialogOpen(false)}
|
||||
data-testid="close-summary-button"
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,13 +5,6 @@ import { useAppStore } from "@/store/app-store";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
Settings,
|
||||
Key,
|
||||
@@ -34,11 +27,22 @@ import {
|
||||
Cat,
|
||||
Atom,
|
||||
Radio,
|
||||
LayoutGrid,
|
||||
Minimize2,
|
||||
Square,
|
||||
Maximize2,
|
||||
} from "lucide-react";
|
||||
|
||||
export function SettingsView() {
|
||||
const { apiKeys, setApiKeys, setCurrentView, theme, setTheme } =
|
||||
useAppStore();
|
||||
const {
|
||||
apiKeys,
|
||||
setApiKeys,
|
||||
setCurrentView,
|
||||
theme,
|
||||
setTheme,
|
||||
kanbanCardDetailLevel,
|
||||
setKanbanCardDetailLevel,
|
||||
} = useAppStore();
|
||||
const [anthropicKey, setAnthropicKey] = useState(apiKeys.anthropic);
|
||||
const [googleKey, setGoogleKey] = useState(apiKeys.google);
|
||||
const [showAnthropicKey, setShowAnthropicKey] = useState(false);
|
||||
@@ -548,6 +552,81 @@ export function SettingsView() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Kanban Card Display Section */}
|
||||
<div className="rounded-xl border border-white/10 bg-zinc-900/50 backdrop-blur-md overflow-hidden">
|
||||
<div className="p-6 border-b border-white/10">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<LayoutGrid className="w-5 h-5 text-brand-500" />
|
||||
<h2 className="text-lg font-semibold text-white">
|
||||
Kanban Card Display
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-sm text-zinc-400">
|
||||
Control how much information is displayed on Kanban cards.
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="space-y-3">
|
||||
<Label className="text-zinc-300">Detail Level</Label>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<button
|
||||
onClick={() => setKanbanCardDetailLevel("minimal")}
|
||||
className={`flex flex-col items-center justify-center gap-2 px-4 py-4 rounded-lg border transition-all ${
|
||||
kanbanCardDetailLevel === "minimal"
|
||||
? "bg-white/5 border-brand-500 text-white"
|
||||
: "bg-zinc-950/50 border-white/10 text-zinc-400 hover:text-white hover:bg-white/5"
|
||||
}`}
|
||||
data-testid="kanban-detail-minimal"
|
||||
>
|
||||
<Minimize2 className="w-5 h-5" />
|
||||
<span className="font-medium text-sm">Minimal</span>
|
||||
<span className="text-xs text-zinc-500 text-center">
|
||||
Title & category only
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setKanbanCardDetailLevel("standard")}
|
||||
className={`flex flex-col items-center justify-center gap-2 px-4 py-4 rounded-lg border transition-all ${
|
||||
kanbanCardDetailLevel === "standard"
|
||||
? "bg-white/5 border-brand-500 text-white"
|
||||
: "bg-zinc-950/50 border-white/10 text-zinc-400 hover:text-white hover:bg-white/5"
|
||||
}`}
|
||||
data-testid="kanban-detail-standard"
|
||||
>
|
||||
<Square className="w-5 h-5" />
|
||||
<span className="font-medium text-sm">Standard</span>
|
||||
<span className="text-xs text-zinc-500 text-center">
|
||||
Steps & progress
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setKanbanCardDetailLevel("detailed")}
|
||||
className={`flex flex-col items-center justify-center gap-2 px-4 py-4 rounded-lg border transition-all ${
|
||||
kanbanCardDetailLevel === "detailed"
|
||||
? "bg-white/5 border-brand-500 text-white"
|
||||
: "bg-zinc-950/50 border-white/10 text-zinc-400 hover:text-white hover:bg-white/5"
|
||||
}`}
|
||||
data-testid="kanban-detail-detailed"
|
||||
>
|
||||
<Maximize2 className="w-5 h-5" />
|
||||
<span className="font-medium text-sm">Detailed</span>
|
||||
<span className="text-xs text-zinc-500 text-center">
|
||||
Model, tools & tasks
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-zinc-500">
|
||||
<strong>Minimal:</strong> Shows only title and category
|
||||
<br />
|
||||
<strong>Standard:</strong> Adds steps preview and progress bar
|
||||
<br />
|
||||
<strong>Detailed:</strong> Shows all info including model,
|
||||
tool calls, task list, and summaries
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Save Button */}
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
|
||||
Reference in New Issue
Block a user