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
15 changes: 14 additions & 1 deletion src/autocoder/agent/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"}:
Expand All @@ -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,
Expand All @@ -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
)
Expand Down
7 changes: 7 additions & 0 deletions src/autocoder/core/project_runtime_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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]:
Expand All @@ -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
Expand All @@ -66,6 +71,7 @@ def defaults() -> "ProjectRuntimeSettings":
stop_when_done=True,
locks_enabled=True,
worker_verify=True,
playwright_headless=False,
)


Expand All @@ -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
Expand Down
6 changes: 6 additions & 0 deletions src/autocoder/server/routers/project_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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
Expand All @@ -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),
)


Expand Down Expand Up @@ -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)

Expand Down
3 changes: 3 additions & 0 deletions src/autocoder/server/services/assistant_chat_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
2 changes: 2 additions & 0 deletions tests/test_project_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions ui/src/components/NewProjectModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
1 change: 1 addition & 0 deletions ui/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ export interface ProjectRuntimeSettings {
stop_when_done: boolean
locks_enabled: boolean
worker_verify: boolean
playwright_headless: boolean
}

// Setup types
Expand Down
20 changes: 20 additions & 0 deletions ui/src/pages/SettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,26 @@ export function SettingsPage({
className="w-5 h-5 mt-1"
/>
</label>

<label className="neo-card p-3 flex items-start justify-between gap-3 cursor-pointer">
<div>
<div className="font-display font-bold text-sm uppercase">Playwright headless</div>
<div className="text-xs text-[var(--color-neo-text-secondary)]">
Runs browser automation without a visible window (standard mode only).
</div>
</div>
<input
type="checkbox"
checked={Boolean(runtimeSettings.playwright_headless)}
onChange={(e) =>
updateRuntimeSettings.mutate({
...runtimeSettings,
playwright_headless: Boolean(e.target.checked),
})
}
className="w-5 h-5 mt-1"
/>
</label>
</div>

{(runtimeSettingsQ.isLoading || updateRuntimeSettings.isPending) && (
Expand Down