diff --git a/client.py b/client.py index 6ce7dfb..7e166a5 100644 --- a/client.py +++ b/client.py @@ -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() @@ -180,7 +180,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, }, } @@ -272,6 +272,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, diff --git a/security.py b/security.py index 4e03117..9c9405f 100644 --- a/security.py +++ b/security.py @@ -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. diff --git a/server/__init__.py b/server/__init__.py index 6db0793..e2558b4 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -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()) diff --git a/server/main.py b/server/main.py index 9340315..2eeeac1 100644 --- a/server/main.py +++ b/server/main.py @@ -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 diff --git a/server/services/spec_chat_session.py b/server/services/spec_chat_session.py index 818179d..4d2fb54 100644 --- a/server/services/spec_chat_session.py +++ b/server/services/spec_chat_session.py @@ -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()), diff --git a/start_ui.bat b/start_ui.bat index 8616b1a..2c59753 100644 --- a/start_ui.bat +++ b/start_ui.bat @@ -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 ( diff --git a/start_ui.py b/start_ui.py index 267ae12..749c26d 100644 --- a/start_ui.py +++ b/start_ui.py @@ -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" @@ -182,17 +187,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: diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 339721a..ef46cc9 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -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} /> ) : (
@@ -441,6 +449,23 @@ function App() { /> )} + {/* Spec Creation Chat - for creating spec from empty kanban */} + {showSpecChat && selectedProject && ( +
+ { + setShowSpecChat(false) + // Refresh projects to update has_spec + queryClient.invalidateQueries({ queryKey: ['projects'] }) + queryClient.invalidateQueries({ queryKey: ['features', selectedProject] }) + }} + onCancel={() => setShowSpecChat(false)} + onExitToProject={() => setShowSpecChat(false)} + /> +
+ )} + {/* Debug Log Viewer - fixed to bottom */} {selectedProject && ( setAssistantOpen(!assistantOpen)} diff --git a/ui/src/components/KanbanBoard.tsx b/ui/src/components/KanbanBoard.tsx index 9861fbc..430b13d 100644 --- a/ui/src/components/KanbanBoard.tsx +++ b/ui/src/components/KanbanBoard.tsx @@ -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} /> 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({
{features.length === 0 ? (
- No features + {showCreateSpec && onCreateSpec ? ( +
+

No spec created yet

+ +
+ ) : ( + 'No features' + )}
) : ( features.map((feature, index) => (