Skip to content
Open
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
3 changes: 1 addition & 2 deletions .github/workflows/ubuntu-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,7 @@ jobs:
pytest-mock \
-r requirements/requirements.in \
-r requirements/requirements-help.in \
-r requirements/requirements-playwright.in \
".[help,playwright]"
".[help]"
env:
PYO3_USE_ABI3_FORWARD_COMPATIBILITY: "1"

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/windows-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install uv
uv pip install --system pytest pytest-asyncio pytest-mock -r requirements/requirements.in -r requirements/requirements-help.in -r requirements/requirements-playwright.in '.[help,playwright]'
uv pip install --system pytest pytest-asyncio pytest-mock -r requirements/requirements.in -r requirements/requirements-help.in '.[help]'
env:
PYO3_USE_ABI3_FORWARD_COMPATIBILITY: "1"

Expand Down
1 change: 0 additions & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
include requirements/requirements.in
include requirements/requirements-dev.in
include requirements/requirements-help.in
include requirements/requirements-playwright.in

global-exclude .DS_Store

Expand Down
18 changes: 12 additions & 6 deletions cecli/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
DEEP_MERGE_JSON_FIELDS: frozenset[str] = frozenset(
{
"agent_config",
"browser",
"mcp_servers",
"hooks",
"model_providers",
Expand Down Expand Up @@ -547,6 +548,17 @@ def get_parser(default_config_files, git_root):
)

##########
group = parser.add_argument_group("Browser Settings")
group.add_argument(
"--browser",
metavar="BROWSER_CONFIG_JSON",
default=None,
help=(
"Specify browser-harness configuration as a JSON/YAML string or file path (e.g., "
'\'{"cdp_url": "http://127.0.0.1:9222", "launch": true}\')'
),
)

group = parser.add_argument_group("Security Settings")
group.add_argument(
"--security-config",
Expand Down Expand Up @@ -1145,12 +1157,6 @@ def get_parser(default_config_files, git_root):
help="Always say yes to every confirmation (including cli commands)",
default=False,
)
group.add_argument(
"--disable-playwright",
action="store_true",
help="Never prompt for or attempt to install Playwright for web scraping (default: False).",
default=False,
)
group.add_argument(
"--disable-scraping",
action="store_true",
Expand Down
33 changes: 33 additions & 0 deletions cecli/coders/agent_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from cecli.helpers.agents.service import AgentService
from cecli.helpers.agents.tree import AgentTree
from cecli.helpers.background_commands import BackgroundCommandManager
from cecli.helpers.browser import export_active_cdp_url
from cecli.helpers.conversation import ConversationService, MessageTag
from cecli.helpers.similarity import (
cosine_similarity,
Expand Down Expand Up @@ -131,6 +132,8 @@ def __init__(self, *args, **kwargs):
def post_init(self):
super().post_init()

export_active_cdp_url(getattr(self.args, "browser", None), include_default_ports=True)

if not self._inherited_tools:
# Populate per-instance tool and server filters from config
self.registered_tools["included"] = set(
Expand Down Expand Up @@ -265,6 +268,7 @@ def _get_agent_config(self, config=None):
# "context_summary",
# "directory_structure",
"environment_info",
"browser_automation",
# "git_status",
# "symbol_outline",
"todo_list",
Expand Down Expand Up @@ -496,6 +500,7 @@ def _calculate_context_block_tokens(self, force=False):
self.context_blocks_cache = {}
block_types = [
"environment_info",
"browser_automation",
"directory_structure",
"git_status",
"symbol_outline",
Expand Down Expand Up @@ -538,6 +543,8 @@ def _generate_context_block(self, block_name):
content = self.get_todo_list()
elif block_name == "skills":
content = self.get_skills_context()
elif block_name == "browser_automation":
content = self.get_browser_automation_context()
elif block_name == "servers":
content = self.get_servers_context()
elif block_name == "orchestration":
Expand Down Expand Up @@ -1711,6 +1718,32 @@ def get_skills_content(self):
self.io.tool_error(f"Error generating skills content context: {str(e)}")
return None

def get_browser_automation_context(self):
"""
Generate a brief browser-harness usage context block.

Returns None when browser-harness is not installed.
"""
from cecli.helpers.browser import is_browser_harness_available

if not self.use_enhanced_context or not is_browser_harness_available():
return None

result = '<context name="browser_automation" from="agent">\n'
result += "## Browser Automation\n\n"
result += (
"`browser-harness` drives a real Chrome/Chromium browser. Run it with a heredoc;"
" cecli sets `BU_CDP_URL` automatically so it attaches to a CDP browser:\n\n"
"```bash\n"
"browser-harness <<'PY'\n"
"print(page_info())\n"
"PY\n"
"```"
)
result += "\n</context>"

return result

def get_servers_context(self):
"""
Generate a context block for available MCP servers.
Expand Down
6 changes: 4 additions & 2 deletions cecli/coders/base_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -4165,6 +4165,7 @@ async def show_send_output(self, completion):
async def show_send_output_stream(self, completion):
received_content = False
chunk_index = 0
track_loops = self.edit_format in ("agent", "subagent")
content_detector = LoopDetector(self.repetition_penalties)
tool_detector = LoopDetector(self.repetition_penalties)
loop_detected = False
Expand Down Expand Up @@ -4213,7 +4214,8 @@ async def show_send_output_stream(self, completion):
tool_call_chunk.function.arguments
)

tool_detector.push(tool_call_chunk.function.arguments)
if track_loops:
tool_detector.push(tool_call_chunk.function.arguments)

except (AttributeError, IndexError):
# Handle cases where the response structure doesn't match expectations
Expand Down Expand Up @@ -4279,7 +4281,7 @@ async def show_send_output_stream(self, completion):
chunk._hidden_params["created_at"] = chunk_index
self.partial_response_chunks.append(chunk)

if text:
if text and track_loops:
content_detector.push(text)

if self.show_pretty():
Expand Down
27 changes: 7 additions & 20 deletions cecli/commands/web.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

from cecli.commands.utils.base_command import BaseCommand
from cecli.commands.utils.helpers import format_command_result
from cecli.helpers.browser import is_browser_harness_available
from cecli.helpers.conversation import ConversationService, MessageTag
from cecli.scrape import Scraper, install_playwright
from cecli.scrape import Scraper


class WebCommand(BaseCommand):
Expand All @@ -25,27 +26,12 @@ async def execute(cls, io, coder, args, **kwargs):
scraper = kwargs.get("scraper")

if not scraper:
# Get disable_playwright from coder args
disable_playwright = (
getattr(coder.args, "disable_playwright", False) if coder and coder.args else False
)
if disable_playwright:
res = False
else:
try:
res = await install_playwright(io)
if not res:
io.tool_warning("Unable to initialize playwright.")
except Exception:
io.tool_warning("Unable to initialize playwright.")
res = False

# Get verify_ssl from kwargs or use default
verify_ssl = kwargs.get("verify_ssl", True)

scraper = Scraper(
print_error=io.tool_error,
playwright_available=res,
browser_harness_available=is_browser_harness_available(),
browser_config=getattr(getattr(coder, "args", None), "browser", None),
verify_ssl=verify_ssl,
)

Expand Down Expand Up @@ -88,6 +74,7 @@ def get_help(cls) -> str:
help_text += (
"\nThis command scrapes a webpage, converts it to markdown, and adds it to the chat.\n"
)
help_text += "It uses Playwright for JavaScript-rendered pages when available.\n"
help_text += "Use --disable-playwright to disable Playwright and use simpler scraping.\n"
help_text += (
"It uses browser-harness for JavaScript-rendered pages and falls back to plain HTTP.\n"
)
return help_text
19 changes: 19 additions & 0 deletions cecli/helpers/agents/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,21 @@ def _get_tui(coder: Any) -> Any:
# If it is already a plain reference (e.g., in tests), use it directly.
return tui_ref

def _ensure_registry_loaded(self) -> None:
"""Load sub-agent definitions when nothing has built the registry yet.

Only ``AgentCoder`` bootstraps the registry at construction, so
non-agent coders (e.g. ask mode) would otherwise report every
sub-agent as unknown to commands such as ``/creator``.
"""
if self._global_registry:
return

self.build_registry(
list(getattr(self.coder, "sub_agent_paths", None) or []),
root=getattr(self.coder, "root", None),
)

def _resolve_model_sentinel(self, value: str, parent_coder: Any):
"""Resolve a sentinel model marker to the appropriate Model instance.

Expand Down Expand Up @@ -713,6 +728,10 @@ async def _create_sub_agent_coder(
)

config = self._global_registry.get(name)
if not config:
self._ensure_registry_loaded()
config = self._global_registry.get(name)

if not config:
raise ValueError(
f"Unknown sub-agent '{name}'. " f"Available: {list(self._global_registry.keys())}"
Expand Down
Loading
Loading