Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions client.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
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()
Expand Down Expand Up @@ -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,
},
}
Expand Down Expand Up @@ -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,
Expand Down
22 changes: 22 additions & 0 deletions security.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
9 changes: 9 additions & 0 deletions server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,12 @@
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())
6 changes: 6 additions & 0 deletions server/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,17 @@
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
Expand Down
13 changes: 4 additions & 9 deletions server/services/spec_chat_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,15 +179,10 @@ async def start(self) -> AsyncGenerator[dict, None]:
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()),
Expand Down
6 changes: 6 additions & 0 deletions start_ui.bat
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Comment on lines +12 to +16
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid killing unrelated processes on port 8888.
The current block force-kills any listener on 8888, which can terminate unrelated services. Consider prompting the user (or filtering to expected process names) before killing.

🐛 Proposed safer prompt before killing
-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 Kill any existing processes on port 8888 (with confirmation)
+echo Checking for processes on port 8888...
+for /f "tokens=5" %%a in ('netstat -aon ^| findstr ":8888" ^| findstr "LISTENING"') do (
+    echo Found PID %%a listening on 8888.
+    choice /M "Terminate PID %%a?"
+    if errorlevel 2 (
+        echo Skipping PID %%a
+    ) else (
+        taskkill /F /PID %%a >nul 2>&1
+    )
+)
🤖 Prompt for AI Agents
In `@start_ui.bat` around lines 12 - 16, The script indiscriminately force-kills
any PID listening on port 8888 using the for loop and taskkill, which can
terminate unrelated services; update the block around the for /f (...) loop and
taskkill usage to (1) resolve the PID(s) for port 8888, (2) for each PID call
tasklist /FI "PID eq <pid>" or equivalent to retrieve the Image Name, (3) prompt
the user with the PID and process name and require confirmation before calling
taskkill, and/or add an explicit whitelist of expected process names to
auto-kill; ensure the flow handles multiple PIDs and aborts safely if the user
declines or no whitelist match is found.


REM Check if Python is available
where python >nul 2>&1
if %ERRORLEVEL% neq 0 (
Expand Down
20 changes: 16 additions & 4 deletions start_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
--dev Run in development mode with Vite hot reload
"""

import asyncio
import os
import shutil
import socket
Expand All @@ -28,6 +29,10 @@
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"
Expand Down Expand Up @@ -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)
Comment on lines 189 to +207
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix misleading hot‑reload messaging and env comment.

--reload is explicitly not used, so “with hot reload” in the docstring/log is inaccurate, and the PYTHONASYNCIODEBUG comment doesn’t match the code.

📝 Suggested cleanup
-def start_production_server(port: int):
-    """Start FastAPI server in production mode with hot reload."""
+def start_production_server(port: int):
+    """Start FastAPI server in production mode."""
@@
-    print(f"\n  Starting server at http://127.0.0.1:{port} (with hot reload)")
+    print(f"\n  Starting server at http://127.0.0.1:{port}")
@@
-    # Set PYTHONASYNCIODEBUG to help with Windows subprocess issues
     env = os.environ.copy()
+    # Optional: enable asyncio debug when troubleshooting Windows subprocess issues
+    # env["PYTHONASYNCIODEBUG"] = "1"
🤖 Prompt for AI Agents
In `@start_ui.py` around lines 189 - 207, The docstring and printed message in
start_production_server are misleading because the code launches uvicorn without
--reload (so there is no hot reload) and the comment about PYTHONASYNCIODEBUG
doesn't match since the env copy never sets that variable; update the
start_production_server function to (1) change the docstring and the print() to
state that the server is started in production mode without hot reload, and (2)
either remove/adjust the PYTHONASYNCIODEBUG comment or actually set
env["PYTHONASYNCIODEBUG"] = "1" before passing env into subprocess.Popen
(referencing start_production_server, venv_python, env, and the subprocess.Popen
call to locate the code).



def main() -> None:
Expand Down
27 changes: 26 additions & 1 deletion ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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'
Expand All @@ -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],
Expand Down Expand Up @@ -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' }}>
Expand Down Expand Up @@ -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
Expand All @@ -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)}
Expand Down
6 changes: 5 additions & 1 deletion ui/src/components/KanbanBoard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
21 changes: 19 additions & 2 deletions ui/src/components/KanbanColumn.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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 = {
Expand All @@ -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(
Expand Down Expand Up @@ -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) => (
Expand Down