mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-01-31 14:43:35 +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
|
- **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
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 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
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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."""
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
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 { 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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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'
|
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({
|
||||||
|
|||||||
Reference in New Issue
Block a user