regcode 0.6.0__py3-none-any.whl → 0.6.2__py3-none-any.whl

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/__init__.py CHANGED
@@ -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.6.0"
5
+ __version__ = "0.6.2"
regcode/main.py CHANGED
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import json
6
6
  import warnings
7
+ from datetime import datetime, timezone
7
8
  from enum import Enum
8
9
  from pathlib import Path
9
10
  from typing import Any, Callable
@@ -444,11 +445,48 @@ class Agent:
444
445
  self._history = [m for m in messages if m.get("role") != "system"]
445
446
  return final_text or response_message.get("content", "") or ""
446
447
 
448
+ @staticmethod
449
+ def _read_agents_md(directory: str | None = None) -> str:
450
+ """Read AGENTS.md from the given directory if it exists.
451
+
452
+ Returns the file contents with a header, or an empty string if
453
+ the file is not found.
454
+ """
455
+ search_dirs: list[Path] = []
456
+ if directory:
457
+ search_dirs.append(Path(directory))
458
+ search_dirs.append(Path.cwd())
459
+ for candidate in search_dirs:
460
+ for name in ("AGENTS.md", "agents.md", "Agents.md"):
461
+ p = candidate / name
462
+ if p.exists():
463
+ try:
464
+ return p.read_text(encoding="utf-8")
465
+ except OSError:
466
+ continue
467
+ return ""
468
+
447
469
  def _build_system_prompt(self) -> str:
448
470
  """Build the full system prompt including tool definitions."""
471
+ now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
472
+ date_section = f"\n\nCurrent time: {now}\n"
473
+ agents_md = self._read_agents_md(self._allowed_dir)
474
+ agents_md_section = ""
475
+ if agents_md:
476
+ agents_md_section = (
477
+ f"\n\n{'=' * 60}\n"
478
+ "AGENT INSTRUCTIONS (injected from AGENTS.md):\n"
479
+ f"{'=' * 60}\n\n{agents_md}"
480
+ )
449
481
  tool_descriptions = self._format_tool_descriptions()
450
482
  notes_section = self._format_review_notes()
451
- return f"{self.system_prompt}{tool_descriptions}{notes_section}"
483
+ return (
484
+ f"{self.system_prompt}"
485
+ f"{date_section}"
486
+ f"{agents_md_section}"
487
+ f"{tool_descriptions}"
488
+ f"{notes_section}"
489
+ )
452
490
 
453
491
  def _build_system_prompt_no_tools(self) -> str:
454
492
  """Build the system prompt WITHOUT tool definitions.
@@ -460,7 +498,19 @@ class Agent:
460
498
  stripped, because the accumulated review findings remain relevant
461
499
  context for the LLM's final response.
462
500
  """
463
- return f"{self.system_prompt}{self._format_review_notes()}"
501
+ now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
502
+ date_section = f"\n\nCurrent time: {now}\n"
503
+ agents_md = self._read_agents_md(self._allowed_dir)
504
+ agents_md_section = ""
505
+ if agents_md:
506
+ agents_md_section = (
507
+ f"\n\n{'=' * 60}\n"
508
+ "AGENT INSTRUCTIONS (injected from AGENTS.md):\n"
509
+ f"{'=' * 60}\n\n{agents_md}"
510
+ )
511
+ parts = [self.system_prompt, date_section, agents_md_section]
512
+ parts.append(self._format_review_notes())
513
+ return "".join(parts)
464
514
 
465
515
  def _stream(
466
516
  self,
regcode/tracing.py CHANGED
@@ -39,9 +39,17 @@ def _initialize_tracing(config: Config | None = None) -> None:
39
39
  if not _tracing_config.tracing_enabled():
40
40
  return
41
41
 
42
- if _tracing_config.mlflow_requires_auth and _tracing_config.mlflow_username and _tracing_config.mlflow_password:
43
- MLFLOW_TRACKING_USERNAME.set(_tracing_config.mlflow_username)
44
- MLFLOW_TRACKING_PASSWORD.set(_tracing_config.mlflow_password)
42
+ if (
43
+ _tracing_config.mlflow_requires_auth
44
+ and _tracing_config.mlflow_username
45
+ and _tracing_config.mlflow_password
46
+ ):
47
+ MLFLOW_TRACKING_USERNAME.set(
48
+ _tracing_config.mlflow_username
49
+ )
50
+ MLFLOW_TRACKING_PASSWORD.set(
51
+ _tracing_config.mlflow_password
52
+ )
45
53
 
46
54
  _logger.info(f"Setting up MLflow tracing with URL: {_tracing_config.mlflow_url}")
47
55
 
@@ -76,14 +84,16 @@ def agent_trace(user_message: str, tool_calls_count: int = 0) -> Iterator[None]:
76
84
  with agent_trace("Fix the bug", 3):
77
85
  result = agent.chat("Fix the bug")
78
86
  """
79
- if not _tracing_initialized or not _tracing_config or not _tracing_config.tracing_enabled():
87
+ if (
88
+ not _tracing_initialized
89
+ or not _tracing_config
90
+ or not _tracing_config.tracing_enabled()
91
+ ):
80
92
  yield
81
93
  return
82
94
 
83
- trace_name = f"agent.turn.{tool_calls_count}tools"
84
-
85
95
  try:
86
- with mlflow.start_trace() as trace:
96
+ with mlflow.start_trace():
87
97
  # Log input attributes for better trace UI
88
98
  mlflow.set_tag("user_message", user_message[:200])
89
99
  mlflow.set_tag("tool_calls", tool_calls_count)
regcode/tui.py CHANGED
@@ -1,9 +1,9 @@
1
1
  """Terminal UI for the RegCode agent CLI using Textual.
2
2
 
3
3
  Three-zone layout:
4
- Zone 1: Status bar — [RegCode 0.1.0] [model] [$cost] [tokens]
5
- Zone 2: Chat area — scrollable conversation with clear separation
6
- Zone 3: Input bar — text input with progress indicator
4
+ Zone 1: Top status bar — version | model | cost | tokens
5
+ Zone 2: Chat history — scrollable RichLog with colored messages
6
+ Zone 3: Bottom input — text input with progress indicator
7
7
  """
8
8
 
9
9
  from __future__ import annotations
@@ -11,14 +11,15 @@ from __future__ import annotations
11
11
  import logging
12
12
  import re
13
13
  import threading
14
- from dataclasses import dataclass
15
14
  from typing import Any
16
15
 
16
+ from rich.console import Console
17
17
  from rich.markup import escape as rich_escape
18
+ from rich.text import Text
18
19
  from textual.app import App, ComposeResult
19
20
  from textual.binding import Binding
20
21
  from textual.containers import Container
21
- from textual.widgets import Input, RichLog
22
+ from textual.widgets import Input, Label, RichLog
22
23
 
23
24
  from regcode import __version__
24
25
  from regcode.main import Agent, AgentStatus, ToolCall
@@ -26,24 +27,11 @@ from regcode.main import Agent, AgentStatus, ToolCall
26
27
  _logger = logging.getLogger(__name__)
27
28
 
28
29
 
29
- # ---------------------------------------------------------------------------
30
- # Message data structure
31
- # ---------------------------------------------------------------------------
32
-
33
-
34
- @dataclass
35
- class ChatMessage:
36
- """A single message block in the chat history."""
37
- role: str # "user", "agent", "tool", "system", "separator"
38
- content: str = ""
39
- markup: str = "" # pre-rendered Rich markup string
40
- is_markdown: bool = False
41
-
42
-
43
30
  # ---------------------------------------------------------------------------
44
31
  # ChatApp — main Textual application
45
32
  # ---------------------------------------------------------------------------
46
33
 
34
+
47
35
  class ChatApp(App):
48
36
  """Clean modern TUI for RegCode agent."""
49
37
 
@@ -51,67 +39,67 @@ class ChatApp(App):
51
39
 
52
40
  CSS = """
53
41
  Screen {
54
- background: $boost;
42
+ background: #1e1e2e;
55
43
  }
56
44
 
57
- /* ── Zone 1: Top status bar (single line) ─────────────────── */
45
+ /* ── Zone 1: Status bar ───────────────────────────────────── */
58
46
  #top-bar {
59
47
  dock: top;
60
48
  height: 1;
61
- padding: 0 3;
62
- background: $primary;
49
+ padding: 0 4;
50
+ background: #313244;
51
+ color: #cdd6f4;
63
52
  content-align: center middle;
64
53
  }
65
54
 
66
- /* ── Zone 2: Chat history area (takes all remaining space) ── */
55
+ /* ── Zone 2: Chat history ──────────────────────────────────── */
67
56
  #chat-area {
68
57
  width: 100%;
69
58
  height: 1fr;
70
- padding: 0 3;
71
- margin: 0;
72
- border: none;
59
+ margin: 0 2;
73
60
  overflow-y: auto;
74
- background: $boost;
61
+ background: #1e1e2e;
62
+ color: #cdd6f4;
63
+ scrollbar-color: #585b70;
64
+ scrollbar-background: #181825;
65
+ scrollbar-gutter: auto;
75
66
  }
76
67
 
77
- #chat-area RichLog {
78
- color: $text;
79
- background: transparent;
68
+ RichLog#chat-area {
69
+ padding: 0 2;
80
70
  }
81
71
 
82
- /* ── Zone 3: Bottom input bar ─────────────────────────────── */
72
+ /* ── Zone 3: Bottom input bar ──────────────────────────────── */
83
73
  #bottom-bar {
84
74
  dock: bottom;
85
75
  layout: horizontal;
86
76
  height: 3;
87
77
  padding: 0 1;
88
- background: $surface;
89
- border: none;
78
+ background: #313244;
90
79
  }
91
80
 
92
- #progress-bar {
81
+ #progress-label {
93
82
  dock: left;
94
83
  width: 30;
95
84
  content-align: center middle;
96
- padding: 0 1;
85
+ color: #a6adc8;
97
86
  }
98
87
 
99
88
  #input-box {
100
89
  width: 1fr;
101
90
  height: 3;
102
- min-height: 3;
103
91
  padding: 0 2;
104
- background: $surface;
105
- color: $text;
106
- border: round $warning;
92
+ background: #45475a;
93
+ color: #cdd6f4;
94
+ border: round #89b4fa;
95
+ align: center middle;
107
96
  }
108
97
 
109
98
  Input:focus {
110
- border: round $success;
111
- text-style: bold;
99
+ border: round #a6e3a1;
112
100
  }
113
101
 
114
- /* ── Hide defaults ────────────────────────────────────────── */
102
+ /* ── Hide defaults ─────────────────────────────────────────── */
115
103
  Footer {
116
104
  display: none;
117
105
  }
@@ -137,48 +125,115 @@ class ChatApp(App):
137
125
 
138
126
  # Streaming state
139
127
  self._streaming = False
140
- self._streaming_started = False
141
128
  self._current_response: list[str] = []
142
- self._stream_pending = ""
143
- self._stream_has_label = False
144
- self._stream_line_open = False
145
- self._stream_trim_leading = True
146
129
 
147
130
  # Cost / token display
148
131
  self._model_name: str = "unknown"
149
132
  self._cumulative_cost: float = 0.0
150
133
  self._total_tokens: int = 0
151
- self._context_window: int = 128000
134
+ self._context_window: int = 0
152
135
  self._pending_tool_line: str = ""
153
136
 
154
- # ── Compose ───────────────────────────────────────────────────
137
+ # ── Helpers ───────────────────────────────────────────────────
155
138
 
156
- def compose(self) -> ComposeResult:
157
- # Zone 1: Status bar — RichLog renders Rich markup
158
- yield RichLog(
159
- id="top-bar",
160
- markup=True,
161
- max_lines=1,
162
- auto_scroll=False,
139
+ def _get_chat_area(self) -> RichLog:
140
+ """Get the chat area RichLog, initializing on first access."""
141
+ return self.query_one("#chat-area", RichLog)
142
+
143
+ @staticmethod
144
+ def _format_model_name(model: str) -> str:
145
+ """Extract short model name: split on '/', take last segment."""
146
+ if "/" in model:
147
+ return model.rsplit("/", 1)[-1]
148
+ return model
149
+
150
+ def _get_input_placeholder(self) -> str:
151
+ """Generate input placeholder from current model + context window."""
152
+ short = self._format_model_name(self._model_name)
153
+ ctx_k = round(self._context_window / 1000) if self._context_window > 0 else 128
154
+ return f"{short} ({ctx_k}k context)"
155
+
156
+ def _append_rich(self, text: str) -> None:
157
+ """Append Rich-markup or Markdown text to the chat area."""
158
+ if not text:
159
+ return
160
+ ta = self._get_chat_area()
161
+ rich_text = self._markdown_to_rich(text)
162
+ ta.write(rich_text)
163
+ ta.scroll_end(animate=False)
164
+
165
+ def _insert_rich(self, text: str) -> None:
166
+ """Insert Rich-markup or Markdown text into the chat area (no auto-scroll)."""
167
+ if not text:
168
+ return
169
+ ta = self._get_chat_area()
170
+ rich_text = self._markdown_to_rich(text)
171
+ ta.write(rich_text)
172
+
173
+ # ── Markdown → Rich markup conversion ─────────────────────────
174
+
175
+ @staticmethod
176
+ def _markdown_to_rich(text: str) -> Text:
177
+ """Convert Markdown syntax to Rich markup for RichLog rendering."""
178
+ result = text
179
+
180
+ # Code blocks (fenced with triple backticks) — keep as-is, they'll be plain
181
+ # Inline code: `code` → [dim]code[/dim]
182
+ result = re.sub(
183
+ r"`([^`]+)`", r"[dim]\1[/dim]", result
184
+ )
185
+
186
+ # Bold: **text** → [bold]text[/bold]
187
+ result = re.sub(r"\*\*([^*]+)\*\*", r"[bold]\1[/bold]", result)
188
+
189
+ # Italic: *text* → [italic]text[/italic]
190
+ result = re.sub(r"\*([^*]+)\*", r"[italic]\1[/italic]", result)
191
+
192
+ # Strikethrough: ~~text~~ → [dim]text[/dim]
193
+ result = re.sub(r"~~([^~]+)~~", r"[dim]\1[/dim]", result)
194
+
195
+ # Headers: ### text → [bold]text[/bold]
196
+ result = re.sub(
197
+ r"^#{1,6}\s+(.+)$", r"[bold]\1[/bold]", result, flags=re.MULTILINE
163
198
  )
164
199
 
165
- # Zone 2: Chat history
166
- yield RichLog(
167
- id="chat-area",
168
- markup=True,
169
- max_lines=1000,
170
- auto_scroll=True,
171
- wrap=True,
200
+ # Blockquotes: > text → [dim]> text[/dim]
201
+ result = re.sub(
202
+ r"^>\s+(.+)$", r"[dim]> \1[/dim]", result, flags=re.MULTILINE
172
203
  )
173
204
 
174
- # Zone 3: Input bar
205
+ # Unordered list items: - item or * item → [yellow]- item[/yellow]
206
+ result = re.sub(
207
+ r"^[\-\*]\s+(.+)$", r"[yellow]• \1[/yellow]", result, flags=re.MULTILINE
208
+ )
209
+
210
+ # Ordered list items: 1. item → [yellow]1. item[/yellow]
211
+ result = re.sub(
212
+ r"^\d+\.\s+(.+)$", r"[yellow]\1[/yellow]", result, flags=re.MULTILINE
213
+ )
214
+
215
+ # Horizontal rules: --- or ***
216
+ result = re.sub(
217
+ r"^[-]{3,}$", r"[dim]" + "─" * 40 + "[/dim]", result, flags=re.MULTILINE
218
+ )
219
+ result = re.sub(
220
+ r"^\*{3,}$", r"[dim]" + "─" * 40 + "[/dim]", result, flags=re.MULTILINE
221
+ )
222
+
223
+ return Text.from_markup(result.rstrip("\n"))
224
+
225
+ # ── Compose ───────────────────────────────────────────────────
226
+
227
+ def compose(self) -> ComposeResult:
228
+ # Zone 1: Status bar
229
+ yield Label(id="top-bar")
230
+
231
+ # Zone 2: Chat history — RichLog natively renders Rich markup
232
+ yield RichLog(id="chat-area", wrap=True, highlight=True)
233
+
234
+ # Zone 3: Bottom input bar
175
235
  yield Container(
176
- RichLog(
177
- id="progress-bar",
178
- markup=True,
179
- max_lines=1,
180
- auto_scroll=False,
181
- ),
236
+ Label(id="progress-label"),
182
237
  Input(
183
238
  id="input-box",
184
239
  placeholder="Type message... (Q to quit)",
@@ -192,24 +247,24 @@ class ChatApp(App):
192
247
  self._update_top_bar()
193
248
  self._update_progress(0, 0)
194
249
 
195
- log = self.query_one("#chat-area", RichLog)
196
- log.write(
197
- "[dim]Type a message and press Enter to start chatting.[/]\n"
250
+ ta = self._get_chat_area()
251
+ ta.clear()
252
+ self._append_rich(
253
+ f"[dim]RegCode v{__version__}[/]"
254
+ " — Minimalistic Coding Agent\n",
198
255
  )
199
- log.write(
200
- "[dim] q Quit the application[/]\n"
201
- )
202
- log.write(
203
- "[dim] c Clear the chat history[/]\n"
256
+ self._append_rich(
257
+ "[dim]Type a message and press Enter to start chatting.[/]\n"
204
258
  )
205
- log.write(
259
+ self._append_rich("[dim] q Quit the application[/]\n")
260
+ self._append_rich("[dim] c Clear the chat history[/]\n")
261
+ self._append_rich(
206
262
  "[dim] e Reset session (clear history & tools)[/]\n"
207
263
  )
208
- log.write(
209
- "[dim]" + "─" * 60 + "[/]\n"
210
- )
264
+ self._append_rich("[dim]" + "\u2500" * 60 + "[/]\n")
211
265
 
212
266
  self.set_timer(0.1, self._focus_input)
267
+ self.set_timer(0.15, self._set_input_placeholder)
213
268
 
214
269
  # ── Focus management ──────────────────────────────────────────
215
270
 
@@ -217,6 +272,14 @@ class ChatApp(App):
217
272
  input_widget = self.query_one("#input-box", Input)
218
273
  input_widget.focus()
219
274
 
275
+ def _set_input_placeholder(self) -> None:
276
+ """Set the input box placeholder to show model + context info."""
277
+ try:
278
+ input_widget = self.query_one("#input-box", Input)
279
+ input_widget.placeholder = self._get_input_placeholder()
280
+ except Exception:
281
+ pass
282
+
220
283
  # ── Status bar ────────────────────────────────────────────────
221
284
 
222
285
  def _update_top_bar(self) -> None:
@@ -228,14 +291,18 @@ class ChatApp(App):
228
291
  display_name = display_name.rsplit("\\", 1)[-1]
229
292
 
230
293
  cost_str = f"${self._cumulative_cost:.6f}"
294
+ tokens_str = f"{self._total_tokens} tok"
231
295
 
232
- bar = self.query_one("#top-bar", RichLog)
233
- bar.clear()
234
- bar.write(
235
- "[cyan]RegCode " + __version__
236
- + "[/] [dim]"
237
- + display_name + "[/] "
238
- + cost_str
296
+ # Guard: only query DOM when the app is actually mounted
297
+ try:
298
+ label = self.query_one("#top-bar", Label)
299
+ except Exception:
300
+ return
301
+ label.value = (
302
+ f"[cyan]RegCode {__version__}[/] "
303
+ f"[dim]{display_name}[/] "
304
+ f"[yellow]{cost_str}[/] "
305
+ f"[dim]{tokens_str}[/]"
239
306
  )
240
307
 
241
308
  # ── Key handling ──────────────────────────────────────────────
@@ -244,17 +311,17 @@ class ChatApp(App):
244
311
  if event.key == "q":
245
312
  self.exit()
246
313
  elif event.key == "c":
247
- self.query_one("#chat-area", RichLog).clear()
314
+ self._get_chat_area().clear()
248
315
  elif event.key == "e":
249
316
  self._handle_reset_session()
250
317
 
251
318
  def _handle_reset_session(self) -> None:
252
- log = self.query_one("#chat-area", RichLog)
253
- log.clear()
254
- log.write("[bold cyan]RegCode v" + __version__
255
- + " — Minimalistic Coding Agent[/]\n")
256
- log.write("[dim]Session reset. Type a message to begin.[/]\n")
257
- log.write("[dim]" + "─" * 60 + "[/]\n")
319
+ ta = self._get_chat_area()
320
+ ta.clear()
321
+ self._append_rich(f"[bold cyan]RegCode v{__version__}[/]\n")
322
+ self._append_rich("[dim]— Minimalistic Coding Agent[/]\n")
323
+ self._append_rich("[dim]Session reset. Type a message to begin.[/]\n")
324
+ self._append_rich("[dim]" + "\u2500" * 60 + "[/]\n")
258
325
 
259
326
  self._reset_streaming_state()
260
327
 
@@ -275,9 +342,13 @@ class ChatApp(App):
275
342
  try:
276
343
  provider = agent.config.provider
277
344
  self._model_name = getattr(provider, "model", "unknown")
345
+ # Sync context window from config if available
346
+ self._context_window = getattr(
347
+ agent.config.agent, "context_window", 0
348
+ )
278
349
  except Exception:
279
350
  self._model_name = "unknown"
280
- # _update_cost_display() is called in on_mount() after the app starts
351
+ self._update_cost_display()
281
352
 
282
353
  # ── Input handling ────────────────────────────────────────────
283
354
 
@@ -296,7 +367,7 @@ class ChatApp(App):
296
367
  return
297
368
 
298
369
  if message == "/clear":
299
- self.query_one("#chat-area", RichLog).clear()
370
+ self._get_chat_area().clear()
300
371
  return
301
372
 
302
373
  if message == "/reset":
@@ -317,12 +388,8 @@ class ChatApp(App):
317
388
  self._reset_streaming_state()
318
389
 
319
390
  # Mark user message in chat
320
- log = self.query_one("#chat-area", RichLog)
321
- log.write(
322
- "[bold green][USER][/bold green] "
323
- + rich_escape(message) + "\n"
324
- )
325
- log.write("[dim]" + "─" * 60 + "[/dim]\n")
391
+ self._append_rich(f"[bold green][USER][/bold green] {rich_escape(message)}")
392
+ self._append_rich("[dim]" + "─" * 40 + "[/dim]\n")
326
393
 
327
394
  # Update input placeholder
328
395
  input_widget = self.query_one("#input-box", Input)
@@ -338,9 +405,7 @@ class ChatApp(App):
338
405
 
339
406
  def _run_agent_sync(self, message: str) -> None:
340
407
  """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.
408
+ # Suppress noise from library loggers during agent execution
344
409
  _noisy_loggers = (
345
410
  "LiteLLM",
346
411
  "mlflow",
@@ -373,7 +438,7 @@ class ChatApp(App):
373
438
  on_text_chunk=self.on_text_chunk,
374
439
  )
375
440
 
376
- # Fallback: if no chunks were emitted, render final text.
441
+ # Fallback: if no chunks were emitted, render final text
377
442
  if result and not self._current_response:
378
443
  self.call_from_thread(
379
444
  self._set_fallback_response, result
@@ -385,7 +450,7 @@ class ChatApp(App):
385
450
  )
386
451
  finally:
387
452
  self.call_from_thread(self._finish_message)
388
- # Restore logger levels in the same thread (background thread, not main)
453
+ # Restore logger levels in the same thread
389
454
  for name, level in _old_levels.items():
390
455
  logging.getLogger(name).setLevel(level)
391
456
 
@@ -401,47 +466,38 @@ class ChatApp(App):
401
466
  self, status: AgentStatus, tool_call_info=None
402
467
  ) -> None:
403
468
  """Render status updates on the main thread."""
404
- log = self.query_one("#chat-area", RichLog)
405
-
406
- # Flush any pending stream text BEFORE status output (pitfall #9)
469
+ # Flush any pending stream text BEFORE status output
407
470
  self._flush_stream_for_status()
408
471
 
409
472
  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.
413
473
  tool_name = rich_escape(str(tool_call_info.tool_name))
414
- parts = [f"[dim] [dim]⚙[/dim] {tool_name}[/dim]"]
474
+ parts = [f"[dim] \u2699\uFE0F {tool_name}[/dim]"]
415
475
  if tool_call_info.tool_args:
416
476
  for k, v in tool_call_info.tool_args.items():
417
477
  val = rich_escape(str(v))
418
478
  parts.append(
419
- f" [dim]{rich_escape(str(k))}=[/dim]{val}"
479
+ f"[dim] {rich_escape(str(k))}=[/dim] {val}"
420
480
  )
421
481
  self._pending_tool_line = " ".join(parts)
422
482
 
423
483
  elif status == AgentStatus.TOOL_RESULT \
424
484
  and tool_call_info is not None:
425
- # Emit the complete tool line with ✓ appended
426
- log.write(self._pending_tool_line + " [dim]✓[/dim]\n")
485
+ self._insert_rich(self._pending_tool_line + " [dim]✓[/]\n")
427
486
  self._pending_tool_line = ""
428
487
 
429
488
  elif status == AgentStatus.COMPACTION_START:
430
- log.write(
431
- "[dim] ~ Compacting context window...[/dim]\n"
432
- )
433
- log.write("[dim] " + "─" * 60 + "[/dim]\n")
489
+ self._insert_rich(" ~ Compacting context window...\n")
490
+ self._insert_rich(" " + "─" * 60 + "\n")
434
491
 
435
492
  elif status == AgentStatus.BUDGET_EXHAUSTED:
436
- log.write(
437
- "[dim] [yellow]Tool budget exhausted. "
438
- "Agent responding.[/yellow][/dim]\n"
493
+ self._insert_rich(
494
+ " [yellow]Tool budget exhausted. Agent responding."
495
+ "[/yellow]\n",
439
496
  )
440
497
 
441
498
  elif status == AgentStatus.IDLE:
442
- log.write(
443
- "[dim] Agent is idle. "
444
- "Waiting for your input.[/dim]\n"
499
+ self._insert_rich(
500
+ " Agent is idle. Waiting for your input.\n",
445
501
  )
446
502
 
447
503
  def on_result(
@@ -473,64 +529,23 @@ class ChatApp(App):
473
529
  def on_text_chunk(self, chunk: str) -> None:
474
530
  """Handle streaming text chunks — buffer for final rendering."""
475
531
  self._current_response.append(chunk)
476
- self._streaming_started = True
477
532
  if not self._streaming:
478
533
  self._streaming = True
479
534
  self.call_from_thread(self._start_streaming_label)
480
535
 
481
536
  def _start_streaming_label(self) -> None:
482
537
  """Write the agent label once at the start of streaming."""
483
- log = self.query_one("#chat-area", RichLog)
484
- log.write("[bold blue][AGENT][/bold blue]\n")
538
+ self._insert_rich("[bold blue][AGENT][/bold blue]\n")
485
539
 
486
540
  # ── Stream flushing ───────────────────────────────────────────
487
541
 
488
542
  def _flush_stream_for_status(self) -> None:
489
- """Flush stream text before status/tool output (pitfall #9)."""
490
- if self._stream_pending:
491
- pending = self._stream_pending.rstrip("\n")
492
- self._stream_pending = ""
493
- if pending:
494
- self._emit_stream_text(pending)
495
- self._ensure_stream_line_break()
496
-
497
- def _flush_stream_buffer(self, flush_partial: bool) -> None:
498
- """Flush buffered stream text by line; optionally flush remainder."""
499
- while "\n" in self._stream_pending:
500
- line, self._stream_pending = (
501
- self._stream_pending.split("\n", 1)
502
- )
503
- self._emit_stream_text(line + "\n")
504
-
505
- if flush_partial and self._stream_pending:
506
- self._emit_stream_text(self._stream_pending)
507
- self._stream_pending = ""
508
- elif len(self._stream_pending) >= 120:
509
- self._emit_stream_text(self._stream_pending)
510
- self._stream_pending = ""
511
-
512
- def _emit_stream_text(self, text: str) -> None:
513
- """Emit a streamed text segment to history."""
514
- if not text:
515
- return
516
- if self._stream_trim_leading:
517
- text = text.lstrip("\n")
518
- self._stream_trim_leading = False
519
- if not text:
520
- return
521
- log = self.query_one("#chat-area", RichLog)
522
- if not self._stream_has_label:
523
- log.write(rich_escape(text))
524
- self._stream_has_label = True
525
- else:
526
- log.write(rich_escape(text))
527
- self._stream_line_open = not text.endswith("\n")
528
-
529
- def _ensure_stream_line_break(self) -> None:
530
- """Ensure subsequent non-stream lines start on a fresh line."""
531
- if self._stream_has_label and self._stream_line_open:
532
- self.query_one("#chat-area", RichLog).write("\n")
533
- self._stream_line_open = False
543
+ """Flush accumulated stream text before status/tool output."""
544
+ if self._current_response:
545
+ text = "".join(self._current_response)
546
+ self._current_response.clear()
547
+ if text.strip():
548
+ self._insert_rich(text)
534
549
 
535
550
  # ── Progress & cost display ───────────────────────────────────
536
551
 
@@ -538,6 +553,8 @@ class ChatApp(App):
538
553
  """Update the progress bar on the bottom bar."""
539
554
  if total <= 0:
540
555
  total = self._context_window
556
+ if total <= 0 and self._agent is not None:
557
+ total = self._agent.config.agent.context_window
541
558
  if total <= 0:
542
559
  total = 128000
543
560
  pct = min(100, int(current / max(total, 1) * 100))
@@ -546,46 +563,24 @@ class ChatApp(App):
546
563
  empty = bar_width - filled
547
564
  bar = "\u2588" * filled + "\u2591" * empty
548
565
 
549
- if pct >= 90:
550
- color = "red"
551
- elif pct >= 70:
552
- color = "yellow"
553
- elif pct >= 40:
554
- color = "blue"
566
+ # Format tokens as e.g. "12.3k"
567
+ if current >= 1000:
568
+ tokens_str = f"{current / 1000:.1f}k"
555
569
  else:
556
- color = "green"
570
+ tokens_str = str(current)
557
571
 
558
- pct_str = f" {pct}%"
559
- tokens_str = f" {current}/{total}"
560
- label = (
561
- f"[bold {color}]{bar}[/] {pct_str} "
562
- f"[dim]{tokens_str}[/dim]"
563
- )
572
+ label_text = f"{bar} {tokens_str}"
564
573
 
565
- progress_widget = self.query_one("#progress-bar", RichLog)
566
- progress_widget.clear()
567
- progress_widget.write(label)
574
+ widget = self.query_one("#progress-label", Label)
575
+ try:
576
+ widget.update(label_text)
577
+ except Exception:
578
+ pass
568
579
 
569
580
  def _update_cost_display(self) -> None:
570
581
  """Update the top bar with cost and model info."""
571
582
  self._update_top_bar()
572
583
 
573
- # ── Markdown rendering helper ─────────────────────────────────
574
-
575
- def _render_markdown_to_string(self, md_text: str) -> str:
576
- """Render Markdown to a Rich-compatible string for RichLog."""
577
- from io import StringIO
578
-
579
- from rich.console import Console
580
- from rich.markdown import Markdown
581
-
582
- buf = StringIO()
583
- console = Console(
584
- file=buf, force_terminal=True, width=200
585
- )
586
- console.print(Markdown(md_text), soft_wrap=True)
587
- return buf.getvalue()
588
-
589
584
  # ── Finish message / response rendering ───────────────────────
590
585
 
591
586
  def _finish_message(self) -> None:
@@ -596,23 +591,13 @@ class ChatApp(App):
596
591
 
597
592
  try:
598
593
  if self._current_response:
599
- response = "".join(
600
- self._current_response
601
- ).lstrip("\n")
594
+ response = "".join(self._current_response).lstrip("\n")
602
595
  response = re.sub(r"\n{3,}", "\n\n", response)
603
596
  if response:
604
- log = self.query_one("#chat-area", RichLog)
605
597
  if not streaming:
606
- log.write(
607
- "[bold blue][AGENT][/bold blue]\n"
608
- )
609
- # Render Markdown for the final formatted output.
610
- log.write(
611
- self._render_markdown_to_string(response)
612
- )
613
- log.write(
614
- "[dim]" + "─" * 60 + "[/dim]\n"
615
- )
598
+ self._insert_rich("[bold blue][AGENT][/bold blue]\n")
599
+ self._insert_rich(response)
600
+ self._insert_rich("\n[dim]" + "─" * 40 + "[/dim]\n")
616
601
  except Exception:
617
602
  self._write_error_line(
618
603
  "Failed to render agent response."
@@ -622,10 +607,10 @@ class ChatApp(App):
622
607
  self._reset_streaming_state()
623
608
 
624
609
  input_widget = self.query_one("#input-box", Input)
625
- input_widget.placeholder = "Type message... (Q to quit)"
610
+ input_widget.placeholder = self._get_input_placeholder()
626
611
  input_widget.focus()
627
612
 
628
- # Process next queued message if any.
613
+ # Process next queued message if any
629
614
  self._process_next_queued()
630
615
 
631
616
  def _set_fallback_response(self, response: str) -> None:
@@ -647,12 +632,8 @@ class ChatApp(App):
647
632
  def _write_error_line(self, message: str) -> None:
648
633
  """Write an error line safely on the UI thread."""
649
634
  try:
650
- log = self.query_one("#chat-area", RichLog)
651
- log.write(
652
- "[red][ERROR][/red] "
653
- + rich_escape(message) + "\n"
654
- )
655
- log.write("[dim]" + "─" * 60 + "[/dim]\n")
635
+ self._insert_rich("[red][ERROR][/red] " + rich_escape(message) + "\n")
636
+ self._insert_rich("─" * 60 + "\n")
656
637
  except Exception:
657
638
  pass
658
639
  finally:
@@ -661,22 +642,22 @@ class ChatApp(App):
661
642
 
662
643
  # ── Utility ───────────────────────────────────────────────────
663
644
 
664
- def _write_system_message(self, text: str) -> None:
665
- """Write a system message to the history log (backward compat)."""
666
- try:
667
- log = self.query_one("#chat-area", RichLog)
668
- log.write(text + "\n")
669
- except Exception:
670
- pass
671
-
672
645
  def _reset_streaming_state(self) -> None:
673
- """Reset all streaming-related state for a new turn."""
646
+ """Reset streaming-related state for a new turn."""
674
647
  self._streaming = False
675
- self._streaming_started = False
676
- self._stream_pending = ""
677
- self._stream_has_label = False
678
- self._stream_line_open = False
679
- self._stream_trim_leading = True
648
+ self._current_response.clear()
649
+
650
+ # ── Test-friendly helpers (mirrors ChatUI methods) ────────────
651
+
652
+ def _add_user_message(self, message: str) -> None:
653
+ """Add a user message to the chat area (for tests)."""
654
+ self._append_rich(
655
+ f"[bold green][USER][/bold green] {rich_escape(message)}\n"
656
+ )
657
+
658
+ def _add_separator(self) -> None:
659
+ """Add a separator line to the chat area (for tests)."""
660
+ self._append_rich("[dim]" + "\u2500" * 60 + "[/dim]\n")
680
661
 
681
662
  # ── Backward-compatible API (for cli.py integration) ──────────
682
663
 
@@ -690,48 +671,35 @@ class ChatApp(App):
690
671
  return self._agent_running
691
672
 
692
673
  def print_welcome(self, version: str = "0.1.0") -> None:
693
- log = self.query_one("#chat-area", RichLog)
694
- log.write(
695
- "[bold cyan]"
696
- f"RegCode - Minimalistic Coding Agent v{version}[/]\n"
674
+ self._insert_rich(
675
+ f"[bold cyan]RegCode - Minimalistic Coding Agent v{version}[/]\n"
697
676
  )
698
- log.write("[dim]Type your message and press Enter.[/]\n")
677
+ self._insert_rich("[dim]Type your message and press Enter.[/]\n")
699
678
 
700
679
  def print_separator(self) -> None:
701
- self.query_one("#chat-area", RichLog).write(
702
- "[dim]" + "─" * 60 + "[/dim]\n"
703
- )
680
+ self._insert_rich("─" * 60 + "\n")
704
681
 
705
682
  def print_user_message(self, message: str) -> None:
706
- self.query_one("#chat-area", RichLog).write(
707
- "[bold green]"
708
- f"[USER][/bold green] {rich_escape(message)}\n"
683
+ self._insert_rich(
684
+ f"[bold green][USER][/bold green] {rich_escape(message)}\n",
709
685
  )
710
686
 
711
687
  def print_assistant_message(self, message: str) -> None:
712
- log = self.query_one("#chat-area", RichLog)
713
- log.write("[bold blue][AGENT][/bold blue]\n")
714
- log.write(self._render_markdown_to_string(message))
688
+ self._insert_rich("[bold blue][AGENT][/bold blue]\n")
689
+ self._insert_rich(message)
715
690
 
716
691
  def print_tool_call(
717
692
  self, tool_name: str,
718
693
  tool_args: dict | None = None
719
694
  ) -> None:
720
- parts = [
721
- f"[dim] [dim]⚙[/dim] "
722
- f"{rich_escape(str(tool_name))}[/dim]"
723
- ]
695
+ parts = [f"[dim] ⚙ {rich_escape(str(tool_name))}[/dim]"]
724
696
  if tool_args:
725
697
  for k, v in tool_args.items():
726
698
  val = rich_escape(str(v))
727
699
  parts.append(
728
- f" [dim]"
729
- f"{rich_escape(str(k))}=[/dim]"
730
- f"{val}"
700
+ f"[dim] {rich_escape(str(k))}=[/dim] {val}"
731
701
  )
732
- self.query_one("#chat-area", RichLog).write(
733
- " ".join(parts) + "\n"
734
- )
702
+ self._insert_rich(" ".join(parts) + "\n")
735
703
 
736
704
  def print_tool_result(
737
705
  self, tool_name, result, error: bool = False
@@ -760,33 +728,25 @@ class ChatApp(App):
760
728
  return None
761
729
 
762
730
  def print_exit(self) -> None:
763
- self.query_one("#chat-area", RichLog).write(
764
- "[dim]Goodbye![/]\n"
765
- )
731
+ self._insert_rich("[dim]Goodbye![/]\n")
766
732
 
767
733
  def print_round_complete(self) -> None:
768
- self.query_one("#chat-area", RichLog).write(
769
- "[dim]" + "─" * 60 + "[/dim]\n"
770
- )
734
+ self._insert_rich("─" * 60 + "\n")
771
735
 
772
736
  def print_interrupted(self) -> None:
773
- self.query_one("#chat-area", RichLog).write(
774
- "[bold red]"
775
- "Interrupted by user. Goodbye![/]\n"
776
- )
737
+ self._insert_rich("[bold red]Interrupted by user. Goodbye![/]\n")
777
738
 
778
739
  def start_streaming(self) -> None:
779
- log = self.query_one("#chat-area", RichLog)
780
- log.write("[dim]...[/]\n")
740
+ self._insert_rich("[dim]...[/]\n")
781
741
 
782
742
  def end_streaming(self) -> None:
783
- self.query_one("#chat-area", RichLog).write("\n")
743
+ self._insert_rich("\n")
784
744
 
785
745
  def print_prompt(self) -> None:
786
746
  pass
787
747
 
788
748
  def clear(self) -> None:
789
- self.query_one("#chat-area", RichLog).clear()
749
+ self._get_chat_area().clear()
790
750
  self._current_response.clear()
791
751
 
792
752
  def set_context_progress(
@@ -817,11 +777,40 @@ class ChatApp(App):
817
777
  def stop_input_thread(self) -> None:
818
778
  pass
819
779
 
780
+ def _rich_to_ansi(self, text: str) -> str:
781
+ """Convert Rich markup to ANSI escape codes for display.
782
+
783
+ Uses a cached Console instance to avoid per-call overhead.
784
+ """
785
+ from io import StringIO
786
+
787
+ try:
788
+ rendered = self._rich_console.render_str(text)
789
+ buf = StringIO()
790
+ self._rich_console.print(rendered, file=buf, end="")
791
+ return buf.getvalue()
792
+ except Exception:
793
+ return text
794
+
820
795
  def run_app(self) -> None:
821
796
  """Run the Textual application."""
822
797
  self.run()
823
798
 
824
799
 
800
+ # ---------------------------------------------------------------------------
801
+ # Cached Rich console singleton — created once, reused by all instances
802
+ # ---------------------------------------------------------------------------
803
+
804
+ _rich_console = Console(
805
+ force_terminal=True,
806
+ width=200,
807
+ color_system="standard",
808
+ )
809
+
810
+ # Make it accessible on ChatApp for _rich_to_ansi
811
+ ChatApp._rich_console = _rich_console # type: ignore[attr-defined]
812
+
813
+
825
814
  # ---------------------------------------------------------------------------
826
815
  # Legacy _ColorFormatter for backward compatibility
827
816
  # ---------------------------------------------------------------------------
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: regcode
3
- Version: 0.6.0
3
+ Version: 0.6.2
4
4
  Summary: Minimalistic coding agent with Python API and CLI
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: click>=8.0
@@ -12,7 +12,7 @@ Requires-Dist: pydantic-settings>=2.14.2
12
12
  Requires-Dist: pytest>=8.0
13
13
  Requires-Dist: pyyaml>=6.0
14
14
  Requires-Dist: ruff>=0.8
15
- Requires-Dist: textual>=8.2.7
15
+ Requires-Dist: textual>=2.0.0
16
16
  Provides-Extra: dev
17
17
  Description-Content-Type: text/markdown
18
18
 
@@ -1,14 +1,14 @@
1
- regcode/__init__.py,sha256=vvjAdeKm7uyNzSuJ8lZwRXTdbR4cXh82GoqzzPvUvwQ,168
1
+ regcode/__init__.py,sha256=PDGu5z3A9XTW8PUPY3nSTva-m21BiTnKIowZ0j_sdC0,168
2
2
  regcode/cli.py,sha256=jygDB1u_T5_sXm3TxvUPe9VZpC3-42A4pEMwW4PVLcc,5432
3
3
  regcode/config.py,sha256=b0_uyquT2zVUa14cMG0NE-0zJJnsP6Pc73uu3gXf84Y,6664
4
4
  regcode/conversation_manager.py,sha256=7b1hB7881ydPkCvePJIZ-HgtUi95FglJ7MyFAYw2H1o,5636
5
5
  regcode/dependencies.py,sha256=xi6_XuL4xOqk3ORIe7a92q5oHidiECeefVTjevHiIwU,2357
6
- regcode/main.py,sha256=bpHUL__qXv7cN3Kf1C-NBzo4h6Oz_bhQnLdFrRaAn5Y,42190
6
+ regcode/main.py,sha256=vNqKd_Mbhj00ybtlduBYBI4NwwwiQ90AWNsNT-sJfqM,44054
7
7
  regcode/monty_sandbox.py,sha256=UxTFmczQr5F4nlDe1gJ5Ul6GX_5KJkVYekVia-JKKvw,12930
8
8
  regcode/permissions.py,sha256=9R1dz6ypxBq_tNj-l4ixq_KJ-kAbXHarOey5EmnZzgU,784
9
9
  regcode/sandbox.py,sha256=v7oyl4PZVs4uMGGJRQC7YdQNLf7pbtRpVMXuNfnslJI,11414
10
- regcode/tracing.py,sha256=sN3hB9BJZY8CQedDkS7z4K1JAuN8-8rFTd9NnOiKN3U,3525
11
- regcode/tui.py,sha256=GFmXXTQNpJ3N6TgwQtUL-DFIQ0WlxUFwlPTNkoR4u8Q,32586
10
+ regcode/tracing.py,sha256=Uq6h_fT8k2_oVY5cJgM88IFaqN4hSjYyD7xnG1gJuy0,3568
11
+ regcode/tui.py,sha256=Zc_3JdpvtuAuvJFDVd1v7wxDIfc2sK4W3xMy8gNosxo,33036
12
12
  regcode/tools/__init__.py,sha256=VG2pxPkVSRUaLZ-tgNTcvcJSXUOuVLeCT0yx7k1g2pQ,325
13
13
  regcode/tools/base.py,sha256=D4oalxZb-cKvCMFeghyKSoLy_M7LUVPMeFTUPmrcuyY,3538
14
14
  regcode/tools/builtins.py,sha256=FhyXiyriV35HKrEsVLGa25eqBhEBHDAVpfXN08DgG3M,46330
@@ -16,7 +16,7 @@ regcode/tools/registry.py,sha256=jrGtKLlRPgfFQFGlCAaxEfoR0ZXForQUmZexM5-HnEo,263
16
16
  regcode/tools/review_notes.py,sha256=zdbKTKv2hlnfd7Q-F0xlg8n2IHt7h0KssS0OyyY2gcM,3743
17
17
  regcode/tools/web_crawl.py,sha256=7weNwU6RAJNiyc04vcPD64ODh5_oqoFlec5vyMRKh90,5594
18
18
  regcode/tools/web_search.py,sha256=g9na3CrlPvefYIIfopdMN1H3Xh5K8DonqeRuf1-ucWk,5010
19
- regcode-0.6.0.dist-info/METADATA,sha256=qoSstrrZQmXzSn5KBpJGSX_3mq0XfcbAqWbCDarIYCc,9091
20
- regcode-0.6.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
21
- regcode-0.6.0.dist-info/entry_points.txt,sha256=DzSdoP-DefYTNnnuwctq7neSx19XjfB78Jzk-c1UcsQ,44
22
- regcode-0.6.0.dist-info/RECORD,,
19
+ regcode-0.6.2.dist-info/METADATA,sha256=uSxfmg3H0vGSibzpYpHIBL_aA89EUSM9pRMDdqGlIMU,9091
20
+ regcode-0.6.2.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
21
+ regcode-0.6.2.dist-info/entry_points.txt,sha256=DzSdoP-DefYTNnnuwctq7neSx19XjfB78Jzk-c1UcsQ,44
22
+ regcode-0.6.2.dist-info/RECORD,,