Adding features work

This commit is contained in:
Auto
2025-12-30 16:11:08 +02:00
parent 5ffb6a4c5e
commit cb65cfe151
15 changed files with 562 additions and 126 deletions

View File

@@ -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"
---

111
CLAUDE.md Normal file
View File

@@ -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

20
SAMPLE_PROMPT.md Normal file
View File

@@ -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.

View File

@@ -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

View File

@@ -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,
)

View File

@@ -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({

View File

@@ -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."""

View File

@@ -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:

View File

@@ -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<Feature | null>(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() {
</header>
{/* Main Content */}
<main className="max-w-7xl mx-auto px-4 py-8">
<main className={`max-w-7xl mx-auto px-4 py-8 ${debugOpen ? 'pb-80' : ''}`}>
{!selectedProject ? (
<div className="neo-empty-state mt-12">
<h2 className="font-display text-2xl font-bold mb-2">
@@ -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' && (
<div className="neo-card p-8 text-center">
<Loader2 size={32} className="animate-spin mx-auto mb-4 text-[var(--color-neo-progress)]" />
<h3 className="font-display font-bold text-xl mb-2">
Initializing Features...
</h3>
<p className="text-[var(--color-neo-text-secondary)]">
The agent is reading your spec and creating features. This may take a moment.
</p>
</div>
)}
{/* Kanban Board */}
<KanbanBoard
features={features}
@@ -123,6 +142,16 @@ function App() {
onClose={() => setSelectedFeature(null)}
/>
)}
{/* Debug Log Viewer - fixed to bottom */}
{selectedProject && (
<DebugLogViewer
logs={wsState.logs}
isOpen={debugOpen}
onToggle={() => setDebugOpen(!debugOpen)}
onClear={wsState.clearLogs}
/>
)}
</div>
)
}

View File

@@ -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<HTMLDivElement>(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<HTMLDivElement>) => {
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 (
<div
className={`fixed bottom-0 left-0 right-0 z-40 transition-all duration-200 ${
isOpen ? 'h-72' : 'h-10'
}`}
>
{/* Header bar */}
<div
className="flex items-center justify-between h-10 px-4 bg-[#1a1a1a] border-t-3 border-black cursor-pointer"
onClick={onToggle}
>
<div className="flex items-center gap-2">
<Terminal size={16} className="text-green-400" />
<span className="font-mono text-sm text-white font-bold">
Debug
</span>
{logs.length > 0 && (
<span className="px-2 py-0.5 text-xs font-mono bg-[#333] text-gray-300 rounded">
{logs.length}
</span>
)}
{!autoScroll && isOpen && (
<span className="px-2 py-0.5 text-xs font-mono bg-yellow-600 text-white rounded">
Paused
</span>
)}
</div>
<div className="flex items-center gap-2">
{isOpen && (
<button
onClick={(e) => {
e.stopPropagation()
onClear()
}}
className="p-1.5 hover:bg-[#333] rounded transition-colors"
title="Clear logs"
>
<Trash2 size={14} className="text-gray-400" />
</button>
)}
<div className="p-1">
{isOpen ? (
<ChevronDown size={16} className="text-gray-400" />
) : (
<ChevronUp size={16} className="text-gray-400" />
)}
</div>
</div>
</div>
{/* Log content area */}
{isOpen && (
<div
ref={scrollRef}
onScroll={handleScroll}
className="h-[calc(100%-2.5rem)] overflow-y-auto bg-[#1a1a1a] p-2 font-mono text-sm"
>
{logs.length === 0 ? (
<div className="flex items-center justify-center h-full text-gray-500">
No logs yet. Start the agent to see output.
</div>
) : (
<div className="space-y-0.5">
{logs.map((log, index) => {
const level = getLogLevel(log.line)
const colorClass = getLogColor(level)
const timestamp = formatTimestamp(log.timestamp)
return (
<div
key={`${log.timestamp}-${index}`}
className="flex gap-2 hover:bg-[#2a2a2a] px-1 py-0.5 rounded"
>
<span className="text-gray-500 select-none shrink-0">
{timestamp}
</span>
<span className={`${colorClass} whitespace-pre-wrap break-all`}>
{log.line}
</span>
</div>
)
})}
</div>
)}
</div>
)}
</div>
)
}

View File

@@ -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<SpecMethod | null>(null)
const [error, setError] = useState<string | null>(null)
const [initializerStatus, setInitializerStatus] = useState<InitializerStatus>('idle')
const [initializerError, setInitializerError] = useState<string | null>(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}
/>
</div>
)

View File

@@ -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<string | null>(null)
@@ -241,18 +249,50 @@ export function SpecCreationChat({
{/* Completion footer */}
{isComplete && (
<div className="p-4 border-t-3 border-[var(--color-neo-border)] bg-[var(--color-neo-done)]">
<div className={`p-4 border-t-3 border-[var(--color-neo-border)] ${
initializerStatus === 'error' ? 'bg-[var(--color-neo-danger)]' : 'bg-[var(--color-neo-done)]'
}`}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<CheckCircle2 size={20} />
<span className="font-bold">Specification created successfully!</span>
{initializerStatus === 'starting' ? (
<>
<Loader2 size={20} className="animate-spin" />
<span className="font-bold">Starting agent...</span>
</>
) : initializerStatus === 'error' ? (
<>
<AlertCircle size={20} className="text-white" />
<span className="font-bold text-white">
{initializerError || 'Failed to start agent'}
</span>
</>
) : (
<>
<CheckCircle2 size={20} />
<span className="font-bold">Specification created successfully!</span>
</>
)}
</div>
<div className="flex items-center gap-2">
{initializerStatus === 'error' && onRetryInitializer && (
<button
onClick={onRetryInitializer}
className="neo-btn bg-white"
>
<RotateCcw size={14} />
Retry
</button>
)}
{initializerStatus === 'idle' && (
<button
onClick={() => onComplete('')}
className="neo-btn neo-btn-primary"
>
Continue to Project
<ArrowRight size={16} />
</button>
)}
</div>
<button
onClick={() => onComplete('')}
className="neo-btn bg-white"
>
Continue to Project
</button>
</div>
</div>
)}

View File

@@ -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
}

View File

@@ -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"}
{"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"}

View File

@@ -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({