diff --git a/.claude/commands/create-spec.md b/.claude/commands/create-spec.md index a979bfd..b26a22a 100644 --- a/.claude/commands/create-spec.md +++ b/.claude/commands/create-spec.md @@ -37,13 +37,7 @@ You are the **Spec Creation Assistant** - an expert at translating project ideas - **Derive** technical details (database schema, API endpoints, architecture) yourself - Only ask technical questions if the user wants to be involved in those decisions -**USE THE AskUserQuestion TOOL** for structured questions. This provides a much better UX with: - -- Multiple-choice options displayed as clickable buttons -- Tabs for grouping related questions -- Free-form "Other" option automatically included - -Use AskUserQuestion whenever you have questions with clear options (involvement level, scale, yes/no choices, preferences). Use regular conversation for open-ended exploration (describing features, walking through user flows). +**Use conversational questions** to gather information. For questions with clear options, present them as numbered choices that the user can select from. For open-ended exploration, use natural conversation. --- @@ -78,34 +72,26 @@ Do NOT immediately jump to Phase 2. Let the user answer, acknowledge their respo ## Phase 2: Involvement Level -**Use AskUserQuestion tool here.** Example: +Ask the user about their involvement preference: -``` -Question: "How involved do you want to be in technical decisions?" -Header: "Involvement" -Options: - - Label: "Quick Mode (Recommended)" - Description: "I'll describe what I want, you handle database, API, and architecture" - - Label: "Detailed Mode" - Description: "I want input on technology choices and architecture decisions" -``` +> "How involved do you want to be in technical decisions? +> +> 1. **Quick Mode (Recommended)** - You describe what you want, I'll handle database, API, and architecture +> 2. **Detailed Mode** - You want input on technology choices and architecture decisions +> +> Which would you prefer?" **If Quick Mode**: Skip to Phase 3, then go to Phase 4 (Features). You will derive technical details yourself. **If Detailed Mode**: Go through all phases, asking technical questions. ## Phase 3: Technology Preferences -**For Quick Mode users**, also ask about tech preferences (can combine in same AskUserQuestion): +**For Quick Mode users**, also ask about tech preferences: -``` -Question: "Any technology preferences, or should I choose sensible defaults?" -Header: "Tech Stack" -Options: - - Label: "Use defaults (Recommended)" - Description: "React, Node.js, SQLite - solid choices for most apps" - - Label: "I have preferences" - Description: "I'll specify my preferred languages/frameworks" -``` +> "Any technology preferences, or should I choose sensible defaults? +> +> 1. **Use defaults (Recommended)** - React, Node.js, SQLite - solid choices for most apps +> 2. **I have preferences** - I'll specify my preferred languages/frameworks" **For Detailed Mode users**, ask specific tech questions about frontend, backend, database, etc. @@ -117,26 +103,14 @@ This is where you spend most of your time. Ask questions in plain language that > "Walk me through your app. What does a user see when they first open it? What can they do?" -**Then use AskUserQuestion for quick yes/no feature areas.** Example: +**Then ask about key feature areas:** -``` -Questions (can ask up to 4 at once): -1. Question: "Do users need to log in / have accounts?" - Header: "Accounts" - Options: Yes (with profiles, settings) | No (anonymous use) | Maybe (optional accounts) - -2. Question: "Should this work well on mobile phones?" - Header: "Mobile" - Options: Yes (fully responsive) | Desktop only | Basic mobile support - -3. Question: "Do users need to search or filter content?" - Header: "Search" - Options: Yes | No | Basic only - -4. Question: "Any sharing or collaboration features?" - Header: "Sharing" - Options: Yes | No | Maybe later -``` +> "Let me ask about a few common feature areas: +> +> 1. **User Accounts** - Do users need to log in / have accounts? (Yes with profiles, No anonymous use, or Maybe optional) +> 2. **Mobile Support** - Should this work well on mobile phones? (Yes fully responsive, Desktop only, or Basic mobile) +> 3. **Search** - Do users need to search or filter content? (Yes, No, or Basic only) +> 4. **Sharing** - Any sharing or collaboration features? (Yes, No, or Maybe later)" **Then drill into the "Yes" answers with open conversation:** @@ -182,19 +156,13 @@ Questions (can ask up to 4 at once): **4i. Security & Access Control (if app has authentication)** -**Use AskUserQuestion for roles:** +Ask about user roles: -``` -Question: "Who are the different types of users?" -Header: "User Roles" -Options: - - Label: "Just regular users" - Description: "Everyone has the same permissions" - - Label: "Users + Admins" - Description: "Regular users and administrators with extra powers" - - Label: "Multiple roles" - Description: "Several distinct user types (e.g., viewer, editor, manager, admin)" -``` +> "Who are the different types of users? +> +> 1. **Just regular users** - Everyone has the same permissions +> 2. **Users + Admins** - Regular users and administrators with extra powers +> 3. **Multiple roles** - Several distinct user types (e.g., viewer, editor, manager, admin)" **If multiple roles, explore in conversation:** @@ -329,17 +297,12 @@ Present everything gathered: First ask in conversation if they want to make changes. -**Then use AskUserQuestion for final confirmation:** +**Then ask for final confirmation:** -``` -Question: "Ready to generate the specification files?" -Header: "Generate" -Options: - - Label: "Yes, generate files" - Description: "Create app_spec.txt and update prompt files" - - Label: "I have changes" - Description: "Let me add or modify something first" -``` +> "Ready to generate the specification files? +> +> 1. **Yes, generate files** - Create app_spec.txt and update prompt files +> 2. **I have changes** - Let me add or modify something first" --- diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..661b405 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,111 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is an autonomous coding agent system with a React-based UI. It uses the Claude Agent SDK to build complete applications over multiple sessions using a two-agent pattern: + +1. **Initializer Agent** - First session reads an app spec and creates features in a SQLite database +2. **Coding Agent** - Subsequent sessions implement features one by one, marking them as passing + +## Commands + +### Python Backend + +```bash +# Create and activate virtual environment +python -m venv venv +venv\Scripts\activate # Windows +source venv/bin/activate # macOS/Linux + +# Install dependencies +pip install -r requirements.txt + +# Run the main CLI launcher +python start.py + +# Run agent directly for a specific project +python autonomous_agent_demo.py --project-dir PROJECT_NAME +``` + +### React UI (in ui/ directory) + +```bash +cd ui +npm install +npm run dev # Development server +npm run build # Production build +npm run lint # Run ESLint +``` + +## Architecture + +### Core Python Modules + +- `start.py` - CLI launcher with project creation/selection menu +- `autonomous_agent_demo.py` - Entry point for running the agent +- `agent.py` - Agent session loop using Claude Agent SDK +- `client.py` - ClaudeSDKClient configuration with security hooks and MCP servers +- `security.py` - Bash command allowlist validation (ALLOWED_COMMANDS whitelist) +- `prompts.py` - Prompt template loading with project-specific fallback +- `progress.py` - Progress tracking, database queries, webhook notifications + +### Feature Management + +Features are stored in SQLite (`features.db`) via SQLAlchemy. The agent interacts with features through an MCP server: + +- `mcp_server/feature_mcp.py` - MCP server exposing feature management tools +- `api/database.py` - SQLAlchemy models (Feature table with priority, category, name, description, steps, passes) + +MCP tools available to the agent: +- `feature_get_stats` - Progress statistics +- `feature_get_next` - Get highest-priority pending feature +- `feature_get_for_regression` - Random passing features for regression testing +- `feature_mark_passing` - Mark feature complete +- `feature_skip` - Move feature to end of queue +- `feature_create_bulk` - Initialize all features (used by initializer) + +### React UI (ui/) + +- Tech stack: React 18, TypeScript, TanStack Query, Tailwind CSS v4, Radix UI +- `src/App.tsx` - Main app with project selection, kanban board, agent controls +- `src/hooks/useWebSocket.ts` - Real-time updates via WebSocket +- `src/hooks/useProjects.ts` - React Query hooks for API calls +- `src/lib/api.ts` - REST API client +- `src/lib/types.ts` - TypeScript type definitions + +### Project Structure for Generated Apps + +Generated projects are stored in `generations/PROJECT_NAME/` with: +- `prompts/app_spec.txt` - Application specification (XML format) +- `prompts/initializer_prompt.md` - First session prompt +- `prompts/coding_prompt.md` - Continuation session prompt +- `features.db` - SQLite database with feature test cases + +### Security Model + +Defense-in-depth approach configured in `client.py`: +1. OS-level sandbox for bash commands +2. Filesystem restricted to project directory only +3. Bash commands validated against `ALLOWED_COMMANDS` in `security.py` + +## Claude Code Integration + +- `.claude/commands/create-spec.md` - `/create-spec` slash command for interactive spec creation +- `.claude/skills/frontend-design/SKILL.md` - Skill for distinctive UI design +- `.claude/templates/` - Prompt templates copied to new projects + +## Key Patterns + +### Prompt Loading Fallback Chain + +1. Project-specific: `generations/{project}/prompts/{name}.md` +2. Base template: `.claude/templates/{name}.template.md` + +### Agent Session Flow + +1. Check if `features.db` has features (determines initializer vs coding agent) +2. Create ClaudeSDKClient with security settings +3. Send prompt and stream response +4. Auto-continue with 3-second delay between sessions diff --git a/SAMPLE_PROMPT.md b/SAMPLE_PROMPT.md new file mode 100644 index 0000000..8a23117 --- /dev/null +++ b/SAMPLE_PROMPT.md @@ -0,0 +1,20 @@ +Let's call it Simple Todo. This is a really simple web app that I can use to track my to-do items using a Kanban +board. I should be able to add to-dos and then drag and drop them through the Kanban board. The different columns in +the Kanban board are: + +- To Do +- In Progress +- Done + +The app should use a neobrutalism design. + +There is no need for user authentication either. All the to-dos will be stored in local storage, so each user has +access to all of their to-dos when they open their browser. So do not worry about implementing a backend with user +authentication or a database. Simply store everything in local storage. As for the design, please try to avoid AI +slop, so use your front-end design skills to design something beautiful and practical. As for the content of the +to-dos, we should store: + +- The name or the title at the very least +- Optionally, we can also set tags, due dates, and priorities which should be represented as beautiful little badges + on the to-do card Users should have the ability to easily clear out all the completed To-Dos. They should also be + able to filter and search for To-Dos as well. diff --git a/client.py b/client.py index dce7e35..115743b 100644 --- a/client.py +++ b/client.py @@ -7,6 +7,7 @@ Functions for creating and configuring the Claude Agent SDK client. import json import os +import shutil import sys from pathlib import Path @@ -131,9 +132,17 @@ def create_client(project_dir: Path, model: str): print(" - Project settings enabled (skills, commands, CLAUDE.md)") print() + # Use system Claude CLI instead of bundled one (avoids Bun runtime crash on Windows) + system_cli = shutil.which("claude") + if system_cli: + print(f" - Using system CLI: {system_cli}") + else: + print(" - Warning: System Claude CLI not found, using bundled CLI") + return ClaudeSDKClient( options=ClaudeAgentOptions( model=model, + cli_path=system_cli, # Use system CLI to avoid bundled Bun crash (exit code 3) system_prompt="You are an expert full-stack developer building a production-quality web application.", setting_sources=["project"], # Enable skills, commands, and CLAUDE.md from project dir max_buffer_size=10 * 1024 * 1024, # 10MB for large Playwright screenshots diff --git a/server/main.py b/server/main.py index 3528f5b..a2f6b42 100644 --- a/server/main.py +++ b/server/main.py @@ -49,8 +49,8 @@ app.add_middleware( allow_origins=[ "http://localhost:5173", # Vite dev server "http://127.0.0.1:5173", - "http://localhost:8000", # Production - "http://127.0.0.1:8000", + "http://localhost:8888", # Production + "http://127.0.0.1:8888", ], allow_credentials=True, allow_methods=["*"], @@ -167,6 +167,6 @@ if __name__ == "__main__": uvicorn.run( "server.main:app", host="127.0.0.1", # Localhost only for security - port=8000, + port=8888, reload=True, ) diff --git a/server/routers/spec_creation.py b/server/routers/spec_creation.py index 1f289e9..abf7453 100644 --- a/server/routers/spec_creation.py +++ b/server/routers/spec_creation.py @@ -136,13 +136,27 @@ async def spec_chat_websocket(websocket: WebSocket, project_name: str): # Create and start a new session session = await create_session(project_name) + # Track spec completion state + spec_complete_received = False + spec_path = None + # Stream the initial greeting async for chunk in session.start(): - await websocket.send_json(chunk) - - # Check for completion + # Track spec_complete but don't send complete yet if chunk.get("type") == "spec_complete": - await websocket.send_json({"type": "complete"}) + spec_complete_received = True + spec_path = chunk.get("path") + await websocket.send_json(chunk) + continue + + # When response_done arrives, send complete if spec was done + if chunk.get("type") == "response_done": + await websocket.send_json(chunk) + if spec_complete_received: + await websocket.send_json({"type": "complete", "path": spec_path}) + continue + + await websocket.send_json(chunk) elif msg_type == "message": # User sent a message @@ -163,13 +177,27 @@ async def spec_chat_websocket(websocket: WebSocket, project_name: str): }) continue + # Track spec completion state + spec_complete_received = False + spec_path = None + # Stream Claude's response async for chunk in session.send_message(user_content): - await websocket.send_json(chunk) - - # Check for completion + # Track spec_complete but don't send complete yet if chunk.get("type") == "spec_complete": - await websocket.send_json({"type": "complete"}) + spec_complete_received = True + spec_path = chunk.get("path") + await websocket.send_json(chunk) + continue + + # When response_done arrives, send complete if spec was done + if chunk.get("type") == "response_done": + await websocket.send_json(chunk) + if spec_complete_received: + await websocket.send_json({"type": "complete", "path": spec_path}) + continue + + await websocket.send_json(chunk) elif msg_type == "answer": # User answered a structured question @@ -196,12 +224,27 @@ async def spec_chat_websocket(websocket: WebSocket, project_name: str): else: user_response = str(answers) + # Track spec completion state + spec_complete_received = False + spec_path = None + # Stream Claude's response async for chunk in session.send_message(user_response): - await websocket.send_json(chunk) - + # Track spec_complete but don't send complete yet if chunk.get("type") == "spec_complete": - await websocket.send_json({"type": "complete"}) + spec_complete_received = True + spec_path = chunk.get("path") + await websocket.send_json(chunk) + continue + + # When response_done arrives, send complete if spec was done + if chunk.get("type") == "response_done": + await websocket.send_json(chunk) + if spec_complete_received: + await websocket.send_json({"type": "complete", "path": spec_path}) + continue + + await websocket.send_json(chunk) else: await websocket.send_json({ diff --git a/server/services/spec_chat_session.py b/server/services/spec_chat_session.py index 6ee6e11..88af0bd 100644 --- a/server/services/spec_chat_session.py +++ b/server/services/spec_chat_session.py @@ -8,6 +8,7 @@ Uses the create-spec.md skill to guide users through app spec creation. import asyncio import logging +import shutil import threading from datetime import datetime from pathlib import Path @@ -87,15 +88,19 @@ class SpecChatSession: system_prompt = skill_content.replace("$ARGUMENTS", project_path) # Create Claude SDK client with limited tools for spec creation + # Use Opus for best quality spec generation + # Use system CLI to avoid bundled Bun runtime crash (exit code 3) on Windows + system_cli = shutil.which("claude") try: self.client = ClaudeSDKClient( options=ClaudeAgentOptions( - model="claude-sonnet-4-20250514", + model="claude-opus-4-5-20251101", + cli_path=system_cli, system_prompt=system_prompt, allowed_tools=[ "Read", "Write", - "AskUserQuestion", + "Glob", ], max_turns=100, cwd=str(ROOT_DIR.resolve()), @@ -169,7 +174,7 @@ class SpecChatSession: """ Internal method to query Claude and stream responses. - Handles tool calls (AskUserQuestion, Write) and text responses. + Handles tool calls (Write) and text responses. """ if not self.client: return @@ -178,6 +183,7 @@ class SpecChatSession: await self.client.query(message) current_text = "" + pending_spec_write = None # Track if we're waiting for app_spec.txt write result # Stream the response using receive_response async for msg in self.client.receive_response(): @@ -207,27 +213,17 @@ class SpecChatSession: tool_input = getattr(block, "input", {}) tool_id = getattr(block, "id", "") - if tool_name == "AskUserQuestion": - # Convert AskUserQuestion to structured UI - questions = tool_input.get("questions", []) - yield { - "type": "question", - "questions": questions, - "tool_id": tool_id - } - # The SDK handles tool results internally - - elif tool_name == "Write": - # File being written - the SDK handles this + if tool_name == "Write": + # File being written - track for verification file_path = tool_input.get("file_path", "") - # Check if this is the app_spec.txt file + # Track if this is the app_spec.txt file if "app_spec.txt" in str(file_path): - self.complete = True - yield { - "type": "spec_complete", - "path": str(file_path) + pending_spec_write = { + "tool_id": tool_id, + "path": file_path } + logger.info(f"Write tool called for app_spec.txt: {file_path}") elif "initializer_prompt.md" in str(file_path): yield { @@ -236,15 +232,36 @@ class SpecChatSession: } elif msg_type == "UserMessage" and hasattr(msg, "content"): - # Tool results - the SDK handles these automatically - # We just watch for any errors + # Tool results - check for write confirmations and errors for block in msg.content: block_type = type(block).__name__ if block_type == "ToolResultBlock": is_error = getattr(block, "is_error", False) + tool_use_id = getattr(block, "tool_use_id", "") + if is_error: content = getattr(block, "content", "Unknown error") logger.warning(f"Tool error: {content}") + # If the spec write failed, clear the pending write + if pending_spec_write and tool_use_id == pending_spec_write.get("tool_id"): + logger.error(f"app_spec.txt write failed: {content}") + pending_spec_write = None + else: + # Tool succeeded - check if it was the spec write + if pending_spec_write and tool_use_id == pending_spec_write.get("tool_id"): + spec_path = pending_spec_write["path"] + # Verify the file actually exists + full_path = ROOT_DIR / spec_path + if full_path.exists(): + logger.info(f"app_spec.txt verified at: {full_path}") + self.complete = True + yield { + "type": "spec_complete", + "path": str(spec_path) + } + else: + logger.error(f"app_spec.txt not found after write: {full_path}") + pending_spec_write = None def is_complete(self) -> bool: """Check if spec creation is complete.""" diff --git a/start_ui.py b/start_ui.py index 4ae9acc..4edf1c9 100644 --- a/start_ui.py +++ b/start_ui.py @@ -40,7 +40,7 @@ def print_step(step: int, total: int, message: str) -> None: print("-" * 50) -def find_available_port(start: int = 8000, max_attempts: int = 10) -> int: +def find_available_port(start: int = 8888, max_attempts: int = 10) -> int: """Find an available port starting from the given port.""" for port in range(start, start + max_attempts): try: diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 8d67635..a299f19 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -8,7 +8,8 @@ import { ProgressDashboard } from './components/ProgressDashboard' import { SetupWizard } from './components/SetupWizard' import { AddFeatureForm } from './components/AddFeatureForm' import { FeatureModal } from './components/FeatureModal' -import { Plus } from 'lucide-react' +import { DebugLogViewer } from './components/DebugLogViewer' +import { Plus, Loader2 } from 'lucide-react' import type { Feature } from './lib/types' function App() { @@ -16,6 +17,7 @@ function App() { const [showAddFeature, setShowAddFeature] = useState(false) const [selectedFeature, setSelectedFeature] = useState(null) const [setupComplete, setSetupComplete] = useState(true) // Start optimistic + const [debugOpen, setDebugOpen] = useState(false) const { data: projects, isLoading: projectsLoading } = useProjects() const { data: features } = useFeatures(selectedProject) @@ -78,7 +80,7 @@ function App() { {/* Main Content */} -
+
{!selectedProject ? (

@@ -98,6 +100,23 @@ function App() { isConnected={wsState.isConnected} /> + {/* Initializing Features State - show when agent is running but no features yet */} + {features && + features.pending.length === 0 && + features.in_progress.length === 0 && + features.done.length === 0 && + wsState.agentStatus === 'running' && ( +
+ +

+ Initializing Features... +

+

+ The agent is reading your spec and creating features. This may take a moment. +

+
+ )} + {/* Kanban Board */} setSelectedFeature(null)} /> )} + + {/* Debug Log Viewer - fixed to bottom */} + {selectedProject && ( + setDebugOpen(!debugOpen)} + onClear={wsState.clearLogs} + /> + )}

) } diff --git a/ui/src/components/DebugLogViewer.tsx b/ui/src/components/DebugLogViewer.tsx new file mode 100644 index 0000000..d4b20e7 --- /dev/null +++ b/ui/src/components/DebugLogViewer.tsx @@ -0,0 +1,177 @@ +/** + * Debug Log Viewer Component + * + * Collapsible panel at the bottom of the screen showing real-time + * agent output (tool calls, results, steps). Similar to browser DevTools. + */ + +import { useEffect, useRef, useState } from 'react' +import { ChevronUp, ChevronDown, Trash2, Terminal } from 'lucide-react' + +interface DebugLogViewerProps { + logs: Array<{ line: string; timestamp: string }> + isOpen: boolean + onToggle: () => void + onClear: () => void +} + +type LogLevel = 'error' | 'warn' | 'debug' | 'info' + +export function DebugLogViewer({ + logs, + isOpen, + onToggle, + onClear, +}: DebugLogViewerProps) { + const scrollRef = useRef(null) + const [autoScroll, setAutoScroll] = useState(true) + + // Auto-scroll to bottom when new logs arrive (if user hasn't scrolled up) + useEffect(() => { + if (autoScroll && scrollRef.current && isOpen) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight + } + }, [logs, autoScroll, isOpen]) + + // Detect if user scrolled up + const handleScroll = (e: React.UIEvent) => { + const el = e.currentTarget + const isAtBottom = el.scrollHeight - el.scrollTop <= el.clientHeight + 50 + setAutoScroll(isAtBottom) + } + + // Parse log level from line content + const getLogLevel = (line: string): LogLevel => { + const lowerLine = line.toLowerCase() + if (lowerLine.includes('error') || lowerLine.includes('exception') || lowerLine.includes('traceback')) { + return 'error' + } + if (lowerLine.includes('warn') || lowerLine.includes('warning')) { + return 'warn' + } + if (lowerLine.includes('debug')) { + return 'debug' + } + return 'info' + } + + // Get color class for log level + const getLogColor = (level: LogLevel): string => { + switch (level) { + case 'error': + return 'text-red-400' + case 'warn': + return 'text-yellow-400' + case 'debug': + return 'text-gray-400' + case 'info': + default: + return 'text-green-400' + } + } + + // Format timestamp to HH:MM:SS + const formatTimestamp = (timestamp: string): string => { + try { + const date = new Date(timestamp) + return date.toLocaleTimeString('en-US', { + hour12: false, + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }) + } catch { + return '' + } + } + + return ( +
+ {/* Header bar */} +
+
+ + + Debug + + {logs.length > 0 && ( + + {logs.length} + + )} + {!autoScroll && isOpen && ( + + Paused + + )} +
+ +
+ {isOpen && ( + + )} +
+ {isOpen ? ( + + ) : ( + + )} +
+
+
+ + {/* Log content area */} + {isOpen && ( +
+ {logs.length === 0 ? ( +
+ No logs yet. Start the agent to see output. +
+ ) : ( +
+ {logs.map((log, index) => { + const level = getLogLevel(log.line) + const colorClass = getLogColor(level) + const timestamp = formatTimestamp(log.timestamp) + + return ( +
+ + {timestamp} + + + {log.line} + +
+ ) + })} +
+ )} +
+ )} +
+ ) +} diff --git a/ui/src/components/NewProjectModal.tsx b/ui/src/components/NewProjectModal.tsx index bbbd58c..c47b596 100644 --- a/ui/src/components/NewProjectModal.tsx +++ b/ui/src/components/NewProjectModal.tsx @@ -12,6 +12,9 @@ import { useState } from 'react' import { X, Bot, FileEdit, ArrowRight, ArrowLeft, Loader2, CheckCircle2 } from 'lucide-react' import { useCreateProject } from '../hooks/useProjects' import { SpecCreationChat } from './SpecCreationChat' +import { startAgent } from '../lib/api' + +type InitializerStatus = 'idle' | 'starting' | 'error' type Step = 'name' | 'method' | 'chat' | 'complete' type SpecMethod = 'claude' | 'manual' @@ -31,6 +34,8 @@ export function NewProjectModal({ const [projectName, setProjectName] = useState('') const [_specMethod, setSpecMethod] = useState(null) const [error, setError] = useState(null) + const [initializerStatus, setInitializerStatus] = useState('idle') + const [initializerError, setInitializerError] = useState(null) // Suppress unused variable warning - specMethod may be used in future void _specMethod @@ -89,12 +94,27 @@ export function NewProjectModal({ } } - const handleSpecComplete = () => { - setStep('complete') - setTimeout(() => { - onProjectCreated(projectName.trim()) - handleClose() - }, 1500) + const handleSpecComplete = async () => { + // Auto-start the initializer agent + setInitializerStatus('starting') + try { + await startAgent(projectName.trim()) + // Success - navigate to project + setStep('complete') + setTimeout(() => { + onProjectCreated(projectName.trim()) + handleClose() + }, 1500) + } catch (err) { + setInitializerStatus('error') + setInitializerError(err instanceof Error ? err.message : 'Failed to start agent') + } + } + + const handleRetryInitializer = () => { + setInitializerError(null) + setInitializerStatus('idle') + handleSpecComplete() } const handleChatCancel = () => { @@ -108,6 +128,8 @@ export function NewProjectModal({ setProjectName('') setSpecMethod(null) setError(null) + setInitializerStatus('idle') + setInitializerError(null) onClose() } @@ -126,6 +148,9 @@ export function NewProjectModal({ projectName={projectName.trim()} onComplete={handleSpecComplete} onCancel={handleChatCancel} + initializerStatus={initializerStatus} + initializerError={initializerError} + onRetryInitializer={handleRetryInitializer} /> ) diff --git a/ui/src/components/SpecCreationChat.tsx b/ui/src/components/SpecCreationChat.tsx index caa7b50..a64aac3 100644 --- a/ui/src/components/SpecCreationChat.tsx +++ b/ui/src/components/SpecCreationChat.tsx @@ -6,22 +6,30 @@ */ import { useEffect, useRef, useState } from 'react' -import { Send, X, CheckCircle2, AlertCircle, Wifi, WifiOff, RotateCcw } from 'lucide-react' +import { Send, X, CheckCircle2, AlertCircle, Wifi, WifiOff, RotateCcw, Loader2, ArrowRight } from 'lucide-react' import { useSpecChat } from '../hooks/useSpecChat' import { ChatMessage } from './ChatMessage' import { QuestionOptions } from './QuestionOptions' import { TypingIndicator } from './TypingIndicator' +type InitializerStatus = 'idle' | 'starting' | 'error' + interface SpecCreationChatProps { projectName: string onComplete: (specPath: string) => void onCancel: () => void + initializerStatus?: InitializerStatus + initializerError?: string | null + onRetryInitializer?: () => void } export function SpecCreationChat({ projectName, onComplete, onCancel, + initializerStatus = 'idle', + initializerError = null, + onRetryInitializer, }: SpecCreationChatProps) { const [input, setInput] = useState('') const [error, setError] = useState(null) @@ -241,18 +249,50 @@ export function SpecCreationChat({ {/* Completion footer */} {isComplete && ( -
+
- - Specification created successfully! + {initializerStatus === 'starting' ? ( + <> + + Starting agent... + + ) : initializerStatus === 'error' ? ( + <> + + + {initializerError || 'Failed to start agent'} + + + ) : ( + <> + + Specification created successfully! + + )} +
+
+ {initializerStatus === 'error' && onRetryInitializer && ( + + )} + {initializerStatus === 'idle' && ( + + )}
-
)} diff --git a/ui/src/hooks/useSpecChat.ts b/ui/src/hooks/useSpecChat.ts index 0c435e6..4442466 100644 --- a/ui/src/hooks/useSpecChat.ts +++ b/ui/src/hooks/useSpecChat.ts @@ -203,7 +203,9 @@ export function useSpecChat({ }, ]) - onComplete?.(data.path) + // NOTE: Do NOT auto-call onComplete here! + // User should click "Continue to Project" button to start the agent. + // This matches the CLI behavior where user closes the chat manually. break } diff --git a/ui/tsconfig.tsbuildinfo b/ui/tsconfig.tsbuildinfo index 2d7a635..4e16d75 100644 --- a/ui/tsconfig.tsbuildinfo +++ b/ui/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/addfeatureform.tsx","./src/components/agentcontrol.tsx","./src/components/chatmessage.tsx","./src/components/featurecard.tsx","./src/components/featuremodal.tsx","./src/components/kanbanboard.tsx","./src/components/kanbancolumn.tsx","./src/components/newprojectmodal.tsx","./src/components/progressdashboard.tsx","./src/components/projectselector.tsx","./src/components/questionoptions.tsx","./src/components/setupwizard.tsx","./src/components/speccreationchat.tsx","./src/components/typingindicator.tsx","./src/hooks/useprojects.ts","./src/hooks/usespecchat.ts","./src/hooks/usewebsocket.ts","./src/lib/api.ts","./src/lib/types.ts"],"version":"5.6.3"} \ No newline at end of file +{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/addfeatureform.tsx","./src/components/agentcontrol.tsx","./src/components/chatmessage.tsx","./src/components/debuglogviewer.tsx","./src/components/featurecard.tsx","./src/components/featuremodal.tsx","./src/components/kanbanboard.tsx","./src/components/kanbancolumn.tsx","./src/components/newprojectmodal.tsx","./src/components/progressdashboard.tsx","./src/components/projectselector.tsx","./src/components/questionoptions.tsx","./src/components/setupwizard.tsx","./src/components/speccreationchat.tsx","./src/components/typingindicator.tsx","./src/hooks/useprojects.ts","./src/hooks/usespecchat.ts","./src/hooks/usewebsocket.ts","./src/lib/api.ts","./src/lib/types.ts"],"version":"5.6.3"} \ No newline at end of file diff --git a/ui/vite.config.ts b/ui/vite.config.ts index 9e002c7..8be09ba 100644 --- a/ui/vite.config.ts +++ b/ui/vite.config.ts @@ -4,7 +4,7 @@ import tailwindcss from '@tailwindcss/vite' import path from 'path' // Backend port - can be overridden via VITE_API_PORT env var -const apiPort = process.env.VITE_API_PORT || '8000' +const apiPort = process.env.VITE_API_PORT || '8888' // https://vite.dev/config/ export default defineConfig({