diff --git a/src/autocoder/agent/client.py b/src/autocoder/agent/client.py index 59855e0b..c4d50a7e 100644 --- a/src/autocoder/agent/client.py +++ b/src/autocoder/agent/client.py @@ -240,6 +240,9 @@ def create_client( if not yolo_mode: # Include Playwright MCP server for browser automation (standard mode only) playwright_args = ["@playwright/mcp@latest", "--viewport-size", "1280x720"] + headless_raw = os.environ.get("PLAYWRIGHT_HEADLESS", "") + if headless_raw.strip().lower() in {"1", "true", "yes", "on"}: + playwright_args.append("--headless") # Default on: keep browser profile in memory (prevents cross-agent tab/profile conflicts). isolated_raw = os.environ.get("AUTOCODER_PLAYWRIGHT_ISOLATED", "") if isolated_raw.strip().lower() not in {"0", "false", "no", "off"}: @@ -264,6 +267,16 @@ def create_client( if lock_guard is not None: pre_tool_use_matchers.insert(1, HookMatcher(matcher="Write|Edit|MultiEdit", hooks=[lock_guard.pre_tool_use])) + max_turns = 300 + raw_max_turns = os.environ.get("AUTOCODER_AGENT_MAX_TURNS") + if raw_max_turns is not None: + try: + parsed = int(str(raw_max_turns).strip()) + if parsed > 0: + max_turns = parsed + except Exception: + pass + return ClaudeSDKClient( options=ClaudeAgentOptions( model=model, @@ -274,7 +287,7 @@ def create_client( allowed_tools=allowed_tools + (LOCK_MCP_TOOLS if "locks" in mcp_servers else []), mcp_servers=mcp_servers, hooks={"PreToolUse": pre_tool_use_matchers}, - max_turns=1000, + max_turns=max_turns, cwd=str(project_dir.resolve()), settings=str(settings_file.resolve()), # Use absolute path ) diff --git a/src/autocoder/core/project_runtime_settings.py b/src/autocoder/core/project_runtime_settings.py index 4ed1991b..3fd3ca43 100644 --- a/src/autocoder/core/project_runtime_settings.py +++ b/src/autocoder/core/project_runtime_settings.py @@ -33,6 +33,9 @@ class ProjectRuntimeSettings: locks_enabled: bool worker_verify: bool + # Browser (Playwright MCP) + playwright_headless: bool + def to_env(self) -> dict[str, str]: planner_enabled = bool(self.planner_enabled or self.planner_required) return { @@ -43,6 +46,7 @@ def to_env(self) -> dict[str, str]: "AUTOCODER_STOP_WHEN_DONE": "1" if self.stop_when_done else "0", "AUTOCODER_LOCKS_ENABLED": "1" if self.locks_enabled else "0", "AUTOCODER_WORKER_VERIFY": "1" if self.worker_verify else "0", + "PLAYWRIGHT_HEADLESS": "1" if self.playwright_headless else "0", } def to_dict(self) -> dict[str, Any]: @@ -54,6 +58,7 @@ def to_dict(self) -> dict[str, Any]: "stop_when_done": bool(self.stop_when_done), "locks_enabled": bool(self.locks_enabled), "worker_verify": bool(self.worker_verify), + "playwright_headless": bool(self.playwright_headless), } @staticmethod @@ -66,6 +71,7 @@ def defaults() -> "ProjectRuntimeSettings": stop_when_done=True, locks_enabled=True, worker_verify=True, + playwright_headless=False, ) @@ -92,6 +98,7 @@ def load_project_runtime_settings(project_dir: str | Path) -> ProjectRuntimeSett stop_when_done=bool(merged.get("stop_when_done", True)), locks_enabled=bool(merged.get("locks_enabled", True)), worker_verify=bool(merged.get("worker_verify", True)), + playwright_headless=bool(merged.get("playwright_headless", False)), ) except Exception: return None diff --git a/src/autocoder/server/routers/project_settings.py b/src/autocoder/server/routers/project_settings.py index 8497d94c..0ee27c53 100644 --- a/src/autocoder/server/routers/project_settings.py +++ b/src/autocoder/server/routers/project_settings.py @@ -94,6 +94,9 @@ class ProjectRuntimeSettingsModel(BaseModel): locks_enabled: bool = True worker_verify: bool = True + # Browser (Playwright MCP) + playwright_headless: bool = False + def to_core(self) -> ProjectRuntimeSettings: return ProjectRuntimeSettings( planner_enabled=bool(self.planner_enabled), @@ -103,6 +106,7 @@ def to_core(self) -> ProjectRuntimeSettings: stop_when_done=bool(self.stop_when_done), locks_enabled=bool(self.locks_enabled), worker_verify=bool(self.worker_verify), + playwright_headless=bool(self.playwright_headless), ) @staticmethod @@ -115,6 +119,7 @@ def from_core(v: ProjectRuntimeSettings) -> "ProjectRuntimeSettingsModel": stop_when_done=bool(v.stop_when_done), locks_enabled=bool(v.locks_enabled), worker_verify=bool(v.worker_verify), + playwright_headless=bool(v.playwright_headless), ) @@ -150,6 +155,7 @@ async def get_project_runtime_settings(project_name: str): stop_when_done=bool(base.stop_when_done), locks_enabled=bool(base.locks_enabled), worker_verify=bool(base.worker_verify), + playwright_headless=False, ) return ProjectRuntimeSettingsModel.from_core(inherited) diff --git a/src/autocoder/server/services/assistant_chat_session.py b/src/autocoder/server/services/assistant_chat_session.py index 890068ec..5a2b3e41 100644 --- a/src/autocoder/server/services/assistant_chat_session.py +++ b/src/autocoder/server/services/assistant_chat_session.py @@ -358,6 +358,9 @@ async def start(self) -> AsyncGenerator[dict, None]: # Build MCP servers config - features and Playwright playwright_args = ["@playwright/mcp@latest", "--viewport-size", "1280x720"] + headless_raw = os.environ.get("PLAYWRIGHT_HEADLESS", "") + if headless_raw.strip().lower() in {"1", "true", "yes", "on"}: + playwright_args.append("--headless") isolated_raw = os.environ.get("AUTOCODER_PLAYWRIGHT_ISOLATED", "") if isolated_raw.strip().lower() not in {"0", "false", "no", "off"}: playwright_args.append("--isolated") diff --git a/tests/test_project_settings.py b/tests/test_project_settings.py index 55531d9f..4b5e577a 100644 --- a/tests/test_project_settings.py +++ b/tests/test_project_settings.py @@ -20,6 +20,7 @@ def test_project_runtime_settings_roundtrip_and_env_override(tmp_path: Path): stop_when_done=False, locks_enabled=False, worker_verify=False, + playwright_headless=True, ) save_project_runtime_settings(project_dir, settings) @@ -32,6 +33,7 @@ def test_project_runtime_settings_roundtrip_and_env_override(tmp_path: Path): ) assert overridden["AUTOCODER_STOP_WHEN_DONE"] == "0" assert overridden["AUTOCODER_PLANNER_REQUIRED"] == "1" + assert overridden["PLAYWRIGHT_HEADLESS"] == "1" not_overridden = apply_project_runtime_settings_env( project_dir, dict(base_env), override_existing=False diff --git a/ui/src/components/NewProjectModal.tsx b/ui/src/components/NewProjectModal.tsx index db044b62..0bff32ca 100644 --- a/ui/src/components/NewProjectModal.tsx +++ b/ui/src/components/NewProjectModal.tsx @@ -207,6 +207,7 @@ export function NewProjectModal({ stop_when_done: stopWhenDone, locks_enabled: true, worker_verify: true, + playwright_headless: false, }) } catch (e: any) { setError(e instanceof Error ? e.message : 'Failed to save project runtime settings') diff --git a/ui/src/lib/types.ts b/ui/src/lib/types.ts index fd2c987a..d9b1743f 100644 --- a/ui/src/lib/types.ts +++ b/ui/src/lib/types.ts @@ -187,6 +187,7 @@ export interface ProjectRuntimeSettings { stop_when_done: boolean locks_enabled: boolean worker_verify: boolean + playwright_headless: boolean } // Setup types diff --git a/ui/src/pages/SettingsPage.tsx b/ui/src/pages/SettingsPage.tsx index d2e28c70..2c1a09c7 100644 --- a/ui/src/pages/SettingsPage.tsx +++ b/ui/src/pages/SettingsPage.tsx @@ -337,6 +337,26 @@ export function SettingsPage({ className="w-5 h-5 mt-1" /> + + {(runtimeSettingsQ.isLoading || updateRuntimeSettings.isPending) && (