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 - **Derive** technical details (database schema, API endpoints, architecture) yourself
- Only ask technical questions if the user wants to be involved in those decisions - 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: **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.
- 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).
--- ---
@@ -78,34 +72,26 @@ Do NOT immediately jump to Phase 2. Let the user answer, acknowledge their respo
## Phase 2: Involvement Level ## Phase 2: Involvement Level
**Use AskUserQuestion tool here.** Example: Ask the user about their involvement preference:
``` > "How involved do you want to be in technical decisions?
Question: "How involved do you want to be in technical decisions?" >
Header: "Involvement" > 1. **Quick Mode (Recommended)** - You describe what you want, I'll handle database, API, and architecture
Options: > 2. **Detailed Mode** - You want input on technology choices and architecture decisions
- Label: "Quick Mode (Recommended)" >
Description: "I'll describe what I want, you handle database, API, and architecture" > Which would you prefer?"
- Label: "Detailed Mode"
Description: "I want input on technology choices and architecture decisions"
```
**If Quick Mode**: Skip to Phase 3, then go to Phase 4 (Features). You will derive technical details yourself. **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. **If Detailed Mode**: Go through all phases, asking technical questions.
## Phase 3: Technology Preferences ## 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:
``` > "Any technology preferences, or should I choose sensible defaults?
Question: "Any technology preferences, or should I choose sensible defaults?" >
Header: "Tech Stack" > 1. **Use defaults (Recommended)** - React, Node.js, SQLite - solid choices for most apps
Options: > 2. **I have preferences** - I'll specify my preferred languages/frameworks"
- 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"
```
**For Detailed Mode users**, ask specific tech questions about frontend, backend, database, etc. **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?" > "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:**
``` > "Let me ask about a few common feature areas:
Questions (can ask up to 4 at once): >
1. Question: "Do users need to log in / have accounts?" > 1. **User Accounts** - Do users need to log in / have accounts? (Yes with profiles, No anonymous use, or Maybe optional)
Header: "Accounts" > 2. **Mobile Support** - Should this work well on mobile phones? (Yes fully responsive, Desktop only, or Basic mobile)
Options: Yes (with profiles, settings) | No (anonymous use) | Maybe (optional accounts) > 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)"
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
```
**Then drill into the "Yes" answers with open conversation:** **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)** **4i. Security & Access Control (if app has authentication)**
**Use AskUserQuestion for roles:** Ask about user roles:
``` > "Who are the different types of users?
Question: "Who are the different types of users?" >
Header: "User Roles" > 1. **Just regular users** - Everyone has the same permissions
Options: > 2. **Users + Admins** - Regular users and administrators with extra powers
- Label: "Just regular users" > 3. **Multiple roles** - Several distinct user types (e.g., viewer, editor, manager, admin)"
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)"
```
**If multiple roles, explore in conversation:** **If multiple roles, explore in conversation:**
@@ -329,17 +297,12 @@ Present everything gathered:
First ask in conversation if they want to make changes. First ask in conversation if they want to make changes.
**Then use AskUserQuestion for final confirmation:** **Then ask for final confirmation:**
``` > "Ready to generate the specification files?
Question: "Ready to generate the specification files?" >
Header: "Generate" > 1. **Yes, generate files** - Create app_spec.txt and update prompt files
Options: > 2. **I have changes** - Let me add or modify something first"
- 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"
```
--- ---

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 json
import os import os
import shutil
import sys import sys
from pathlib import Path 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(" - Project settings enabled (skills, commands, CLAUDE.md)")
print() 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( return ClaudeSDKClient(
options=ClaudeAgentOptions( options=ClaudeAgentOptions(
model=model, 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.", 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 setting_sources=["project"], # Enable skills, commands, and CLAUDE.md from project dir
max_buffer_size=10 * 1024 * 1024, # 10MB for large Playwright screenshots max_buffer_size=10 * 1024 * 1024, # 10MB for large Playwright screenshots

View File

@@ -49,8 +49,8 @@ app.add_middleware(
allow_origins=[ allow_origins=[
"http://localhost:5173", # Vite dev server "http://localhost:5173", # Vite dev server
"http://127.0.0.1:5173", "http://127.0.0.1:5173",
"http://localhost:8000", # Production "http://localhost:8888", # Production
"http://127.0.0.1:8000", "http://127.0.0.1:8888",
], ],
allow_credentials=True, allow_credentials=True,
allow_methods=["*"], allow_methods=["*"],
@@ -167,6 +167,6 @@ if __name__ == "__main__":
uvicorn.run( uvicorn.run(
"server.main:app", "server.main:app",
host="127.0.0.1", # Localhost only for security host="127.0.0.1", # Localhost only for security
port=8000, port=8888,
reload=True, reload=True,
) )

View File

@@ -136,13 +136,27 @@ async def spec_chat_websocket(websocket: WebSocket, project_name: str):
# Create and start a new session # Create and start a new session
session = await create_session(project_name) session = await create_session(project_name)
# Track spec completion state
spec_complete_received = False
spec_path = None
# Stream the initial greeting # Stream the initial greeting
async for chunk in session.start(): async for chunk in session.start():
await websocket.send_json(chunk) # Track spec_complete but don't send complete yet
# Check for completion
if chunk.get("type") == "spec_complete": 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": elif msg_type == "message":
# User sent a message # User sent a message
@@ -163,13 +177,27 @@ async def spec_chat_websocket(websocket: WebSocket, project_name: str):
}) })
continue continue
# Track spec completion state
spec_complete_received = False
spec_path = None
# Stream Claude's response # Stream Claude's response
async for chunk in session.send_message(user_content): async for chunk in session.send_message(user_content):
await websocket.send_json(chunk) # Track spec_complete but don't send complete yet
# Check for completion
if chunk.get("type") == "spec_complete": 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": elif msg_type == "answer":
# User answered a structured question # User answered a structured question
@@ -196,12 +224,27 @@ async def spec_chat_websocket(websocket: WebSocket, project_name: str):
else: else:
user_response = str(answers) user_response = str(answers)
# Track spec completion state
spec_complete_received = False
spec_path = None
# Stream Claude's response # Stream Claude's response
async for chunk in session.send_message(user_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": 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: else:
await websocket.send_json({ 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 asyncio
import logging import logging
import shutil
import threading import threading
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
@@ -87,15 +88,19 @@ class SpecChatSession:
system_prompt = skill_content.replace("$ARGUMENTS", project_path) system_prompt = skill_content.replace("$ARGUMENTS", project_path)
# Create Claude SDK client with limited tools for spec creation # 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: try:
self.client = ClaudeSDKClient( self.client = ClaudeSDKClient(
options=ClaudeAgentOptions( options=ClaudeAgentOptions(
model="claude-sonnet-4-20250514", model="claude-opus-4-5-20251101",
cli_path=system_cli,
system_prompt=system_prompt, system_prompt=system_prompt,
allowed_tools=[ allowed_tools=[
"Read", "Read",
"Write", "Write",
"AskUserQuestion", "Glob",
], ],
max_turns=100, max_turns=100,
cwd=str(ROOT_DIR.resolve()), cwd=str(ROOT_DIR.resolve()),
@@ -169,7 +174,7 @@ class SpecChatSession:
""" """
Internal method to query Claude and stream responses. 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: if not self.client:
return return
@@ -178,6 +183,7 @@ class SpecChatSession:
await self.client.query(message) await self.client.query(message)
current_text = "" current_text = ""
pending_spec_write = None # Track if we're waiting for app_spec.txt write result
# Stream the response using receive_response # Stream the response using receive_response
async for msg in self.client.receive_response(): async for msg in self.client.receive_response():
@@ -207,27 +213,17 @@ class SpecChatSession:
tool_input = getattr(block, "input", {}) tool_input = getattr(block, "input", {})
tool_id = getattr(block, "id", "") tool_id = getattr(block, "id", "")
if tool_name == "AskUserQuestion": if tool_name == "Write":
# Convert AskUserQuestion to structured UI # File being written - track for verification
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
file_path = tool_input.get("file_path", "") 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): if "app_spec.txt" in str(file_path):
self.complete = True pending_spec_write = {
yield { "tool_id": tool_id,
"type": "spec_complete", "path": file_path
"path": str(file_path)
} }
logger.info(f"Write tool called for app_spec.txt: {file_path}")
elif "initializer_prompt.md" in str(file_path): elif "initializer_prompt.md" in str(file_path):
yield { yield {
@@ -236,15 +232,36 @@ class SpecChatSession:
} }
elif msg_type == "UserMessage" and hasattr(msg, "content"): elif msg_type == "UserMessage" and hasattr(msg, "content"):
# Tool results - the SDK handles these automatically # Tool results - check for write confirmations and errors
# We just watch for any errors
for block in msg.content: for block in msg.content:
block_type = type(block).__name__ block_type = type(block).__name__
if block_type == "ToolResultBlock": if block_type == "ToolResultBlock":
is_error = getattr(block, "is_error", False) is_error = getattr(block, "is_error", False)
tool_use_id = getattr(block, "tool_use_id", "")
if is_error: if is_error:
content = getattr(block, "content", "Unknown error") content = getattr(block, "content", "Unknown error")
logger.warning(f"Tool error: {content}") 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: def is_complete(self) -> bool:
"""Check if spec creation is complete.""" """Check if spec creation is complete."""

View File

@@ -40,7 +40,7 @@ def print_step(step: int, total: int, message: str) -> None:
print("-" * 50) 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.""" """Find an available port starting from the given port."""
for port in range(start, start + max_attempts): for port in range(start, start + max_attempts):
try: try:

View File

@@ -8,7 +8,8 @@ import { ProgressDashboard } from './components/ProgressDashboard'
import { SetupWizard } from './components/SetupWizard' import { SetupWizard } from './components/SetupWizard'
import { AddFeatureForm } from './components/AddFeatureForm' import { AddFeatureForm } from './components/AddFeatureForm'
import { FeatureModal } from './components/FeatureModal' 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' import type { Feature } from './lib/types'
function App() { function App() {
@@ -16,6 +17,7 @@ function App() {
const [showAddFeature, setShowAddFeature] = useState(false) const [showAddFeature, setShowAddFeature] = useState(false)
const [selectedFeature, setSelectedFeature] = useState<Feature | null>(null) const [selectedFeature, setSelectedFeature] = useState<Feature | null>(null)
const [setupComplete, setSetupComplete] = useState(true) // Start optimistic const [setupComplete, setSetupComplete] = useState(true) // Start optimistic
const [debugOpen, setDebugOpen] = useState(false)
const { data: projects, isLoading: projectsLoading } = useProjects() const { data: projects, isLoading: projectsLoading } = useProjects()
const { data: features } = useFeatures(selectedProject) const { data: features } = useFeatures(selectedProject)
@@ -78,7 +80,7 @@ function App() {
</header> </header>
{/* Main Content */} {/* 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 ? ( {!selectedProject ? (
<div className="neo-empty-state mt-12"> <div className="neo-empty-state mt-12">
<h2 className="font-display text-2xl font-bold mb-2"> <h2 className="font-display text-2xl font-bold mb-2">
@@ -98,6 +100,23 @@ function App() {
isConnected={wsState.isConnected} 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 */} {/* Kanban Board */}
<KanbanBoard <KanbanBoard
features={features} features={features}
@@ -123,6 +142,16 @@ function App() {
onClose={() => setSelectedFeature(null)} onClose={() => setSelectedFeature(null)}
/> />
)} )}
{/* Debug Log Viewer - fixed to bottom */}
{selectedProject && (
<DebugLogViewer
logs={wsState.logs}
isOpen={debugOpen}
onToggle={() => setDebugOpen(!debugOpen)}
onClear={wsState.clearLogs}
/>
)}
</div> </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 { X, Bot, FileEdit, ArrowRight, ArrowLeft, Loader2, CheckCircle2 } from 'lucide-react'
import { useCreateProject } from '../hooks/useProjects' import { useCreateProject } from '../hooks/useProjects'
import { SpecCreationChat } from './SpecCreationChat' import { SpecCreationChat } from './SpecCreationChat'
import { startAgent } from '../lib/api'
type InitializerStatus = 'idle' | 'starting' | 'error'
type Step = 'name' | 'method' | 'chat' | 'complete' type Step = 'name' | 'method' | 'chat' | 'complete'
type SpecMethod = 'claude' | 'manual' type SpecMethod = 'claude' | 'manual'
@@ -31,6 +34,8 @@ export function NewProjectModal({
const [projectName, setProjectName] = useState('') const [projectName, setProjectName] = useState('')
const [_specMethod, setSpecMethod] = useState<SpecMethod | null>(null) const [_specMethod, setSpecMethod] = useState<SpecMethod | null>(null)
const [error, setError] = useState<string | 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 // Suppress unused variable warning - specMethod may be used in future
void _specMethod void _specMethod
@@ -89,12 +94,27 @@ export function NewProjectModal({
} }
} }
const handleSpecComplete = () => { const handleSpecComplete = async () => {
setStep('complete') // Auto-start the initializer agent
setTimeout(() => { setInitializerStatus('starting')
onProjectCreated(projectName.trim()) try {
handleClose() await startAgent(projectName.trim())
}, 1500) // 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 = () => { const handleChatCancel = () => {
@@ -108,6 +128,8 @@ export function NewProjectModal({
setProjectName('') setProjectName('')
setSpecMethod(null) setSpecMethod(null)
setError(null) setError(null)
setInitializerStatus('idle')
setInitializerError(null)
onClose() onClose()
} }
@@ -126,6 +148,9 @@ export function NewProjectModal({
projectName={projectName.trim()} projectName={projectName.trim()}
onComplete={handleSpecComplete} onComplete={handleSpecComplete}
onCancel={handleChatCancel} onCancel={handleChatCancel}
initializerStatus={initializerStatus}
initializerError={initializerError}
onRetryInitializer={handleRetryInitializer}
/> />
</div> </div>
) )

View File

@@ -6,22 +6,30 @@
*/ */
import { useEffect, useRef, useState } from 'react' 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 { useSpecChat } from '../hooks/useSpecChat'
import { ChatMessage } from './ChatMessage' import { ChatMessage } from './ChatMessage'
import { QuestionOptions } from './QuestionOptions' import { QuestionOptions } from './QuestionOptions'
import { TypingIndicator } from './TypingIndicator' import { TypingIndicator } from './TypingIndicator'
type InitializerStatus = 'idle' | 'starting' | 'error'
interface SpecCreationChatProps { interface SpecCreationChatProps {
projectName: string projectName: string
onComplete: (specPath: string) => void onComplete: (specPath: string) => void
onCancel: () => void onCancel: () => void
initializerStatus?: InitializerStatus
initializerError?: string | null
onRetryInitializer?: () => void
} }
export function SpecCreationChat({ export function SpecCreationChat({
projectName, projectName,
onComplete, onComplete,
onCancel, onCancel,
initializerStatus = 'idle',
initializerError = null,
onRetryInitializer,
}: SpecCreationChatProps) { }: SpecCreationChatProps) {
const [input, setInput] = useState('') const [input, setInput] = useState('')
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
@@ -241,18 +249,50 @@ export function SpecCreationChat({
{/* Completion footer */} {/* Completion footer */}
{isComplete && ( {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 justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<CheckCircle2 size={20} /> {initializerStatus === 'starting' ? (
<span className="font-bold">Specification created successfully!</span> <>
<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> </div>
<button
onClick={() => onComplete('')}
className="neo-btn bg-white"
>
Continue to Project
</button>
</div> </div>
</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 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' import path from 'path'
// Backend port - can be overridden via VITE_API_PORT env var // 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/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({