mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 09:13:08 +00:00
Merge pull request #97 from AutoMaker-Org/ui-tweaks
feat: implement completed features management in BoardView and Kanban…
This commit is contained in:
@@ -1,172 +0,0 @@
|
|||||||
import {
|
|
||||||
query,
|
|
||||||
Options,
|
|
||||||
SDKAssistantMessage,
|
|
||||||
} from "@anthropic-ai/claude-agent-sdk";
|
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
|
||||||
import path from "path";
|
|
||||||
|
|
||||||
const systemPrompt = `You are an AI assistant helping users build software. You are part of the Automaker application,
|
|
||||||
which is designed to help developers plan, design, and implement software projects autonomously.
|
|
||||||
|
|
||||||
Your role is to:
|
|
||||||
- Help users define their project requirements and specifications
|
|
||||||
- Ask clarifying questions to better understand their needs
|
|
||||||
- Suggest technical approaches and architectures
|
|
||||||
- Guide them through the development process
|
|
||||||
- Be conversational and helpful
|
|
||||||
- Write, edit, and modify code files as requested
|
|
||||||
- Execute commands and tests
|
|
||||||
- Search and analyze the codebase
|
|
||||||
|
|
||||||
When discussing projects, help users think through:
|
|
||||||
- Core functionality and features
|
|
||||||
- Technical stack choices
|
|
||||||
- Data models and architecture
|
|
||||||
- User experience considerations
|
|
||||||
- Testing strategies
|
|
||||||
|
|
||||||
You have full access to the codebase and can:
|
|
||||||
- Read files to understand existing code
|
|
||||||
- Write new files
|
|
||||||
- Edit existing files
|
|
||||||
- Run bash commands
|
|
||||||
- Search for code patterns
|
|
||||||
- Execute tests and builds`;
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const { messages, workingDirectory } = await request.json();
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
"[API] CLAUDE_CODE_OAUTH_TOKEN present:",
|
|
||||||
!!process.env.CLAUDE_CODE_OAUTH_TOKEN
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!process.env.CLAUDE_CODE_OAUTH_TOKEN) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "CLAUDE_CODE_OAUTH_TOKEN not configured" },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the last user message
|
|
||||||
const lastMessage = messages[messages.length - 1];
|
|
||||||
|
|
||||||
// Determine working directory - default to parent of app directory
|
|
||||||
const cwd = workingDirectory || path.resolve(process.cwd(), "..");
|
|
||||||
|
|
||||||
console.log("[API] Working directory:", cwd);
|
|
||||||
|
|
||||||
// Create query with options that enable code modification
|
|
||||||
const options: Options = {
|
|
||||||
// model: "claude-sonnet-4-20250514",
|
|
||||||
model: "claude-opus-4-5-20251101",
|
|
||||||
systemPrompt,
|
|
||||||
maxTurns: 20,
|
|
||||||
cwd,
|
|
||||||
// Enable all core tools for code modification
|
|
||||||
allowedTools: [
|
|
||||||
"Read",
|
|
||||||
"Write",
|
|
||||||
"Edit",
|
|
||||||
"Glob",
|
|
||||||
"Grep",
|
|
||||||
"Bash",
|
|
||||||
"WebSearch",
|
|
||||||
"WebFetch",
|
|
||||||
],
|
|
||||||
// Auto-accept file edits within the working directory
|
|
||||||
permissionMode: "acceptEdits",
|
|
||||||
// Enable sandbox for safer bash execution
|
|
||||||
sandbox: {
|
|
||||||
enabled: true,
|
|
||||||
autoAllowBashIfSandboxed: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Convert message history to SDK format to preserve conversation context
|
|
||||||
// Include both user and assistant messages for full context
|
|
||||||
const sessionId = `api-session-${Date.now()}`;
|
|
||||||
const conversationMessages = messages.map(
|
|
||||||
(msg: { role: string; content: string }) => {
|
|
||||||
if (msg.role === "user") {
|
|
||||||
return {
|
|
||||||
type: "user" as const,
|
|
||||||
message: {
|
|
||||||
role: "user" as const,
|
|
||||||
content: msg.content,
|
|
||||||
},
|
|
||||||
parent_tool_use_id: null,
|
|
||||||
session_id: sessionId,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// Assistant message
|
|
||||||
return {
|
|
||||||
type: "assistant" as const,
|
|
||||||
message: {
|
|
||||||
role: "assistant" as const,
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: "text" as const,
|
|
||||||
text: msg.content,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
session_id: sessionId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Execute query with full conversation context
|
|
||||||
const queryResult = query({
|
|
||||||
prompt:
|
|
||||||
conversationMessages.length > 0
|
|
||||||
? conversationMessages
|
|
||||||
: lastMessage.content,
|
|
||||||
options,
|
|
||||||
});
|
|
||||||
|
|
||||||
let responseText = "";
|
|
||||||
const toolUses: Array<{ name: string; input: unknown }> = [];
|
|
||||||
|
|
||||||
// Collect the response from the async generator
|
|
||||||
for await (const msg of queryResult) {
|
|
||||||
if (msg.type === "assistant") {
|
|
||||||
const assistantMsg = msg as SDKAssistantMessage;
|
|
||||||
if (assistantMsg.message.content) {
|
|
||||||
for (const block of assistantMsg.message.content) {
|
|
||||||
if (block.type === "text") {
|
|
||||||
responseText += block.text;
|
|
||||||
} else if (block.type === "tool_use") {
|
|
||||||
// Track tool usage for transparency
|
|
||||||
toolUses.push({
|
|
||||||
name: block.name,
|
|
||||||
input: block.input,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (msg.type === "result") {
|
|
||||||
if (msg.subtype === "success") {
|
|
||||||
if (msg.result) {
|
|
||||||
responseText = msg.result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
content: responseText || "Sorry, I couldn't generate a response.",
|
|
||||||
toolUses: toolUses.length > 0 ? toolUses : undefined,
|
|
||||||
});
|
|
||||||
} catch (error: unknown) {
|
|
||||||
console.error("Claude API error:", error);
|
|
||||||
const errorMessage =
|
|
||||||
error instanceof Error
|
|
||||||
? error.message
|
|
||||||
: "Failed to get response from Claude";
|
|
||||||
return NextResponse.json({ error: errorMessage }, { status: 500 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
167
apps/app/src/components/layout/project-setup-dialog.tsx
Normal file
167
apps/app/src/components/layout/project-setup-dialog.tsx
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Sparkles, Clock } from "lucide-react";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
// Feature count options
|
||||||
|
export type FeatureCount = 20 | 50 | 100;
|
||||||
|
const FEATURE_COUNT_OPTIONS: {
|
||||||
|
value: FeatureCount;
|
||||||
|
label: string;
|
||||||
|
warning?: string;
|
||||||
|
}[] = [
|
||||||
|
{ value: 20, label: "20" },
|
||||||
|
{ value: 50, label: "50", warning: "May take up to 5 minutes" },
|
||||||
|
{ value: 100, label: "100", warning: "May take up to 5 minutes" },
|
||||||
|
];
|
||||||
|
|
||||||
|
interface ProjectSetupDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
projectOverview: string;
|
||||||
|
onProjectOverviewChange: (value: string) => void;
|
||||||
|
generateFeatures: boolean;
|
||||||
|
onGenerateFeaturesChange: (value: boolean) => void;
|
||||||
|
featureCount: FeatureCount;
|
||||||
|
onFeatureCountChange: (value: FeatureCount) => void;
|
||||||
|
onCreateSpec: () => void;
|
||||||
|
onSkip: () => void;
|
||||||
|
isCreatingSpec: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProjectSetupDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
projectOverview,
|
||||||
|
onProjectOverviewChange,
|
||||||
|
generateFeatures,
|
||||||
|
onGenerateFeaturesChange,
|
||||||
|
featureCount,
|
||||||
|
onFeatureCountChange,
|
||||||
|
onCreateSpec,
|
||||||
|
onSkip,
|
||||||
|
isCreatingSpec,
|
||||||
|
}: ProjectSetupDialogProps) {
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={open}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
onOpenChange(open);
|
||||||
|
if (!open && !isCreatingSpec) {
|
||||||
|
onSkip();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogContent className="max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Set Up Your Project</DialogTitle>
|
||||||
|
<DialogDescription className="text-muted-foreground">
|
||||||
|
We didn't find an app_spec.txt file. Let us help you generate
|
||||||
|
your app_spec.txt to help describe your project for our system.
|
||||||
|
We'll analyze your project's tech stack and create a
|
||||||
|
comprehensive specification.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4 py-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">Project Overview</label>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Describe what your project does and what features you want to
|
||||||
|
build. Be as detailed as you want - this will help us create a
|
||||||
|
better specification.
|
||||||
|
</p>
|
||||||
|
<textarea
|
||||||
|
className="w-full h-48 p-3 rounded-md border border-border bg-background font-mono text-sm resize-none focus:outline-none focus:ring-2 focus:ring-ring"
|
||||||
|
value={projectOverview}
|
||||||
|
onChange={(e) => onProjectOverviewChange(e.target.value)}
|
||||||
|
placeholder="e.g., A project management tool that allows teams to track tasks, manage sprints, and visualize progress through kanban boards. It should support user authentication, real-time updates, and file attachments..."
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start space-x-3 pt-2">
|
||||||
|
<Checkbox
|
||||||
|
id="sidebar-generate-features"
|
||||||
|
checked={generateFeatures}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
onGenerateFeaturesChange(checked === true)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label
|
||||||
|
htmlFor="sidebar-generate-features"
|
||||||
|
className="text-sm font-medium cursor-pointer"
|
||||||
|
>
|
||||||
|
Generate feature list
|
||||||
|
</label>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Automatically create features in the features folder from the
|
||||||
|
implementation roadmap after the spec is generated.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Feature Count Selection - only shown when generateFeatures is enabled */}
|
||||||
|
{generateFeatures && (
|
||||||
|
<div className="space-y-2 pt-2 pl-7">
|
||||||
|
<label className="text-sm font-medium">Number of Features</label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{FEATURE_COUNT_OPTIONS.map((option) => (
|
||||||
|
<Button
|
||||||
|
key={option.value}
|
||||||
|
type="button"
|
||||||
|
variant={
|
||||||
|
featureCount === option.value ? "default" : "outline"
|
||||||
|
}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onFeatureCountChange(option.value)}
|
||||||
|
className={cn(
|
||||||
|
"flex-1 transition-all",
|
||||||
|
featureCount === option.value
|
||||||
|
? "bg-primary hover:bg-primary/90 text-primary-foreground"
|
||||||
|
: "bg-muted/30 hover:bg-muted/50 border-border"
|
||||||
|
)}
|
||||||
|
data-testid={`feature-count-${option.value}`}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{FEATURE_COUNT_OPTIONS.find((o) => o.value === featureCount)
|
||||||
|
?.warning && (
|
||||||
|
<p className="text-xs text-amber-500 flex items-center gap-1">
|
||||||
|
<Clock className="w-3 h-3" />
|
||||||
|
{
|
||||||
|
FEATURE_COUNT_OPTIONS.find((o) => o.value === featureCount)
|
||||||
|
?.warning
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="ghost" onClick={onSkip}>
|
||||||
|
Skip for now
|
||||||
|
</Button>
|
||||||
|
<Button onClick={onCreateSpec} disabled={!projectOverview.trim()}>
|
||||||
|
<Sparkles className="w-4 h-4 mr-2" />
|
||||||
|
Generate Spec
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -79,10 +79,13 @@ import {
|
|||||||
} from "@/lib/project-init";
|
} from "@/lib/project-init";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { themeOptions } from "@/config/theme-options";
|
import { themeOptions } from "@/config/theme-options";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
|
||||||
import type { SpecRegenerationEvent } from "@/types/electron";
|
import type { SpecRegenerationEvent } from "@/types/electron";
|
||||||
import { DeleteProjectDialog } from "@/components/views/settings-view/components/delete-project-dialog";
|
import { DeleteProjectDialog } from "@/components/views/settings-view/components/delete-project-dialog";
|
||||||
import { NewProjectModal } from "@/components/new-project-modal";
|
import { NewProjectModal } from "@/components/new-project-modal";
|
||||||
|
import {
|
||||||
|
ProjectSetupDialog,
|
||||||
|
type FeatureCount,
|
||||||
|
} from "@/components/layout/project-setup-dialog";
|
||||||
import {
|
import {
|
||||||
DndContext,
|
DndContext,
|
||||||
DragEndEvent,
|
DragEndEvent,
|
||||||
@@ -149,7 +152,8 @@ function SortableProjectItem({
|
|||||||
"flex items-center gap-2 px-2.5 py-2 rounded-lg cursor-pointer transition-all duration-200",
|
"flex items-center gap-2 px-2.5 py-2 rounded-lg cursor-pointer transition-all duration-200",
|
||||||
"text-muted-foreground hover:text-foreground hover:bg-accent/80",
|
"text-muted-foreground hover:text-foreground hover:bg-accent/80",
|
||||||
isDragging && "bg-accent shadow-lg scale-[1.02]",
|
isDragging && "bg-accent shadow-lg scale-[1.02]",
|
||||||
isHighlighted && "bg-brand-500/10 text-foreground ring-1 ring-brand-500/20"
|
isHighlighted &&
|
||||||
|
"bg-brand-500/10 text-foreground ring-1 ring-brand-500/20"
|
||||||
)}
|
)}
|
||||||
data-testid={`project-option-${project.id}`}
|
data-testid={`project-option-${project.id}`}
|
||||||
>
|
>
|
||||||
@@ -170,7 +174,9 @@ function SortableProjectItem({
|
|||||||
onClick={() => onSelect(project)}
|
onClick={() => onSelect(project)}
|
||||||
>
|
>
|
||||||
<Folder className="h-4 w-4 shrink-0 text-muted-foreground" />
|
<Folder className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||||
<span className="flex-1 truncate text-sm font-medium">{project.name}</span>
|
<span className="flex-1 truncate text-sm font-medium">
|
||||||
|
{project.name}
|
||||||
|
</span>
|
||||||
{currentProjectId === project.id && (
|
{currentProjectId === project.id && (
|
||||||
<Check className="h-4 w-4 text-brand-500 shrink-0" />
|
<Check className="h-4 w-4 text-brand-500 shrink-0" />
|
||||||
)}
|
)}
|
||||||
@@ -258,6 +264,7 @@ export function Sidebar() {
|
|||||||
const [setupProjectPath, setSetupProjectPath] = useState("");
|
const [setupProjectPath, setSetupProjectPath] = useState("");
|
||||||
const [projectOverview, setProjectOverview] = useState("");
|
const [projectOverview, setProjectOverview] = useState("");
|
||||||
const [generateFeatures, setGenerateFeatures] = useState(true);
|
const [generateFeatures, setGenerateFeatures] = useState(true);
|
||||||
|
const [featureCount, setFeatureCount] = useState<FeatureCount>(50);
|
||||||
const [showSpecIndicator, setShowSpecIndicator] = useState(true);
|
const [showSpecIndicator, setShowSpecIndicator] = useState(true);
|
||||||
|
|
||||||
// Derive isCreatingSpec from store state
|
// Derive isCreatingSpec from store state
|
||||||
@@ -463,7 +470,9 @@ export function Sidebar() {
|
|||||||
const result = await api.specRegeneration.create(
|
const result = await api.specRegeneration.create(
|
||||||
setupProjectPath,
|
setupProjectPath,
|
||||||
projectOverview.trim(),
|
projectOverview.trim(),
|
||||||
generateFeatures
|
generateFeatures,
|
||||||
|
undefined, // analyzeProject - use default
|
||||||
|
generateFeatures ? featureCount : undefined // only pass maxFeatures if generating features
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
@@ -472,6 +481,12 @@ export function Sidebar() {
|
|||||||
toast.error("Failed to create specification", {
|
toast.error("Failed to create specification", {
|
||||||
description: result.error,
|
description: result.error,
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
// Show processing toast to inform user
|
||||||
|
toast.info("Generating app specification...", {
|
||||||
|
description:
|
||||||
|
"This may take a minute. You'll be notified when complete.",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
// If successful, we'll wait for the events to update the state
|
// If successful, we'll wait for the events to update the state
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -481,7 +496,13 @@ export function Sidebar() {
|
|||||||
description: error instanceof Error ? error.message : "Unknown error",
|
description: error instanceof Error ? error.message : "Unknown error",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [setupProjectPath, projectOverview, setSpecCreatingForProject]);
|
}, [
|
||||||
|
setupProjectPath,
|
||||||
|
projectOverview,
|
||||||
|
generateFeatures,
|
||||||
|
featureCount,
|
||||||
|
setSpecCreatingForProject,
|
||||||
|
]);
|
||||||
|
|
||||||
// Handle skipping setup
|
// Handle skipping setup
|
||||||
const handleSkipSetup = useCallback(() => {
|
const handleSkipSetup = useCallback(() => {
|
||||||
@@ -1248,16 +1269,55 @@ export function Sidebar() {
|
|||||||
className="size-8 group-hover:rotate-12 transition-transform duration-300 ease-out"
|
className="size-8 group-hover:rotate-12 transition-transform duration-300 ease-out"
|
||||||
>
|
>
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="bg-collapsed" x1="0" y1="0" x2="256" y2="256" gradientUnits="userSpaceOnUse">
|
<linearGradient
|
||||||
<stop offset="0%" style={{ stopColor: 'var(--brand-400)' }} />
|
id="bg-collapsed"
|
||||||
<stop offset="100%" style={{ stopColor: 'var(--brand-600)' }} />
|
x1="0"
|
||||||
|
y1="0"
|
||||||
|
x2="256"
|
||||||
|
y2="256"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop
|
||||||
|
offset="0%"
|
||||||
|
style={{ stopColor: "var(--brand-400)" }}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset="100%"
|
||||||
|
style={{ stopColor: "var(--brand-600)" }}
|
||||||
|
/>
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<filter id="iconShadow-collapsed" x="-20%" y="-20%" width="140%" height="140%">
|
<filter
|
||||||
<feDropShadow dx="0" dy="4" stdDeviation="4" floodColor="#000000" floodOpacity="0.25" />
|
id="iconShadow-collapsed"
|
||||||
|
x="-20%"
|
||||||
|
y="-20%"
|
||||||
|
width="140%"
|
||||||
|
height="140%"
|
||||||
|
>
|
||||||
|
<feDropShadow
|
||||||
|
dx="0"
|
||||||
|
dy="4"
|
||||||
|
stdDeviation="4"
|
||||||
|
floodColor="#000000"
|
||||||
|
floodOpacity="0.25"
|
||||||
|
/>
|
||||||
</filter>
|
</filter>
|
||||||
</defs>
|
</defs>
|
||||||
<rect x="16" y="16" width="224" height="224" rx="56" fill="url(#bg-collapsed)" />
|
<rect
|
||||||
<g fill="none" stroke="#FFFFFF" strokeWidth="20" strokeLinecap="round" strokeLinejoin="round" filter="url(#iconShadow-collapsed)">
|
x="16"
|
||||||
|
y="16"
|
||||||
|
width="224"
|
||||||
|
height="224"
|
||||||
|
rx="56"
|
||||||
|
fill="url(#bg-collapsed)"
|
||||||
|
/>
|
||||||
|
<g
|
||||||
|
fill="none"
|
||||||
|
stroke="#FFFFFF"
|
||||||
|
strokeWidth="20"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
filter="url(#iconShadow-collapsed)"
|
||||||
|
>
|
||||||
<path d="M92 92 L52 128 L92 164" />
|
<path d="M92 92 L52 128 L92 164" />
|
||||||
<path d="M144 72 L116 184" />
|
<path d="M144 72 L116 184" />
|
||||||
<path d="M164 92 L204 128 L164 164" />
|
<path d="M164 92 L204 128 L164 164" />
|
||||||
@@ -1265,12 +1325,7 @@ export function Sidebar() {
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div className={cn("flex items-center gap-1", "hidden lg:flex")}>
|
||||||
className={cn(
|
|
||||||
"flex items-center gap-1",
|
|
||||||
"hidden lg:flex"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 256 256"
|
viewBox="0 0 256 256"
|
||||||
@@ -1279,16 +1334,55 @@ export function Sidebar() {
|
|||||||
className="h-[36.8px] w-[36.8px] group-hover:rotate-12 transition-transform duration-300 ease-out"
|
className="h-[36.8px] w-[36.8px] group-hover:rotate-12 transition-transform duration-300 ease-out"
|
||||||
>
|
>
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="bg-expanded" x1="0" y1="0" x2="256" y2="256" gradientUnits="userSpaceOnUse">
|
<linearGradient
|
||||||
<stop offset="0%" style={{ stopColor: 'var(--brand-400)' }} />
|
id="bg-expanded"
|
||||||
<stop offset="100%" style={{ stopColor: 'var(--brand-600)' }} />
|
x1="0"
|
||||||
|
y1="0"
|
||||||
|
x2="256"
|
||||||
|
y2="256"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop
|
||||||
|
offset="0%"
|
||||||
|
style={{ stopColor: "var(--brand-400)" }}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset="100%"
|
||||||
|
style={{ stopColor: "var(--brand-600)" }}
|
||||||
|
/>
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<filter id="iconShadow-expanded" x="-20%" y="-20%" width="140%" height="140%">
|
<filter
|
||||||
<feDropShadow dx="0" dy="4" stdDeviation="4" floodColor="#000000" floodOpacity="0.25" />
|
id="iconShadow-expanded"
|
||||||
|
x="-20%"
|
||||||
|
y="-20%"
|
||||||
|
width="140%"
|
||||||
|
height="140%"
|
||||||
|
>
|
||||||
|
<feDropShadow
|
||||||
|
dx="0"
|
||||||
|
dy="4"
|
||||||
|
stdDeviation="4"
|
||||||
|
floodColor="#000000"
|
||||||
|
floodOpacity="0.25"
|
||||||
|
/>
|
||||||
</filter>
|
</filter>
|
||||||
</defs>
|
</defs>
|
||||||
<rect x="16" y="16" width="224" height="224" rx="56" fill="url(#bg-expanded)" />
|
<rect
|
||||||
<g fill="none" stroke="#FFFFFF" strokeWidth="20" strokeLinecap="round" strokeLinejoin="round" filter="url(#iconShadow-expanded)">
|
x="16"
|
||||||
|
y="16"
|
||||||
|
width="224"
|
||||||
|
height="224"
|
||||||
|
rx="56"
|
||||||
|
fill="url(#bg-expanded)"
|
||||||
|
/>
|
||||||
|
<g
|
||||||
|
fill="none"
|
||||||
|
stroke="#FFFFFF"
|
||||||
|
strokeWidth="20"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
filter="url(#iconShadow-expanded)"
|
||||||
|
>
|
||||||
<path d="M92 92 L52 128 L92 164" />
|
<path d="M92 92 L52 128 L92 164" />
|
||||||
<path d="M144 72 L116 184" />
|
<path d="M144 72 L116 184" />
|
||||||
<path d="M164 92 L204 128 L164 164" />
|
<path d="M164 92 L204 128 L164 164" />
|
||||||
@@ -1371,7 +1465,7 @@ export function Sidebar() {
|
|||||||
onClick={() => setShowTrashDialog(true)}
|
onClick={() => setShowTrashDialog(true)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"group flex items-center justify-center px-3 h-[42px] rounded-xl",
|
"group flex items-center justify-center px-3 h-[42px] rounded-xl",
|
||||||
"relative overflow-hidden",
|
"relative",
|
||||||
"text-muted-foreground hover:text-destructive",
|
"text-muted-foreground hover:text-destructive",
|
||||||
// Subtle background that turns red on hover
|
// Subtle background that turns red on hover
|
||||||
"bg-accent/20 hover:bg-destructive/15",
|
"bg-accent/20 hover:bg-destructive/15",
|
||||||
@@ -1385,7 +1479,7 @@ export function Sidebar() {
|
|||||||
>
|
>
|
||||||
<Recycle className="size-4 shrink-0 transition-transform duration-200 group-hover:rotate-12" />
|
<Recycle className="size-4 shrink-0 transition-transform duration-200 group-hover:rotate-12" />
|
||||||
{trashedProjects.length > 0 && (
|
{trashedProjects.length > 0 && (
|
||||||
<span className="absolute -top-1.5 -right-1.5 flex items-center justify-center min-w-4 h-4 px-1 text-[9px] font-bold rounded-full bg-destructive text-destructive-foreground shadow-sm">
|
<span className="absolute -top-1.5 -right-1.5 z-10 flex items-center justify-center min-w-4 h-4 px-1 text-[9px] font-bold rounded-full bg-red-500 text-white shadow-md ring-1 ring-red-600/50">
|
||||||
{trashedProjects.length > 9 ? "9+" : trashedProjects.length}
|
{trashedProjects.length > 9 ? "9+" : trashedProjects.length}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -1413,7 +1507,8 @@ export function Sidebar() {
|
|||||||
"text-foreground titlebar-no-drag min-w-0",
|
"text-foreground titlebar-no-drag min-w-0",
|
||||||
"transition-all duration-200 ease-out",
|
"transition-all duration-200 ease-out",
|
||||||
"hover:scale-[1.01] active:scale-[0.99]",
|
"hover:scale-[1.01] active:scale-[0.99]",
|
||||||
isProjectPickerOpen && "from-brand-500/10 to-brand-600/5 border-brand-500/30 ring-2 ring-brand-500/20 shadow-lg shadow-brand-500/5"
|
isProjectPickerOpen &&
|
||||||
|
"from-brand-500/10 to-brand-600/5 border-brand-500/30 ring-2 ring-brand-500/20 shadow-lg shadow-brand-500/5"
|
||||||
)}
|
)}
|
||||||
data-testid="project-selector"
|
data-testid="project-selector"
|
||||||
>
|
>
|
||||||
@@ -1430,10 +1525,12 @@ export function Sidebar() {
|
|||||||
>
|
>
|
||||||
{formatShortcut(shortcuts.projectPicker, true)}
|
{formatShortcut(shortcuts.projectPicker, true)}
|
||||||
</span>
|
</span>
|
||||||
<ChevronDown className={cn(
|
<ChevronDown
|
||||||
|
className={cn(
|
||||||
"h-4 w-4 text-muted-foreground shrink-0 transition-transform duration-200",
|
"h-4 w-4 text-muted-foreground shrink-0 transition-transform duration-200",
|
||||||
isProjectPickerOpen && "rotate-180"
|
isProjectPickerOpen && "rotate-180"
|
||||||
)} />
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
@@ -1499,7 +1596,11 @@ export function Sidebar() {
|
|||||||
{/* Keyboard hint */}
|
{/* Keyboard hint */}
|
||||||
<div className="px-2 pt-2 mt-1.5 border-t border-border/50">
|
<div className="px-2 pt-2 mt-1.5 border-t border-border/50">
|
||||||
<p className="text-[10px] text-muted-foreground text-center tracking-wide">
|
<p className="text-[10px] text-muted-foreground text-center tracking-wide">
|
||||||
<span className="text-foreground/60">arrow</span> navigate <span className="mx-1 text-foreground/30">|</span> <span className="text-foreground/60">enter</span> select <span className="mx-1 text-foreground/30">|</span> <span className="text-foreground/60">esc</span> close
|
<span className="text-foreground/60">arrow</span> navigate{" "}
|
||||||
|
<span className="mx-1 text-foreground/30">|</span>{" "}
|
||||||
|
<span className="text-foreground/60">enter</span> select{" "}
|
||||||
|
<span className="mx-1 text-foreground/30">|</span>{" "}
|
||||||
|
<span className="text-foreground/60">esc</span> close
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
@@ -1531,7 +1632,10 @@ export function Sidebar() {
|
|||||||
<MoreVertical className="w-4 h-4" />
|
<MoreVertical className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" className="w-56 bg-popover/95 backdrop-blur-xl">
|
<DropdownMenuContent
|
||||||
|
align="end"
|
||||||
|
className="w-56 bg-popover/95 backdrop-blur-xl"
|
||||||
|
>
|
||||||
{/* Project Theme Submenu */}
|
{/* Project Theme Submenu */}
|
||||||
<DropdownMenuSub>
|
<DropdownMenuSub>
|
||||||
<DropdownMenuSubTrigger data-testid="project-theme-trigger">
|
<DropdownMenuSubTrigger data-testid="project-theme-trigger">
|
||||||
@@ -1809,13 +1913,15 @@ export function Sidebar() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bottom Section - Running Agents / Bug Report / Settings */}
|
{/* Bottom Section - Running Agents / Bug Report / Settings */}
|
||||||
<div className={cn(
|
<div
|
||||||
|
className={cn(
|
||||||
"shrink-0",
|
"shrink-0",
|
||||||
// Top border with gradient fade
|
// Top border with gradient fade
|
||||||
"border-t border-border/40",
|
"border-t border-border/40",
|
||||||
// Elevated background for visual separation
|
// Elevated background for visual separation
|
||||||
"bg-gradient-to-t from-background/10 via-sidebar/50 to-transparent"
|
"bg-gradient-to-t from-background/10 via-sidebar/50 to-transparent"
|
||||||
)}>
|
)}
|
||||||
|
>
|
||||||
{/* Course Promo Badge */}
|
{/* Course Promo Badge */}
|
||||||
<CoursePromoBadge sidebarOpen={sidebarOpen} />
|
<CoursePromoBadge sidebarOpen={sidebarOpen} />
|
||||||
{/* Wiki Link */}
|
{/* Wiki Link */}
|
||||||
@@ -1865,14 +1971,16 @@ export function Sidebar() {
|
|||||||
Wiki
|
Wiki
|
||||||
</span>
|
</span>
|
||||||
{!sidebarOpen && (
|
{!sidebarOpen && (
|
||||||
<span className={cn(
|
<span
|
||||||
|
className={cn(
|
||||||
"absolute left-full ml-3 px-2.5 py-1.5 rounded-lg",
|
"absolute left-full ml-3 px-2.5 py-1.5 rounded-lg",
|
||||||
"bg-popover text-popover-foreground text-xs font-medium",
|
"bg-popover text-popover-foreground text-xs font-medium",
|
||||||
"border border-border shadow-lg",
|
"border border-border shadow-lg",
|
||||||
"opacity-0 group-hover:opacity-100",
|
"opacity-0 group-hover:opacity-100",
|
||||||
"transition-all duration-200 whitespace-nowrap z-50",
|
"transition-all duration-200 whitespace-nowrap z-50",
|
||||||
"translate-x-1 group-hover:translate-x-0"
|
"translate-x-1 group-hover:translate-x-0"
|
||||||
)}>
|
)}
|
||||||
|
>
|
||||||
Wiki
|
Wiki
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -1957,14 +2065,16 @@ export function Sidebar() {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{!sidebarOpen && (
|
{!sidebarOpen && (
|
||||||
<span className={cn(
|
<span
|
||||||
|
className={cn(
|
||||||
"absolute left-full ml-3 px-2.5 py-1.5 rounded-lg",
|
"absolute left-full ml-3 px-2.5 py-1.5 rounded-lg",
|
||||||
"bg-popover text-popover-foreground text-xs font-medium",
|
"bg-popover text-popover-foreground text-xs font-medium",
|
||||||
"border border-border shadow-lg",
|
"border border-border shadow-lg",
|
||||||
"opacity-0 group-hover:opacity-100",
|
"opacity-0 group-hover:opacity-100",
|
||||||
"transition-all duration-200 whitespace-nowrap z-50",
|
"transition-all duration-200 whitespace-nowrap z-50",
|
||||||
"translate-x-1 group-hover:translate-x-0"
|
"translate-x-1 group-hover:translate-x-0"
|
||||||
)}>
|
)}
|
||||||
|
>
|
||||||
Running Agents
|
Running Agents
|
||||||
{runningAgentsCount > 0 && (
|
{runningAgentsCount > 0 && (
|
||||||
<span className="ml-2 px-1.5 py-0.5 bg-brand-500 text-white rounded-full text-[10px] font-semibold">
|
<span className="ml-2 px-1.5 py-0.5 bg-brand-500 text-white rounded-full text-[10px] font-semibold">
|
||||||
@@ -2035,14 +2145,16 @@ export function Sidebar() {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{!sidebarOpen && (
|
{!sidebarOpen && (
|
||||||
<span className={cn(
|
<span
|
||||||
|
className={cn(
|
||||||
"absolute left-full ml-3 px-2.5 py-1.5 rounded-lg",
|
"absolute left-full ml-3 px-2.5 py-1.5 rounded-lg",
|
||||||
"bg-popover text-popover-foreground text-xs font-medium",
|
"bg-popover text-popover-foreground text-xs font-medium",
|
||||||
"border border-border shadow-lg",
|
"border border-border shadow-lg",
|
||||||
"opacity-0 group-hover:opacity-100",
|
"opacity-0 group-hover:opacity-100",
|
||||||
"transition-all duration-200 whitespace-nowrap z-50",
|
"transition-all duration-200 whitespace-nowrap z-50",
|
||||||
"translate-x-1 group-hover:translate-x-0"
|
"translate-x-1 group-hover:translate-x-0"
|
||||||
)}>
|
)}
|
||||||
|
>
|
||||||
Settings
|
Settings
|
||||||
<span className="ml-2 px-1.5 py-0.5 bg-muted rounded text-[10px] font-mono text-muted-foreground">
|
<span className="ml-2 px-1.5 py-0.5 bg-muted rounded text-[10px] font-mono text-muted-foreground">
|
||||||
{formatShortcut(shortcuts.settings, true)}
|
{formatShortcut(shortcuts.settings, true)}
|
||||||
@@ -2141,80 +2253,19 @@ export function Sidebar() {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* New Project Setup Dialog */}
|
{/* New Project Setup Dialog */}
|
||||||
<Dialog
|
<ProjectSetupDialog
|
||||||
open={showSetupDialog}
|
open={showSetupDialog}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={setShowSetupDialog}
|
||||||
if (!open && !isCreatingSpec) {
|
projectOverview={projectOverview}
|
||||||
handleSkipSetup();
|
onProjectOverviewChange={setProjectOverview}
|
||||||
}
|
generateFeatures={generateFeatures}
|
||||||
}}
|
onGenerateFeaturesChange={setGenerateFeatures}
|
||||||
>
|
featureCount={featureCount}
|
||||||
<DialogContent className="max-w-2xl bg-popover/95 backdrop-blur-xl">
|
onFeatureCountChange={setFeatureCount}
|
||||||
<DialogHeader>
|
onCreateSpec={handleCreateInitialSpec}
|
||||||
<DialogTitle>Set Up Your Project</DialogTitle>
|
onSkip={handleSkipSetup}
|
||||||
<DialogDescription className="text-muted-foreground">
|
isCreatingSpec={isCreatingSpec}
|
||||||
We didn't find an app_spec.txt file. Let us help you generate
|
|
||||||
your app_spec.txt to help describe your project for our system.
|
|
||||||
We'll analyze your project's tech stack and create a
|
|
||||||
comprehensive specification.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div className="space-y-4 py-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-sm font-medium">Project Overview</label>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Describe what your project does and what features you want to
|
|
||||||
build. Be as detailed as you want - this will help us create a
|
|
||||||
better specification.
|
|
||||||
</p>
|
|
||||||
<textarea
|
|
||||||
className="w-full h-48 p-3 rounded-lg border border-border bg-background/50 font-mono text-sm resize-none focus:outline-none focus:ring-2 focus:ring-brand-500/30 focus:border-brand-500/50 transition-all"
|
|
||||||
value={projectOverview}
|
|
||||||
onChange={(e) => setProjectOverview(e.target.value)}
|
|
||||||
placeholder="e.g., A project management tool that allows teams to track tasks, manage sprints, and visualize progress through kanban boards. It should support user authentication, real-time updates, and file attachments..."
|
|
||||||
autoFocus
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-start space-x-3 pt-2">
|
|
||||||
<Checkbox
|
|
||||||
id="sidebar-generate-features"
|
|
||||||
checked={generateFeatures}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
setGenerateFeatures(checked === true)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<label
|
|
||||||
htmlFor="sidebar-generate-features"
|
|
||||||
className="text-sm font-medium cursor-pointer"
|
|
||||||
>
|
|
||||||
Generate feature list
|
|
||||||
</label>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Automatically create features in the features folder from the
|
|
||||||
implementation roadmap after the spec is generated.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button variant="ghost" onClick={handleSkipSetup}>
|
|
||||||
Skip for now
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleCreateInitialSpec}
|
|
||||||
disabled={!projectOverview.trim()}
|
|
||||||
className="bg-gradient-to-r from-brand-500 to-brand-600 hover:from-brand-600 hover:to-brand-600 text-white border-0"
|
|
||||||
>
|
|
||||||
<Sparkles className="w-4 h-4 mr-2" />
|
|
||||||
Generate Spec
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
{/* New Project Onboarding Dialog */}
|
{/* New Project Onboarding Dialog */}
|
||||||
<Dialog
|
<Dialog
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ const DropdownMenuSubContent = React.forwardRef<
|
|||||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Portal>
|
||||||
<DropdownMenuPrimitive.SubContent
|
<DropdownMenuPrimitive.SubContent
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -51,6 +52,7 @@ const DropdownMenuSubContent = React.forwardRef<
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
</DropdownMenuPrimitive.Portal>
|
||||||
))
|
))
|
||||||
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName
|
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName
|
||||||
|
|
||||||
|
|||||||
@@ -90,6 +90,8 @@ import {
|
|||||||
Maximize2,
|
Maximize2,
|
||||||
Shuffle,
|
Shuffle,
|
||||||
ImageIcon,
|
ImageIcon,
|
||||||
|
Archive,
|
||||||
|
ArchiveRestore,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Slider } from "@/components/ui/slider";
|
import { Slider } from "@/components/ui/slider";
|
||||||
@@ -112,9 +114,21 @@ type ColumnId = Feature["status"];
|
|||||||
|
|
||||||
const COLUMNS: { id: ColumnId; title: string; colorClass: string }[] = [
|
const COLUMNS: { id: ColumnId; title: string; colorClass: string }[] = [
|
||||||
{ id: "backlog", title: "Backlog", colorClass: "bg-[var(--status-backlog)]" },
|
{ id: "backlog", title: "Backlog", colorClass: "bg-[var(--status-backlog)]" },
|
||||||
{ id: "in_progress", title: "In Progress", colorClass: "bg-[var(--status-in-progress)]" },
|
{
|
||||||
{ id: "waiting_approval", title: "Waiting Approval", colorClass: "bg-[var(--status-waiting)]" },
|
id: "in_progress",
|
||||||
{ id: "verified", title: "Verified", colorClass: "bg-[var(--status-success)]" },
|
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 = {
|
type ModelOption = {
|
||||||
@@ -208,6 +222,9 @@ export function BoardView() {
|
|||||||
useState(false);
|
useState(false);
|
||||||
const [showBoardBackgroundModal, setShowBoardBackgroundModal] =
|
const [showBoardBackgroundModal, setShowBoardBackgroundModal] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
const [showCompletedModal, setShowCompletedModal] = useState(false);
|
||||||
|
const [deleteCompletedFeature, setDeleteCompletedFeature] =
|
||||||
|
useState<Feature | null>(null);
|
||||||
const [persistedCategories, setPersistedCategories] = useState<string[]>([]);
|
const [persistedCategories, setPersistedCategories] = useState<string[]>([]);
|
||||||
const [showFollowUpDialog, setShowFollowUpDialog] = useState(false);
|
const [showFollowUpDialog, setShowFollowUpDialog] = useState(false);
|
||||||
const [followUpFeature, setFollowUpFeature] = useState<Feature | null>(null);
|
const [followUpFeature, setFollowUpFeature] = useState<Feature | null>(null);
|
||||||
@@ -270,7 +287,7 @@ export function BoardView() {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Subscribe to spec regeneration events to clear state on completion
|
// Subscribe to spec regeneration events to clear creating state on completion
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
if (!api.specRegeneration) return;
|
if (!api.specRegeneration) return;
|
||||||
@@ -490,6 +507,30 @@ export function BoardView() {
|
|||||||
}
|
}
|
||||||
}, [currentProject, setFeatures]);
|
}, [currentProject, setFeatures]);
|
||||||
|
|
||||||
|
// Subscribe to spec regeneration complete events to refresh kanban board
|
||||||
|
useEffect(() => {
|
||||||
|
const api = getElectronAPI();
|
||||||
|
if (!api.specRegeneration) return;
|
||||||
|
|
||||||
|
const unsubscribe = api.specRegeneration.onEvent((event) => {
|
||||||
|
// Refresh the kanban board when spec regeneration completes for the current project
|
||||||
|
if (
|
||||||
|
event.type === "spec_regeneration_complete" &&
|
||||||
|
currentProject &&
|
||||||
|
event.projectPath === currentProject.path
|
||||||
|
) {
|
||||||
|
console.log(
|
||||||
|
"[BoardView] Spec regeneration complete, refreshing features"
|
||||||
|
);
|
||||||
|
loadFeatures();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
}, [currentProject, loadFeatures]);
|
||||||
|
|
||||||
// Load persisted categories from file
|
// Load persisted categories from file
|
||||||
const loadCategories = useCallback(async () => {
|
const loadCategories = useCallback(async () => {
|
||||||
if (!currentProject) return;
|
if (!currentProject) return;
|
||||||
@@ -844,34 +885,12 @@ export function BoardView() {
|
|||||||
// Same column, nothing to do
|
// Same column, nothing to do
|
||||||
if (targetStatus === draggedFeature.status) return;
|
if (targetStatus === draggedFeature.status) return;
|
||||||
|
|
||||||
// Check concurrency limit before moving to in_progress (only for backlog -> in_progress and if running agent)
|
|
||||||
if (
|
|
||||||
targetStatus === "in_progress" &&
|
|
||||||
draggedFeature.status === "backlog" &&
|
|
||||||
!autoMode.canStartNewTask
|
|
||||||
) {
|
|
||||||
console.log("[Board] Cannot start new task - at max concurrency limit");
|
|
||||||
toast.error("Concurrency limit reached", {
|
|
||||||
description: `You can only have ${autoMode.maxConcurrency} task${
|
|
||||||
autoMode.maxConcurrency > 1 ? "s" : ""
|
|
||||||
} running at a time. Wait for a task to complete or increase the limit.`,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle different drag scenarios
|
// Handle different drag scenarios
|
||||||
if (draggedFeature.status === "backlog") {
|
if (draggedFeature.status === "backlog") {
|
||||||
// From backlog
|
// From backlog
|
||||||
if (targetStatus === "in_progress") {
|
if (targetStatus === "in_progress") {
|
||||||
// Update with startedAt timestamp
|
// Use helper function to handle concurrency check and start implementation
|
||||||
const updates = {
|
await handleStartImplementation(draggedFeature);
|
||||||
status: targetStatus,
|
|
||||||
startedAt: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
updateFeature(featureId, updates);
|
|
||||||
persistFeatureUpdate(featureId, updates);
|
|
||||||
console.log("[Board] Feature moved to in_progress, starting agent...");
|
|
||||||
await handleRunFeature(draggedFeature);
|
|
||||||
} else {
|
} else {
|
||||||
moveFeature(featureId, targetStatus);
|
moveFeature(featureId, targetStatus);
|
||||||
persistFeatureUpdate(featureId, { status: targetStatus });
|
persistFeatureUpdate(featureId, { status: targetStatus });
|
||||||
@@ -1144,6 +1163,28 @@ export function BoardView() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helper function to start implementing a feature (from backlog to in_progress)
|
||||||
|
const handleStartImplementation = async (feature: Feature) => {
|
||||||
|
if (!autoMode.canStartNewTask) {
|
||||||
|
toast.error("Concurrency limit reached", {
|
||||||
|
description: `You can only have ${autoMode.maxConcurrency} task${
|
||||||
|
autoMode.maxConcurrency > 1 ? "s" : ""
|
||||||
|
} running at a time. Wait for a task to complete or increase the limit.`,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updates = {
|
||||||
|
status: "in_progress" as const,
|
||||||
|
startedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
updateFeature(feature.id, updates);
|
||||||
|
persistFeatureUpdate(feature.id, updates);
|
||||||
|
console.log("[Board] Feature moved to in_progress, starting agent...");
|
||||||
|
await handleRunFeature(feature);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
const handleVerifyFeature = async (feature: Feature) => {
|
const handleVerifyFeature = async (feature: Feature) => {
|
||||||
if (!currentProject) return;
|
if (!currentProject) return;
|
||||||
|
|
||||||
@@ -1497,6 +1538,47 @@ export function BoardView() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Complete a verified feature (move to completed/archived)
|
||||||
|
const handleCompleteFeature = (feature: Feature) => {
|
||||||
|
console.log("[Board] Completing feature:", {
|
||||||
|
id: feature.id,
|
||||||
|
description: feature.description,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updates = {
|
||||||
|
status: "completed" as const,
|
||||||
|
};
|
||||||
|
updateFeature(feature.id, updates);
|
||||||
|
persistFeatureUpdate(feature.id, updates);
|
||||||
|
|
||||||
|
toast.success("Feature completed", {
|
||||||
|
description: `Archived: ${feature.description.slice(0, 50)}${
|
||||||
|
feature.description.length > 50 ? "..." : ""
|
||||||
|
}`,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Unarchive a completed feature (move back to verified)
|
||||||
|
const handleUnarchiveFeature = (feature: Feature) => {
|
||||||
|
console.log("[Board] Unarchiving feature:", {
|
||||||
|
id: feature.id,
|
||||||
|
description: feature.description,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updates = {
|
||||||
|
status: "verified" as const,
|
||||||
|
};
|
||||||
|
updateFeature(feature.id, updates);
|
||||||
|
persistFeatureUpdate(feature.id, updates);
|
||||||
|
|
||||||
|
toast.success("Feature restored", {
|
||||||
|
description: `Moved back to verified: ${feature.description.slice(
|
||||||
|
0,
|
||||||
|
50
|
||||||
|
)}${feature.description.length > 50 ? "..." : ""}`,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const checkContextExists = async (featureId: string): Promise<boolean> => {
|
const checkContextExists = async (featureId: string): Promise<boolean> => {
|
||||||
if (!currentProject) return false;
|
if (!currentProject) return false;
|
||||||
|
|
||||||
@@ -1518,6 +1600,11 @@ export function BoardView() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Memoize completed features for the archive modal
|
||||||
|
const completedFeatures = useMemo(() => {
|
||||||
|
return features.filter((f) => f.status === "completed");
|
||||||
|
}, [features]);
|
||||||
|
|
||||||
// Memoize column features to prevent unnecessary re-renders
|
// Memoize column features to prevent unnecessary re-renders
|
||||||
const columnFeaturesMap = useMemo(() => {
|
const columnFeaturesMap = useMemo(() => {
|
||||||
const map: Record<ColumnId, Feature[]> = {
|
const map: Record<ColumnId, Feature[]> = {
|
||||||
@@ -1525,6 +1612,7 @@ export function BoardView() {
|
|||||||
in_progress: [],
|
in_progress: [],
|
||||||
waiting_approval: [],
|
waiting_approval: [],
|
||||||
verified: [],
|
verified: [],
|
||||||
|
completed: [], // Completed features are shown in the archive modal, not as a column
|
||||||
};
|
};
|
||||||
|
|
||||||
// Filter features by search query (case-insensitive)
|
// Filter features by search query (case-insensitive)
|
||||||
@@ -1903,6 +1991,31 @@ export function BoardView() {
|
|||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
{/* Completed/Archived Features Button */}
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setShowCompletedModal(true)}
|
||||||
|
className="h-8 px-2 relative"
|
||||||
|
data-testid="completed-features-button"
|
||||||
|
>
|
||||||
|
<Archive className="w-4 h-4" />
|
||||||
|
{completedFeatures.length > 0 && (
|
||||||
|
<span className="absolute -top-1 -right-1 bg-brand-500 text-white text-[10px] font-bold rounded-full w-4 h-4 flex items-center justify-center">
|
||||||
|
{completedFeatures.length > 99
|
||||||
|
? "99+"
|
||||||
|
: completedFeatures.length}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Completed Features ({completedFeatures.length})</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
{/* Kanban Card Detail Level Toggle */}
|
{/* Kanban Card Detail Level Toggle */}
|
||||||
<div
|
<div
|
||||||
className="flex items-center rounded-lg bg-secondary border border-border"
|
className="flex items-center rounded-lg bg-secondary border border-border"
|
||||||
@@ -2067,7 +2180,7 @@ export function BoardView() {
|
|||||||
data-testid="start-next-button"
|
data-testid="start-next-button"
|
||||||
>
|
>
|
||||||
<FastForward className="w-3 h-3 mr-1" />
|
<FastForward className="w-3 h-3 mr-1" />
|
||||||
Pull Top
|
Make
|
||||||
</HotkeyButton>
|
</HotkeyButton>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -2107,6 +2220,12 @@ export function BoardView() {
|
|||||||
onCommit={() => handleCommitFeature(feature)}
|
onCommit={() => handleCommitFeature(feature)}
|
||||||
onRevert={() => handleRevertFeature(feature)}
|
onRevert={() => handleRevertFeature(feature)}
|
||||||
onMerge={() => handleMergeFeature(feature)}
|
onMerge={() => handleMergeFeature(feature)}
|
||||||
|
onComplete={() =>
|
||||||
|
handleCompleteFeature(feature)
|
||||||
|
}
|
||||||
|
onImplement={() =>
|
||||||
|
handleStartImplementation(feature)
|
||||||
|
}
|
||||||
hasContext={featuresWithContext.has(feature.id)}
|
hasContext={featuresWithContext.has(feature.id)}
|
||||||
isCurrentAutoTask={runningAutoTasks.includes(
|
isCurrentAutoTask={runningAutoTasks.includes(
|
||||||
feature.id
|
feature.id
|
||||||
@@ -2131,10 +2250,12 @@ export function BoardView() {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DragOverlay dropAnimation={{
|
<DragOverlay
|
||||||
|
dropAnimation={{
|
||||||
duration: 200,
|
duration: 200,
|
||||||
easing: 'cubic-bezier(0.18, 0.67, 0.6, 1.22)',
|
easing: "cubic-bezier(0.18, 0.67, 0.6, 1.22)",
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
{activeFeature && (
|
{activeFeature && (
|
||||||
<Card className="w-72 rotate-2 shadow-2xl shadow-black/25 border-primary/50 bg-card/95 backdrop-blur-sm transition-transform">
|
<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">
|
<CardHeader className="p-3">
|
||||||
@@ -2160,6 +2281,136 @@ export function BoardView() {
|
|||||||
onOpenChange={setShowBoardBackgroundModal}
|
onOpenChange={setShowBoardBackgroundModal}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Completed Features Modal */}
|
||||||
|
<Dialog open={showCompletedModal} onOpenChange={setShowCompletedModal}>
|
||||||
|
<DialogContent className="max-w-5xl max-h-[80vh] overflow-hidden flex flex-col">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<Archive className="w-5 h-5 text-brand-500" />
|
||||||
|
Completed Features
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{completedFeatures.length === 0
|
||||||
|
? "No completed features yet. Features you complete will appear here."
|
||||||
|
: `${completedFeatures.length} completed feature${
|
||||||
|
completedFeatures.length === 1 ? "" : "s"
|
||||||
|
}`}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="flex-1 overflow-y-auto py-4">
|
||||||
|
{completedFeatures.length === 0 ? (
|
||||||
|
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
|
||||||
|
<Archive className="w-12 h-12 mb-4 opacity-50" />
|
||||||
|
<p className="text-lg font-medium">No completed features</p>
|
||||||
|
<p className="text-sm">
|
||||||
|
Complete features from the Verified column to archive them
|
||||||
|
here.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||||
|
{completedFeatures.map((feature) => (
|
||||||
|
<Card
|
||||||
|
key={feature.id}
|
||||||
|
className="flex flex-col"
|
||||||
|
data-testid={`completed-card-${feature.id}`}
|
||||||
|
>
|
||||||
|
<CardHeader className="p-3 pb-2 flex-1">
|
||||||
|
<CardTitle className="text-sm leading-tight line-clamp-3">
|
||||||
|
{feature.description || feature.summary || feature.id}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="text-xs mt-1 truncate">
|
||||||
|
{feature.category || "Uncategorized"}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<div className="p-3 pt-0 flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
className="flex-1 h-7 text-xs"
|
||||||
|
onClick={() => handleUnarchiveFeature(feature)}
|
||||||
|
data-testid={`unarchive-${feature.id}`}
|
||||||
|
>
|
||||||
|
<ArchiveRestore className="w-3 h-3 mr-1" />
|
||||||
|
Restore
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 w-7 p-0 text-muted-foreground hover:text-destructive"
|
||||||
|
onClick={() => setDeleteCompletedFeature(feature)}
|
||||||
|
data-testid={`delete-completed-${feature.id}`}
|
||||||
|
title="Delete"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => setShowCompletedModal(false)}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Delete Completed Feature Confirmation Dialog */}
|
||||||
|
<Dialog
|
||||||
|
open={!!deleteCompletedFeature}
|
||||||
|
onOpenChange={(open) => !open && setDeleteCompletedFeature(null)}
|
||||||
|
>
|
||||||
|
<DialogContent data-testid="delete-completed-confirmation-dialog">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2 text-destructive">
|
||||||
|
<Trash2 className="w-5 h-5" />
|
||||||
|
Delete Feature
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Are you sure you want to permanently delete this feature?
|
||||||
|
<span className="block mt-2 font-medium text-foreground">
|
||||||
|
"{deleteCompletedFeature?.description?.slice(0, 100)}
|
||||||
|
{(deleteCompletedFeature?.description?.length ?? 0) > 100
|
||||||
|
? "..."
|
||||||
|
: ""}
|
||||||
|
"
|
||||||
|
</span>
|
||||||
|
<span className="block mt-2 text-destructive font-medium">
|
||||||
|
This action cannot be undone.
|
||||||
|
</span>
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => setDeleteCompletedFeature(null)}
|
||||||
|
data-testid="cancel-delete-completed-button"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={async () => {
|
||||||
|
if (deleteCompletedFeature) {
|
||||||
|
await handleDeleteFeature(deleteCompletedFeature.id);
|
||||||
|
setDeleteCompletedFeature(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
data-testid="confirm-delete-completed-button"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4 mr-2" />
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
{/* Add Feature Dialog */}
|
{/* Add Feature Dialog */}
|
||||||
<Dialog
|
<Dialog
|
||||||
open={showAddDialog}
|
open={showAddDialog}
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ import {
|
|||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
Brain,
|
Brain,
|
||||||
|
Wand2,
|
||||||
|
Archive,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { CountUpTimer } from "@/components/ui/count-up-timer";
|
import { CountUpTimer } from "@/components/ui/count-up-timer";
|
||||||
import { getElectronAPI } from "@/lib/electron";
|
import { getElectronAPI } from "@/lib/electron";
|
||||||
@@ -103,6 +105,8 @@ interface KanbanCardProps {
|
|||||||
onCommit?: () => void;
|
onCommit?: () => void;
|
||||||
onRevert?: () => void;
|
onRevert?: () => void;
|
||||||
onMerge?: () => void;
|
onMerge?: () => void;
|
||||||
|
onImplement?: () => void;
|
||||||
|
onComplete?: () => void;
|
||||||
hasContext?: boolean;
|
hasContext?: boolean;
|
||||||
isCurrentAutoTask?: boolean;
|
isCurrentAutoTask?: boolean;
|
||||||
shortcutKey?: string;
|
shortcutKey?: string;
|
||||||
@@ -128,6 +132,8 @@ export const KanbanCard = memo(function KanbanCard({
|
|||||||
onCommit,
|
onCommit,
|
||||||
onRevert,
|
onRevert,
|
||||||
onMerge,
|
onMerge,
|
||||||
|
onImplement,
|
||||||
|
onComplete,
|
||||||
hasContext,
|
hasContext,
|
||||||
isCurrentAutoTask,
|
isCurrentAutoTask,
|
||||||
shortcutKey,
|
shortcutKey,
|
||||||
@@ -436,7 +442,82 @@ export const KanbanCard = memo(function KanbanCard({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!isCurrentAutoTask && (
|
{!isCurrentAutoTask && feature.status === "backlog" && (
|
||||||
|
<div className="absolute top-2 right-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-6 w-6 p-0 hover:bg-white/10 text-muted-foreground hover:text-destructive"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleDeleteClick(e);
|
||||||
|
}}
|
||||||
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
|
data-testid={`delete-backlog-${feature.id}`}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!isCurrentAutoTask &&
|
||||||
|
(feature.status === "waiting_approval" ||
|
||||||
|
feature.status === "verified") && (
|
||||||
|
<div className="absolute top-2 right-2 flex items-center gap-1">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-6 w-6 p-0 hover:bg-white/10 text-muted-foreground hover:text-foreground"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onEdit();
|
||||||
|
}}
|
||||||
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
|
data-testid={`edit-${
|
||||||
|
feature.status === "waiting_approval" ? "waiting" : "verified"
|
||||||
|
}-${feature.id}`}
|
||||||
|
title="Edit"
|
||||||
|
>
|
||||||
|
<Edit className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
{onViewOutput && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-6 w-6 p-0 hover:bg-white/10 text-muted-foreground hover:text-foreground"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onViewOutput();
|
||||||
|
}}
|
||||||
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
|
data-testid={`logs-${
|
||||||
|
feature.status === "waiting_approval"
|
||||||
|
? "waiting"
|
||||||
|
: "verified"
|
||||||
|
}-${feature.id}`}
|
||||||
|
title="Logs"
|
||||||
|
>
|
||||||
|
<FileText className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-6 w-6 p-0 hover:bg-white/10 text-muted-foreground hover:text-destructive"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleDeleteClick(e);
|
||||||
|
}}
|
||||||
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
|
data-testid={`delete-${
|
||||||
|
feature.status === "waiting_approval" ? "waiting" : "verified"
|
||||||
|
}-${feature.id}`}
|
||||||
|
title="Delete"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!isCurrentAutoTask && feature.status === "in_progress" && (
|
||||||
<div className="absolute top-2 right-2">
|
<div className="absolute top-2 right-2">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
@@ -463,7 +544,7 @@ export const KanbanCard = memo(function KanbanCard({
|
|||||||
<Edit className="w-3 h-3 mr-2" />
|
<Edit className="w-3 h-3 mr-2" />
|
||||||
Edit
|
Edit
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
{onViewOutput && feature.status !== "backlog" && (
|
{onViewOutput && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -647,7 +728,8 @@ export const KanbanCard = memo(function KanbanCard({
|
|||||||
"break-words hyphens-auto line-clamp-2 leading-relaxed",
|
"break-words hyphens-auto line-clamp-2 leading-relaxed",
|
||||||
todo.status === "completed" &&
|
todo.status === "completed" &&
|
||||||
"text-muted-foreground/60 line-through",
|
"text-muted-foreground/60 line-through",
|
||||||
todo.status === "in_progress" && "text-[var(--status-warning)]",
|
todo.status === "in_progress" &&
|
||||||
|
"text-[var(--status-warning)]",
|
||||||
todo.status === "pending" &&
|
todo.status === "pending" &&
|
||||||
"text-muted-foreground/80"
|
"text-muted-foreground/80"
|
||||||
)}
|
)}
|
||||||
@@ -833,11 +915,12 @@ export const KanbanCard = memo(function KanbanCard({
|
|||||||
)}
|
)}
|
||||||
{!isCurrentAutoTask && feature.status === "verified" && (
|
{!isCurrentAutoTask && feature.status === "verified" && (
|
||||||
<>
|
<>
|
||||||
{hasContext && onViewOutput && (
|
{/* Logs button - styled like Refine */}
|
||||||
|
{onViewOutput && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-7 text-[11px] px-2"
|
className="flex-1 h-7 text-xs min-w-0"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onViewOutput();
|
onViewOutput();
|
||||||
@@ -845,7 +928,25 @@ export const KanbanCard = memo(function KanbanCard({
|
|||||||
onPointerDown={(e) => e.stopPropagation()}
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
data-testid={`view-output-verified-${feature.id}`}
|
data-testid={`view-output-verified-${feature.id}`}
|
||||||
>
|
>
|
||||||
<FileText className="w-3 h-3" />
|
<FileText className="w-3 h-3 mr-1 shrink-0" />
|
||||||
|
<span className="truncate">Logs</span>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{/* Complete button */}
|
||||||
|
{onComplete && (
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
size="sm"
|
||||||
|
className="flex-1 h-7 text-xs min-w-0 bg-brand-500 hover:bg-brand-600"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onComplete();
|
||||||
|
}}
|
||||||
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
|
data-testid={`complete-${feature.id}`}
|
||||||
|
>
|
||||||
|
<Archive className="w-3 h-3 mr-1 shrink-0" />
|
||||||
|
<span className="truncate">Complete</span>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@@ -876,6 +977,7 @@ export const KanbanCard = memo(function KanbanCard({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
)}
|
)}
|
||||||
|
{/* Refine prompt button */}
|
||||||
{onFollowUp && (
|
{onFollowUp && (
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
@@ -888,8 +990,8 @@ export const KanbanCard = memo(function KanbanCard({
|
|||||||
onPointerDown={(e) => e.stopPropagation()}
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
data-testid={`follow-up-${feature.id}`}
|
data-testid={`follow-up-${feature.id}`}
|
||||||
>
|
>
|
||||||
<MessageSquare className="w-3 h-3 mr-1 shrink-0" />
|
<Wand2 className="w-3 h-3 mr-1 shrink-0" />
|
||||||
<span className="truncate">Follow-up</span>
|
<span className="truncate">Refine</span>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{hasWorktree && onMerge && (
|
{hasWorktree && onMerge && (
|
||||||
@@ -927,6 +1029,40 @@ export const KanbanCard = memo(function KanbanCard({
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{!isCurrentAutoTask && feature.status === "backlog" && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
className="flex-1 h-7 text-xs"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onEdit();
|
||||||
|
}}
|
||||||
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
|
data-testid={`edit-backlog-${feature.id}`}
|
||||||
|
>
|
||||||
|
<Edit className="w-3 h-3 mr-1" />
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
{onImplement && (
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
size="sm"
|
||||||
|
className="flex-1 h-7 text-xs"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onImplement();
|
||||||
|
}}
|
||||||
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
|
data-testid={`make-${feature.id}`}
|
||||||
|
>
|
||||||
|
<PlayCircle className="w-3 h-3 mr-1" />
|
||||||
|
Make
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
|
|||||||
93
apps/app/src/config/model-config.ts
Normal file
93
apps/app/src/config/model-config.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
/**
|
||||||
|
* Model Configuration - Centralized model settings for the app
|
||||||
|
*
|
||||||
|
* Models can be overridden via environment variables:
|
||||||
|
* - AUTOMAKER_MODEL_CHAT: Model for chat interactions
|
||||||
|
* - AUTOMAKER_MODEL_DEFAULT: Fallback model for all operations
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Claude model aliases for convenience
|
||||||
|
*/
|
||||||
|
export const CLAUDE_MODEL_MAP: Record<string, string> = {
|
||||||
|
haiku: "claude-haiku-4-5",
|
||||||
|
sonnet: "claude-sonnet-4-20250514",
|
||||||
|
opus: "claude-opus-4-5-20251101",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default models per use case
|
||||||
|
*/
|
||||||
|
export const DEFAULT_MODELS = {
|
||||||
|
chat: "claude-opus-4-5-20251101",
|
||||||
|
default: "claude-opus-4-5-20251101",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a model alias to a full model string
|
||||||
|
*/
|
||||||
|
export function resolveModelString(
|
||||||
|
modelKey?: string,
|
||||||
|
defaultModel: string = DEFAULT_MODELS.default
|
||||||
|
): string {
|
||||||
|
if (!modelKey) {
|
||||||
|
return defaultModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Full Claude model string - pass through
|
||||||
|
if (modelKey.includes("claude-")) {
|
||||||
|
return modelKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check alias map
|
||||||
|
const resolved = CLAUDE_MODEL_MAP[modelKey];
|
||||||
|
if (resolved) {
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown key - use default
|
||||||
|
return defaultModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the model for chat operations
|
||||||
|
*
|
||||||
|
* Priority:
|
||||||
|
* 1. Explicit model parameter
|
||||||
|
* 2. AUTOMAKER_MODEL_CHAT environment variable
|
||||||
|
* 3. AUTOMAKER_MODEL_DEFAULT environment variable
|
||||||
|
* 4. Default chat model
|
||||||
|
*/
|
||||||
|
export function getChatModel(explicitModel?: string): string {
|
||||||
|
if (explicitModel) {
|
||||||
|
return resolveModelString(explicitModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
const envModel =
|
||||||
|
process.env.AUTOMAKER_MODEL_CHAT || process.env.AUTOMAKER_MODEL_DEFAULT;
|
||||||
|
|
||||||
|
if (envModel) {
|
||||||
|
return resolveModelString(envModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
return DEFAULT_MODELS.chat;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default allowed tools for chat interactions
|
||||||
|
*/
|
||||||
|
export const CHAT_TOOLS = [
|
||||||
|
"Read",
|
||||||
|
"Write",
|
||||||
|
"Edit",
|
||||||
|
"Glob",
|
||||||
|
"Grep",
|
||||||
|
"Bash",
|
||||||
|
"WebSearch",
|
||||||
|
"WebFetch",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default max turns for chat
|
||||||
|
*/
|
||||||
|
export const CHAT_MAX_TURNS = 1000;
|
||||||
@@ -148,15 +148,17 @@ export interface SpecRegenerationAPI {
|
|||||||
projectPath: string,
|
projectPath: string,
|
||||||
projectOverview: string,
|
projectOverview: string,
|
||||||
generateFeatures?: boolean,
|
generateFeatures?: boolean,
|
||||||
analyzeProject?: boolean
|
analyzeProject?: boolean,
|
||||||
|
maxFeatures?: number
|
||||||
) => Promise<{ success: boolean; error?: string }>;
|
) => Promise<{ success: boolean; error?: string }>;
|
||||||
generate: (
|
generate: (
|
||||||
projectPath: string,
|
projectPath: string,
|
||||||
projectDefinition: string,
|
projectDefinition: string,
|
||||||
generateFeatures?: boolean,
|
generateFeatures?: boolean,
|
||||||
analyzeProject?: boolean
|
analyzeProject?: boolean,
|
||||||
|
maxFeatures?: number
|
||||||
) => Promise<{ success: boolean; error?: string }>;
|
) => Promise<{ success: boolean; error?: string }>;
|
||||||
generateFeatures: (projectPath: string) => Promise<{
|
generateFeatures: (projectPath: string, maxFeatures?: number) => Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
}>;
|
}>;
|
||||||
@@ -1836,7 +1838,9 @@ function createMockSpecRegenerationAPI(): SpecRegenerationAPI {
|
|||||||
create: async (
|
create: async (
|
||||||
projectPath: string,
|
projectPath: string,
|
||||||
projectOverview: string,
|
projectOverview: string,
|
||||||
generateFeatures = true
|
generateFeatures = true,
|
||||||
|
_analyzeProject?: boolean,
|
||||||
|
maxFeatures?: number
|
||||||
) => {
|
) => {
|
||||||
if (mockSpecRegenerationRunning) {
|
if (mockSpecRegenerationRunning) {
|
||||||
return { success: false, error: "Spec creation is already running" };
|
return { success: false, error: "Spec creation is already running" };
|
||||||
@@ -1844,7 +1848,7 @@ function createMockSpecRegenerationAPI(): SpecRegenerationAPI {
|
|||||||
|
|
||||||
mockSpecRegenerationRunning = true;
|
mockSpecRegenerationRunning = true;
|
||||||
console.log(
|
console.log(
|
||||||
`[Mock] Creating initial spec for: ${projectPath}, generateFeatures: ${generateFeatures}`
|
`[Mock] Creating initial spec for: ${projectPath}, generateFeatures: ${generateFeatures}, maxFeatures: ${maxFeatures}`
|
||||||
);
|
);
|
||||||
|
|
||||||
// Simulate async spec creation
|
// Simulate async spec creation
|
||||||
@@ -1856,7 +1860,9 @@ function createMockSpecRegenerationAPI(): SpecRegenerationAPI {
|
|||||||
generate: async (
|
generate: async (
|
||||||
projectPath: string,
|
projectPath: string,
|
||||||
projectDefinition: string,
|
projectDefinition: string,
|
||||||
generateFeatures = false
|
generateFeatures = false,
|
||||||
|
_analyzeProject?: boolean,
|
||||||
|
maxFeatures?: number
|
||||||
) => {
|
) => {
|
||||||
if (mockSpecRegenerationRunning) {
|
if (mockSpecRegenerationRunning) {
|
||||||
return {
|
return {
|
||||||
@@ -1867,7 +1873,7 @@ function createMockSpecRegenerationAPI(): SpecRegenerationAPI {
|
|||||||
|
|
||||||
mockSpecRegenerationRunning = true;
|
mockSpecRegenerationRunning = true;
|
||||||
console.log(
|
console.log(
|
||||||
`[Mock] Regenerating spec for: ${projectPath}, generateFeatures: ${generateFeatures}`
|
`[Mock] Regenerating spec for: ${projectPath}, generateFeatures: ${generateFeatures}, maxFeatures: ${maxFeatures}`
|
||||||
);
|
);
|
||||||
|
|
||||||
// Simulate async spec regeneration
|
// Simulate async spec regeneration
|
||||||
@@ -1880,7 +1886,7 @@ function createMockSpecRegenerationAPI(): SpecRegenerationAPI {
|
|||||||
return { success: true };
|
return { success: true };
|
||||||
},
|
},
|
||||||
|
|
||||||
generateFeatures: async (projectPath: string) => {
|
generateFeatures: async (projectPath: string, maxFeatures?: number) => {
|
||||||
if (mockSpecRegenerationRunning) {
|
if (mockSpecRegenerationRunning) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@@ -1890,7 +1896,7 @@ function createMockSpecRegenerationAPI(): SpecRegenerationAPI {
|
|||||||
|
|
||||||
mockSpecRegenerationRunning = true;
|
mockSpecRegenerationRunning = true;
|
||||||
console.log(
|
console.log(
|
||||||
`[Mock] Generating features from existing spec for: ${projectPath}`
|
`[Mock] Generating features from existing spec for: ${projectPath}, maxFeatures: ${maxFeatures}`
|
||||||
);
|
);
|
||||||
|
|
||||||
// Simulate async feature generation
|
// Simulate async feature generation
|
||||||
|
|||||||
@@ -582,28 +582,32 @@ export class HttpApiClient implements ElectronAPI {
|
|||||||
projectPath: string,
|
projectPath: string,
|
||||||
projectOverview: string,
|
projectOverview: string,
|
||||||
generateFeatures?: boolean,
|
generateFeatures?: boolean,
|
||||||
analyzeProject?: boolean
|
analyzeProject?: boolean,
|
||||||
|
maxFeatures?: number
|
||||||
) =>
|
) =>
|
||||||
this.post("/api/spec-regeneration/create", {
|
this.post("/api/spec-regeneration/create", {
|
||||||
projectPath,
|
projectPath,
|
||||||
projectOverview,
|
projectOverview,
|
||||||
generateFeatures,
|
generateFeatures,
|
||||||
analyzeProject,
|
analyzeProject,
|
||||||
|
maxFeatures,
|
||||||
}),
|
}),
|
||||||
generate: (
|
generate: (
|
||||||
projectPath: string,
|
projectPath: string,
|
||||||
projectDefinition: string,
|
projectDefinition: string,
|
||||||
generateFeatures?: boolean,
|
generateFeatures?: boolean,
|
||||||
analyzeProject?: boolean
|
analyzeProject?: boolean,
|
||||||
|
maxFeatures?: number
|
||||||
) =>
|
) =>
|
||||||
this.post("/api/spec-regeneration/generate", {
|
this.post("/api/spec-regeneration/generate", {
|
||||||
projectPath,
|
projectPath,
|
||||||
projectDefinition,
|
projectDefinition,
|
||||||
generateFeatures,
|
generateFeatures,
|
||||||
analyzeProject,
|
analyzeProject,
|
||||||
|
maxFeatures,
|
||||||
}),
|
}),
|
||||||
generateFeatures: (projectPath: string) =>
|
generateFeatures: (projectPath: string, maxFeatures?: number) =>
|
||||||
this.post("/api/spec-regeneration/generate-features", { projectPath }),
|
this.post("/api/spec-regeneration/generate-features", { projectPath, maxFeatures }),
|
||||||
stop: () => this.post("/api/spec-regeneration/stop"),
|
stop: () => this.post("/api/spec-regeneration/stop"),
|
||||||
status: () => this.get("/api/spec-regeneration/status"),
|
status: () => this.get("/api/spec-regeneration/status"),
|
||||||
onEvent: (callback: (event: SpecRegenerationEvent) => void) => {
|
onEvent: (callback: (event: SpecRegenerationEvent) => void) => {
|
||||||
|
|||||||
@@ -277,7 +277,12 @@ export interface Feature {
|
|||||||
category: string;
|
category: string;
|
||||||
description: string;
|
description: string;
|
||||||
steps: string[];
|
steps: string[];
|
||||||
status: "backlog" | "in_progress" | "waiting_approval" | "verified";
|
status:
|
||||||
|
| "backlog"
|
||||||
|
| "in_progress"
|
||||||
|
| "waiting_approval"
|
||||||
|
| "verified"
|
||||||
|
| "completed";
|
||||||
images?: FeatureImage[];
|
images?: FeatureImage[];
|
||||||
imagePaths?: FeatureImagePath[]; // Paths to temp files for agent context
|
imagePaths?: FeatureImagePath[]; // Paths to temp files for agent context
|
||||||
startedAt?: string; // ISO timestamp for when the card moved to in_progress
|
startedAt?: string; // ISO timestamp for when the card moved to in_progress
|
||||||
|
|||||||
8
apps/app/src/types/electron.d.ts
vendored
8
apps/app/src/types/electron.d.ts
vendored
@@ -267,7 +267,8 @@ export interface SpecRegenerationAPI {
|
|||||||
projectPath: string,
|
projectPath: string,
|
||||||
projectOverview: string,
|
projectOverview: string,
|
||||||
generateFeatures?: boolean,
|
generateFeatures?: boolean,
|
||||||
analyzeProject?: boolean
|
analyzeProject?: boolean,
|
||||||
|
maxFeatures?: number
|
||||||
) => Promise<{
|
) => Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
@@ -277,13 +278,14 @@ export interface SpecRegenerationAPI {
|
|||||||
projectPath: string,
|
projectPath: string,
|
||||||
projectDefinition: string,
|
projectDefinition: string,
|
||||||
generateFeatures?: boolean,
|
generateFeatures?: boolean,
|
||||||
analyzeProject?: boolean
|
analyzeProject?: boolean,
|
||||||
|
maxFeatures?: number
|
||||||
) => Promise<{
|
) => Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
generateFeatures: (projectPath: string) => Promise<{
|
generateFeatures: (projectPath: string, maxFeatures?: number) => Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
}>;
|
}>;
|
||||||
|
|||||||
@@ -48,7 +48,9 @@ export function resolveModelString(
|
|||||||
// Look up Claude model alias
|
// Look up Claude model alias
|
||||||
const resolved = CLAUDE_MODEL_MAP[modelKey];
|
const resolved = CLAUDE_MODEL_MAP[modelKey];
|
||||||
if (resolved) {
|
if (resolved) {
|
||||||
console.log(`[ModelResolver] Resolved model alias: "${modelKey}" -> "${resolved}"`);
|
console.log(
|
||||||
|
`[ModelResolver] Resolved model alias: "${modelKey}" -> "${resolved}"`
|
||||||
|
);
|
||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,8 +75,5 @@ export function getEffectiveModel(
|
|||||||
sessionModel?: string,
|
sessionModel?: string,
|
||||||
defaultModel?: string
|
defaultModel?: string
|
||||||
): string {
|
): string {
|
||||||
return resolveModelString(
|
return resolveModelString(explicitModel || sessionModel, defaultModel);
|
||||||
explicitModel || sessionModel,
|
|
||||||
defaultModel
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
291
apps/server/src/lib/sdk-options.ts
Normal file
291
apps/server/src/lib/sdk-options.ts
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
/**
|
||||||
|
* SDK Options Factory - Centralized configuration for Claude Agent SDK
|
||||||
|
*
|
||||||
|
* Provides presets for common use cases:
|
||||||
|
* - Spec generation: Long-running analysis with read-only tools
|
||||||
|
* - Feature generation: Quick JSON generation from specs
|
||||||
|
* - Feature building: Autonomous feature implementation with full tool access
|
||||||
|
* - Suggestions: Analysis with read-only tools
|
||||||
|
* - Chat: Full tool access for interactive coding
|
||||||
|
*
|
||||||
|
* Uses model-resolver for consistent model handling across the application.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Options } from "@anthropic-ai/claude-agent-sdk";
|
||||||
|
import {
|
||||||
|
resolveModelString,
|
||||||
|
DEFAULT_MODELS,
|
||||||
|
CLAUDE_MODEL_MAP,
|
||||||
|
} from "./model-resolver.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tool presets for different use cases
|
||||||
|
*/
|
||||||
|
export const TOOL_PRESETS = {
|
||||||
|
/** Read-only tools for analysis */
|
||||||
|
readOnly: ["Read", "Glob", "Grep"] as const,
|
||||||
|
|
||||||
|
/** Tools for spec generation that needs to read the codebase */
|
||||||
|
specGeneration: ["Read", "Glob", "Grep"] as const,
|
||||||
|
|
||||||
|
/** Full tool access for feature implementation */
|
||||||
|
fullAccess: [
|
||||||
|
"Read",
|
||||||
|
"Write",
|
||||||
|
"Edit",
|
||||||
|
"Glob",
|
||||||
|
"Grep",
|
||||||
|
"Bash",
|
||||||
|
"WebSearch",
|
||||||
|
"WebFetch",
|
||||||
|
] as const,
|
||||||
|
|
||||||
|
/** Tools for chat/interactive mode */
|
||||||
|
chat: [
|
||||||
|
"Read",
|
||||||
|
"Write",
|
||||||
|
"Edit",
|
||||||
|
"Glob",
|
||||||
|
"Grep",
|
||||||
|
"Bash",
|
||||||
|
"WebSearch",
|
||||||
|
"WebFetch",
|
||||||
|
] as const,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Max turns presets for different use cases
|
||||||
|
*/
|
||||||
|
export const MAX_TURNS = {
|
||||||
|
/** Quick operations that shouldn't need many iterations */
|
||||||
|
quick: 5,
|
||||||
|
|
||||||
|
/** Standard operations */
|
||||||
|
standard: 20,
|
||||||
|
|
||||||
|
/** Long-running operations like full spec generation */
|
||||||
|
extended: 50,
|
||||||
|
|
||||||
|
/** Very long operations that may require extensive exploration */
|
||||||
|
maximum: 1000,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model presets for different use cases
|
||||||
|
*
|
||||||
|
* These can be overridden via environment variables:
|
||||||
|
* - AUTOMAKER_MODEL_SPEC: Model for spec generation
|
||||||
|
* - AUTOMAKER_MODEL_FEATURES: Model for feature generation
|
||||||
|
* - AUTOMAKER_MODEL_SUGGESTIONS: Model for suggestions
|
||||||
|
* - AUTOMAKER_MODEL_CHAT: Model for chat
|
||||||
|
* - AUTOMAKER_MODEL_DEFAULT: Fallback model for all operations
|
||||||
|
*/
|
||||||
|
export function getModelForUseCase(
|
||||||
|
useCase: "spec" | "features" | "suggestions" | "chat" | "auto" | "default",
|
||||||
|
explicitModel?: string
|
||||||
|
): string {
|
||||||
|
// Explicit model takes precedence
|
||||||
|
if (explicitModel) {
|
||||||
|
return resolveModelString(explicitModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check environment variable override for this use case
|
||||||
|
const envVarMap: Record<string, string | undefined> = {
|
||||||
|
spec: process.env.AUTOMAKER_MODEL_SPEC,
|
||||||
|
features: process.env.AUTOMAKER_MODEL_FEATURES,
|
||||||
|
suggestions: process.env.AUTOMAKER_MODEL_SUGGESTIONS,
|
||||||
|
chat: process.env.AUTOMAKER_MODEL_CHAT,
|
||||||
|
auto: process.env.AUTOMAKER_MODEL_AUTO,
|
||||||
|
default: process.env.AUTOMAKER_MODEL_DEFAULT,
|
||||||
|
};
|
||||||
|
|
||||||
|
const envModel = envVarMap[useCase] || envVarMap.default;
|
||||||
|
if (envModel) {
|
||||||
|
return resolveModelString(envModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultModels: Record<string, string> = {
|
||||||
|
spec: CLAUDE_MODEL_MAP["haiku"], // used to generate app specs
|
||||||
|
features: CLAUDE_MODEL_MAP["haiku"], // used to generate features from app specs
|
||||||
|
suggestions: CLAUDE_MODEL_MAP["haiku"], // used for suggestions
|
||||||
|
chat: CLAUDE_MODEL_MAP["haiku"], // used for chat
|
||||||
|
auto: CLAUDE_MODEL_MAP["opus"], // used to implement kanban cards
|
||||||
|
default: CLAUDE_MODEL_MAP["opus"],
|
||||||
|
};
|
||||||
|
|
||||||
|
return resolveModelString(defaultModels[useCase] || DEFAULT_MODELS.claude);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base options that apply to all SDK calls
|
||||||
|
*/
|
||||||
|
function getBaseOptions(): Partial<Options> {
|
||||||
|
return {
|
||||||
|
permissionMode: "acceptEdits",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options configuration for creating SDK options
|
||||||
|
*/
|
||||||
|
export interface CreateSdkOptionsConfig {
|
||||||
|
/** Working directory for the agent */
|
||||||
|
cwd: string;
|
||||||
|
|
||||||
|
/** Optional explicit model override */
|
||||||
|
model?: string;
|
||||||
|
|
||||||
|
/** Optional session model (used as fallback if explicit model not provided) */
|
||||||
|
sessionModel?: string;
|
||||||
|
|
||||||
|
/** Optional system prompt */
|
||||||
|
systemPrompt?: string;
|
||||||
|
|
||||||
|
/** Optional abort controller for cancellation */
|
||||||
|
abortController?: AbortController;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create SDK options for spec generation
|
||||||
|
*
|
||||||
|
* Configuration:
|
||||||
|
* - Uses read-only tools for codebase analysis
|
||||||
|
* - Extended turns for thorough exploration
|
||||||
|
* - Opus model by default (can be overridden)
|
||||||
|
*/
|
||||||
|
export function createSpecGenerationOptions(
|
||||||
|
config: CreateSdkOptionsConfig
|
||||||
|
): Options {
|
||||||
|
return {
|
||||||
|
...getBaseOptions(),
|
||||||
|
model: getModelForUseCase("spec", config.model),
|
||||||
|
maxTurns: MAX_TURNS.maximum,
|
||||||
|
cwd: config.cwd,
|
||||||
|
allowedTools: [...TOOL_PRESETS.specGeneration],
|
||||||
|
...(config.systemPrompt && { systemPrompt: config.systemPrompt }),
|
||||||
|
...(config.abortController && { abortController: config.abortController }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create SDK options for feature generation from specs
|
||||||
|
*
|
||||||
|
* Configuration:
|
||||||
|
* - Uses read-only tools (just needs to read the spec)
|
||||||
|
* - Quick turns since it's mostly JSON generation
|
||||||
|
* - Sonnet model by default for speed
|
||||||
|
*/
|
||||||
|
export function createFeatureGenerationOptions(
|
||||||
|
config: CreateSdkOptionsConfig
|
||||||
|
): Options {
|
||||||
|
return {
|
||||||
|
...getBaseOptions(),
|
||||||
|
model: getModelForUseCase("features", config.model),
|
||||||
|
maxTurns: MAX_TURNS.quick,
|
||||||
|
cwd: config.cwd,
|
||||||
|
allowedTools: [...TOOL_PRESETS.readOnly],
|
||||||
|
...(config.systemPrompt && { systemPrompt: config.systemPrompt }),
|
||||||
|
...(config.abortController && { abortController: config.abortController }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create SDK options for generating suggestions
|
||||||
|
*
|
||||||
|
* Configuration:
|
||||||
|
* - Uses read-only tools for analysis
|
||||||
|
* - Quick turns for focused suggestions
|
||||||
|
* - Opus model by default for thorough analysis
|
||||||
|
*/
|
||||||
|
export function createSuggestionsOptions(
|
||||||
|
config: CreateSdkOptionsConfig
|
||||||
|
): Options {
|
||||||
|
return {
|
||||||
|
...getBaseOptions(),
|
||||||
|
model: getModelForUseCase("suggestions", config.model),
|
||||||
|
maxTurns: MAX_TURNS.quick,
|
||||||
|
cwd: config.cwd,
|
||||||
|
allowedTools: [...TOOL_PRESETS.readOnly],
|
||||||
|
...(config.systemPrompt && { systemPrompt: config.systemPrompt }),
|
||||||
|
...(config.abortController && { abortController: config.abortController }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create SDK options for chat/interactive mode
|
||||||
|
*
|
||||||
|
* Configuration:
|
||||||
|
* - Full tool access for code modification
|
||||||
|
* - Standard turns for interactive sessions
|
||||||
|
* - Model priority: explicit model > session model > chat default
|
||||||
|
* - Sandbox enabled for bash safety
|
||||||
|
*/
|
||||||
|
export function createChatOptions(config: CreateSdkOptionsConfig): Options {
|
||||||
|
// Model priority: explicit model > session model > chat default
|
||||||
|
const effectiveModel = config.model || config.sessionModel;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...getBaseOptions(),
|
||||||
|
model: getModelForUseCase("chat", effectiveModel),
|
||||||
|
maxTurns: MAX_TURNS.standard,
|
||||||
|
cwd: config.cwd,
|
||||||
|
allowedTools: [...TOOL_PRESETS.chat],
|
||||||
|
sandbox: {
|
||||||
|
enabled: true,
|
||||||
|
autoAllowBashIfSandboxed: true,
|
||||||
|
},
|
||||||
|
...(config.systemPrompt && { systemPrompt: config.systemPrompt }),
|
||||||
|
...(config.abortController && { abortController: config.abortController }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create SDK options for autonomous feature building/implementation
|
||||||
|
*
|
||||||
|
* Configuration:
|
||||||
|
* - Full tool access for code modification and implementation
|
||||||
|
* - Extended turns for thorough feature implementation
|
||||||
|
* - Uses default model (can be overridden)
|
||||||
|
* - Sandbox enabled for bash safety
|
||||||
|
*/
|
||||||
|
export function createAutoModeOptions(config: CreateSdkOptionsConfig): Options {
|
||||||
|
return {
|
||||||
|
...getBaseOptions(),
|
||||||
|
model: getModelForUseCase("auto", config.model),
|
||||||
|
maxTurns: MAX_TURNS.maximum,
|
||||||
|
cwd: config.cwd,
|
||||||
|
allowedTools: [...TOOL_PRESETS.fullAccess],
|
||||||
|
sandbox: {
|
||||||
|
enabled: true,
|
||||||
|
autoAllowBashIfSandboxed: true,
|
||||||
|
},
|
||||||
|
...(config.systemPrompt && { systemPrompt: config.systemPrompt }),
|
||||||
|
...(config.abortController && { abortController: config.abortController }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create custom SDK options with explicit configuration
|
||||||
|
*
|
||||||
|
* Use this when the preset options don't fit your use case.
|
||||||
|
*/
|
||||||
|
export function createCustomOptions(
|
||||||
|
config: CreateSdkOptionsConfig & {
|
||||||
|
maxTurns?: number;
|
||||||
|
allowedTools?: readonly string[];
|
||||||
|
sandbox?: { enabled: boolean; autoAllowBashIfSandboxed?: boolean };
|
||||||
|
}
|
||||||
|
): Options {
|
||||||
|
return {
|
||||||
|
...getBaseOptions(),
|
||||||
|
model: getModelForUseCase("default", config.model),
|
||||||
|
maxTurns: config.maxTurns ?? MAX_TURNS.maximum,
|
||||||
|
cwd: config.cwd,
|
||||||
|
allowedTools: config.allowedTools
|
||||||
|
? [...config.allowedTools]
|
||||||
|
: [...TOOL_PRESETS.readOnly],
|
||||||
|
...(config.sandbox && { sandbox: config.sandbox }),
|
||||||
|
...(config.systemPrompt && { systemPrompt: config.systemPrompt }),
|
||||||
|
...(config.abortController && { abortController: config.abortController }),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -2,23 +2,29 @@
|
|||||||
* Generate features from existing app_spec.txt
|
* Generate features from existing app_spec.txt
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { query, type Options } from "@anthropic-ai/claude-agent-sdk";
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import type { EventEmitter } from "../../lib/events.js";
|
import type { EventEmitter } from "../../lib/events.js";
|
||||||
import { createLogger } from "../../lib/logger.js";
|
import { createLogger } from "../../lib/logger.js";
|
||||||
|
import { createFeatureGenerationOptions } from "../../lib/sdk-options.js";
|
||||||
import { logAuthStatus } from "./common.js";
|
import { logAuthStatus } from "./common.js";
|
||||||
import { parseAndCreateFeatures } from "./parse-and-create-features.js";
|
import { parseAndCreateFeatures } from "./parse-and-create-features.js";
|
||||||
|
|
||||||
const logger = createLogger("SpecRegeneration");
|
const logger = createLogger("SpecRegeneration");
|
||||||
|
|
||||||
|
const DEFAULT_MAX_FEATURES = 50;
|
||||||
|
|
||||||
export async function generateFeaturesFromSpec(
|
export async function generateFeaturesFromSpec(
|
||||||
projectPath: string,
|
projectPath: string,
|
||||||
events: EventEmitter,
|
events: EventEmitter,
|
||||||
abortController: AbortController
|
abortController: AbortController,
|
||||||
|
maxFeatures?: number
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const featureCount = maxFeatures ?? DEFAULT_MAX_FEATURES;
|
||||||
logger.debug("========== generateFeaturesFromSpec() started ==========");
|
logger.debug("========== generateFeaturesFromSpec() started ==========");
|
||||||
logger.debug("projectPath:", projectPath);
|
logger.debug("projectPath:", projectPath);
|
||||||
|
logger.debug("maxFeatures:", featureCount);
|
||||||
|
|
||||||
// Read existing spec
|
// Read existing spec
|
||||||
const specPath = path.join(projectPath, ".automaker", "app_spec.txt");
|
const specPath = path.join(projectPath, ".automaker", "app_spec.txt");
|
||||||
@@ -29,6 +35,10 @@ export async function generateFeaturesFromSpec(
|
|||||||
try {
|
try {
|
||||||
spec = await fs.readFile(specPath, "utf-8");
|
spec = await fs.readFile(specPath, "utf-8");
|
||||||
logger.info(`Spec loaded successfully (${spec.length} chars)`);
|
logger.info(`Spec loaded successfully (${spec.length} chars)`);
|
||||||
|
logger.info(`Spec preview (first 500 chars): ${spec.substring(0, 500)}`);
|
||||||
|
logger.info(
|
||||||
|
`Spec preview (last 500 chars): ${spec.substring(spec.length - 500)}`
|
||||||
|
);
|
||||||
} catch (readError) {
|
} catch (readError) {
|
||||||
logger.error("❌ Failed to read spec file:", readError);
|
logger.error("❌ Failed to read spec file:", readError);
|
||||||
events.emit("spec-regeneration:event", {
|
events.emit("spec-regeneration:event", {
|
||||||
@@ -66,9 +76,16 @@ Format as JSON:
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
Generate 5-15 features that build on each other logically.`;
|
Generate ${featureCount} features that build on each other logically.
|
||||||
|
|
||||||
logger.debug("Prompt length:", `${prompt.length} chars`);
|
IMPORTANT: Do not ask for clarification. The specification is provided above. Generate the JSON immediately.`;
|
||||||
|
|
||||||
|
logger.info("========== PROMPT BEING SENT ==========");
|
||||||
|
logger.info(`Prompt length: ${prompt.length} chars`);
|
||||||
|
logger.info(
|
||||||
|
`Prompt preview (first 1000 chars):\n${prompt.substring(0, 1000)}`
|
||||||
|
);
|
||||||
|
logger.info("========== END PROMPT PREVIEW ==========");
|
||||||
|
|
||||||
events.emit("spec-regeneration:event", {
|
events.emit("spec-regeneration:event", {
|
||||||
type: "spec_regeneration_progress",
|
type: "spec_regeneration_progress",
|
||||||
@@ -76,14 +93,10 @@ Generate 5-15 features that build on each other logically.`;
|
|||||||
projectPath: projectPath,
|
projectPath: projectPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
const options: Options = {
|
const options = createFeatureGenerationOptions({
|
||||||
model: "claude-sonnet-4-20250514",
|
|
||||||
maxTurns: 5,
|
|
||||||
cwd: projectPath,
|
cwd: projectPath,
|
||||||
allowedTools: ["Read", "Glob"],
|
|
||||||
permissionMode: "acceptEdits",
|
|
||||||
abortController,
|
abortController,
|
||||||
};
|
});
|
||||||
|
|
||||||
logger.debug("SDK Options:", JSON.stringify(options, null, 2));
|
logger.debug("SDK Options:", JSON.stringify(options, null, 2));
|
||||||
logger.info("Calling Claude Agent SDK query() for features...");
|
logger.info("Calling Claude Agent SDK query() for features...");
|
||||||
@@ -120,7 +133,7 @@ Generate 5-15 features that build on each other logically.`;
|
|||||||
if (msg.type === "assistant" && msg.message.content) {
|
if (msg.type === "assistant" && msg.message.content) {
|
||||||
for (const block of msg.message.content) {
|
for (const block of msg.message.content) {
|
||||||
if (block.type === "text") {
|
if (block.type === "text") {
|
||||||
responseText = block.text;
|
responseText += block.text;
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Feature text block received (${block.text.length} chars)`
|
`Feature text block received (${block.text.length} chars)`
|
||||||
);
|
);
|
||||||
@@ -147,6 +160,9 @@ Generate 5-15 features that build on each other logically.`;
|
|||||||
|
|
||||||
logger.info(`Feature stream complete. Total messages: ${messageCount}`);
|
logger.info(`Feature stream complete. Total messages: ${messageCount}`);
|
||||||
logger.info(`Feature response length: ${responseText.length} chars`);
|
logger.info(`Feature response length: ${responseText.length} chars`);
|
||||||
|
logger.info("========== FULL RESPONSE TEXT ==========");
|
||||||
|
logger.info(responseText);
|
||||||
|
logger.info("========== END RESPONSE TEXT ==========");
|
||||||
|
|
||||||
await parseAndCreateFeatures(projectPath, responseText, events);
|
await parseAndCreateFeatures(projectPath, responseText, events);
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,13 @@
|
|||||||
* Generate app_spec.txt from project overview
|
* Generate app_spec.txt from project overview
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { query, type Options } from "@anthropic-ai/claude-agent-sdk";
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import type { EventEmitter } from "../../lib/events.js";
|
import type { EventEmitter } from "../../lib/events.js";
|
||||||
import { getAppSpecFormatInstruction } from "../../lib/app-spec-format.js";
|
import { getAppSpecFormatInstruction } from "../../lib/app-spec-format.js";
|
||||||
import { createLogger } from "../../lib/logger.js";
|
import { createLogger } from "../../lib/logger.js";
|
||||||
|
import { createSpecGenerationOptions } from "../../lib/sdk-options.js";
|
||||||
import { logAuthStatus } from "./common.js";
|
import { logAuthStatus } from "./common.js";
|
||||||
import { generateFeaturesFromSpec } from "./generate-features-from-spec.js";
|
import { generateFeaturesFromSpec } from "./generate-features-from-spec.js";
|
||||||
|
|
||||||
@@ -19,13 +20,16 @@ export async function generateSpec(
|
|||||||
events: EventEmitter,
|
events: EventEmitter,
|
||||||
abortController: AbortController,
|
abortController: AbortController,
|
||||||
generateFeatures?: boolean,
|
generateFeatures?: boolean,
|
||||||
analyzeProject?: boolean
|
analyzeProject?: boolean,
|
||||||
|
maxFeatures?: number
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
logger.debug("========== generateSpec() started ==========");
|
logger.info("========== generateSpec() started ==========");
|
||||||
logger.debug("projectPath:", projectPath);
|
logger.info("projectPath:", projectPath);
|
||||||
logger.debug("projectOverview length:", `${projectOverview.length} chars`);
|
logger.info("projectOverview length:", `${projectOverview.length} chars`);
|
||||||
logger.debug("generateFeatures:", generateFeatures);
|
logger.info("projectOverview preview:", projectOverview.substring(0, 300));
|
||||||
logger.debug("analyzeProject:", analyzeProject);
|
logger.info("generateFeatures:", generateFeatures);
|
||||||
|
logger.info("analyzeProject:", analyzeProject);
|
||||||
|
logger.info("maxFeatures:", maxFeatures);
|
||||||
|
|
||||||
// Build the prompt based on whether we should analyze the project
|
// Build the prompt based on whether we should analyze the project
|
||||||
let analysisInstructions = "";
|
let analysisInstructions = "";
|
||||||
@@ -63,21 +67,20 @@ ${analysisInstructions}
|
|||||||
|
|
||||||
${getAppSpecFormatInstruction()}`;
|
${getAppSpecFormatInstruction()}`;
|
||||||
|
|
||||||
logger.debug("Prompt length:", `${prompt.length} chars`);
|
logger.info("========== PROMPT BEING SENT ==========");
|
||||||
|
logger.info(`Prompt length: ${prompt.length} chars`);
|
||||||
|
logger.info(`Prompt preview (first 500 chars):\n${prompt.substring(0, 500)}`);
|
||||||
|
logger.info("========== END PROMPT PREVIEW ==========");
|
||||||
|
|
||||||
events.emit("spec-regeneration:event", {
|
events.emit("spec-regeneration:event", {
|
||||||
type: "spec_progress",
|
type: "spec_progress",
|
||||||
content: "Starting spec generation...\n",
|
content: "Starting spec generation...\n",
|
||||||
});
|
});
|
||||||
|
|
||||||
const options: Options = {
|
const options = createSpecGenerationOptions({
|
||||||
model: "claude-opus-4-5-20251101",
|
|
||||||
maxTurns: 10,
|
|
||||||
cwd: projectPath,
|
cwd: projectPath,
|
||||||
allowedTools: ["Read", "Glob", "Grep"],
|
|
||||||
permissionMode: "acceptEdits",
|
|
||||||
abortController,
|
abortController,
|
||||||
};
|
});
|
||||||
|
|
||||||
logger.debug("SDK Options:", JSON.stringify(options, null, 2));
|
logger.debug("SDK Options:", JSON.stringify(options, null, 2));
|
||||||
logger.info("Calling Claude Agent SDK query()...");
|
logger.info("Calling Claude Agent SDK query()...");
|
||||||
@@ -98,32 +101,48 @@ ${getAppSpecFormatInstruction()}`;
|
|||||||
let responseText = "";
|
let responseText = "";
|
||||||
let messageCount = 0;
|
let messageCount = 0;
|
||||||
|
|
||||||
logger.debug("Starting to iterate over stream...");
|
logger.info("Starting to iterate over stream...");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for await (const msg of stream) {
|
for await (const msg of stream) {
|
||||||
messageCount++;
|
messageCount++;
|
||||||
logger.debug(
|
logger.info(
|
||||||
`Stream message #${messageCount}:`,
|
`Stream message #${messageCount}: type=${msg.type}, subtype=${
|
||||||
JSON.stringify(
|
(msg as any).subtype
|
||||||
{ type: msg.type, subtype: (msg as any).subtype },
|
}`
|
||||||
null,
|
|
||||||
2
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (msg.type === "assistant" && msg.message.content) {
|
if (msg.type === "assistant") {
|
||||||
for (const block of msg.message.content) {
|
// Log the full message structure to debug
|
||||||
|
logger.info(`Assistant msg keys: ${Object.keys(msg).join(", ")}`);
|
||||||
|
const msgAny = msg as any;
|
||||||
|
if (msgAny.message) {
|
||||||
|
logger.info(
|
||||||
|
`msg.message keys: ${Object.keys(msgAny.message).join(", ")}`
|
||||||
|
);
|
||||||
|
if (msgAny.message.content) {
|
||||||
|
logger.info(
|
||||||
|
`msg.message.content length: ${msgAny.message.content.length}`
|
||||||
|
);
|
||||||
|
for (const block of msgAny.message.content) {
|
||||||
|
logger.info(
|
||||||
|
`Block keys: ${Object.keys(block).join(", ")}, type: ${
|
||||||
|
block.type
|
||||||
|
}`
|
||||||
|
);
|
||||||
if (block.type === "text") {
|
if (block.type === "text") {
|
||||||
responseText += block.text;
|
responseText += block.text;
|
||||||
logger.debug(`Text block received (${block.text.length} chars)`);
|
logger.info(
|
||||||
|
`Text block received (${block.text.length} chars), total now: ${responseText.length} chars`
|
||||||
|
);
|
||||||
|
logger.info(`Text preview: ${block.text.substring(0, 200)}...`);
|
||||||
events.emit("spec-regeneration:event", {
|
events.emit("spec-regeneration:event", {
|
||||||
type: "spec_regeneration_progress",
|
type: "spec_regeneration_progress",
|
||||||
content: block.text,
|
content: block.text,
|
||||||
projectPath: projectPath,
|
projectPath: projectPath,
|
||||||
});
|
});
|
||||||
} else if (block.type === "tool_use") {
|
} else if (block.type === "tool_use") {
|
||||||
logger.debug("Tool use:", block.name);
|
logger.info("Tool use:", block.name);
|
||||||
events.emit("spec-regeneration:event", {
|
events.emit("spec-regeneration:event", {
|
||||||
type: "spec_tool",
|
type: "spec_tool",
|
||||||
tool: block.name,
|
tool: block.name,
|
||||||
@@ -131,12 +150,47 @@ ${getAppSpecFormatInstruction()}`;
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
logger.warn("msg.message.content is falsy");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.warn("msg.message is falsy");
|
||||||
|
// Log full message to see structure
|
||||||
|
logger.info(
|
||||||
|
`Full assistant msg: ${JSON.stringify(msg).substring(0, 1000)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
} else if (msg.type === "result" && (msg as any).subtype === "success") {
|
} else if (msg.type === "result" && (msg as any).subtype === "success") {
|
||||||
logger.debug("Received success result");
|
logger.info("Received success result");
|
||||||
responseText = (msg as any).result || responseText;
|
logger.info(`Result value: "${(msg as any).result}"`);
|
||||||
|
logger.info(
|
||||||
|
`Current responseText length before result: ${responseText.length}`
|
||||||
|
);
|
||||||
|
// Only use result if it has content, otherwise keep accumulated text
|
||||||
|
if ((msg as any).result && (msg as any).result.length > 0) {
|
||||||
|
logger.info("Using result value as responseText");
|
||||||
|
responseText = (msg as any).result;
|
||||||
|
} else {
|
||||||
|
logger.info("Result is empty, keeping accumulated responseText");
|
||||||
|
}
|
||||||
|
} else if (msg.type === "result") {
|
||||||
|
// Handle all result types
|
||||||
|
const subtype = (msg as any).subtype;
|
||||||
|
logger.info(`Result message: subtype=${subtype}`);
|
||||||
|
if (subtype === "error_max_turns") {
|
||||||
|
logger.error(
|
||||||
|
"❌ Hit max turns limit! Claude used too many tool calls."
|
||||||
|
);
|
||||||
|
logger.info(`responseText so far: ${responseText.length} chars`);
|
||||||
|
}
|
||||||
} else if ((msg as { type: string }).type === "error") {
|
} else if ((msg as { type: string }).type === "error") {
|
||||||
logger.error("❌ Received error message from stream:");
|
logger.error("❌ Received error message from stream:");
|
||||||
logger.error("Error message:", JSON.stringify(msg, null, 2));
|
logger.error("Error message:", JSON.stringify(msg, null, 2));
|
||||||
|
} else if (msg.type === "user") {
|
||||||
|
// Log user messages (tool results)
|
||||||
|
logger.info(
|
||||||
|
`User message (tool result): ${JSON.stringify(msg).substring(0, 500)}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (streamError) {
|
} catch (streamError) {
|
||||||
@@ -147,16 +201,31 @@ ${getAppSpecFormatInstruction()}`;
|
|||||||
|
|
||||||
logger.info(`Stream iteration complete. Total messages: ${messageCount}`);
|
logger.info(`Stream iteration complete. Total messages: ${messageCount}`);
|
||||||
logger.info(`Response text length: ${responseText.length} chars`);
|
logger.info(`Response text length: ${responseText.length} chars`);
|
||||||
|
logger.info("========== FINAL RESPONSE TEXT ==========");
|
||||||
|
logger.info(responseText || "(empty)");
|
||||||
|
logger.info("========== END RESPONSE TEXT ==========");
|
||||||
|
|
||||||
|
if (!responseText || responseText.trim().length === 0) {
|
||||||
|
logger.error("❌ WARNING: responseText is empty! Nothing to save.");
|
||||||
|
}
|
||||||
|
|
||||||
// Save spec
|
// Save spec
|
||||||
const specDir = path.join(projectPath, ".automaker");
|
const specDir = path.join(projectPath, ".automaker");
|
||||||
const specPath = path.join(specDir, "app_spec.txt");
|
const specPath = path.join(specDir, "app_spec.txt");
|
||||||
|
|
||||||
logger.info("Saving spec to:", specPath);
|
logger.info("Saving spec to:", specPath);
|
||||||
|
logger.info(`Content to save (${responseText.length} chars)`);
|
||||||
|
|
||||||
await fs.mkdir(specDir, { recursive: true });
|
await fs.mkdir(specDir, { recursive: true });
|
||||||
await fs.writeFile(specPath, responseText);
|
await fs.writeFile(specPath, responseText);
|
||||||
|
|
||||||
|
// Verify the file was written
|
||||||
|
const savedContent = await fs.readFile(specPath, "utf-8");
|
||||||
|
logger.info(`Verified saved file: ${savedContent.length} chars`);
|
||||||
|
if (savedContent.length === 0) {
|
||||||
|
logger.error("❌ File was saved but is empty!");
|
||||||
|
}
|
||||||
|
|
||||||
logger.info("Spec saved successfully");
|
logger.info("Spec saved successfully");
|
||||||
|
|
||||||
// Emit spec completion event
|
// Emit spec completion event
|
||||||
@@ -185,7 +254,8 @@ ${getAppSpecFormatInstruction()}`;
|
|||||||
await generateFeaturesFromSpec(
|
await generateFeaturesFromSpec(
|
||||||
projectPath,
|
projectPath,
|
||||||
events,
|
events,
|
||||||
featureAbortController
|
featureAbortController,
|
||||||
|
maxFeatures
|
||||||
);
|
);
|
||||||
// Final completion will be emitted by generateFeaturesFromSpec -> parseAndCreateFeatures
|
// Final completion will be emitted by generateFeaturesFromSpec -> parseAndCreateFeatures
|
||||||
} catch (featureError) {
|
} catch (featureError) {
|
||||||
|
|||||||
@@ -14,23 +14,32 @@ export async function parseAndCreateFeatures(
|
|||||||
content: string,
|
content: string,
|
||||||
events: EventEmitter
|
events: EventEmitter
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
logger.debug("========== parseAndCreateFeatures() started ==========");
|
logger.info("========== parseAndCreateFeatures() started ==========");
|
||||||
logger.debug("Content length:", `${content.length} chars`);
|
logger.info(`Content length: ${content.length} chars`);
|
||||||
|
logger.info("========== CONTENT RECEIVED FOR PARSING ==========");
|
||||||
|
logger.info(content);
|
||||||
|
logger.info("========== END CONTENT ==========");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Extract JSON from response
|
// Extract JSON from response
|
||||||
logger.debug("Extracting JSON from response...");
|
logger.info("Extracting JSON from response...");
|
||||||
|
logger.info(`Looking for pattern: /{[\\s\\S]*"features"[\\s\\S]*}/`);
|
||||||
const jsonMatch = content.match(/\{[\s\S]*"features"[\s\S]*\}/);
|
const jsonMatch = content.match(/\{[\s\S]*"features"[\s\S]*\}/);
|
||||||
if (!jsonMatch) {
|
if (!jsonMatch) {
|
||||||
logger.error("❌ No valid JSON found in response");
|
logger.error("❌ No valid JSON found in response");
|
||||||
logger.error("Content preview:", content.substring(0, 500));
|
logger.error("Full content received:");
|
||||||
|
logger.error(content);
|
||||||
throw new Error("No valid JSON found in response");
|
throw new Error("No valid JSON found in response");
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(`JSON match found (${jsonMatch[0].length} chars)`);
|
logger.info(`JSON match found (${jsonMatch[0].length} chars)`);
|
||||||
|
logger.info("========== MATCHED JSON ==========");
|
||||||
|
logger.info(jsonMatch[0]);
|
||||||
|
logger.info("========== END MATCHED JSON ==========");
|
||||||
|
|
||||||
const parsed = JSON.parse(jsonMatch[0]);
|
const parsed = JSON.parse(jsonMatch[0]);
|
||||||
logger.info(`Parsed ${parsed.features?.length || 0} features`);
|
logger.info(`Parsed ${parsed.features?.length || 0} features`);
|
||||||
|
logger.info("Parsed features:", JSON.stringify(parsed.features, null, 2));
|
||||||
|
|
||||||
const featuresDir = path.join(projectPath, ".automaker", "features");
|
const featuresDir = path.join(projectPath, ".automaker", "features");
|
||||||
await fs.mkdir(featuresDir, { recursive: true });
|
await fs.mkdir(featuresDir, { recursive: true });
|
||||||
|
|||||||
@@ -22,12 +22,13 @@ export function createCreateHandler(events: EventEmitter) {
|
|||||||
logger.debug("Request body:", JSON.stringify(req.body, null, 2));
|
logger.debug("Request body:", JSON.stringify(req.body, null, 2));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { projectPath, projectOverview, generateFeatures, analyzeProject } =
|
const { projectPath, projectOverview, generateFeatures, analyzeProject, maxFeatures } =
|
||||||
req.body as {
|
req.body as {
|
||||||
projectPath: string;
|
projectPath: string;
|
||||||
projectOverview: string;
|
projectOverview: string;
|
||||||
generateFeatures?: boolean;
|
generateFeatures?: boolean;
|
||||||
analyzeProject?: boolean;
|
analyzeProject?: boolean;
|
||||||
|
maxFeatures?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
logger.debug("Parsed params:");
|
logger.debug("Parsed params:");
|
||||||
@@ -38,6 +39,7 @@ export function createCreateHandler(events: EventEmitter) {
|
|||||||
);
|
);
|
||||||
logger.debug(" generateFeatures:", generateFeatures);
|
logger.debug(" generateFeatures:", generateFeatures);
|
||||||
logger.debug(" analyzeProject:", analyzeProject);
|
logger.debug(" analyzeProject:", analyzeProject);
|
||||||
|
logger.debug(" maxFeatures:", maxFeatures);
|
||||||
|
|
||||||
if (!projectPath || !projectOverview) {
|
if (!projectPath || !projectOverview) {
|
||||||
logger.error("Missing required parameters");
|
logger.error("Missing required parameters");
|
||||||
@@ -68,7 +70,8 @@ export function createCreateHandler(events: EventEmitter) {
|
|||||||
events,
|
events,
|
||||||
abortController,
|
abortController,
|
||||||
generateFeatures,
|
generateFeatures,
|
||||||
analyzeProject
|
analyzeProject,
|
||||||
|
maxFeatures
|
||||||
)
|
)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
logError(error, "Generation failed with error");
|
logError(error, "Generation failed with error");
|
||||||
|
|||||||
@@ -22,9 +22,13 @@ export function createGenerateFeaturesHandler(events: EventEmitter) {
|
|||||||
logger.debug("Request body:", JSON.stringify(req.body, null, 2));
|
logger.debug("Request body:", JSON.stringify(req.body, null, 2));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { projectPath } = req.body as { projectPath: string };
|
const { projectPath, maxFeatures } = req.body as {
|
||||||
|
projectPath: string;
|
||||||
|
maxFeatures?: number;
|
||||||
|
};
|
||||||
|
|
||||||
logger.debug("projectPath:", projectPath);
|
logger.debug("projectPath:", projectPath);
|
||||||
|
logger.debug("maxFeatures:", maxFeatures);
|
||||||
|
|
||||||
if (!projectPath) {
|
if (!projectPath) {
|
||||||
logger.error("Missing projectPath parameter");
|
logger.error("Missing projectPath parameter");
|
||||||
@@ -45,7 +49,12 @@ export function createGenerateFeaturesHandler(events: EventEmitter) {
|
|||||||
setRunningState(true, abortController);
|
setRunningState(true, abortController);
|
||||||
logger.info("Starting background feature generation task...");
|
logger.info("Starting background feature generation task...");
|
||||||
|
|
||||||
generateFeaturesFromSpec(projectPath, events, abortController)
|
generateFeaturesFromSpec(
|
||||||
|
projectPath,
|
||||||
|
events,
|
||||||
|
abortController,
|
||||||
|
maxFeatures
|
||||||
|
)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
logError(error, "Feature generation failed with error");
|
logError(error, "Feature generation failed with error");
|
||||||
events.emit("spec-regeneration:event", {
|
events.emit("spec-regeneration:event", {
|
||||||
|
|||||||
@@ -27,11 +27,13 @@ export function createGenerateHandler(events: EventEmitter) {
|
|||||||
projectDefinition,
|
projectDefinition,
|
||||||
generateFeatures,
|
generateFeatures,
|
||||||
analyzeProject,
|
analyzeProject,
|
||||||
|
maxFeatures,
|
||||||
} = req.body as {
|
} = req.body as {
|
||||||
projectPath: string;
|
projectPath: string;
|
||||||
projectDefinition: string;
|
projectDefinition: string;
|
||||||
generateFeatures?: boolean;
|
generateFeatures?: boolean;
|
||||||
analyzeProject?: boolean;
|
analyzeProject?: boolean;
|
||||||
|
maxFeatures?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
logger.debug("Parsed params:");
|
logger.debug("Parsed params:");
|
||||||
@@ -42,6 +44,7 @@ export function createGenerateHandler(events: EventEmitter) {
|
|||||||
);
|
);
|
||||||
logger.debug(" generateFeatures:", generateFeatures);
|
logger.debug(" generateFeatures:", generateFeatures);
|
||||||
logger.debug(" analyzeProject:", analyzeProject);
|
logger.debug(" analyzeProject:", analyzeProject);
|
||||||
|
logger.debug(" maxFeatures:", maxFeatures);
|
||||||
|
|
||||||
if (!projectPath || !projectDefinition) {
|
if (!projectPath || !projectDefinition) {
|
||||||
logger.error("Missing required parameters");
|
logger.error("Missing required parameters");
|
||||||
@@ -71,7 +74,8 @@ export function createGenerateHandler(events: EventEmitter) {
|
|||||||
events,
|
events,
|
||||||
abortController,
|
abortController,
|
||||||
generateFeatures,
|
generateFeatures,
|
||||||
analyzeProject
|
analyzeProject,
|
||||||
|
maxFeatures
|
||||||
)
|
)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
logError(error, "Generation failed with error");
|
logError(error, "Generation failed with error");
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
* Business logic for generating suggestions
|
* Business logic for generating suggestions
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { query, type Options } from "@anthropic-ai/claude-agent-sdk";
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
||||||
import type { EventEmitter } from "../../lib/events.js";
|
import type { EventEmitter } from "../../lib/events.js";
|
||||||
import { createLogger } from "../../lib/logger.js";
|
import { createLogger } from "../../lib/logger.js";
|
||||||
|
import { createSuggestionsOptions } from "../../lib/sdk-options.js";
|
||||||
|
|
||||||
const logger = createLogger("Suggestions");
|
const logger = createLogger("Suggestions");
|
||||||
|
|
||||||
@@ -54,14 +55,10 @@ Format your response as JSON:
|
|||||||
content: `Starting ${suggestionType} analysis...\n`,
|
content: `Starting ${suggestionType} analysis...\n`,
|
||||||
});
|
});
|
||||||
|
|
||||||
const options: Options = {
|
const options = createSuggestionsOptions({
|
||||||
model: "claude-opus-4-5-20251101",
|
|
||||||
maxTurns: 5,
|
|
||||||
cwd: projectPath,
|
cwd: projectPath,
|
||||||
allowedTools: ["Read", "Glob", "Grep"],
|
|
||||||
permissionMode: "acceptEdits",
|
|
||||||
abortController,
|
abortController,
|
||||||
};
|
});
|
||||||
|
|
||||||
const stream = query({ prompt, options });
|
const stream = query({ prompt, options });
|
||||||
let responseText = "";
|
let responseText = "";
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { ProviderFactory } from "../providers/provider-factory.js";
|
|||||||
import type { ExecuteOptions } from "../providers/types.js";
|
import type { ExecuteOptions } from "../providers/types.js";
|
||||||
import { readImageAsBase64 } from "../lib/image-handler.js";
|
import { readImageAsBase64 } from "../lib/image-handler.js";
|
||||||
import { buildPromptWithImages } from "../lib/prompt-builder.js";
|
import { buildPromptWithImages } from "../lib/prompt-builder.js";
|
||||||
import { getEffectiveModel } from "../lib/model-resolver.js";
|
import { createChatOptions } from "../lib/sdk-options.js";
|
||||||
import { isAbortError } from "../lib/error-handler.js";
|
import { isAbortError } from "../lib/error-handler.js";
|
||||||
|
|
||||||
interface Message {
|
interface Message {
|
||||||
@@ -176,8 +176,19 @@ export class AgentService {
|
|||||||
await this.saveSession(sessionId, session.messages);
|
await this.saveSession(sessionId, session.messages);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use session model, parameter model, or default
|
// Build SDK options using centralized configuration
|
||||||
const effectiveModel = getEffectiveModel(model, session.model);
|
const sdkOptions = createChatOptions({
|
||||||
|
cwd: workingDirectory || session.workingDirectory,
|
||||||
|
model: model,
|
||||||
|
sessionModel: session.model,
|
||||||
|
systemPrompt: this.getSystemPrompt(),
|
||||||
|
abortController: session.abortController!,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Extract model, maxTurns, and allowedTools from SDK options
|
||||||
|
const effectiveModel = sdkOptions.model!;
|
||||||
|
const maxTurns = sdkOptions.maxTurns;
|
||||||
|
const allowedTools = sdkOptions.allowedTools as string[] | undefined;
|
||||||
|
|
||||||
// Get provider for this model
|
// Get provider for this model
|
||||||
const provider = ProviderFactory.getProviderForModel(effectiveModel);
|
const provider = ProviderFactory.getProviderForModel(effectiveModel);
|
||||||
@@ -192,17 +203,8 @@ export class AgentService {
|
|||||||
model: effectiveModel,
|
model: effectiveModel,
|
||||||
cwd: workingDirectory || session.workingDirectory,
|
cwd: workingDirectory || session.workingDirectory,
|
||||||
systemPrompt: this.getSystemPrompt(),
|
systemPrompt: this.getSystemPrompt(),
|
||||||
maxTurns: 20,
|
maxTurns: maxTurns,
|
||||||
allowedTools: [
|
allowedTools: allowedTools,
|
||||||
"Read",
|
|
||||||
"Write",
|
|
||||||
"Edit",
|
|
||||||
"Glob",
|
|
||||||
"Grep",
|
|
||||||
"Bash",
|
|
||||||
"WebSearch",
|
|
||||||
"WebFetch",
|
|
||||||
],
|
|
||||||
abortController: session.abortController!,
|
abortController: session.abortController!,
|
||||||
conversationHistory:
|
conversationHistory:
|
||||||
conversationHistory.length > 0 ? conversationHistory : undefined,
|
conversationHistory.length > 0 ? conversationHistory : undefined,
|
||||||
|
|||||||
@@ -9,16 +9,16 @@
|
|||||||
* - Verification and merge workflows
|
* - Verification and merge workflows
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { AbortError } from "@anthropic-ai/claude-agent-sdk";
|
|
||||||
import { ProviderFactory } from "../providers/provider-factory.js";
|
import { ProviderFactory } from "../providers/provider-factory.js";
|
||||||
import type { ExecuteOptions } from "../providers/types.js";
|
import type { ExecuteOptions } from "../providers/types.js";
|
||||||
import { exec } from "child_process";
|
import { exec } from "child_process";
|
||||||
import { promisify } from "util";
|
import { promisify } from "util";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import type { EventEmitter, EventType } from "../lib/events.js";
|
import type { EventEmitter } from "../lib/events.js";
|
||||||
import { buildPromptWithImages } from "../lib/prompt-builder.js";
|
import { buildPromptWithImages } from "../lib/prompt-builder.js";
|
||||||
import { resolveModelString, DEFAULT_MODELS } from "../lib/model-resolver.js";
|
import { resolveModelString, DEFAULT_MODELS } from "../lib/model-resolver.js";
|
||||||
|
import { createAutoModeOptions } from "../lib/sdk-options.js";
|
||||||
import { isAbortError, classifyError } from "../lib/error-handler.js";
|
import { isAbortError, classifyError } from "../lib/error-handler.js";
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
@@ -1085,7 +1085,18 @@ When done, summarize what you implemented and any notes for the developer.`;
|
|||||||
imagePaths?: string[],
|
imagePaths?: string[],
|
||||||
model?: string
|
model?: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const finalModel = resolveModelString(model, DEFAULT_MODELS.claude);
|
// Build SDK options using centralized configuration for feature implementation
|
||||||
|
const sdkOptions = createAutoModeOptions({
|
||||||
|
cwd: workDir,
|
||||||
|
model: model,
|
||||||
|
abortController,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Extract model, maxTurns, and allowedTools from SDK options
|
||||||
|
const finalModel = sdkOptions.model!;
|
||||||
|
const maxTurns = sdkOptions.maxTurns;
|
||||||
|
const allowedTools = sdkOptions.allowedTools as string[] | undefined;
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`[AutoMode] runAgent called for feature ${featureId} with model: ${finalModel}`
|
`[AutoMode] runAgent called for feature ${featureId} with model: ${finalModel}`
|
||||||
);
|
);
|
||||||
@@ -1108,9 +1119,9 @@ When done, summarize what you implemented and any notes for the developer.`;
|
|||||||
const options: ExecuteOptions = {
|
const options: ExecuteOptions = {
|
||||||
prompt: promptContent,
|
prompt: promptContent,
|
||||||
model: finalModel,
|
model: finalModel,
|
||||||
maxTurns: 50,
|
maxTurns: maxTurns,
|
||||||
cwd: workDir,
|
cwd: workDir,
|
||||||
allowedTools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash"],
|
allowedTools: allowedTools,
|
||||||
abortController,
|
abortController,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user