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 +1 -1
- regcode/main.py +52 -2
- regcode/tracing.py +17 -7
- regcode/tui.py +273 -284
- {regcode-0.6.0.dist-info → regcode-0.6.2.dist-info}/METADATA +2 -2
- {regcode-0.6.0.dist-info → regcode-0.6.2.dist-info}/RECORD +8 -8
- {regcode-0.6.0.dist-info → regcode-0.6.2.dist-info}/WHEEL +0 -0
- {regcode-0.6.0.dist-info → regcode-0.6.2.dist-info}/entry_points.txt +0 -0
regcode/__init__.py
CHANGED
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
|
|
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
|
-
|
|
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
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
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()
|
|
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:
|
|
5
|
-
Zone 2: Chat
|
|
6
|
-
Zone 3:
|
|
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:
|
|
42
|
+
background: #1e1e2e;
|
|
55
43
|
}
|
|
56
44
|
|
|
57
|
-
/* ── Zone 1:
|
|
45
|
+
/* ── Zone 1: Status bar ───────────────────────────────────── */
|
|
58
46
|
#top-bar {
|
|
59
47
|
dock: top;
|
|
60
48
|
height: 1;
|
|
61
|
-
padding: 0
|
|
62
|
-
background:
|
|
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
|
|
55
|
+
/* ── Zone 2: Chat history ──────────────────────────────────── */
|
|
67
56
|
#chat-area {
|
|
68
57
|
width: 100%;
|
|
69
58
|
height: 1fr;
|
|
70
|
-
|
|
71
|
-
margin: 0;
|
|
72
|
-
border: none;
|
|
59
|
+
margin: 0 2;
|
|
73
60
|
overflow-y: auto;
|
|
74
|
-
background:
|
|
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
|
|
78
|
-
|
|
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:
|
|
89
|
-
border: none;
|
|
78
|
+
background: #313244;
|
|
90
79
|
}
|
|
91
80
|
|
|
92
|
-
#progress-
|
|
81
|
+
#progress-label {
|
|
93
82
|
dock: left;
|
|
94
83
|
width: 30;
|
|
95
84
|
content-align: center middle;
|
|
96
|
-
|
|
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:
|
|
105
|
-
color:
|
|
106
|
-
border: round
|
|
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
|
|
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 =
|
|
134
|
+
self._context_window: int = 0
|
|
152
135
|
self._pending_tool_line: str = ""
|
|
153
136
|
|
|
154
|
-
# ──
|
|
137
|
+
# ── Helpers ───────────────────────────────────────────────────
|
|
155
138
|
|
|
156
|
-
def
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
#
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
200
|
-
"[dim]
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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.
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
321
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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]
|
|
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"
|
|
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
|
-
|
|
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
|
-
|
|
431
|
-
|
|
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
|
-
|
|
437
|
-
"
|
|
438
|
-
"
|
|
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
|
-
|
|
443
|
-
"
|
|
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
|
-
|
|
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
|
|
490
|
-
if self.
|
|
491
|
-
|
|
492
|
-
self.
|
|
493
|
-
if
|
|
494
|
-
self.
|
|
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
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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
|
-
|
|
570
|
+
tokens_str = str(current)
|
|
557
571
|
|
|
558
|
-
|
|
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
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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
|
-
|
|
607
|
-
|
|
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 =
|
|
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
|
-
|
|
651
|
-
|
|
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
|
|
646
|
+
"""Reset streaming-related state for a new turn."""
|
|
674
647
|
self._streaming = False
|
|
675
|
-
self.
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
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
|
-
|
|
694
|
-
|
|
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
|
-
|
|
677
|
+
self._insert_rich("[dim]Type your message and press Enter.[/]\n")
|
|
699
678
|
|
|
700
679
|
def print_separator(self) -> None:
|
|
701
|
-
self.
|
|
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.
|
|
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
|
-
|
|
713
|
-
|
|
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.
|
|
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.
|
|
764
|
-
"[dim]Goodbye![/]\n"
|
|
765
|
-
)
|
|
731
|
+
self._insert_rich("[dim]Goodbye![/]\n")
|
|
766
732
|
|
|
767
733
|
def print_round_complete(self) -> None:
|
|
768
|
-
self.
|
|
769
|
-
"[dim]" + "─" * 60 + "[/dim]\n"
|
|
770
|
-
)
|
|
734
|
+
self._insert_rich("─" * 60 + "\n")
|
|
771
735
|
|
|
772
736
|
def print_interrupted(self) -> None:
|
|
773
|
-
self.
|
|
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
|
-
|
|
780
|
-
log.write("[dim]...[/]\n")
|
|
740
|
+
self._insert_rich("[dim]...[/]\n")
|
|
781
741
|
|
|
782
742
|
def end_streaming(self) -> None:
|
|
783
|
-
self.
|
|
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.
|
|
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.
|
|
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>=
|
|
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=
|
|
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=
|
|
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=
|
|
11
|
-
regcode/tui.py,sha256=
|
|
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.
|
|
20
|
-
regcode-0.6.
|
|
21
|
-
regcode-0.6.
|
|
22
|
-
regcode-0.6.
|
|
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,,
|
|
File without changes
|
|
File without changes
|