mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-03-21 12:53:09 +00:00
Compare commits
18 Commits
d65fa0ca56
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fca1f6a5e2 | ||
|
|
b15f45c094 | ||
|
|
f999e1937d | ||
|
|
8b2251331d | ||
|
|
7f875c3bbd | ||
|
|
e26ca3761b | ||
|
|
5d3c04a3c7 | ||
|
|
df23a978cb | ||
|
|
41c1a14ae3 | ||
|
|
472064c3da | ||
|
|
afc2f4ac3c | ||
|
|
dceb535ade | ||
|
|
4f102e7bc2 | ||
|
|
9af0f309b7 | ||
|
|
49442f0d43 | ||
|
|
f786879908 | ||
|
|
dcdd06e02e | ||
|
|
b7aef15c3b |
@@ -55,10 +55,10 @@ Pull request(s): $ARGUMENTS
|
||||
- Reviewing large, unfocused PRs is impractical and error-prone; the review cannot provide adequate assurance for such changes
|
||||
|
||||
6. **Vision Alignment Check**
|
||||
- Read the project's README.md and CLAUDE.md to understand the application's core purpose
|
||||
- Assess whether this PR aligns with the application's intended functionality
|
||||
- If the changes deviate significantly from the core vision or add functionality that doesn't serve the application's purpose, note this in the review
|
||||
- This is not a blocker, but should be flagged for the reviewer's consideration
|
||||
- **VISION.md protection**: First, check whether the PR diff modifies `VISION.md` in any way (edits, deletions, renames). If it does, **stop the review immediately** — verdict is **DON'T MERGE**. VISION.md is immutable and no PR is permitted to alter it. Explain this to the user and skip all remaining steps.
|
||||
- Read the project's `VISION.md`, `README.md`, and `CLAUDE.md` to understand the application's core purpose and mandatory architectural constraints
|
||||
- Assess whether this PR aligns with the vision defined in `VISION.md`
|
||||
- **Vision deviation is a merge blocker.** If the PR introduces functionality, integrations, or architectural changes that conflict with `VISION.md`, the verdict must be **DON'T MERGE**. This is not negotiable — the vision document takes precedence over any PR rationale.
|
||||
|
||||
7. **Safety Assessment**
|
||||
- Provide a review on whether the PR is safe to merge as-is
|
||||
|
||||
18
.claude/launch.json
Normal file
18
.claude/launch.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "backend",
|
||||
"runtimeExecutable": "python",
|
||||
"runtimeArgs": ["-m", "uvicorn", "server.main:app", "--host", "127.0.0.1", "--port", "8888", "--reload"],
|
||||
"port": 8888
|
||||
},
|
||||
{
|
||||
"name": "frontend",
|
||||
"runtimeExecutable": "cmd",
|
||||
"runtimeArgs": ["/c", "cd ui && npx vite"],
|
||||
"port": 5173
|
||||
}
|
||||
],
|
||||
"autoVerify": true
|
||||
}
|
||||
@@ -65,7 +65,7 @@ python autonomous_agent_demo.py --project-dir my-app --yolo
|
||||
# Parallel mode: run multiple agents concurrently (1-5 agents)
|
||||
python autonomous_agent_demo.py --project-dir my-app --parallel --max-concurrency 3
|
||||
|
||||
# Batch mode: implement multiple features per agent session (1-3)
|
||||
# Batch mode: implement multiple features per agent session (1-15)
|
||||
python autonomous_agent_demo.py --project-dir my-app --batch-size 3
|
||||
|
||||
# Batch specific features by ID
|
||||
@@ -496,9 +496,9 @@ The orchestrator enforces strict bounds on concurrent processes:
|
||||
|
||||
### Multi-Feature Batching
|
||||
|
||||
Agents can implement multiple features per session using `--batch-size` (1-3, default: 3):
|
||||
Agents can implement multiple features per session using `--batch-size` (1-15, default: 3):
|
||||
- `--batch-size N` - Max features per coding agent batch
|
||||
- `--testing-batch-size N` - Features per testing batch (1-5, default: 3)
|
||||
- `--testing-batch-size N` - Features per testing batch (1-15, default: 3)
|
||||
- `--batch-features 1,2,3` - Specific feature IDs for batch implementation
|
||||
- `--testing-batch-features 1,2,3` - Specific feature IDs for batch regression testing
|
||||
- `prompts.py` provides `get_batch_feature_prompt()` for multi-feature prompt generation
|
||||
|
||||
22
VISION.md
Normal file
22
VISION.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# VISION
|
||||
|
||||
This document defines the mandatory project vision for AutoForge. All contributions must align with these principles. PRs that deviate from this vision will be rejected. This file itself is immutable via PR — any PR that modifies VISION.md will be rejected outright.
|
||||
|
||||
## Claude Agent SDK Exclusivity
|
||||
|
||||
AutoForge is a wrapper around the **Claude Agent SDK**. This is a foundational architectural decision, not a preference.
|
||||
|
||||
**What this means:**
|
||||
|
||||
- AutoForge only supports providers, models, and integrations that work through the Claude Agent SDK.
|
||||
- We will not integrate with, accommodate, or add support for other AI SDKs, CLIs, or coding agent platforms (e.g., Codex, OpenCode, Aider, Continue, Cursor agents, or similar tools).
|
||||
|
||||
**Why:**
|
||||
|
||||
Each platform has its own approach to MCP tools, skills, context management, and feature integration. Attempting to support multiple agent frameworks creates an unsustainable maintenance burden and dilutes the quality of the core experience. By committing to the Claude Agent SDK exclusively, we can build deep, reliable integration rather than shallow compatibility across many targets.
|
||||
|
||||
**In practice:**
|
||||
|
||||
- PRs adding support for non-Claude agent frameworks will be rejected.
|
||||
- PRs introducing abstractions designed to make AutoForge "agent-agnostic" will be rejected.
|
||||
- Alternative API providers (e.g., Vertex AI, AWS Bedrock) are acceptable only when accessed through the Claude Agent SDK's own configuration.
|
||||
19
agent.py
19
agent.py
@@ -74,7 +74,15 @@ async def run_agent_session(
|
||||
await client.query(message)
|
||||
|
||||
# Collect response text and show tool use
|
||||
# Retry receive_response() on MessageParseError — the SDK raises this for
|
||||
# unknown CLI message types (e.g. "rate_limit_event") which kills the async
|
||||
# generator. The subprocess is still alive so we restart to read remaining
|
||||
# messages from the buffered channel.
|
||||
response_text = ""
|
||||
max_parse_retries = 50
|
||||
parse_retries = 0
|
||||
while True:
|
||||
try:
|
||||
async for msg in client.receive_response():
|
||||
msg_type = type(msg).__name__
|
||||
|
||||
@@ -115,6 +123,17 @@ async def run_agent_session(
|
||||
# Tool succeeded - just show brief confirmation
|
||||
print(" [Done]", flush=True)
|
||||
|
||||
break # Normal completion
|
||||
except Exception as inner_exc:
|
||||
if type(inner_exc).__name__ == "MessageParseError":
|
||||
parse_retries += 1
|
||||
if parse_retries > max_parse_retries:
|
||||
print(f"Too many unrecognized CLI messages ({parse_retries}), stopping")
|
||||
break
|
||||
print(f"Ignoring unrecognized message from Claude CLI: {inner_exc}")
|
||||
continue
|
||||
raise # Re-raise to outer except
|
||||
|
||||
print("\n" + "-" * 70 + "\n")
|
||||
return "continue", response_text
|
||||
|
||||
|
||||
@@ -176,14 +176,14 @@ Authentication:
|
||||
"--testing-batch-size",
|
||||
type=int,
|
||||
default=3,
|
||||
help="Number of features per testing batch (1-5, default: 3)",
|
||||
help="Number of features per testing batch (1-15, default: 3)",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--batch-size",
|
||||
type=int,
|
||||
default=3,
|
||||
help="Max features per coding agent batch (1-3, default: 3)",
|
||||
help="Max features per coding agent batch (1-15, default: 3)",
|
||||
)
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "autoforge-ai",
|
||||
"version": "0.1.13",
|
||||
"version": "0.1.17",
|
||||
"description": "Autonomous coding agent with web UI - build complete apps with AI",
|
||||
"license": "AGPL-3.0",
|
||||
"bin": {
|
||||
|
||||
@@ -131,7 +131,7 @@ def _dump_database_state(feature_dicts: list[dict], label: str = ""):
|
||||
MAX_PARALLEL_AGENTS = 5
|
||||
MAX_TOTAL_AGENTS = 10
|
||||
DEFAULT_CONCURRENCY = 3
|
||||
DEFAULT_TESTING_BATCH_SIZE = 3 # Number of features per testing batch (1-5)
|
||||
DEFAULT_TESTING_BATCH_SIZE = 3 # Number of features per testing batch (1-15)
|
||||
POLL_INTERVAL = 5 # seconds between checking for ready features
|
||||
MAX_FEATURE_RETRIES = 3 # Maximum times to retry a failed feature
|
||||
INITIALIZER_TIMEOUT = 1800 # 30 minutes timeout for initializer
|
||||
@@ -168,7 +168,7 @@ class ParallelOrchestrator:
|
||||
yolo_mode: Whether to run in YOLO mode (skip testing agents entirely)
|
||||
testing_agent_ratio: Number of regression testing agents to maintain (0-3).
|
||||
0 = disabled, 1-3 = maintain that many testing agents running independently.
|
||||
testing_batch_size: Number of features to include per testing session (1-5).
|
||||
testing_batch_size: Number of features to include per testing session (1-15).
|
||||
Each testing agent receives this many features to regression test.
|
||||
on_output: Callback for agent output (feature_id, line)
|
||||
on_status: Callback for agent status changes (feature_id, status)
|
||||
@@ -178,8 +178,8 @@ class ParallelOrchestrator:
|
||||
self.model = model
|
||||
self.yolo_mode = yolo_mode
|
||||
self.testing_agent_ratio = min(max(testing_agent_ratio, 0), 3) # Clamp 0-3
|
||||
self.testing_batch_size = min(max(testing_batch_size, 1), 5) # Clamp 1-5
|
||||
self.batch_size = min(max(batch_size, 1), 3) # Clamp 1-3
|
||||
self.testing_batch_size = min(max(testing_batch_size, 1), 15) # Clamp 1-15
|
||||
self.batch_size = min(max(batch_size, 1), 15) # Clamp 1-15
|
||||
self.on_output = on_output
|
||||
self.on_status = on_status
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Production runtime dependencies only
|
||||
# For development, use requirements.txt (includes ruff, mypy, pytest)
|
||||
claude-agent-sdk>=0.1.0,<0.2.0
|
||||
claude-agent-sdk>=0.1.39,<0.2.0
|
||||
python-dotenv>=1.0.0
|
||||
sqlalchemy>=2.0.0
|
||||
fastapi>=0.115.0
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
claude-agent-sdk>=0.1.0,<0.2.0
|
||||
claude-agent-sdk>=0.1.39,<0.2.0
|
||||
python-dotenv>=1.0.0
|
||||
sqlalchemy>=2.0.0
|
||||
fastapi>=0.115.0
|
||||
|
||||
@@ -36,6 +36,7 @@ from .routers import (
|
||||
features_router,
|
||||
filesystem_router,
|
||||
projects_router,
|
||||
scaffold_router,
|
||||
schedules_router,
|
||||
settings_router,
|
||||
spec_creation_router,
|
||||
@@ -169,6 +170,7 @@ app.include_router(filesystem_router)
|
||||
app.include_router(assistant_chat_router)
|
||||
app.include_router(settings_router)
|
||||
app.include_router(terminal_router)
|
||||
app.include_router(scaffold_router)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
|
||||
@@ -12,6 +12,7 @@ from .expand_project import router as expand_project_router
|
||||
from .features import router as features_router
|
||||
from .filesystem import router as filesystem_router
|
||||
from .projects import router as projects_router
|
||||
from .scaffold import router as scaffold_router
|
||||
from .schedules import router as schedules_router
|
||||
from .settings import router as settings_router
|
||||
from .spec_creation import router as spec_creation_router
|
||||
@@ -29,4 +30,5 @@ __all__ = [
|
||||
"assistant_chat_router",
|
||||
"settings_router",
|
||||
"terminal_router",
|
||||
"scaffold_router",
|
||||
]
|
||||
|
||||
@@ -17,11 +17,11 @@ from ..utils.project_helpers import get_project_path as _get_project_path
|
||||
from ..utils.validation import validate_project_name
|
||||
|
||||
|
||||
def _get_settings_defaults() -> tuple[bool, str, int, bool, int]:
|
||||
def _get_settings_defaults() -> tuple[bool, str, int, bool, int, int]:
|
||||
"""Get defaults from global settings.
|
||||
|
||||
Returns:
|
||||
Tuple of (yolo_mode, model, testing_agent_ratio, playwright_headless, batch_size)
|
||||
Tuple of (yolo_mode, model, testing_agent_ratio, playwright_headless, batch_size, testing_batch_size)
|
||||
"""
|
||||
import sys
|
||||
root = Path(__file__).parent.parent.parent
|
||||
@@ -47,7 +47,12 @@ def _get_settings_defaults() -> tuple[bool, str, int, bool, int]:
|
||||
except (ValueError, TypeError):
|
||||
batch_size = 3
|
||||
|
||||
return yolo_mode, model, testing_agent_ratio, playwright_headless, batch_size
|
||||
try:
|
||||
testing_batch_size = int(settings.get("testing_batch_size", "3"))
|
||||
except (ValueError, TypeError):
|
||||
testing_batch_size = 3
|
||||
|
||||
return yolo_mode, model, testing_agent_ratio, playwright_headless, batch_size, testing_batch_size
|
||||
|
||||
|
||||
router = APIRouter(prefix="/api/projects/{project_name}/agent", tags=["agent"])
|
||||
@@ -96,7 +101,7 @@ async def start_agent(
|
||||
manager = get_project_manager(project_name)
|
||||
|
||||
# Get defaults from global settings if not provided in request
|
||||
default_yolo, default_model, default_testing_ratio, playwright_headless, default_batch_size = _get_settings_defaults()
|
||||
default_yolo, default_model, default_testing_ratio, playwright_headless, default_batch_size, default_testing_batch_size = _get_settings_defaults()
|
||||
|
||||
yolo_mode = request.yolo_mode if request.yolo_mode is not None else default_yolo
|
||||
model = request.model if request.model else default_model
|
||||
@@ -104,6 +109,7 @@ async def start_agent(
|
||||
testing_agent_ratio = request.testing_agent_ratio if request.testing_agent_ratio is not None else default_testing_ratio
|
||||
|
||||
batch_size = default_batch_size
|
||||
testing_batch_size = default_testing_batch_size
|
||||
|
||||
success, message = await manager.start(
|
||||
yolo_mode=yolo_mode,
|
||||
@@ -112,6 +118,7 @@ async def start_agent(
|
||||
testing_agent_ratio=testing_agent_ratio,
|
||||
playwright_headless=playwright_headless,
|
||||
batch_size=batch_size,
|
||||
testing_batch_size=testing_batch_size,
|
||||
)
|
||||
|
||||
# Notify scheduler of manual start (to prevent auto-stop during scheduled window)
|
||||
|
||||
136
server/routers/scaffold.py
Normal file
136
server/routers/scaffold.py
Normal file
@@ -0,0 +1,136 @@
|
||||
"""
|
||||
Scaffold Router
|
||||
================
|
||||
|
||||
SSE streaming endpoint for running project scaffold commands.
|
||||
Supports templated project creation (e.g., Next.js agentic starter).
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .filesystem import is_path_blocked
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/api/scaffold", tags=["scaffold"])
|
||||
|
||||
# Hardcoded templates — no arbitrary commands allowed
|
||||
TEMPLATES: dict[str, list[str]] = {
|
||||
"agentic-starter": ["npx", "create-agentic-app@latest", ".", "-y", "-p", "npm", "--skip-git"],
|
||||
}
|
||||
|
||||
|
||||
class ScaffoldRequest(BaseModel):
|
||||
template: str
|
||||
target_path: str
|
||||
|
||||
|
||||
def _sse_event(data: dict) -> str:
|
||||
"""Format a dict as an SSE data line."""
|
||||
return f"data: {json.dumps(data)}\n\n"
|
||||
|
||||
|
||||
async def _stream_scaffold(template: str, target_path: str, request: Request):
|
||||
"""Run the scaffold command and yield SSE events."""
|
||||
# Validate template
|
||||
if template not in TEMPLATES:
|
||||
yield _sse_event({"type": "error", "message": f"Unknown template: {template}"})
|
||||
return
|
||||
|
||||
# Validate path
|
||||
path = Path(target_path)
|
||||
try:
|
||||
path = path.resolve()
|
||||
except (OSError, ValueError) as e:
|
||||
yield _sse_event({"type": "error", "message": f"Invalid path: {e}"})
|
||||
return
|
||||
|
||||
if is_path_blocked(path):
|
||||
yield _sse_event({"type": "error", "message": "Access to this directory is not allowed"})
|
||||
return
|
||||
|
||||
if not path.exists() or not path.is_dir():
|
||||
yield _sse_event({"type": "error", "message": "Target directory does not exist"})
|
||||
return
|
||||
|
||||
# Check npx is available
|
||||
npx_name = "npx"
|
||||
if sys.platform == "win32":
|
||||
npx_name = "npx.cmd"
|
||||
|
||||
if not shutil.which(npx_name):
|
||||
yield _sse_event({"type": "error", "message": "npx is not available. Please install Node.js."})
|
||||
return
|
||||
|
||||
# Build command
|
||||
argv = list(TEMPLATES[template])
|
||||
if sys.platform == "win32" and not argv[0].lower().endswith(".cmd"):
|
||||
argv[0] = argv[0] + ".cmd"
|
||||
|
||||
process = None
|
||||
try:
|
||||
popen_kwargs: dict = {
|
||||
"stdout": subprocess.PIPE,
|
||||
"stderr": subprocess.STDOUT,
|
||||
"stdin": subprocess.DEVNULL,
|
||||
"cwd": str(path),
|
||||
}
|
||||
if sys.platform == "win32":
|
||||
popen_kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
|
||||
|
||||
process = subprocess.Popen(argv, **popen_kwargs)
|
||||
logger.info("Scaffold process started: pid=%s, template=%s, path=%s", process.pid, template, target_path)
|
||||
|
||||
# Stream stdout lines
|
||||
assert process.stdout is not None
|
||||
for raw_line in iter(process.stdout.readline, b""):
|
||||
# Check if client disconnected
|
||||
if await request.is_disconnected():
|
||||
logger.info("Client disconnected during scaffold, terminating process")
|
||||
break
|
||||
|
||||
line = raw_line.decode("utf-8", errors="replace").rstrip("\n\r")
|
||||
yield _sse_event({"type": "output", "line": line})
|
||||
# Yield control to event loop so disconnect checks work
|
||||
await asyncio.sleep(0)
|
||||
|
||||
process.wait()
|
||||
exit_code = process.returncode
|
||||
success = exit_code == 0
|
||||
logger.info("Scaffold process completed: exit_code=%s, template=%s", exit_code, template)
|
||||
yield _sse_event({"type": "complete", "success": success, "exit_code": exit_code})
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Scaffold error: %s", e)
|
||||
yield _sse_event({"type": "error", "message": str(e)})
|
||||
|
||||
finally:
|
||||
if process and process.poll() is None:
|
||||
try:
|
||||
process.terminate()
|
||||
process.wait(timeout=5)
|
||||
except Exception:
|
||||
process.kill()
|
||||
|
||||
|
||||
@router.post("/run")
|
||||
async def run_scaffold(body: ScaffoldRequest, request: Request):
|
||||
"""Run a scaffold template command with SSE streaming output."""
|
||||
return StreamingResponse(
|
||||
_stream_scaffold(body.template, body.target_path, request),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"X-Accel-Buffering": "no",
|
||||
},
|
||||
)
|
||||
@@ -113,6 +113,7 @@ async def get_settings():
|
||||
testing_agent_ratio=_parse_int(all_settings.get("testing_agent_ratio"), 1),
|
||||
playwright_headless=_parse_bool(all_settings.get("playwright_headless"), default=True),
|
||||
batch_size=_parse_int(all_settings.get("batch_size"), 3),
|
||||
testing_batch_size=_parse_int(all_settings.get("testing_batch_size"), 3),
|
||||
api_provider=api_provider,
|
||||
api_base_url=all_settings.get("api_base_url"),
|
||||
api_has_auth_token=bool(all_settings.get("api_auth_token")),
|
||||
@@ -138,6 +139,9 @@ async def update_settings(update: SettingsUpdate):
|
||||
if update.batch_size is not None:
|
||||
set_setting("batch_size", str(update.batch_size))
|
||||
|
||||
if update.testing_batch_size is not None:
|
||||
set_setting("testing_batch_size", str(update.testing_batch_size))
|
||||
|
||||
# API provider settings
|
||||
if update.api_provider is not None:
|
||||
old_provider = get_setting("api_provider", "claude")
|
||||
@@ -177,6 +181,7 @@ async def update_settings(update: SettingsUpdate):
|
||||
testing_agent_ratio=_parse_int(all_settings.get("testing_agent_ratio"), 1),
|
||||
playwright_headless=_parse_bool(all_settings.get("playwright_headless"), default=True),
|
||||
batch_size=_parse_int(all_settings.get("batch_size"), 3),
|
||||
testing_batch_size=_parse_int(all_settings.get("testing_batch_size"), 3),
|
||||
api_provider=api_provider,
|
||||
api_base_url=all_settings.get("api_base_url"),
|
||||
api_has_auth_token=bool(all_settings.get("api_auth_token")),
|
||||
|
||||
@@ -444,7 +444,8 @@ class SettingsResponse(BaseModel):
|
||||
ollama_mode: bool = False # True when api_provider is "ollama"
|
||||
testing_agent_ratio: int = 1 # Regression testing agents (0-3)
|
||||
playwright_headless: bool = True
|
||||
batch_size: int = 3 # Features per coding agent batch (1-3)
|
||||
batch_size: int = 3 # Features per coding agent batch (1-15)
|
||||
testing_batch_size: int = 3 # Features per testing agent batch (1-15)
|
||||
api_provider: str = "claude"
|
||||
api_base_url: str | None = None
|
||||
api_has_auth_token: bool = False # Never expose actual token
|
||||
@@ -463,7 +464,8 @@ class SettingsUpdate(BaseModel):
|
||||
model: str | None = None
|
||||
testing_agent_ratio: int | None = None # 0-3
|
||||
playwright_headless: bool | None = None
|
||||
batch_size: int | None = None # Features per agent batch (1-3)
|
||||
batch_size: int | None = None # Features per agent batch (1-15)
|
||||
testing_batch_size: int | None = None # Features per testing agent batch (1-15)
|
||||
api_provider: str | None = None
|
||||
api_base_url: str | None = Field(None, max_length=500)
|
||||
api_auth_token: str | None = Field(None, max_length=500) # Write-only, never returned
|
||||
@@ -500,8 +502,15 @@ class SettingsUpdate(BaseModel):
|
||||
@field_validator('batch_size')
|
||||
@classmethod
|
||||
def validate_batch_size(cls, v: int | None) -> int | None:
|
||||
if v is not None and (v < 1 or v > 3):
|
||||
raise ValueError("batch_size must be between 1 and 3")
|
||||
if v is not None and (v < 1 or v > 15):
|
||||
raise ValueError("batch_size must be between 1 and 15")
|
||||
return v
|
||||
|
||||
@field_validator('testing_batch_size')
|
||||
@classmethod
|
||||
def validate_testing_batch_size(cls, v: int | None) -> int | None:
|
||||
if v is not None and (v < 1 or v > 15):
|
||||
raise ValueError("testing_batch_size must be between 1 and 15")
|
||||
return v
|
||||
|
||||
|
||||
|
||||
@@ -25,7 +25,11 @@ from .assistant_database import (
|
||||
create_conversation,
|
||||
get_messages,
|
||||
)
|
||||
from .chat_constants import ROOT_DIR
|
||||
from .chat_constants import (
|
||||
ROOT_DIR,
|
||||
check_rate_limit_error,
|
||||
safe_receive_response,
|
||||
)
|
||||
|
||||
# Load environment variables from .env file if present
|
||||
load_dotenv()
|
||||
@@ -394,7 +398,8 @@ class AssistantChatSession:
|
||||
full_response = ""
|
||||
|
||||
# Stream the response
|
||||
async for msg in self.client.receive_response():
|
||||
try:
|
||||
async for msg in safe_receive_response(self.client, logger):
|
||||
msg_type = type(msg).__name__
|
||||
|
||||
if msg_type == "AssistantMessage" and hasattr(msg, "content"):
|
||||
@@ -426,6 +431,13 @@ class AssistantChatSession:
|
||||
"tool": tool_name,
|
||||
"input": tool_input,
|
||||
}
|
||||
except Exception as exc:
|
||||
is_rate_limit, _ = check_rate_limit_error(exc)
|
||||
if is_rate_limit:
|
||||
logger.warning(f"Rate limited: {exc}")
|
||||
yield {"type": "error", "content": "Rate limited. Please try again later."}
|
||||
return
|
||||
raise
|
||||
|
||||
# Store the complete response in the database
|
||||
if full_response and self.conversation_id:
|
||||
|
||||
@@ -9,9 +9,10 @@ project root and is re-exported here for convenience so that existing
|
||||
imports (``from .chat_constants import API_ENV_VARS``) continue to work.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import AsyncGenerator
|
||||
from typing import Any, AsyncGenerator
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Root directory of the autoforge project (repository root).
|
||||
@@ -32,6 +33,59 @@ if _root_str not in sys.path:
|
||||
# imports continue to work unchanged.
|
||||
# -------------------------------------------------------------------
|
||||
from env_constants import API_ENV_VARS # noqa: E402, F401
|
||||
from rate_limit_utils import is_rate_limit_error, parse_retry_after # noqa: E402, F401
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def check_rate_limit_error(exc: Exception) -> tuple[bool, int | None]:
|
||||
"""Inspect an exception and determine if it represents a rate-limit.
|
||||
|
||||
Returns ``(is_rate_limit, retry_seconds)``. ``retry_seconds`` is the
|
||||
parsed Retry-After value when available, otherwise ``None`` (caller
|
||||
should use exponential backoff).
|
||||
"""
|
||||
# MessageParseError = unknown CLI message type (e.g. "rate_limit_event").
|
||||
# These are informational events, NOT actual rate limit errors.
|
||||
# The word "rate_limit" in the type name would false-positive the regex.
|
||||
if type(exc).__name__ == "MessageParseError":
|
||||
return False, None
|
||||
|
||||
# For all other exceptions: match error text against known rate-limit patterns
|
||||
exc_str = str(exc)
|
||||
if is_rate_limit_error(exc_str):
|
||||
retry = parse_retry_after(exc_str)
|
||||
return True, retry
|
||||
|
||||
return False, None
|
||||
|
||||
|
||||
async def safe_receive_response(client: Any, log: logging.Logger) -> AsyncGenerator:
|
||||
"""Wrap ``client.receive_response()`` to skip ``MessageParseError``.
|
||||
|
||||
The Claude Code CLI may emit message types (e.g. ``rate_limit_event``)
|
||||
that the installed Python SDK does not recognise, causing
|
||||
``MessageParseError`` which kills the async generator. The CLI
|
||||
subprocess is still alive and the SDK uses a buffered memory channel,
|
||||
so we restart ``receive_response()`` to continue reading remaining
|
||||
messages without losing data.
|
||||
"""
|
||||
max_retries = 50
|
||||
retries = 0
|
||||
while True:
|
||||
try:
|
||||
async for msg in client.receive_response():
|
||||
yield msg
|
||||
return # Normal completion
|
||||
except Exception as exc:
|
||||
if type(exc).__name__ == "MessageParseError":
|
||||
retries += 1
|
||||
if retries > max_retries:
|
||||
log.error(f"Too many unrecognized CLI messages ({retries}), stopping")
|
||||
return
|
||||
log.warning(f"Ignoring unrecognized message from Claude CLI: {exc}")
|
||||
continue
|
||||
raise
|
||||
|
||||
|
||||
async def make_multimodal_message(content_blocks: list[dict]) -> AsyncGenerator[dict, None]:
|
||||
|
||||
@@ -22,7 +22,12 @@ from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from ..schemas import ImageAttachment
|
||||
from .chat_constants import ROOT_DIR, make_multimodal_message
|
||||
from .chat_constants import (
|
||||
ROOT_DIR,
|
||||
check_rate_limit_error,
|
||||
make_multimodal_message,
|
||||
safe_receive_response,
|
||||
)
|
||||
|
||||
# Load environment variables from .env file if present
|
||||
load_dotenv()
|
||||
@@ -299,7 +304,8 @@ class ExpandChatSession:
|
||||
await self.client.query(message)
|
||||
|
||||
# Stream the response
|
||||
async for msg in self.client.receive_response():
|
||||
try:
|
||||
async for msg in safe_receive_response(self.client, logger):
|
||||
msg_type = type(msg).__name__
|
||||
|
||||
if msg_type == "AssistantMessage" and hasattr(msg, "content"):
|
||||
@@ -316,6 +322,13 @@ class ExpandChatSession:
|
||||
"content": text,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
})
|
||||
except Exception as exc:
|
||||
is_rate_limit, _ = check_rate_limit_error(exc)
|
||||
if is_rate_limit:
|
||||
logger.warning(f"Rate limited: {exc}")
|
||||
yield {"type": "error", "content": "Rate limited. Please try again later."}
|
||||
return
|
||||
raise
|
||||
|
||||
def get_features_created(self) -> int:
|
||||
"""Get the total number of features created in this session."""
|
||||
|
||||
@@ -374,6 +374,7 @@ class AgentProcessManager:
|
||||
testing_agent_ratio: int = 1,
|
||||
playwright_headless: bool = True,
|
||||
batch_size: int = 3,
|
||||
testing_batch_size: int = 3,
|
||||
) -> tuple[bool, str]:
|
||||
"""
|
||||
Start the agent as a subprocess.
|
||||
@@ -440,6 +441,9 @@ class AgentProcessManager:
|
||||
# Add --batch-size flag for multi-feature batching
|
||||
cmd.extend(["--batch-size", str(batch_size)])
|
||||
|
||||
# Add --testing-batch-size flag for testing agent batching
|
||||
cmd.extend(["--testing-batch-size", str(testing_batch_size)])
|
||||
|
||||
# Apply headless setting to .playwright/cli.config.json so playwright-cli
|
||||
# picks it up (the only mechanism it supports for headless control)
|
||||
self._apply_playwright_headless(playwright_headless)
|
||||
|
||||
@@ -19,7 +19,12 @@ from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from ..schemas import ImageAttachment
|
||||
from .chat_constants import ROOT_DIR, make_multimodal_message
|
||||
from .chat_constants import (
|
||||
ROOT_DIR,
|
||||
check_rate_limit_error,
|
||||
make_multimodal_message,
|
||||
safe_receive_response,
|
||||
)
|
||||
|
||||
# Load environment variables from .env file if present
|
||||
load_dotenv()
|
||||
@@ -304,8 +309,9 @@ class SpecChatSession:
|
||||
# Store paths for the completion message
|
||||
spec_path = None
|
||||
|
||||
# Stream the response using receive_response
|
||||
async for msg in self.client.receive_response():
|
||||
# Stream the response
|
||||
try:
|
||||
async for msg in safe_receive_response(self.client, logger):
|
||||
msg_type = type(msg).__name__
|
||||
|
||||
if msg_type == "AssistantMessage" and hasattr(msg, "content"):
|
||||
@@ -415,6 +421,13 @@ class SpecChatSession:
|
||||
"type": "spec_complete",
|
||||
"path": str(spec_path)
|
||||
}
|
||||
except Exception as exc:
|
||||
is_rate_limit, _ = check_rate_limit_error(exc)
|
||||
if is_rate_limit:
|
||||
logger.warning(f"Rate limited: {exc}")
|
||||
yield {"type": "error", "content": "Rate limited. Please try again later."}
|
||||
return
|
||||
raise
|
||||
|
||||
def is_complete(self) -> bool:
|
||||
"""Check if spec creation is complete."""
|
||||
|
||||
276
ui/package-lock.json
generated
276
ui/package-lock.json
generated
@@ -56,7 +56,7 @@
|
||||
},
|
||||
"..": {
|
||||
"name": "autoforge-ai",
|
||||
"version": "0.1.13",
|
||||
"version": "0.1.17",
|
||||
"license": "AGPL-3.0",
|
||||
"bin": {
|
||||
"autoforge": "bin/autoforge.js"
|
||||
@@ -1991,9 +1991,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz",
|
||||
"integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
|
||||
"integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -2005,9 +2005,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz",
|
||||
"integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
|
||||
"integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2019,9 +2019,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz",
|
||||
"integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
|
||||
"integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2033,9 +2033,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz",
|
||||
"integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
|
||||
"integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2047,9 +2047,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz",
|
||||
"integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
|
||||
"integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2061,9 +2061,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz",
|
||||
"integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
|
||||
"integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2075,9 +2075,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz",
|
||||
"integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
|
||||
"integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -2089,9 +2089,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz",
|
||||
"integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
|
||||
"integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -2103,9 +2103,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz",
|
||||
"integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2117,9 +2117,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz",
|
||||
"integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2131,9 +2131,23 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz",
|
||||
"integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-musl": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -2145,9 +2159,23 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz",
|
||||
"integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-musl": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -2159,9 +2187,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz",
|
||||
"integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -2173,9 +2201,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz",
|
||||
"integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -2187,9 +2215,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz",
|
||||
"integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -2201,9 +2229,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz",
|
||||
"integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2215,9 +2243,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz",
|
||||
"integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2228,10 +2256,24 @@
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openbsd-x64": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
|
||||
"integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz",
|
||||
"integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
|
||||
"integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2243,9 +2285,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz",
|
||||
"integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
|
||||
"integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2257,9 +2299,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz",
|
||||
"integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
|
||||
"integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -2271,9 +2313,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz",
|
||||
"integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2285,9 +2327,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz",
|
||||
"integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
|
||||
"integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -3042,24 +3084,37 @@
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz",
|
||||
"integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||
"version": "9.0.8",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.8.tgz",
|
||||
"integrity": "sha512-reYkDYtj/b19TeqbNZCV4q9t+Yxylf/rYBsLb42SXJatTv4/ylq5lEiAmhA/IToxO7NI2UzNMghHoHuaqDkAjw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
"brace-expansion": "^5.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
@@ -3227,9 +3282,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
|
||||
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -4757,9 +4812,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"version": "4.17.23",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
|
||||
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.merge": {
|
||||
@@ -5664,9 +5719,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
@@ -6150,9 +6205,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz",
|
||||
"integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
|
||||
"integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -6166,28 +6221,31 @@
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.54.0",
|
||||
"@rollup/rollup-android-arm64": "4.54.0",
|
||||
"@rollup/rollup-darwin-arm64": "4.54.0",
|
||||
"@rollup/rollup-darwin-x64": "4.54.0",
|
||||
"@rollup/rollup-freebsd-arm64": "4.54.0",
|
||||
"@rollup/rollup-freebsd-x64": "4.54.0",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.54.0",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.54.0",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.54.0",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.54.0",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.54.0",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.54.0",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.54.0",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.54.0",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.54.0",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.54.0",
|
||||
"@rollup/rollup-linux-x64-musl": "4.54.0",
|
||||
"@rollup/rollup-openharmony-arm64": "4.54.0",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.54.0",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.54.0",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.54.0",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.54.0",
|
||||
"@rollup/rollup-android-arm-eabi": "4.59.0",
|
||||
"@rollup/rollup-android-arm64": "4.59.0",
|
||||
"@rollup/rollup-darwin-arm64": "4.59.0",
|
||||
"@rollup/rollup-darwin-x64": "4.59.0",
|
||||
"@rollup/rollup-freebsd-arm64": "4.59.0",
|
||||
"@rollup/rollup-freebsd-x64": "4.59.0",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.59.0",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.59.0",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-loong64-musl": "4.59.0",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-ppc64-musl": "4.59.0",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.59.0",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-x64-musl": "4.59.0",
|
||||
"@rollup/rollup-openbsd-x64": "4.59.0",
|
||||
"@rollup/rollup-openharmony-arm64": "4.59.0",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.59.0",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.59.0",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.59.0",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.59.0",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -4,14 +4,15 @@
|
||||
* Multi-step modal for creating new projects:
|
||||
* 1. Enter project name
|
||||
* 2. Select project folder
|
||||
* 3. Choose spec method (Claude or manual)
|
||||
* 4a. If Claude: Show SpecCreationChat
|
||||
* 4b. If manual: Create project and close
|
||||
* 3. Choose project template (blank or agentic starter)
|
||||
* 4. Choose spec method (Claude or manual)
|
||||
* 5a. If Claude: Show SpecCreationChat
|
||||
* 5b. If manual: Create project and close
|
||||
*/
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRef, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { Bot, FileEdit, ArrowRight, ArrowLeft, Loader2, CheckCircle2, Folder } from 'lucide-react'
|
||||
import { Bot, FileEdit, ArrowRight, ArrowLeft, Loader2, CheckCircle2, Folder, Zap, FileCode2, AlertCircle, RotateCcw } from 'lucide-react'
|
||||
import { useCreateProject } from '../hooks/useProjects'
|
||||
import { SpecCreationChat } from './SpecCreationChat'
|
||||
import { FolderBrowser } from './FolderBrowser'
|
||||
@@ -32,8 +33,9 @@ import { Badge } from '@/components/ui/badge'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
|
||||
type InitializerStatus = 'idle' | 'starting' | 'error'
|
||||
type ScaffoldStatus = 'idle' | 'running' | 'success' | 'error'
|
||||
|
||||
type Step = 'name' | 'folder' | 'method' | 'chat' | 'complete'
|
||||
type Step = 'name' | 'folder' | 'template' | 'method' | 'chat' | 'complete'
|
||||
type SpecMethod = 'claude' | 'manual'
|
||||
|
||||
interface NewProjectModalProps {
|
||||
@@ -57,6 +59,10 @@ export function NewProjectModal({
|
||||
const [initializerStatus, setInitializerStatus] = useState<InitializerStatus>('idle')
|
||||
const [initializerError, setInitializerError] = useState<string | null>(null)
|
||||
const [yoloModeSelected, setYoloModeSelected] = useState(false)
|
||||
const [scaffoldStatus, setScaffoldStatus] = useState<ScaffoldStatus>('idle')
|
||||
const [scaffoldOutput, setScaffoldOutput] = useState<string[]>([])
|
||||
const [scaffoldError, setScaffoldError] = useState<string | null>(null)
|
||||
const scaffoldLogRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Suppress unused variable warning - specMethod may be used in future
|
||||
void _specMethod
|
||||
@@ -91,13 +97,84 @@ export function NewProjectModal({
|
||||
|
||||
const handleFolderSelect = (path: string) => {
|
||||
setProjectPath(path)
|
||||
changeStep('method')
|
||||
changeStep('template')
|
||||
}
|
||||
|
||||
const handleFolderCancel = () => {
|
||||
changeStep('name')
|
||||
}
|
||||
|
||||
const handleTemplateSelect = async (choice: 'blank' | 'agentic-starter') => {
|
||||
if (choice === 'blank') {
|
||||
changeStep('method')
|
||||
return
|
||||
}
|
||||
|
||||
if (!projectPath) return
|
||||
|
||||
setScaffoldStatus('running')
|
||||
setScaffoldOutput([])
|
||||
setScaffoldError(null)
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/scaffold/run', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ template: 'agentic-starter', target_path: projectPath }),
|
||||
})
|
||||
|
||||
if (!res.ok || !res.body) {
|
||||
setScaffoldStatus('error')
|
||||
setScaffoldError(`Server error: ${res.status}`)
|
||||
return
|
||||
}
|
||||
|
||||
const reader = res.body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let buffer = ''
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
const lines = buffer.split('\n')
|
||||
buffer = lines.pop() || ''
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.startsWith('data: ')) continue
|
||||
try {
|
||||
const event = JSON.parse(line.slice(6))
|
||||
if (event.type === 'output') {
|
||||
setScaffoldOutput(prev => {
|
||||
const next = [...prev, event.line]
|
||||
return next.length > 100 ? next.slice(-100) : next
|
||||
})
|
||||
// Auto-scroll
|
||||
setTimeout(() => scaffoldLogRef.current?.scrollTo(0, scaffoldLogRef.current.scrollHeight), 0)
|
||||
} else if (event.type === 'complete') {
|
||||
if (event.success) {
|
||||
setScaffoldStatus('success')
|
||||
setTimeout(() => changeStep('method'), 1200)
|
||||
} else {
|
||||
setScaffoldStatus('error')
|
||||
setScaffoldError(`Scaffold exited with code ${event.exit_code}`)
|
||||
}
|
||||
} else if (event.type === 'error') {
|
||||
setScaffoldStatus('error')
|
||||
setScaffoldError(event.message)
|
||||
}
|
||||
} catch {
|
||||
// skip malformed SSE lines
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
setScaffoldStatus('error')
|
||||
setScaffoldError(err instanceof Error ? err.message : 'Failed to run scaffold')
|
||||
}
|
||||
}
|
||||
|
||||
const handleMethodSelect = async (method: SpecMethod) => {
|
||||
setSpecMethod(method)
|
||||
|
||||
@@ -188,13 +265,21 @@ export function NewProjectModal({
|
||||
setInitializerStatus('idle')
|
||||
setInitializerError(null)
|
||||
setYoloModeSelected(false)
|
||||
setScaffoldStatus('idle')
|
||||
setScaffoldOutput([])
|
||||
setScaffoldError(null)
|
||||
onClose()
|
||||
}
|
||||
|
||||
const handleBack = () => {
|
||||
if (step === 'method') {
|
||||
changeStep('folder')
|
||||
changeStep('template')
|
||||
setSpecMethod(null)
|
||||
} else if (step === 'template') {
|
||||
changeStep('folder')
|
||||
setScaffoldStatus('idle')
|
||||
setScaffoldOutput([])
|
||||
setScaffoldError(null)
|
||||
} else if (step === 'folder') {
|
||||
changeStep('name')
|
||||
setProjectPath(null)
|
||||
@@ -255,6 +340,7 @@ export function NewProjectModal({
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{step === 'name' && 'Create New Project'}
|
||||
{step === 'template' && 'Choose Project Template'}
|
||||
{step === 'method' && 'Choose Setup Method'}
|
||||
{step === 'complete' && 'Project Created!'}
|
||||
</DialogTitle>
|
||||
@@ -294,7 +380,127 @@ export function NewProjectModal({
|
||||
</form>
|
||||
)}
|
||||
|
||||
{/* Step 2: Spec Method */}
|
||||
{/* Step 2: Project Template */}
|
||||
{step === 'template' && (
|
||||
<div className="space-y-4">
|
||||
{scaffoldStatus === 'idle' && (
|
||||
<>
|
||||
<DialogDescription>
|
||||
Start with a blank project or use a pre-configured template.
|
||||
</DialogDescription>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Card
|
||||
className="cursor-pointer hover:border-primary transition-colors"
|
||||
onClick={() => handleTemplateSelect('blank')}
|
||||
>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="p-2 bg-secondary rounded-lg">
|
||||
<FileCode2 size={24} className="text-secondary-foreground" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<span className="font-semibold">Blank Project</span>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Start from scratch. AutoForge will scaffold your app based on the spec you define.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
className="cursor-pointer hover:border-primary transition-colors"
|
||||
onClick={() => handleTemplateSelect('agentic-starter')}
|
||||
>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="p-2 bg-primary/10 rounded-lg">
|
||||
<Zap size={24} className="text-primary" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-semibold">Agentic Starter</span>
|
||||
<Badge variant="secondary">Next.js</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Pre-configured Next.js app with BetterAuth, Drizzle ORM, Postgres, and AI capabilities.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="sm:justify-start">
|
||||
<Button variant="ghost" onClick={handleBack}>
|
||||
<ArrowLeft size={16} />
|
||||
Back
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</>
|
||||
)}
|
||||
|
||||
{scaffoldStatus === 'running' && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader2 size={16} className="animate-spin text-primary" />
|
||||
<span className="font-medium">Setting up Agentic Starter...</span>
|
||||
</div>
|
||||
<div
|
||||
ref={scaffoldLogRef}
|
||||
className="bg-muted rounded-lg p-3 max-h-60 overflow-y-auto font-mono text-xs leading-relaxed"
|
||||
>
|
||||
{scaffoldOutput.map((line, i) => (
|
||||
<div key={i} className="whitespace-pre-wrap break-all">{line}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{scaffoldStatus === 'success' && (
|
||||
<div className="text-center py-6">
|
||||
<div className="inline-flex items-center justify-center w-12 h-12 bg-primary/10 rounded-full mb-3">
|
||||
<CheckCircle2 size={24} className="text-primary" />
|
||||
</div>
|
||||
<p className="font-medium">Template ready!</p>
|
||||
<p className="text-sm text-muted-foreground mt-1">Proceeding to setup method...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{scaffoldStatus === 'error' && (
|
||||
<div className="space-y-3">
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle size={16} />
|
||||
<AlertDescription>
|
||||
{scaffoldError || 'An unknown error occurred'}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
{scaffoldOutput.length > 0 && (
|
||||
<div className="bg-muted rounded-lg p-3 max-h-40 overflow-y-auto font-mono text-xs leading-relaxed">
|
||||
{scaffoldOutput.slice(-10).map((line, i) => (
|
||||
<div key={i} className="whitespace-pre-wrap break-all">{line}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter className="sm:justify-start gap-2">
|
||||
<Button variant="ghost" onClick={handleBack}>
|
||||
<ArrowLeft size={16} />
|
||||
Back
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => handleTemplateSelect('agentic-starter')}>
|
||||
<RotateCcw size={16} />
|
||||
Retry
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 3: Spec Method */}
|
||||
{step === 'method' && (
|
||||
<div className="space-y-4">
|
||||
<DialogDescription>
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Slider } from '@/components/ui/slider'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { Button } from '@/components/ui/button'
|
||||
@@ -63,6 +64,12 @@ export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
|
||||
}
|
||||
}
|
||||
|
||||
const handleTestingBatchSizeChange = (size: number) => {
|
||||
if (!updateSettings.isPending) {
|
||||
updateSettings.mutate({ testing_batch_size: size })
|
||||
}
|
||||
}
|
||||
|
||||
const handleProviderChange = (providerId: string) => {
|
||||
if (!updateSettings.isPending) {
|
||||
updateSettings.mutate({ api_provider: providerId })
|
||||
@@ -432,28 +439,34 @@ export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Features per Agent */}
|
||||
{/* Features per Coding Agent */}
|
||||
<div className="space-y-2">
|
||||
<Label className="font-medium">Features per Agent</Label>
|
||||
<Label className="font-medium">Features per Coding Agent</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Number of features assigned to each coding agent
|
||||
Number of features assigned to each coding agent session
|
||||
</p>
|
||||
<div className="flex rounded-lg border overflow-hidden">
|
||||
{[1, 2, 3].map((size) => (
|
||||
<button
|
||||
key={size}
|
||||
onClick={() => handleBatchSizeChange(size)}
|
||||
<Slider
|
||||
min={1}
|
||||
max={15}
|
||||
value={settings.batch_size ?? 3}
|
||||
onChange={handleBatchSizeChange}
|
||||
disabled={isSaving}
|
||||
className={`flex-1 py-2 px-3 text-sm font-medium transition-colors ${
|
||||
(settings.batch_size ?? 1) === size
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-background text-foreground hover:bg-muted'
|
||||
} ${isSaving ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
>
|
||||
{size}
|
||||
</button>
|
||||
))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Features per Testing Agent */}
|
||||
<div className="space-y-2">
|
||||
<Label className="font-medium">Features per Testing Agent</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Number of features assigned to each testing agent session
|
||||
</p>
|
||||
<Slider
|
||||
min={1}
|
||||
max={15}
|
||||
value={settings.testing_batch_size ?? 3}
|
||||
onChange={handleTestingBatchSizeChange}
|
||||
disabled={isSaving}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Update Error */}
|
||||
|
||||
44
ui/src/components/ui/slider.tsx
Normal file
44
ui/src/components/ui/slider.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
interface SliderProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'> {
|
||||
min: number
|
||||
max: number
|
||||
value: number
|
||||
onChange: (value: number) => void
|
||||
label?: string
|
||||
}
|
||||
|
||||
function Slider({
|
||||
className,
|
||||
min,
|
||||
max,
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
...props
|
||||
}: SliderProps) {
|
||||
return (
|
||||
<div className={cn("flex items-center gap-3", className)}>
|
||||
<input
|
||||
type="range"
|
||||
min={min}
|
||||
max={max}
|
||||
value={value}
|
||||
onChange={(e) => onChange(Number(e.target.value))}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
"slider-input h-2 w-full cursor-pointer appearance-none rounded-full bg-input transition-colors",
|
||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
||||
disabled && "cursor-not-allowed opacity-50"
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
<span className="min-w-[2ch] text-center text-sm font-semibold tabular-nums">
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { Slider }
|
||||
@@ -302,6 +302,7 @@ const DEFAULT_SETTINGS: Settings = {
|
||||
testing_agent_ratio: 1,
|
||||
playwright_headless: true,
|
||||
batch_size: 3,
|
||||
testing_batch_size: 3,
|
||||
api_provider: 'claude',
|
||||
api_base_url: null,
|
||||
api_has_auth_token: false,
|
||||
|
||||
@@ -579,7 +579,8 @@ export interface Settings {
|
||||
ollama_mode: boolean
|
||||
testing_agent_ratio: number // Regression testing agents (0-3)
|
||||
playwright_headless: boolean
|
||||
batch_size: number // Features per coding agent batch (1-3)
|
||||
batch_size: number // Features per coding agent batch (1-15)
|
||||
testing_batch_size: number // Features per testing agent batch (1-15)
|
||||
api_provider: string
|
||||
api_base_url: string | null
|
||||
api_has_auth_token: boolean
|
||||
@@ -592,6 +593,7 @@ export interface SettingsUpdate {
|
||||
testing_agent_ratio?: number
|
||||
playwright_headless?: boolean
|
||||
batch_size?: number
|
||||
testing_batch_size?: number
|
||||
api_provider?: string
|
||||
api_base_url?: string
|
||||
api_auth_token?: string
|
||||
|
||||
@@ -1472,3 +1472,53 @@
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--muted-foreground);
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
Slider (range input) styling
|
||||
============================================================================ */
|
||||
|
||||
.slider-input::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary);
|
||||
border: 2px solid var(--primary-foreground);
|
||||
box-shadow: var(--shadow-sm);
|
||||
cursor: pointer;
|
||||
transition: transform 150ms, box-shadow 150ms;
|
||||
}
|
||||
|
||||
.slider-input::-webkit-slider-thumb:hover {
|
||||
transform: scale(1.15);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.slider-input::-moz-range-thumb {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary);
|
||||
border: 2px solid var(--primary-foreground);
|
||||
box-shadow: var(--shadow-sm);
|
||||
cursor: pointer;
|
||||
transition: transform 150ms, box-shadow 150ms;
|
||||
}
|
||||
|
||||
.slider-input::-moz-range-thumb:hover {
|
||||
transform: scale(1.15);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.slider-input::-webkit-slider-runnable-track {
|
||||
height: 8px;
|
||||
border-radius: 9999px;
|
||||
background: var(--input);
|
||||
}
|
||||
|
||||
.slider-input::-moz-range-track {
|
||||
height: 8px;
|
||||
border-radius: 9999px;
|
||||
background: var(--input);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user