Merge pull request #78 from mmereu/master

feat: add Create Spec button and fix Windows asyncio subprocess
This commit is contained in:
Leon van Zyl
2026-01-22 07:58:29 +02:00
committed by GitHub
10 changed files with 116 additions and 19 deletions

View File

@@ -15,7 +15,7 @@ from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient
from claude_agent_sdk.types import HookMatcher
from dotenv import load_dotenv
from security import bash_security_hook
from security import bash_security_hook, web_tools_auto_approve_hook
# Load environment variables from .env file if present
load_dotenv()
@@ -181,7 +181,7 @@ def create_client(
security_settings = {
"sandbox": {"enabled": True, "autoAllowBashIfSandboxed": True},
"permissions": {
"defaultMode": "acceptEdits", # Auto-approve edits within allowed directories
"defaultMode": "bypassPermissions", # Auto-approve all tools
"allow": permissions_list,
},
}
@@ -273,6 +273,7 @@ def create_client(
hooks={
"PreToolUse": [
HookMatcher(matcher="Bash", hooks=[bash_security_hook]),
HookMatcher(matcher="WebFetch|WebSearch", hooks=[web_tools_auto_approve_hook]),
],
},
max_turns=1000,

View File

@@ -309,6 +309,28 @@ def get_command_for_validation(cmd: str, segments: list[str]) -> str:
return ""
async def web_tools_auto_approve_hook(input_data, tool_use_id=None, context=None):
"""
Pre-tool-use hook that auto-approves WebFetch and WebSearch tools.
Workaround for Claude Code bug where these tools are auto-denied in dontAsk mode.
See: https://github.com/anthropics/claude-code/issues/11881
Args:
input_data: Dict containing tool_name and tool_input
tool_use_id: Optional tool use ID
context: Optional context
Returns:
Empty dict to allow (auto-approve)
"""
tool_name = input_data.get("tool_name", "")
if tool_name in ("WebFetch", "WebSearch"):
# Return empty dict = allow/approve the tool
return {}
return {}
async def bash_security_hook(input_data, tool_use_id=None, context=None):
"""
Pre-tool-use hook that validates bash commands using an allowlist.

View File

@@ -6,3 +6,12 @@ Web UI server for the Autonomous Coding Agent.
Provides REST API and WebSocket endpoints for project management,
feature tracking, and agent control.
"""
# Fix Windows asyncio subprocess support - MUST be before any other imports
# that might create an event loop
import sys
if sys.platform == "win32":
import asyncio
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())

View File

@@ -6,11 +6,17 @@ Main entry point for the Autonomous Coding UI server.
Provides REST API, WebSocket, and static file serving.
"""
import asyncio
import os
import shutil
import sys
from contextlib import asynccontextmanager
from pathlib import Path
# Fix for Windows subprocess support in asyncio
if sys.platform == "win32":
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
from dotenv import load_dotenv
# Load environment variables from .env file if present

View File

@@ -179,15 +179,10 @@ class SpecChatSession:
model=model,
cli_path=system_cli,
# System prompt loaded from CLAUDE.md via setting_sources
# This avoids Windows command line length limit (~8191 chars)
setting_sources=["project"],
allowed_tools=[
"Read",
"Write",
"Edit",
"Glob",
],
permission_mode="acceptEdits", # Auto-approve file writes for spec creation
# Include "user" for global skills and subagents from ~/.claude/
setting_sources=["project", "user"],
# No allowed_tools restriction - full access to all tools, skills, subagents
permission_mode="bypassPermissions", # Auto-approve all tools
max_turns=100,
cwd=str(self.project_dir.resolve()),
settings=str(settings_file.resolve()),

View File

@@ -9,6 +9,12 @@ echo AutoCoder UI
echo ====================================
echo.
REM Kill any existing processes on port 8888
echo Cleaning up old processes...
for /f "tokens=5" %%a in ('netstat -aon ^| findstr ":8888" ^| findstr "LISTENING"') do (
taskkill /F /PID %%a >nul 2>&1
)
REM Check if Python is available
where python >nul 2>&1
if %ERRORLEVEL% neq 0 (

View File

@@ -19,6 +19,7 @@ Options:
--dev Run in development mode with Vite hot reload
"""
import asyncio
import os
import shutil
import socket
@@ -28,6 +29,10 @@ import time
import webbrowser
from pathlib import Path
# Fix Windows asyncio subprocess support BEFORE anything else runs
if sys.platform == "win32":
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
ROOT = Path(__file__).parent.absolute()
VENV_DIR = ROOT / "venv"
UI_DIR = ROOT / "ui"
@@ -259,17 +264,24 @@ def start_dev_server(port: int) -> tuple:
def start_production_server(port: int):
"""Start FastAPI server in production mode."""
"""Start FastAPI server in production mode with hot reload."""
venv_python = get_venv_python()
print(f"\n Starting server at http://127.0.0.1:{port}")
print(f"\n Starting server at http://127.0.0.1:{port} (with hot reload)")
# Set PYTHONASYNCIODEBUG to help with Windows subprocess issues
env = os.environ.copy()
# NOTE: --reload is NOT used because on Windows it breaks asyncio subprocess
# support (uvicorn's reload worker doesn't inherit the ProactorEventLoop policy).
# This affects Claude SDK which uses asyncio.create_subprocess_exec.
# For development with hot reload, use: python start_ui.py --dev
return subprocess.Popen([
str(venv_python), "-m", "uvicorn",
"server.main:app",
"--host", "127.0.0.1",
"--port", str(port)
], cwd=str(ROOT))
"--port", str(port),
], cwd=str(ROOT), env=env)
def main() -> None:

View File

@@ -18,6 +18,7 @@ import { CelebrationOverlay } from './components/CelebrationOverlay'
import { AssistantFAB } from './components/AssistantFAB'
import { AssistantPanel } from './components/AssistantPanel'
import { ExpandProjectModal } from './components/ExpandProjectModal'
import { SpecCreationChat } from './components/SpecCreationChat'
import { SettingsModal } from './components/SettingsModal'
import { DevServerControl } from './components/DevServerControl'
import { ViewToggle, type ViewMode } from './components/ViewToggle'
@@ -51,6 +52,7 @@ function App() {
const [showSettings, setShowSettings] = useState(false)
const [showKeyboardHelp, setShowKeyboardHelp] = useState(false)
const [isSpecCreating, setIsSpecCreating] = useState(false)
const [showSpecChat, setShowSpecChat] = useState(false) // For "Create Spec" button in empty kanban
const [darkMode, setDarkMode] = useState(() => {
try {
return localStorage.getItem(DARK_MODE_KEY) === 'true'
@@ -74,6 +76,10 @@ function App() {
useAgentStatus(selectedProject) // Keep polling for status updates
const wsState = useProjectWebSocket(selectedProject)
// Get has_spec from the selected project
const selectedProjectData = projects?.find(p => p.name === selectedProject)
const hasSpec = selectedProjectData?.has_spec ?? true
// Fetch graph data when in graph view
const { data: graphData } = useQuery({
queryKey: ['dependencyGraph', selectedProject],
@@ -391,6 +397,8 @@ function App() {
onAddFeature={() => setShowAddFeature(true)}
onExpandProject={() => setShowExpandProject(true)}
activeAgents={wsState.activeAgents}
onCreateSpec={() => setShowSpecChat(true)}
hasSpec={hasSpec}
/>
) : (
<div className="neo-card overflow-hidden" style={{ height: '600px' }}>
@@ -441,6 +449,23 @@ function App() {
/>
)}
{/* Spec Creation Chat - for creating spec from empty kanban */}
{showSpecChat && selectedProject && (
<div className="fixed inset-0 z-50 bg-[var(--color-neo-bg)]">
<SpecCreationChat
projectName={selectedProject}
onComplete={() => {
setShowSpecChat(false)
// Refresh projects to update has_spec
queryClient.invalidateQueries({ queryKey: ['projects'] })
queryClient.invalidateQueries({ queryKey: ['features', selectedProject] })
}}
onCancel={() => setShowSpecChat(false)}
onExitToProject={() => setShowSpecChat(false)}
/>
</div>
)}
{/* Debug Log Viewer - fixed to bottom */}
{selectedProject && (
<DebugLogViewer
@@ -458,7 +483,7 @@ function App() {
)}
{/* Assistant FAB and Panel - hide when expand modal or spec creation is open */}
{selectedProject && !showExpandProject && !isSpecCreating && (
{selectedProject && !showExpandProject && !isSpecCreating && !showSpecChat && (
<>
<AssistantFAB
onClick={() => setAssistantOpen(!assistantOpen)}

View File

@@ -7,9 +7,11 @@ interface KanbanBoardProps {
onAddFeature?: () => void
onExpandProject?: () => void
activeAgents?: ActiveAgent[]
onCreateSpec?: () => void // Callback to start spec creation
hasSpec?: boolean // Whether the project has a spec
}
export function KanbanBoard({ features, onFeatureClick, onAddFeature, onExpandProject, activeAgents = [] }: KanbanBoardProps) {
export function KanbanBoard({ features, onFeatureClick, onAddFeature, onExpandProject, activeAgents = [], onCreateSpec, hasSpec = true }: KanbanBoardProps) {
const hasFeatures = features && (features.pending.length + features.in_progress.length + features.done.length) > 0
// Combine all features for dependency status calculation
@@ -47,6 +49,8 @@ export function KanbanBoard({ features, onFeatureClick, onAddFeature, onExpandPr
onAddFeature={onAddFeature}
onExpandProject={onExpandProject}
showExpandButton={hasFeatures}
onCreateSpec={onCreateSpec}
showCreateSpec={!hasSpec && !hasFeatures}
/>
<KanbanColumn
title="In Progress"

View File

@@ -1,5 +1,5 @@
import { FeatureCard } from './FeatureCard'
import { Plus, Sparkles } from 'lucide-react'
import { Plus, Sparkles, Wand2 } from 'lucide-react'
import type { Feature, ActiveAgent } from '../lib/types'
interface KanbanColumnProps {
@@ -13,6 +13,8 @@ interface KanbanColumnProps {
onAddFeature?: () => void
onExpandProject?: () => void
showExpandButton?: boolean
onCreateSpec?: () => void // Callback to start spec creation
showCreateSpec?: boolean // Show "Create Spec" button when project has no spec
}
const colorMap = {
@@ -32,6 +34,8 @@ export function KanbanColumn({
onAddFeature,
onExpandProject,
showExpandButton,
onCreateSpec,
showCreateSpec,
}: KanbanColumnProps) {
// Create a map of feature ID to active agent for quick lookup
const agentByFeatureId = new Map(
@@ -81,7 +85,20 @@ export function KanbanColumn({
<div className="p-4 space-y-3 max-h-[600px] overflow-y-auto bg-[var(--color-neo-bg)]">
{features.length === 0 ? (
<div className="text-center py-8 text-[var(--color-neo-text-secondary)]">
No features
{showCreateSpec && onCreateSpec ? (
<div className="space-y-4">
<p>No spec created yet</p>
<button
onClick={onCreateSpec}
className="neo-btn neo-btn-primary inline-flex items-center gap-2"
>
<Wand2 size={18} />
Create Spec with AI
</button>
</div>
) : (
'No features'
)}
</div>
) : (
features.map((feature, index) => (