regcode 0.4.0__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {regcode-0.4.0 → regcode-0.5.0}/PKG-INFO +1 -1
- {regcode-0.4.0 → regcode-0.5.0}/pyproject.toml +1 -1
- {regcode-0.4.0 → regcode-0.5.0}/regcode/__init__.py +1 -1
- {regcode-0.4.0 → regcode-0.5.0}/regcode/cli.py +14 -0
- regcode-0.5.0/regcode/dependencies.py +71 -0
- {regcode-0.4.0 → regcode-0.5.0}/regcode/tools/__init__.py +0 -2
- {regcode-0.4.0 → regcode-0.5.0}/regcode/tools/builtins.py +2 -1
- {regcode-0.4.0 → regcode-0.5.0}/regcode/tools/web_crawl.py +13 -23
- {regcode-0.4.0 → regcode-0.5.0}/regcode/tui.py +32 -3
- regcode-0.5.0/tests/test_dependencies.py +149 -0
- {regcode-0.4.0 → regcode-0.5.0}/uv.lock +1 -1
- regcode-0.4.0/repair_tui.md +0 -115
- {regcode-0.4.0 → regcode-0.5.0}/.gitignore +0 -0
- {regcode-0.4.0 → regcode-0.5.0}/.python-version +0 -0
- {regcode-0.4.0 → regcode-0.5.0}/.qwen/settings.json +0 -0
- {regcode-0.4.0 → regcode-0.5.0}/AGENTS.md +0 -0
- {regcode-0.4.0 → regcode-0.5.0}/GOALS.md +0 -0
- {regcode-0.4.0 → regcode-0.5.0}/README.md +0 -0
- {regcode-0.4.0 → regcode-0.5.0}/config.yaml.template +0 -0
- {regcode-0.4.0 → regcode-0.5.0}/main.py +0 -0
- {regcode-0.4.0 → regcode-0.5.0}/regcode/config.py +0 -0
- {regcode-0.4.0 → regcode-0.5.0}/regcode/conversation_manager.py +0 -0
- {regcode-0.4.0 → regcode-0.5.0}/regcode/main.py +0 -0
- {regcode-0.4.0 → regcode-0.5.0}/regcode/monty_sandbox.py +0 -0
- {regcode-0.4.0 → regcode-0.5.0}/regcode/permissions.py +0 -0
- {regcode-0.4.0 → regcode-0.5.0}/regcode/sandbox.py +0 -0
- {regcode-0.4.0 → regcode-0.5.0}/regcode/tools/base.py +0 -0
- {regcode-0.4.0 → regcode-0.5.0}/regcode/tools/registry.py +0 -0
- {regcode-0.4.0 → regcode-0.5.0}/regcode/tools/review_notes.py +0 -0
- {regcode-0.4.0 → regcode-0.5.0}/regcode/tracing.py +0 -0
- {regcode-0.4.0 → regcode-0.5.0}/tests/__init__.py +0 -0
- {regcode-0.4.0 → regcode-0.5.0}/tests/conftest.py +0 -0
- {regcode-0.4.0 → regcode-0.5.0}/tests/test_api.py +0 -0
- {regcode-0.4.0 → regcode-0.5.0}/tests/test_cli.py +0 -0
- {regcode-0.4.0 → regcode-0.5.0}/tests/test_config.py +0 -0
- {regcode-0.4.0 → regcode-0.5.0}/tests/test_context_manager.py +0 -0
- {regcode-0.4.0 → regcode-0.5.0}/tests/test_main.py +0 -0
- {regcode-0.4.0 → regcode-0.5.0}/tests/test_monty_sandbox.py +0 -0
- {regcode-0.4.0 → regcode-0.5.0}/tests/test_osv_cve.py +0 -0
- {regcode-0.4.0 → regcode-0.5.0}/tests/test_provider_extra.py +0 -0
- {regcode-0.4.0 → regcode-0.5.0}/tests/test_review_notes.py +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
import sys
|
|
2
3
|
from pathlib import Path
|
|
3
4
|
|
|
@@ -5,9 +6,14 @@ import click
|
|
|
5
6
|
import yaml
|
|
6
7
|
|
|
7
8
|
import regcode
|
|
9
|
+
from regcode.dependencies import ensure_playwright_browsers
|
|
8
10
|
from regcode.permissions import AgentPermission
|
|
9
11
|
from regcode.tui import ChatUI
|
|
10
12
|
|
|
13
|
+
# Configure logging
|
|
14
|
+
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
|
|
15
|
+
_logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
11
17
|
|
|
12
18
|
def _confirm(prompt_text):
|
|
13
19
|
"""Ask yes/no question, accepting y/n (case insensitive)."""
|
|
@@ -43,6 +49,14 @@ def cli(ctx):
|
|
|
43
49
|
)
|
|
44
50
|
def chat(model, config_path, stream, full, no_color):
|
|
45
51
|
"""Chat with the agent using a Textual TUI with sticky bottom bar."""
|
|
52
|
+
# Ensure Playwright browsers are available for web_crawl tool
|
|
53
|
+
_logger.info("Checking startup dependencies...")
|
|
54
|
+
if not ensure_playwright_browsers():
|
|
55
|
+
_logger.warning(
|
|
56
|
+
"Playwright browsers are not available. "
|
|
57
|
+
"The web_crawl tool will not function."
|
|
58
|
+
)
|
|
59
|
+
|
|
46
60
|
# Pass None if the file doesn't exist so Config.load falls back
|
|
47
61
|
# to ~/.regcode/config.yaml (matching the TUI's expected behaviour).
|
|
48
62
|
_effective_path = config_path if Path(config_path).exists() else None
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Startup dependency checks and auto-installation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
_logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def ensure_playwright_browsers() -> bool:
|
|
14
|
+
"""Check if Playwright browsers are installed; install if missing.
|
|
15
|
+
|
|
16
|
+
Returns True if browsers are available (either already installed
|
|
17
|
+
or newly installed), False if installation was skipped due to
|
|
18
|
+
user setting PLAYWRIGHT_NO_INSTALL=1.
|
|
19
|
+
"""
|
|
20
|
+
if os.environ.get("PLAYWRIGHT_NO_INSTALL"):
|
|
21
|
+
_logger.warning(
|
|
22
|
+
"PLAYWRIGHT_NO_INSTALL is set; skipping browser check."
|
|
23
|
+
)
|
|
24
|
+
return False
|
|
25
|
+
|
|
26
|
+
# Dynamic import so tests can mock before calling
|
|
27
|
+
try:
|
|
28
|
+
from playwright.sync_api import sync_playwright # noqa: F811
|
|
29
|
+
except ImportError:
|
|
30
|
+
_logger.warning(
|
|
31
|
+
"playwright is not installed. Install with: "
|
|
32
|
+
"uv add playwright playwright-stealth && "
|
|
33
|
+
"uv run playwright install"
|
|
34
|
+
)
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
with sync_playwright() as p:
|
|
39
|
+
# Trigger a simple launch test — if browsers are missing,
|
|
40
|
+
# Chromium.launch() raises BrowserType.launch: Executable
|
|
41
|
+
# doesn't exist. We catch that specific error.
|
|
42
|
+
p.chromium.executable_path # noqa: B018
|
|
43
|
+
# Try to actually launch; this is the definitive check
|
|
44
|
+
browser = p.chromium.launch()
|
|
45
|
+
browser.close()
|
|
46
|
+
return True
|
|
47
|
+
except Exception:
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
# Browsers are missing — attempt automatic installation
|
|
51
|
+
_logger.info("Playwright browsers not found. Installing...")
|
|
52
|
+
try:
|
|
53
|
+
subprocess.run(
|
|
54
|
+
[sys.executable, "-m", "playwright", "install", "--with-deps"],
|
|
55
|
+
check=True,
|
|
56
|
+
capture_output=True,
|
|
57
|
+
)
|
|
58
|
+
_logger.info("Playwright browsers installed successfully.")
|
|
59
|
+
return True
|
|
60
|
+
except subprocess.CalledProcessError as e:
|
|
61
|
+
stderr_text = e.stderr.decode() if e.stderr else ""
|
|
62
|
+
_logger.error(
|
|
63
|
+
"Failed to install Playwright browsers: %s", stderr_text
|
|
64
|
+
)
|
|
65
|
+
return False
|
|
66
|
+
except FileNotFoundError:
|
|
67
|
+
_logger.error(
|
|
68
|
+
"Could not find 'playwright' module. "
|
|
69
|
+
"Install it with: uv add playwright && uv run playwright install"
|
|
70
|
+
)
|
|
71
|
+
return False
|
|
@@ -17,7 +17,7 @@ from typing import Any
|
|
|
17
17
|
|
|
18
18
|
from regcode.monty_sandbox import MontySandbox
|
|
19
19
|
from regcode.tools.base import BaseTool, ToolParam, ToolResult
|
|
20
|
-
|
|
20
|
+
from regcode.tools.web_crawl import WebCrawlerTool
|
|
21
21
|
|
|
22
22
|
class PythonCodeTool(BaseTool):
|
|
23
23
|
"""Execute Python code in a Monty sandbox."""
|
|
@@ -1367,4 +1367,5 @@ def create_default_tools() -> list[BaseTool]:
|
|
|
1367
1367
|
SystemInfoTool(),
|
|
1368
1368
|
FetchGitDiffTool(),
|
|
1369
1369
|
OSVCVETool(),
|
|
1370
|
+
WebCrawlerTool(),
|
|
1370
1371
|
]
|
|
@@ -7,12 +7,13 @@ from typing import Any
|
|
|
7
7
|
from regcode.tools.base import BaseTool, ToolParam, ToolResult
|
|
8
8
|
|
|
9
9
|
try:
|
|
10
|
-
from crawl4ai import AsyncWebCrawler
|
|
10
|
+
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
|
|
11
11
|
from crawl4ai.markdown_generation_strategy import DefaultMarkdownGenerator
|
|
12
12
|
|
|
13
13
|
CRAWL4AI_AVAILABLE = True
|
|
14
14
|
except ImportError:
|
|
15
15
|
AsyncWebCrawler = None # type: ignore
|
|
16
|
+
CrawlerRunConfig = None # type: ignore
|
|
16
17
|
DefaultMarkdownGenerator = None # type: ignore
|
|
17
18
|
CRAWL4AI_AVAILABLE = False
|
|
18
19
|
|
|
@@ -95,24 +96,13 @@ class WebCrawlerTool(BaseTool):
|
|
|
95
96
|
exit_code=1,
|
|
96
97
|
)
|
|
97
98
|
|
|
98
|
-
|
|
99
|
-
import asyncio
|
|
99
|
+
import asyncio
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
try:
|
|
102
|
+
result = asyncio.run(
|
|
102
103
|
self._crawl_async(url, markdown, word_count_threshold, only_text)
|
|
103
104
|
)
|
|
104
105
|
return result
|
|
105
|
-
except ImportError:
|
|
106
|
-
# Fallback: try to run in a new event loop
|
|
107
|
-
import asyncio
|
|
108
|
-
|
|
109
|
-
async def _crawl():
|
|
110
|
-
return await self._crawl_async(
|
|
111
|
-
url, markdown, word_count_threshold, only_text
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
result = asyncio.run(_crawl())
|
|
115
|
-
return result
|
|
116
106
|
except Exception as e:
|
|
117
107
|
return ToolResult(
|
|
118
108
|
output=f"Error crawling URL: {str(e)}",
|
|
@@ -131,18 +121,17 @@ class WebCrawlerTool(BaseTool):
|
|
|
131
121
|
try:
|
|
132
122
|
async with AsyncWebCrawler(verbose=False) as crawler:
|
|
133
123
|
if markdown:
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
word_count_threshold=word_count_threshold
|
|
138
|
-
),
|
|
124
|
+
config = CrawlerRunConfig( # type: ignore[union-attr]
|
|
125
|
+
markdown_generator=DefaultMarkdownGenerator(), # type: ignore[union-attr]
|
|
126
|
+
word_count_threshold=word_count_threshold,
|
|
139
127
|
only_text=only_text,
|
|
140
128
|
)
|
|
129
|
+
result = await crawler.arun(url=url, config=config)
|
|
141
130
|
else:
|
|
142
|
-
|
|
143
|
-
url=url,
|
|
131
|
+
config = CrawlerRunConfig( # type: ignore[union-attr]
|
|
144
132
|
only_text=only_text,
|
|
145
133
|
)
|
|
134
|
+
result = await crawler.arun(url=url, config=config)
|
|
146
135
|
|
|
147
136
|
if not result.success:
|
|
148
137
|
return ToolResult(
|
|
@@ -155,7 +144,8 @@ class WebCrawlerTool(BaseTool):
|
|
|
155
144
|
if markdown and result.markdown:
|
|
156
145
|
output = f"URL: {url}\n\n{result.markdown}"
|
|
157
146
|
else:
|
|
158
|
-
|
|
147
|
+
html = result.html or result.markdown or "No content extracted"
|
|
148
|
+
output = f"URL: {url}\n\n{html}"
|
|
159
149
|
|
|
160
150
|
return ToolResult(
|
|
161
151
|
output=output,
|
|
@@ -44,7 +44,6 @@ class ChatMessage:
|
|
|
44
44
|
# ChatApp — main Textual application
|
|
45
45
|
# ---------------------------------------------------------------------------
|
|
46
46
|
|
|
47
|
-
|
|
48
47
|
class ChatApp(App):
|
|
49
48
|
"""Clean modern TUI for RegCode agent."""
|
|
50
49
|
|
|
@@ -150,6 +149,7 @@ class ChatApp(App):
|
|
|
150
149
|
self._cumulative_cost: float = 0.0
|
|
151
150
|
self._total_tokens: int = 0
|
|
152
151
|
self._context_window: int = 128000
|
|
152
|
+
self._pending_tool_line: str = ""
|
|
153
153
|
|
|
154
154
|
# ── Compose ───────────────────────────────────────────────────
|
|
155
155
|
|
|
@@ -338,6 +338,27 @@ class ChatApp(App):
|
|
|
338
338
|
|
|
339
339
|
def _run_agent_sync(self, message: str) -> None:
|
|
340
340
|
"""Run agent.chat synchronously in a background thread."""
|
|
341
|
+
# Suppress noise from litellm, mlflow, crawl4ai, and other
|
|
342
|
+
# library loggers during agent execution — they spew logging
|
|
343
|
+
# lines to stdout/stderr which corrupt the TUI display.
|
|
344
|
+
_noisy_loggers = (
|
|
345
|
+
"LiteLLM",
|
|
346
|
+
"mlflow",
|
|
347
|
+
"mlflow.tracking",
|
|
348
|
+
"crawl4ai",
|
|
349
|
+
"urllib3",
|
|
350
|
+
"httpx",
|
|
351
|
+
"httpcore",
|
|
352
|
+
"openai",
|
|
353
|
+
"http11",
|
|
354
|
+
"hpack",
|
|
355
|
+
)
|
|
356
|
+
_old_levels: dict[str, int] = {}
|
|
357
|
+
for name in _noisy_loggers:
|
|
358
|
+
logger = logging.getLogger(name)
|
|
359
|
+
_old_levels[name] = logger.level
|
|
360
|
+
logger.setLevel(logging.WARNING)
|
|
361
|
+
|
|
341
362
|
try:
|
|
342
363
|
with self._lock:
|
|
343
364
|
if self._agent is None:
|
|
@@ -364,6 +385,9 @@ class ChatApp(App):
|
|
|
364
385
|
)
|
|
365
386
|
finally:
|
|
366
387
|
self.call_from_thread(self._finish_message)
|
|
388
|
+
# Restore logger levels in the same thread (background thread, not main)
|
|
389
|
+
for name, level in _old_levels.items():
|
|
390
|
+
logging.getLogger(name).setLevel(level)
|
|
367
391
|
|
|
368
392
|
# ── Agent callbacks (any thread → main thread) ────────────────
|
|
369
393
|
|
|
@@ -383,6 +407,9 @@ class ChatApp(App):
|
|
|
383
407
|
self._flush_stream_for_status()
|
|
384
408
|
|
|
385
409
|
if status == AgentStatus.TOOL_CALL and tool_call_info is not None:
|
|
410
|
+
# Buffer the tool call text — RichLog always starts a new line
|
|
411
|
+
# per write(), so we can't append the ✓ on the same line until
|
|
412
|
+
# TOOL_RESULT arrives.
|
|
386
413
|
tool_name = rich_escape(str(tool_call_info.tool_name))
|
|
387
414
|
parts = [f"[dim] [dim]⚙[/dim] {tool_name}[/dim]"]
|
|
388
415
|
if tool_call_info.tool_args:
|
|
@@ -391,11 +418,13 @@ class ChatApp(App):
|
|
|
391
418
|
parts.append(
|
|
392
419
|
f" [dim]{rich_escape(str(k))}=[/dim]{val}"
|
|
393
420
|
)
|
|
394
|
-
|
|
421
|
+
self._pending_tool_line = " ".join(parts)
|
|
395
422
|
|
|
396
423
|
elif status == AgentStatus.TOOL_RESULT \
|
|
397
424
|
and tool_call_info is not None:
|
|
398
|
-
|
|
425
|
+
# Emit the complete tool line with ✓ appended
|
|
426
|
+
log.write(self._pending_tool_line + " [dim]✓[/dim]\n")
|
|
427
|
+
self._pending_tool_line = ""
|
|
399
428
|
|
|
400
429
|
elif status == AgentStatus.COMPACTION_START:
|
|
401
430
|
log.write(
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Tests for regcode.dependencies startup checks."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
from unittest.mock import MagicMock, patch
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _clear_regcode_deps():
|
|
9
|
+
"""Remove cached regcode modules so re-import picks up mocks."""
|
|
10
|
+
for mod in list(sys.modules):
|
|
11
|
+
if mod == "regcode" or mod.startswith("regcode."):
|
|
12
|
+
del sys.modules[mod]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestEnsurePlaywrightBrowsers:
|
|
16
|
+
"""Tests for Playwright browser availability checking."""
|
|
17
|
+
|
|
18
|
+
def test_playwright_not_installed(self):
|
|
19
|
+
"""When playwright is not installed, return False with warning."""
|
|
20
|
+
_clear_regcode_deps()
|
|
21
|
+
|
|
22
|
+
with patch.dict("sys.modules", {
|
|
23
|
+
"playwright": None,
|
|
24
|
+
"playwright.sync_api": None,
|
|
25
|
+
}):
|
|
26
|
+
from regcode import dependencies
|
|
27
|
+
result = dependencies.ensure_playwright_browsers()
|
|
28
|
+
assert result is False
|
|
29
|
+
|
|
30
|
+
def test_playwright_available_no_launch(self):
|
|
31
|
+
"""When launch fails, auto-install is triggered."""
|
|
32
|
+
_clear_regcode_deps()
|
|
33
|
+
|
|
34
|
+
mock_browser = MagicMock()
|
|
35
|
+
mock_browser.close = MagicMock()
|
|
36
|
+
mock_p = MagicMock()
|
|
37
|
+
mock_p.chromium = MagicMock()
|
|
38
|
+
mock_p.chromium.executable_path = "/fake/path"
|
|
39
|
+
mock_p.chromium.launch = MagicMock(side_effect=Exception(
|
|
40
|
+
"BrowserType.launch: Executable doesn't exist"
|
|
41
|
+
))
|
|
42
|
+
mock_instance = MagicMock()
|
|
43
|
+
mock_instance.__enter__ = MagicMock(return_value=mock_p)
|
|
44
|
+
mock_instance.__exit__ = MagicMock(return_value=None)
|
|
45
|
+
|
|
46
|
+
# Mock at the playwright module level so the import in
|
|
47
|
+
# dependencies.py picks up our mock
|
|
48
|
+
with patch.dict("sys.modules", {
|
|
49
|
+
"playwright": MagicMock(),
|
|
50
|
+
"playwright.sync_api": MagicMock(
|
|
51
|
+
sync_playwright=MagicMock(return_value=mock_instance)
|
|
52
|
+
),
|
|
53
|
+
}):
|
|
54
|
+
from regcode.dependencies import ensure_playwright_browsers
|
|
55
|
+
# Auto-install succeeds
|
|
56
|
+
with patch.object(
|
|
57
|
+
subprocess, "run", return_value=MagicMock(returncode=0)
|
|
58
|
+
):
|
|
59
|
+
result = ensure_playwright_browsers()
|
|
60
|
+
assert result is True
|
|
61
|
+
|
|
62
|
+
def test_playwright_available_with_launch(self):
|
|
63
|
+
"""When browser launches successfully, return True."""
|
|
64
|
+
_clear_regcode_deps()
|
|
65
|
+
|
|
66
|
+
mock_browser = MagicMock()
|
|
67
|
+
mock_browser.close = MagicMock()
|
|
68
|
+
mock_p = MagicMock()
|
|
69
|
+
mock_p.chromium = MagicMock()
|
|
70
|
+
mock_p.chromium.executable_path = "/fake/path"
|
|
71
|
+
mock_p.chromium.launch = MagicMock(return_value=mock_browser)
|
|
72
|
+
|
|
73
|
+
mock_instance = MagicMock()
|
|
74
|
+
mock_instance.__enter__ = MagicMock(return_value=mock_p)
|
|
75
|
+
mock_instance.__exit__ = MagicMock(return_value=None)
|
|
76
|
+
|
|
77
|
+
with patch.dict("sys.modules", {
|
|
78
|
+
"playwright": MagicMock(),
|
|
79
|
+
"playwright.sync_api": MagicMock(
|
|
80
|
+
sync_playwright=MagicMock(return_value=mock_instance)
|
|
81
|
+
),
|
|
82
|
+
}):
|
|
83
|
+
from regcode.dependencies import ensure_playwright_browsers
|
|
84
|
+
result = ensure_playwright_browsers()
|
|
85
|
+
assert result is True
|
|
86
|
+
mock_browser.close.assert_called_once()
|
|
87
|
+
|
|
88
|
+
def test_playwright_no_install_env_var(self):
|
|
89
|
+
"""When PLAYWRIGHT_NO_INSTALL=1, skip check and return False."""
|
|
90
|
+
_clear_regcode_deps()
|
|
91
|
+
|
|
92
|
+
with patch.dict("os.environ", {"PLAYWRIGHT_NO_INSTALL": "1"}):
|
|
93
|
+
from regcode.dependencies import ensure_playwright_browsers
|
|
94
|
+
result = ensure_playwright_browsers()
|
|
95
|
+
assert result is False
|
|
96
|
+
|
|
97
|
+
def test_install_failure_returns_false(self):
|
|
98
|
+
"""When auto-install fails, return False with error."""
|
|
99
|
+
_clear_regcode_deps()
|
|
100
|
+
|
|
101
|
+
mock_p = MagicMock()
|
|
102
|
+
mock_p.chromium = MagicMock()
|
|
103
|
+
mock_p.chromium.executable_path = "/fake/path"
|
|
104
|
+
mock_p.chromium.launch = MagicMock(side_effect=Exception("missing"))
|
|
105
|
+
|
|
106
|
+
mock_instance = MagicMock()
|
|
107
|
+
mock_instance.__enter__ = MagicMock(return_value=mock_p)
|
|
108
|
+
mock_instance.__exit__ = MagicMock(return_value=None)
|
|
109
|
+
|
|
110
|
+
with patch.dict("sys.modules", {
|
|
111
|
+
"playwright": MagicMock(),
|
|
112
|
+
"playwright.sync_api": MagicMock(
|
|
113
|
+
sync_playwright=MagicMock(return_value=mock_instance)
|
|
114
|
+
),
|
|
115
|
+
}):
|
|
116
|
+
with patch.object(
|
|
117
|
+
subprocess,
|
|
118
|
+
"run",
|
|
119
|
+
side_effect=subprocess.CalledProcessError(1, "cmd", stderr=b"fail"),
|
|
120
|
+
):
|
|
121
|
+
from regcode.dependencies import ensure_playwright_browsers
|
|
122
|
+
result = ensure_playwright_browsers()
|
|
123
|
+
assert result is False
|
|
124
|
+
|
|
125
|
+
def test_install_file_not_found(self):
|
|
126
|
+
"""When playwright module is not found, return False."""
|
|
127
|
+
_clear_regcode_deps()
|
|
128
|
+
|
|
129
|
+
mock_p = MagicMock()
|
|
130
|
+
mock_p.chromium = MagicMock()
|
|
131
|
+
mock_p.chromium.executable_path = "/fake/path"
|
|
132
|
+
mock_p.chromium.launch = MagicMock(side_effect=Exception("missing"))
|
|
133
|
+
|
|
134
|
+
mock_instance = MagicMock()
|
|
135
|
+
mock_instance.__enter__ = MagicMock(return_value=mock_p)
|
|
136
|
+
mock_instance.__exit__ = MagicMock(return_value=None)
|
|
137
|
+
|
|
138
|
+
with patch.dict("sys.modules", {
|
|
139
|
+
"playwright": MagicMock(),
|
|
140
|
+
"playwright.sync_api": MagicMock(
|
|
141
|
+
sync_playwright=MagicMock(return_value=mock_instance)
|
|
142
|
+
),
|
|
143
|
+
}):
|
|
144
|
+
with patch.object(
|
|
145
|
+
subprocess, "run", side_effect=FileNotFoundError("no playwright")
|
|
146
|
+
):
|
|
147
|
+
from regcode.dependencies import ensure_playwright_browsers
|
|
148
|
+
result = ensure_playwright_browsers()
|
|
149
|
+
assert result is False
|
regcode-0.4.0/repair_tui.md
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
# TUI Streaming Repair — step by step
|
|
2
|
-
|
|
3
|
-
File to edit: `regcode/tui.py` — class `ChatApp`.
|
|
4
|
-
|
|
5
|
-
## Goal
|
|
6
|
-
Stop printing every streamed text chunk on its own line in the chat history.
|
|
7
|
-
Instead, buffer all chunks during streaming and render one final markdown block when the agent finishes.
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## Step 1 — add a state flag
|
|
12
|
-
|
|
13
|
-
In `__init__`, after the other streaming-related lines, add:
|
|
14
|
-
|
|
15
|
-
```python
|
|
16
|
-
self._streaming = False
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
Also reset it in every place that already resets streaming state. For example, in the reset method that sets `_streaming_started = False`, also set:
|
|
20
|
-
|
|
21
|
-
```python
|
|
22
|
-
self._streaming = False
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
And in the final cleanup block of `_finish_message`, also set:
|
|
26
|
-
|
|
27
|
-
```python
|
|
28
|
-
self._streaming = False
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
---
|
|
32
|
-
|
|
33
|
-
## Step 2 — stop high-frequency live writes
|
|
34
|
-
|
|
35
|
-
Replace `on_text_chunk`:
|
|
36
|
-
|
|
37
|
-
- Keep appending to `self._current_response`.
|
|
38
|
-
- Keep updating `self._streaming_started = True`.
|
|
39
|
-
- Remove the call to `_write_streaming_chunk`.
|
|
40
|
-
- Add logic so the first chunk starts a single visual “AGENT:” label.
|
|
41
|
-
|
|
42
|
-
Example:
|
|
43
|
-
|
|
44
|
-
```python
|
|
45
|
-
def on_text_chunk(self, chunk: str) -> None:
|
|
46
|
-
self._current_response.append(chunk)
|
|
47
|
-
self._streaming_started = True
|
|
48
|
-
if not self._streaming:
|
|
49
|
-
self._streaming = True
|
|
50
|
-
self.call_from_thread(self._start_streaming_label)
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
Add this new method:
|
|
54
|
-
|
|
55
|
-
```python
|
|
56
|
-
def _start_streaming_label(self) -> None:
|
|
57
|
-
log = self.query_one("#history", RichLog)
|
|
58
|
-
log.write("[bold blue]AGENT:[/]\n")
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
Remove the old `_write_streaming_chunk` method entirely (or leave it unused if your agent does not delete code).
|
|
62
|
-
|
|
63
|
-
---
|
|
64
|
-
|
|
65
|
-
## Step 3 — fix the final render
|
|
66
|
-
|
|
67
|
-
In `_finish_message`:
|
|
68
|
-
|
|
69
|
-
- Save the flag at the start: `streaming = self._streaming`
|
|
70
|
-
- Then set `self._streaming = False`
|
|
71
|
-
- Only write the `AGENT:` label if we never started streaming
|
|
72
|
-
- Keep the existing markdown rendering logic
|
|
73
|
-
|
|
74
|
-
Example:
|
|
75
|
-
|
|
76
|
-
```python
|
|
77
|
-
def _finish_message(self) -> None:
|
|
78
|
-
self._agent_running = False
|
|
79
|
-
streaming = self._streaming
|
|
80
|
-
self._streaming = False
|
|
81
|
-
try:
|
|
82
|
-
if self._current_response:
|
|
83
|
-
response = "".join(self._current_response).lstrip("\n")
|
|
84
|
-
response = re.sub(r"\n{3,}", "\n\n", response)
|
|
85
|
-
log = self.query_one("#history", RichLog)
|
|
86
|
-
if not streaming:
|
|
87
|
-
log.write("[bold blue]AGENT:[/]\n")
|
|
88
|
-
log.write(self._render_markdown_to_string(response))
|
|
89
|
-
log.write("[dim]────────────────────────────────────────────────────[/]\n")
|
|
90
|
-
except Exception:
|
|
91
|
-
self._write_error_line("Failed to render agent response.")
|
|
92
|
-
finally:
|
|
93
|
-
# reset buffers here
|
|
94
|
-
...
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
---
|
|
98
|
-
|
|
99
|
-
## Step 4 — verify the reset paths
|
|
100
|
-
|
|
101
|
-
Any place that currently resets `_streaming_started = False` must also reset `self._streaming = False`.
|
|
102
|
-
|
|
103
|
-
If a reset method exists, update it. If several reset points exist, patch all of them.
|
|
104
|
-
|
|
105
|
-
---
|
|
106
|
-
|
|
107
|
-
## Rules for the agent
|
|
108
|
-
- Do only the edits above.
|
|
109
|
-
- Do not change CSS or widgets.
|
|
110
|
-
- Do not change markdown rendering in `_render_markdown_to_string`.
|
|
111
|
-
- Do not add extra logs or prints.
|
|
112
|
-
- After editing, the expected behavior is:
|
|
113
|
-
- One `AGENT:` label per message
|
|
114
|
-
- Final markdown appears once
|
|
115
|
-
- Streaming chunks are no longer written one per line
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|