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.
Files changed (41) hide show
  1. {regcode-0.4.0 → regcode-0.5.0}/PKG-INFO +1 -1
  2. {regcode-0.4.0 → regcode-0.5.0}/pyproject.toml +1 -1
  3. {regcode-0.4.0 → regcode-0.5.0}/regcode/__init__.py +1 -1
  4. {regcode-0.4.0 → regcode-0.5.0}/regcode/cli.py +14 -0
  5. regcode-0.5.0/regcode/dependencies.py +71 -0
  6. {regcode-0.4.0 → regcode-0.5.0}/regcode/tools/__init__.py +0 -2
  7. {regcode-0.4.0 → regcode-0.5.0}/regcode/tools/builtins.py +2 -1
  8. {regcode-0.4.0 → regcode-0.5.0}/regcode/tools/web_crawl.py +13 -23
  9. {regcode-0.4.0 → regcode-0.5.0}/regcode/tui.py +32 -3
  10. regcode-0.5.0/tests/test_dependencies.py +149 -0
  11. {regcode-0.4.0 → regcode-0.5.0}/uv.lock +1 -1
  12. regcode-0.4.0/repair_tui.md +0 -115
  13. {regcode-0.4.0 → regcode-0.5.0}/.gitignore +0 -0
  14. {regcode-0.4.0 → regcode-0.5.0}/.python-version +0 -0
  15. {regcode-0.4.0 → regcode-0.5.0}/.qwen/settings.json +0 -0
  16. {regcode-0.4.0 → regcode-0.5.0}/AGENTS.md +0 -0
  17. {regcode-0.4.0 → regcode-0.5.0}/GOALS.md +0 -0
  18. {regcode-0.4.0 → regcode-0.5.0}/README.md +0 -0
  19. {regcode-0.4.0 → regcode-0.5.0}/config.yaml.template +0 -0
  20. {regcode-0.4.0 → regcode-0.5.0}/main.py +0 -0
  21. {regcode-0.4.0 → regcode-0.5.0}/regcode/config.py +0 -0
  22. {regcode-0.4.0 → regcode-0.5.0}/regcode/conversation_manager.py +0 -0
  23. {regcode-0.4.0 → regcode-0.5.0}/regcode/main.py +0 -0
  24. {regcode-0.4.0 → regcode-0.5.0}/regcode/monty_sandbox.py +0 -0
  25. {regcode-0.4.0 → regcode-0.5.0}/regcode/permissions.py +0 -0
  26. {regcode-0.4.0 → regcode-0.5.0}/regcode/sandbox.py +0 -0
  27. {regcode-0.4.0 → regcode-0.5.0}/regcode/tools/base.py +0 -0
  28. {regcode-0.4.0 → regcode-0.5.0}/regcode/tools/registry.py +0 -0
  29. {regcode-0.4.0 → regcode-0.5.0}/regcode/tools/review_notes.py +0 -0
  30. {regcode-0.4.0 → regcode-0.5.0}/regcode/tracing.py +0 -0
  31. {regcode-0.4.0 → regcode-0.5.0}/tests/__init__.py +0 -0
  32. {regcode-0.4.0 → regcode-0.5.0}/tests/conftest.py +0 -0
  33. {regcode-0.4.0 → regcode-0.5.0}/tests/test_api.py +0 -0
  34. {regcode-0.4.0 → regcode-0.5.0}/tests/test_cli.py +0 -0
  35. {regcode-0.4.0 → regcode-0.5.0}/tests/test_config.py +0 -0
  36. {regcode-0.4.0 → regcode-0.5.0}/tests/test_context_manager.py +0 -0
  37. {regcode-0.4.0 → regcode-0.5.0}/tests/test_main.py +0 -0
  38. {regcode-0.4.0 → regcode-0.5.0}/tests/test_monty_sandbox.py +0 -0
  39. {regcode-0.4.0 → regcode-0.5.0}/tests/test_osv_cve.py +0 -0
  40. {regcode-0.4.0 → regcode-0.5.0}/tests/test_provider_extra.py +0 -0
  41. {regcode-0.4.0 → regcode-0.5.0}/tests/test_review_notes.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: regcode
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: Minimalistic coding agent with Python API and CLI
5
5
  Requires-Python: >=3.12
6
6
  Requires-Dist: click>=8.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "regcode"
3
- version = "0.4.0"
3
+ version = "0.5.0"
4
4
  description = "Minimalistic coding agent with Python API and CLI"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -2,4 +2,4 @@ from regcode.config import Config
2
2
  from regcode.main import Agent, AgentStatus, ToolCall
3
3
 
4
4
  __all__ = ["Agent", "Config", "AgentStatus", "ToolCall"]
5
- __version__ = "0.4.0"
5
+ __version__ = "0.5.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
@@ -4,8 +4,6 @@ from regcode.tools.base import BaseTool, ToolParam, ToolResult
4
4
  from regcode.tools.builtins import create_default_tools
5
5
  from regcode.tools.registry import ToolRegistry
6
6
 
7
- __version__ = "0.3.0"
8
-
9
7
  __all__ = [
10
8
  "BaseTool",
11
9
  "ToolParam",
@@ -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
- try:
99
- import asyncio
99
+ import asyncio
100
100
 
101
- result = asyncio.get_event_loop().run_until_complete(
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
- result = await crawler.arun(
135
- url=url,
136
- markdown_generator=DefaultMarkdownGenerator(
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
- result = await crawler.arun(
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
- output = f"URL: {url}\n\n{result.html or result.markdown or 'No content extracted'}"
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
- log.write(" ".join(parts) + "\n")
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
- log.write("[dim] [dim]✓[/dim] tool completed[/dim]\n")
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
@@ -2835,7 +2835,7 @@ wheels = [
2835
2835
 
2836
2836
  [[package]]
2837
2837
  name = "regcode"
2838
- version = "0.4.0"
2838
+ version = "0.5.0"
2839
2839
  source = { editable = "." }
2840
2840
  dependencies = [
2841
2841
  { name = "click" },
@@ -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