mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-01-30 06:12:06 +00:00
Adding features work
This commit is contained in:
@@ -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
111
CLAUDE.md
Normal 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
20
SAMPLE_PROMPT.md
Normal 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.
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
177
ui/src/components/DebugLogViewer.tsx
Normal file
177
ui/src/components/DebugLogViewer.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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"}
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user