regcode 0.1.0__py3-none-any.whl → 0.2.0__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/cli.py CHANGED
@@ -41,14 +41,14 @@ def cli(ctx):
41
41
  help="Disable colored output",
42
42
  )
43
43
  def chat(model, config_path, stream, full, no_color):
44
- """Chat with the agent."""
44
+ """Chat with the agent using a Textual TUI with sticky bottom bar."""
45
45
  config = regcode.Config.load(config_path)
46
46
  if model:
47
47
  config.provider.model = model
48
48
  if full:
49
49
  config.permissions.append(AgentPermission.WRITE)
50
50
 
51
- # Initialize the TUI
51
+ # Initialize the TUI (Textual-based)
52
52
  force_color = not no_color and sys.stdout.isatty()
53
53
  ui = ChatUI(force_color=force_color)
54
54
 
@@ -56,41 +56,14 @@ def chat(model, config_path, stream, full, no_color):
56
56
  config=config,
57
57
  status_callback=ui.handle_status,
58
58
  result_callback=ui.handle_result,
59
+ context_callback=ui.set_context_progress,
59
60
  )
60
61
 
61
- ui.print_welcome(regcode.__version__)
62
-
63
- while True:
64
- try:
65
- msg = ui.get_user_input()
66
- if msg is None:
67
- break # EOF
68
- if msg.lower() in ("exit", "quit", "q"):
69
- ui.print_exit()
70
- break
71
- if not msg:
72
- continue
73
-
74
- # Ensure agent response starts on a fresh line
75
- click.echo()
76
-
77
- # Always stream in TUI so text appears progressively rather than
78
- # all at once after the full tool loop completes.
79
- ui.start_streaming()
80
- agent.chat(msg, stream=True, on_text_chunk=ui.print_text_chunk)
81
- ui.end_streaming()
82
-
83
- ui.print_round_complete()
84
-
85
- except KeyboardInterrupt:
86
- ui.print_interrupted()
87
- break
88
- except EOFError:
89
- ui.print_exit()
90
- break
91
- except Exception as e:
92
- ui.formatter.error(str(e))
93
- break
62
+ # Attach agent to the Textual app
63
+ ui._app.run_agent(agent)
64
+
65
+ # Run the Textual app (replaces the old click-based chat loop)
66
+ ui.run()
94
67
 
95
68
 
96
69
  @cli.command()
regcode/main.py CHANGED
@@ -62,6 +62,7 @@ class Agent:
62
62
  | None
63
63
  )
64
64
  | None = None,
65
+ context_callback: Callable[[int, int], None] | None = None,
65
66
  ) -> None:
66
67
  self.config = config or Config.load()
67
68
  self.system_prompt = self.config.system_prompt
@@ -69,6 +70,7 @@ class Agent:
69
70
  self.registry = ToolRegistry()
70
71
  self._status_callback = status_callback
71
72
  self._result_callback = result_callback
73
+ self._context_callback = context_callback
72
74
  self._allowed_dir = directory
73
75
  self._tool_budget = self.config.tool_budget
74
76
  self._tool_budget_remaining = self._tool_budget
@@ -76,6 +78,7 @@ class Agent:
76
78
 
77
79
  # Context window manager
78
80
  self.context_manager = ConversationManager()
81
+ self._context_window = self.config.agent.context_window
79
82
 
80
83
  # Review note tools - only register when compaction is enabled
81
84
  if self.config.agent.enable_compaction:
@@ -356,7 +359,6 @@ class Agent:
356
359
  AgentStatus.TOOL_RESULT, result_str, is_error, tool_call_obj
357
360
  )
358
361
 
359
- # Deduct one tool budget from remaining budget
360
362
  self._tool_budget_remaining -= 1
361
363
  if self._tool_budget_remaining <= 0:
362
364
  self._tool_budget_exhausted = True
@@ -372,8 +374,6 @@ class Agent:
372
374
  "content": str(result),
373
375
  }
374
376
  )
375
- # Add explicit message telling the agent to stop using
376
- # tools and provide its final response
377
377
  messages.append(
378
378
  {
379
379
  "role": "user",
@@ -399,13 +399,36 @@ class Agent:
399
399
  }
400
400
  )
401
401
  total_tool_calls += 1
402
+ self._update_context_progress(messages)
402
403
  # Safety: if model keeps calling tools without text, force break
403
404
  if total_tool_calls >= 30:
404
405
  break
405
406
  continue
406
407
 
408
+ # Model returned neither text content nor tool calls — re-prompt it
409
+ # to force an answer with the current context state.
410
+ if not message_obj.content and (
411
+ not message_obj.tool_calls or len(message_obj.tool_calls) == 0
412
+ ):
413
+ if total_tool_calls >= 5:
414
+ # After 5 empty responses, just give up and return empty
415
+ break
416
+ messages.append(
417
+ {
418
+ "role": "user",
419
+ "content": (
420
+ "You returned an empty response with no tool calls. "
421
+ "Please provide your answer now based on the current "
422
+ "conversation state. Respond in plain text only."
423
+ ),
424
+ }
425
+ )
426
+ total_tool_calls += 1
427
+ continue
428
+
407
429
  break
408
430
 
431
+ self._update_context_progress(messages)
409
432
  self._history = [m for m in messages if m.get("role") != "system"]
410
433
  return final_text or response_message.get("content", "") or ""
411
434
 
@@ -681,15 +704,52 @@ class Agent:
681
704
  }
682
705
  )
683
706
  total_tool_calls += 1
707
+ self._update_context_progress(messages)
684
708
  if total_tool_calls >= 30:
685
709
  break
686
710
  continue
687
711
 
712
+ # Model returned neither text content nor tool calls — re-prompt it
713
+ # to force an answer with the current context state.
714
+ if not full_text and (
715
+ not response_message.get("tool_calls") or
716
+ len(response_message.get("tool_calls", [])) == 0
717
+ ):
718
+ if total_tool_calls >= 5:
719
+ # After 5 empty responses, just give up and return empty
720
+ break
721
+ messages.append(
722
+ {
723
+ "role": "user",
724
+ "content": (
725
+ "You returned an empty response with no tool calls. "
726
+ "Please provide your answer now based on the current "
727
+ "conversation state. Respond in plain text only."
728
+ ),
729
+ }
730
+ )
731
+ total_tool_calls += 1
732
+ continue
733
+
688
734
  break
689
735
 
736
+ self._update_context_progress(messages)
690
737
  self._history = [m for m in messages if m.get("role") != "system"]
691
738
  return final_text or response_message.get("content", "") or ""
692
739
 
740
+ def _update_context_progress(self, messages: list[dict[str, Any]]) -> None:
741
+ """Estimate token usage and report progress via context_callback."""
742
+ if self._context_callback is None:
743
+ return
744
+ try:
745
+ token_count = litellm.token_counter(
746
+ model=self.config.provider.model,
747
+ messages=messages,
748
+ )
749
+ self._context_callback(token_count, self._context_window)
750
+ except Exception:
751
+ pass
752
+
693
753
  def reset(self) -> None:
694
754
  """Clear conversation history."""
695
755
  self._history = []
regcode/tools/__init__.py CHANGED
@@ -4,6 +4,8 @@ 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.2.0"
8
+
7
9
  __all__ = [
8
10
  "BaseTool",
9
11
  "ToolParam",
regcode/tools/builtins.py CHANGED
@@ -791,10 +791,18 @@ class SearchDirTool(BaseTool):
791
791
 
792
792
 
793
793
  class RunPythonScriptTool(BaseTool):
794
- """Run a Python script from a file."""
794
+ """Execute a Python script file in an isolated Monty sandbox.
795
+
796
+ Reads the script file and executes its contents through the
797
+ Monty sandbox (Rust-based security, resource limits) rather than
798
+ spawning a subprocess on the host.
799
+ """
795
800
 
796
801
  name = "run_script"
797
- description = "Execute a Python script file in the Monty sandbox."
802
+ description = (
803
+ "Execute a Python script file in an isolated Monty sandbox "
804
+ "with Rust-based security and resource limits."
805
+ )
798
806
 
799
807
  @property
800
808
  def params(self) -> list[ToolParam]:
@@ -805,13 +813,6 @@ class RunPythonScriptTool(BaseTool):
805
813
  "Path to the Python script file",
806
814
  required=True,
807
815
  ),
808
- ToolParam(
809
- "args",
810
- "string",
811
- "Command-line arguments for the script",
812
- required=False,
813
- default="",
814
- ),
815
816
  ToolParam(
816
817
  "timeout",
817
818
  "number",
@@ -823,52 +824,52 @@ class RunPythonScriptTool(BaseTool):
823
824
 
824
825
  def execute(self, **kwargs: Any) -> ToolResult:
825
826
  script_path = kwargs.get("script_path")
826
- args = kwargs.get("args", "")
827
827
  timeout = kwargs.get("timeout", 10)
828
828
  if not script_path:
829
829
  return ToolResult(
830
830
  output="Missing required parameter: script_path",
831
831
  error=True,
832
832
  )
833
+ # Coerce timeout to float (LLM may send string like "15")
834
+ try:
835
+ timeout = float(timeout)
836
+ except (TypeError, ValueError):
837
+ timeout = 10.0
838
+
833
839
  sandbox = MontySandbox()
834
840
  try:
835
- script_full = sandbox.sandbox_root / script_path
836
- if not script_full.exists():
841
+ # Read script file from the host filesystem
842
+ p = Path(script_path)
843
+ if not p.exists():
837
844
  return ToolResult(
838
845
  output=f"Script not found: {script_path}",
839
846
  error=True,
840
847
  exit_code=1,
841
848
  )
842
- import subprocess
843
-
844
- cmd = ["python3", str(script_full)]
845
- if args:
846
- cmd.extend(args.split())
847
-
848
- result = subprocess.run(
849
- cmd,
850
- cwd=str(sandbox.sandbox_root),
851
- capture_output=True,
852
- text=True,
853
- timeout=timeout,
854
- )
849
+ try:
850
+ code = p.read_text()
851
+ except Exception as e:
852
+ return ToolResult(
853
+ output=f"Error reading script: {str(e)}",
854
+ error=True,
855
+ exit_code=1,
856
+ )
855
857
 
858
+ # Execute script contents through Monty sandbox (Rust-based isolation)
859
+ result = sandbox.run_python(code, timeout=timeout)
856
860
  return ToolResult(
857
861
  output=(
858
862
  f"stdout: {result.stdout}\n"
859
863
  f"stderr: {result.stderr}\n"
860
- f"exit_code: {result.returncode}"
864
+ f"exit_code: {result.exit_code}\n"
865
+ f"time: {result.execution_time:.2f}s\n"
866
+ f"result: {result.result}"
861
867
  ),
862
- error=result.returncode != 0,
868
+ error=not result.success,
863
869
  stdout=result.stdout,
864
870
  stderr=result.stderr,
865
- exit_code=result.returncode,
866
- )
867
- except subprocess.TimeoutExpired:
868
- return ToolResult(
869
- output="SANDBOX_ERROR: Execution timed out",
870
- error=True,
871
- exit_code=-1,
871
+ exit_code=result.exit_code,
872
+ result=result.result,
872
873
  )
873
874
  finally:
874
875
  sandbox.cleanup()
regcode/tui.py CHANGED
@@ -1,331 +1,660 @@
1
- """Terminal UI for the RegCode agent CLI.
1
+ """Terminal UI for the RegCode agent CLI using Textual.
2
2
 
3
- Provides colored, structured output for user/assistant messages,
4
- tool calls, tool results, and agent status updates.
3
+ Provides a clean, modern chat interface with:
4
+ - Formatted message history with visual separation
5
+ - Sticky bottom bar with progress and input
6
+ - Message queuing during agent execution
5
7
  """
6
8
 
7
9
  from __future__ import annotations
8
10
 
9
- import sys
10
- import textwrap
11
- from enum import Enum
12
- from typing import IO
11
+ import re
12
+ import threading
13
13
 
14
- import click
14
+ from rich.console import Group
15
+ from rich.markdown import Markdown
16
+ from rich.markup import escape
17
+ from rich.text import Text
18
+ from textual.app import App, ComposeResult
19
+ from textual.binding import Binding
20
+ from textual.containers import Container, Vertical
21
+ from textual.widgets import Input, RichLog, Static
15
22
 
23
+ from regcode import __version__
16
24
  from regcode.main import ToolCall
17
25
 
18
- # ANSI color codes
19
- _RESET = "\033[0m"
20
- _BOLD = "\033[1m"
21
- _DIM = "\033[2m"
22
- _UNDERLINE = "\033[4m"
23
-
24
- # Foreground colors
25
- _CYAN = "\033[36m"
26
- _GREEN = "\033[32m"
27
- _YELLOW = "\033[33m"
28
- _MAGENTA = "\033[35m"
29
- _BLUE = "\033[34m"
30
- _RED = "\033[31m"
31
- _GRAY = "\033[90m"
32
- _WHITE = "\033[97m"
33
- _ORANGE = "\033[38;5;208m"
34
-
35
- # Status icons (simple text alternatives for terminal compatibility)
36
- _ICONS = {
37
- "tool_call": "[TOOL]",
38
- "tool_result": "[RESULT]",
39
- "idle": "[IDLE]",
40
- "budget_exhausted": "[BUDGET]",
41
- }
42
-
43
- # Separator characters
44
- _SEP_CHAR = "─"
45
-
46
-
47
- def _wrap_text(text: str, width: int = 72, indent: str = " ") -> str:
48
- """Wrap text with an indent for readable terminal output."""
49
- wrapped = textwrap.fill(text, width=width)
50
- return "\n".join(f"{indent}{line}" for line in wrapped.splitlines())
51
26
 
27
+ class ChatApp(App):
28
+ """Textual chat app with clean modern UI."""
29
+
30
+ TITLE = f"RegCode {__version__}"
31
+ CSS = """
32
+ Screen {
33
+ background: $panel;
34
+ }
35
+
36
+ #chat-pane {
37
+ width: 100%;
38
+ height: 1fr;
39
+ padding: 0;
40
+ margin: 0;
41
+ border: round $accent;
42
+ border-title-color: $accent;
43
+ border-title-align: left;
44
+ }
45
+
46
+ #history {
47
+ width: 100%;
48
+ height: 1fr;
49
+ padding: 0 1;
50
+ margin: 0;
51
+ background: transparent;
52
+ border: none;
53
+ }
54
+
55
+ #history RichLog {
56
+ color: $text;
57
+ background: transparent;
58
+ }
59
+
60
+ #bottom-bar {
61
+ dock: bottom;
62
+ layout: horizontal;
63
+ height: 5;
64
+ padding: 0 1;
65
+ background: $surface;
66
+ border: round $accent;
67
+ }
68
+
69
+ #live-response {
70
+ display: none;
71
+ width: 100%;
72
+ padding: 0 1;
73
+ margin: 0;
74
+ background: transparent;
75
+ color: $text;
76
+ }
77
+
78
+ #progress-bar {
79
+ width: 22;
80
+ content-align: center middle;
81
+ padding: 0 1;
82
+ }
83
+
84
+ #input-box {
85
+ width: 1fr;
86
+ height: 3;
87
+ min-height: 3;
88
+ margin: 0 0 0 1;
89
+ padding: 0 2;
90
+ background: $boost;
91
+ color: $text;
92
+ border: round $warning;
93
+ }
94
+
95
+ Input:focus {
96
+ border: round $success;
97
+ text-style: bold;
98
+ }
99
+
100
+ Footer {
101
+ display: none;
102
+ }
103
+
104
+ Header {
105
+ display: none;
106
+ }
107
+ """
52
108
 
53
- class _ColorFormatter:
54
- """Format terminal output with ANSI colors."""
109
+ BINDINGS = [
110
+ Binding("q", "quit", "Quit"),
111
+ Binding("c", "clear", "Clear"),
112
+ ]
113
+
114
+ def __init__(self) -> None:
115
+ super().__init__()
116
+ self._msg_queue: list[str] = []
117
+ self._queue_lock = threading.Lock()
118
+ self._agent_running = False
119
+ self._agent = None
120
+ self._lock = threading.Lock()
121
+ self._current_response: list[str] = []
122
+ self._streaming_started = False
123
+ self._stream_pending = ""
124
+ self._stream_has_label = False
125
+ self._stream_line_open = False
126
+ self._stream_trim_leading = True
127
+ self._live_stream_text = ""
128
+
129
+ def compose(self) -> ComposeResult:
130
+ yield Vertical(
131
+ RichLog(
132
+ id="history",
133
+ markup=True,
134
+ max_lines=500,
135
+ auto_scroll=True,
136
+ wrap=True,
137
+ ),
138
+ Static(id="live-response"),
139
+ id="chat-pane",
140
+ )
141
+ yield Container(
142
+ Static(id="progress-bar", content="Ready"),
143
+ Input(id="input-box", placeholder="Type message..."),
144
+ id="bottom-bar",
145
+ )
55
146
 
56
- def __init__(self, stream: IO[str] = sys.stdout, force_color: bool = False) -> None:
57
- # Check if stdout is a TTY or force color
58
- self._stream = stream
59
- self._force_color = force_color or sys.stdout.isatty()
147
+ def on_mount(self) -> None:
148
+ """Initialize the app when mounted."""
149
+ self._update_progress(0, 0)
150
+ log = self.query_one("#history", RichLog)
151
+ log.write(f"[bold cyan]RegCode {__version__}[/]\n")
152
+ log.write("[dim]Made by Bijay Regmi in Bonn ❤️[/]\n")
153
+ log.write("[dim]Type your message and press Enter.[/]\n")
154
+ log.write("[dim]────────────────────────────────────────────────────[/]\n")
155
+ self.set_timer(0.1, self._focus_input)
156
+
157
+ def _focus_input(self) -> None:
158
+ input_widget = self.query_one("#input-box", Input)
159
+ input_widget.focus()
160
+
161
+ def on_key(self, event) -> None:
162
+ """Handle keyboard shortcuts."""
163
+ if event.key == "q":
164
+ self.exit()
165
+ elif event.key == "c":
166
+ self.query_one("#history", RichLog).clear()
167
+ self._current_response.clear()
168
+ self._stream_pending = ""
169
+ self._stream_has_label = False
170
+
171
+ # ── Agent callbacks (called from agent thread) ──────────────────
172
+
173
+ def on_status(self, status, tool_call_info=None):
174
+ """Handle agent status updates from any thread."""
175
+ self.call_from_thread(self._render_status, status, tool_call_info)
176
+
177
+ def _render_status(self, status, tool_call_info) -> None:
178
+ """Render status updates on the main thread."""
179
+ log = self.query_one("#history", RichLog)
180
+ if status.value == "tool_call" and tool_call_info is not None:
181
+ tool_name = escape(str(tool_call_info.tool_name))
182
+ parts = [f"[dim]⚙[/dim] [blue]{tool_name}[/blue]"]
183
+ if tool_call_info.tool_args:
184
+ for k, v in tool_call_info.tool_args.items():
185
+ val = escape(str(v))
186
+ parts.append(f" [dim]{escape(str(k))}=[/dim]{val}")
187
+ log.write(" ".join(parts) + "\n")
188
+ elif status.value == "budget_exhausted":
189
+ log.write("[red]Tool budget exhausted.[/]\n")
190
+ elif status.value == "idle":
191
+ log.write("[dim]Agent is idle. Waiting for your input.[/]\n")
60
192
 
61
- def colored(self, color: str, text: str) -> str:
62
- """Return colored text, or plain text if colors disabled."""
63
- if self._force_color:
64
- return f"{color}{text}{_RESET}"
65
- return text
66
-
67
- def user_message(self, message: str) -> None:
68
- """Print a user message with green highlight."""
69
- header = self.colored(_GREEN + _BOLD, "YOU")
70
- self._stream.write(f"{header} ")
71
- self._stream.write(_wrap_text(message) + "\n")
72
- self._stream.flush()
73
-
74
- def assistant_message(self, message: str) -> None:
75
- """Print an assistant message with blue/cyan highlight."""
76
- header = self.colored(_CYAN + _BOLD, "AGENT")
77
- self._stream.write(f"{header} ")
78
- if message.strip():
79
- self._stream.write(_wrap_text(message) + "\n")
193
+ def on_result(
194
+ self,
195
+ status,
196
+ result_str: str,
197
+ error: bool,
198
+ tool_call: ToolCall | None = None,
199
+ ):
200
+ """Handle tool results - silences them."""
201
+ pass
202
+
203
+ def on_context(self, current: int, total: int):
204
+ """Handle context token usage updates."""
205
+ self.call_from_thread(self._update_progress, current, total)
206
+
207
+ def _update_progress(self, current: int, total: int) -> None:
208
+ """Update the progress bar on the bottom bar."""
209
+ if total <= 0:
210
+ total = 1
211
+ pct = min(100, int(current / total * 100))
212
+ bar_width = 11
213
+ filled = max(0, min(bar_width, round(bar_width * pct / 100)))
214
+ empty = bar_width - filled
215
+ bar = "█" * filled + "░" * empty
216
+
217
+ if pct >= 90:
218
+ color = "red"
219
+ elif pct >= 70:
220
+ color = "orange"
221
+ elif pct >= 40:
222
+ color = "yellow"
80
223
  else:
81
- self._stream.write(" (empty response)\n")
82
- self._stream.flush()
83
-
84
- def streaming_indicator(self) -> None:
85
- """Print a streaming indicator with blinking cursor."""
86
- if self._force_color:
87
- self._stream.write(
88
- f"{self.colored(_CYAN + _BOLD, 'AGENT')}"
89
- f" {_DIM}[streaming]{_RESET} "
224
+ color = "green"
225
+
226
+ pct_str = f" {pct}%"
227
+ tokens_str = f" {current}/{total}"
228
+ label = f"[bold {color}]{bar}[/] {pct_str} [dim]{tokens_str}"
229
+
230
+ progress_widget = self.query_one("#progress-bar", Static)
231
+ progress_widget.update(label)
232
+
233
+ def on_text_chunk(self, chunk: str) -> None:
234
+ """Buffer chunks and update live preview during streaming."""
235
+ self._current_response.append(chunk)
236
+ self._streaming_started = True
237
+ self.call_from_thread(self._update_live_stream, chunk)
238
+
239
+ def _update_live_stream(self, chunk: str) -> None:
240
+ """Show in-progress assistant text as markdown in real time."""
241
+ if self._stream_trim_leading:
242
+ chunk = chunk.lstrip("\n")
243
+ self._stream_trim_leading = False
244
+ if not chunk:
245
+ return
246
+
247
+ self._live_stream_text += chunk
248
+ history = self.query_one("#history", RichLog)
249
+ live = self.query_one("#live-response", Static)
250
+ if not live.display:
251
+ live.display = True
252
+ history.styles.height = "auto"
253
+
254
+ md_preview = re.sub(r"\n{3,}", "\n\n", self._live_stream_text)
255
+ live.update(
256
+ Group(
257
+ Text("AGENT:", style="bold blue"),
258
+ Markdown(md_preview),
90
259
  )
260
+ )
261
+
262
+ def _render_stream_chunk(self, chunk: str) -> None:
263
+ """Render streamed chunks directly in history in readable batches."""
264
+ if not self._streaming_started:
265
+ self._streaming_started = True
266
+ self._stream_pending += chunk
267
+ self._flush_stream_buffer(flush_partial=False)
268
+
269
+ def _emit_stream_text(self, text: str) -> None:
270
+ """Emit a streamed text segment to history with AGENT label once."""
271
+ if not text:
272
+ return
273
+ if self._stream_trim_leading:
274
+ text = text.lstrip("\n")
275
+ self._stream_trim_leading = False
276
+ if not text:
277
+ return
278
+ log = self.query_one("#history", RichLog)
279
+ if not self._stream_has_label:
280
+ log.write(f"[bold blue]AGENT:[/] {escape(text)}")
281
+ self._stream_has_label = True
91
282
  else:
92
- self._stream.write("AGENT [streaming] ")
93
- self._stream.flush()
283
+ log.write(escape(text))
284
+ self._stream_line_open = not text.endswith("\n")
285
+
286
+ def _ensure_stream_line_break(self) -> None:
287
+ """Ensure subsequent non-stream lines start on a fresh line."""
288
+ if self._stream_has_label and self._stream_line_open:
289
+ self.query_one("#history", RichLog).write("\n")
290
+ self._stream_line_open = False
291
+
292
+ def _flush_stream_for_status(self) -> None:
293
+ """Flush stream text before status output with compact spacing."""
294
+ if self._stream_pending:
295
+ pending = self._stream_pending
296
+ # Avoid large visual gaps before tool/status lines.
297
+ pending = pending.rstrip("\n")
298
+ self._stream_pending = ""
299
+ if pending:
300
+ self._emit_stream_text(pending)
301
+ self._ensure_stream_line_break()
302
+
303
+ def _flush_stream_buffer(self, flush_partial: bool) -> None:
304
+ """Flush buffered stream text by line; optionally flush remainder."""
305
+ while "\n" in self._stream_pending:
306
+ line, self._stream_pending = self._stream_pending.split("\n", 1)
307
+ self._emit_stream_text(line + "\n")
308
+
309
+ if flush_partial and self._stream_pending:
310
+ self._emit_stream_text(self._stream_pending)
311
+ self._stream_pending = ""
312
+ elif len(self._stream_pending) >= 120:
313
+ # Keep streaming responsive even when the model emits long lines.
314
+ self._emit_stream_text(self._stream_pending)
315
+ self._stream_pending = ""
316
+
317
+ def on_input_submitted(self, event: Input.Submitted) -> None:
318
+ """Process input when user presses Enter."""
319
+ message = event.value.strip()
320
+ if not message:
321
+ return
322
+ input_widget = self.query_one("#input-box", Input)
323
+ input_widget.value = ""
324
+ if message.lower() in ("exit", "quit"):
325
+ self.exit()
326
+ if self._agent_running:
327
+ self._msg_queue.append(message)
328
+ self.notify(f"Message queued: {message[:60]}...")
329
+ return
330
+ self._process_message(message)
331
+
332
+ def _process_message(self, message: str) -> None:
333
+ """Process a user message by running the agent in a background thread."""
334
+ self._agent_running = True
335
+ self._streaming_started = False
336
+ self._current_response.clear()
337
+ self._stream_pending = ""
338
+ self._stream_has_label = False
339
+ self._stream_line_open = False
340
+ self._stream_trim_leading = True
341
+ self._live_stream_text = ""
342
+ history = self.query_one("#history", RichLog)
343
+ history.styles.height = "1fr"
344
+ live = self.query_one("#live-response", Static)
345
+ live.update("")
346
+ live.display = False
347
+ input_widget = self.query_one("#input-box", Input)
348
+ input_widget.placeholder = "Agent running... (Enter queues)"
349
+
350
+ log = self.query_one("#history", RichLog)
351
+ log.write(f"[bold green]YOU:[/] [bold]{escape(message)}[/]\n")
352
+
353
+ t = threading.Thread(target=self._run_agent_sync, args=(message,), daemon=True)
354
+ t.start()
355
+
356
+ def _run_agent_sync(self, message: str) -> None:
357
+ """Run agent.chat synchronously in a background thread."""
358
+ try:
359
+ with self._lock:
360
+ if self._agent:
361
+ result = self._agent.chat(
362
+ message, stream=True, on_text_chunk=self.on_text_chunk
363
+ )
364
+ # Fallback: if no chunks were emitted, render final text.
365
+ if result and not self._current_response:
366
+ self.call_from_thread(self._set_fallback_response, result)
367
+ else:
368
+ self.call_from_thread(
369
+ self._write_error_line, "Agent not initialized!"
370
+ )
371
+ except Exception:
372
+ self.call_from_thread(self._write_error_line, "Agent error!")
373
+ finally:
374
+ self.call_from_thread(self._finish_message)
375
+
376
+ def _write_error_line(self, message: str) -> None:
377
+ """Write an error line safely on the UI thread."""
378
+ history = self.query_one("#history", RichLog)
379
+ history.styles.height = "1fr"
380
+ live = self.query_one("#live-response", Static)
381
+ live.update("")
382
+ live.display = False
383
+ self.query_one("#history", RichLog).write(f"[red]{escape(message)}[/]\n")
384
+
385
+ def _set_fallback_response(self, response: str) -> None:
386
+ """Set fallback response when streaming callback emitted no chunks."""
387
+ if not self._current_response:
388
+ self._current_response.append(response)
389
+ self._streaming_started = False
390
+
391
+ def _finish_message(self) -> None:
392
+ """Called on main thread after agent finishes."""
393
+ self._agent_running = False
394
+ try:
395
+ if self._current_response:
396
+ log = self.query_one("#history", RichLog)
397
+ response = "".join(self._current_response).lstrip("\n")
398
+ # Keep markdown readable but avoid huge accidental vertical gaps.
399
+ response = re.sub(r"\n{3,}", "\n\n", response)
400
+ history = self.query_one("#history", RichLog)
401
+ history.styles.height = "1fr"
402
+ live = self.query_one("#live-response", Static)
403
+ live.update("")
404
+ live.display = False
405
+ log.write("[bold blue]AGENT:[/]")
406
+ log.write(Markdown(response))
407
+ log.write(
408
+ "[dim]────────────────────────────────────────────────────[/]\n"
409
+ )
410
+ except Exception:
411
+ self._write_error_line("Failed to render agent response.")
412
+ finally:
413
+ self._current_response.clear()
414
+ self._stream_pending = ""
415
+ self._stream_has_label = False
416
+ self._stream_line_open = False
417
+ self._stream_trim_leading = True
418
+ input_widget = self.query_one("#input-box", Input)
419
+ input_widget.placeholder = "Type message..."
420
+ input_widget.focus()
421
+ self._process_next_queued()
422
+
423
+ def _process_next_queued(self) -> None:
424
+ """Process the next message from the queue."""
425
+ with self._queue_lock:
426
+ if not self._msg_queue:
427
+ return
428
+ message = self._msg_queue.pop(0)
429
+ self._process_message(message)
430
+
431
+ def run_agent(self, agent) -> None:
432
+ """Attach the agent instance so the app can use it."""
433
+ self._agent = agent
434
+
435
+ # ── Backward-compatible API ─────────────────────────────────────
94
436
 
95
- def tool_call(self, tool_name: str, tool_args: dict | None = None) -> None:
96
- """Print a tool call notification in yellow - compact, no extra blank lines."""
97
- header = self.colored(_YELLOW + _BOLD, _ICONS["tool_call"])
98
- parts = [self.colored(_YELLOW + _BOLD, tool_name)]
99
- if tool_args:
100
- for key, value in tool_args.items():
101
- val_str = str(value)
102
- if len(val_str) > 40:
103
- val_str = val_str[:37] + "..."
104
- parts.append(f"{key}={val_str}")
105
- line = f" {header} " + " ".join(parts)
106
- self._stream.write(line + "\n")
107
- self._stream.flush()
108
-
109
- def tool_result(self, tool_name: str, result: str, error: bool = False) -> None:
110
- """Print a tool result in magenta (or red if error)."""
111
- color = _RED if error else _MAGENTA
112
- header = self.colored(color + _BOLD, _ICONS["tool_result"])
113
- self._stream.write(f"\n{header} {self.colored(color + _BOLD, tool_name)}\n")
114
- truncated = result[:500] + ("..." if len(result) > 500 else "")
115
- self._stream.write(_wrap_text(truncated) + "\n")
116
- self._stream.flush()
117
-
118
- def status_update(self, status: Enum, tool_call_info=None) -> None:
119
- """Print a status update in gray dim text at the bottom area."""
120
- color = _GRAY
121
- if status.value == "budget_exhausted":
122
- color = _RED
123
- elif status.value == "idle":
124
- color = _DIM
437
+ @property
438
+ def formatter(self) -> _ColorFormatter:
439
+ return _ColorFormatter(True)
125
440
 
126
- icon = _ICONS.get(status.value, "[STATUS]")
127
- header = self.colored(color + _BOLD, icon)
441
+ @property
442
+ def agent_running(self) -> bool:
443
+ return self._agent_running
128
444
 
129
- if status.value == "budget_exhausted":
130
- status_text = self.colored(color + _BOLD, "Tool budget exhausted.")
131
- self._stream.write(f"\n{header} {status_text}\n")
132
- elif status.value == "idle":
133
- status_text = self.colored(color, "Agent is idle. Waiting for your input.")
134
- self._stream.write(f"\n{header} {status_text}\n")
135
- else:
136
- status_text = self.colored(color, "Agent is working...")
137
- self._stream.write(f"\n{header} {status_text}\n")
138
- self._stream.flush()
139
-
140
- def separator(self) -> None:
141
- """Print a visual separator line."""
142
- if self._force_color:
143
- self._stream.write(
144
- f"{self.colored(_DIM, _SEP_CHAR * 72)}\n"
145
- )
146
- else:
147
- self._stream.write(f"{_SEP_CHAR * 72}\n")
148
- self._stream.flush()
445
+ def print_welcome(self, version: str = "0.1.0") -> None:
446
+ log = self.query_one("#history", RichLog)
447
+ log.write(f"[bold cyan]RegCode - Minimalistic Coding Agent v{version}[/]\n")
448
+ log.write("[dim]Type your message and press Enter.[/]\n")
149
449
 
150
- def heading(self, text: str) -> None:
151
- """Print a bold heading."""
152
- self._stream.write(f"{_BOLD}{text}{_RESET}\n")
153
- self._stream.flush()
450
+ def print_separator(self) -> None:
451
+ self.query_one("#history", RichLog).write(
452
+ "[dim]────────────────────────────────────────────────────[/]\n"
453
+ )
154
454
 
155
- def info(self, text: str) -> None:
156
- """Print an informational message in gray."""
157
- self._stream.write(f"{self.colored(_GRAY, text)}\n")
158
- self._stream.flush()
455
+ def print_user_message(self, message: str) -> None:
456
+ self.query_one("#history", RichLog).write(
457
+ f"[bold green]YOU:[/] [bold]{escape(message)}[/]\n"
458
+ )
159
459
 
160
- def flush(self) -> None:
161
- """Flush the underlying output stream."""
162
- self._stream.flush()
460
+ def print_assistant_message(self, message: str) -> None:
461
+ log = self.query_one("#history", RichLog)
462
+ log.write("[bold blue]AGENT:[/]")
463
+ log.write(Markdown(message))
163
464
 
164
- def error(self, text: str) -> None:
165
- """Print an error message in red."""
166
- prefix = "ERROR: " + text
167
- self._stream.write(f"{self.colored(_RED + _BOLD, prefix)}\n")
168
- self._stream.flush()
465
+ def print_tool_call(self, tool_name: str, tool_args: dict | None = None) -> None:
466
+ parts = [f"[dim]⚙[/dim] [blue]{escape(str(tool_name))}[/blue]"]
467
+ if tool_args:
468
+ for k, v in tool_args.items():
469
+ val = escape(str(v))
470
+ parts.append(f" [dim]{escape(str(k))}=[/dim]{val}")
471
+ self.query_one("#history", RichLog).write(" ".join(parts) + "\n")
169
472
 
170
- def success(self, text: str) -> None:
171
- """Print a success message in green."""
172
- self._stream.write(f"{self.colored(_GREEN, text)}\n")
173
- self._stream.flush()
473
+ def print_tool_result(self, tool_name, result, error=False):
474
+ pass
174
475
 
476
+ def handle_status(self, status, tool_call_info=None):
477
+ self.on_status(status, tool_call_info)
175
478
 
176
- class ChatUI:
177
- """Terminal UI for the RegCode chat session.
479
+ def handle_result(self, status, result_str, error, tool_call=None):
480
+ self.on_result(status, result_str, error, tool_call)
178
481
 
179
- Provides colored, structured output for user/assistant messages,
180
- tool calls, tool results, and agent status updates.
482
+ def print_text_chunk(self, chunk: str) -> None:
483
+ self.on_text_chunk(chunk)
484
+
485
+ def get_user_input(self) -> str | None:
486
+ return None
181
487
 
182
- Usage:
183
- ui = ChatUI()
184
- ui.print_heading("RegCode v0.1.0")
185
- ui.print_separator()
488
+ def print_exit(self) -> None:
489
+ self.query_one("#history", RichLog).write("[dim]Goodbye![/]\n")
186
490
 
187
- # Set up status callback on the agent
188
- agent = Agent(
189
- config=config,
190
- status_callback=ui.handle_status,
491
+ def print_round_complete(self) -> None:
492
+ self.query_one("#history", RichLog).write(
493
+ "[dim]────────────────────────────────────────────────────[/]\n"
191
494
  )
192
495
 
193
- # Chat loop
194
- ui.print_user_message("Hello")
195
- reply = agent.chat("Hello")
196
- ui.print_assistant_message(reply)
197
- """
496
+ def print_interrupted(self) -> None:
497
+ self.query_one("#history", RichLog).write(
498
+ "[bold red]Interrupted by user. Goodbye![/]\n"
499
+ )
500
+
501
+ def start_streaming(self) -> None:
502
+ log = self.query_one("#history", RichLog)
503
+ log.write("[dim]...[/]\n")
504
+
505
+ def end_streaming(self) -> None:
506
+ self.query_one("#history", RichLog).write("\n")
198
507
 
199
- def __init__(self, stream: IO[str] = sys.stdout, force_color: bool = False) -> None:
200
- self._formatter = _ColorFormatter(stream, force_color)
508
+ def print_prompt(self) -> None:
509
+ pass
510
+
511
+ def clear(self) -> None:
512
+ self.query_one("#history", RichLog).clear()
513
+ self._current_response.clear()
514
+
515
+ def set_context_progress(self, current: int, total: int) -> None:
516
+ self.on_context(current, total)
517
+
518
+ def queue_message(self, message: str) -> None:
519
+ with self._queue_lock:
520
+ self._msg_queue.append(message)
521
+
522
+ def get_queued_messages(self) -> list[str]:
523
+ with self._queue_lock:
524
+ messages = list(self._msg_queue)
525
+ self._msg_queue.clear()
526
+ return messages
527
+
528
+ def is_queue_empty(self) -> bool:
529
+ with self._queue_lock:
530
+ return len(self._msg_queue) == 0
531
+
532
+ def set_agent_running(self, running: bool) -> None:
533
+ self._agent_running = running
534
+
535
+ def start_input_thread(self) -> None:
536
+ pass
537
+
538
+ def stop_input_thread(self) -> None:
539
+ pass
540
+
541
+ def run_app(self) -> None:
542
+ """Run the Textual application."""
543
+ self.run()
544
+
545
+
546
+ class _ColorFormatter:
547
+ """Color formatter for CLI output, with Textual integration."""
548
+
549
+ def __init__(self, stream=None, force_color: bool = True, textui=None) -> None:
201
550
  self._stream = stream
202
- self._streaming = False
551
+ self._force_color = force_color
552
+ self._textui = textui
553
+
554
+ def colored(self, color: str, text: str) -> str:
555
+ return f"{color}{text}\033[0m" if self._force_color else text
556
+
557
+ def context_progress(self, current: int, total: int) -> None:
558
+ if self._stream is not None:
559
+ pct = min(100, int(current / max(total, 1) * 100))
560
+ bar_width = 11
561
+ filled = max(0, min(bar_width, round(bar_width * pct / 100)))
562
+ empty = bar_width - filled
563
+ bar = self.colored("green", "━" * filled) + self.colored("dim", "━" * empty)
564
+ pct_str = self.colored("yellow", f" {pct}%")
565
+ tokens_str = self.colored("dim", f" {current}/{total} tokens")
566
+ self._stream.write(f"{bar}{pct_str}{tokens_str}\n")
567
+
568
+ if self._textui is not None:
569
+ self._textui.set_context_progress(current, total)
570
+
571
+
572
+ class ChatUI:
573
+ """Facade that bridges the old CLI code to the new Textual implementation."""
574
+
575
+ def __init__(self, force_color: bool = True) -> None:
576
+ self._app = ChatApp()
203
577
 
204
578
  @property
205
579
  def formatter(self) -> _ColorFormatter:
206
- """Return the underlying formatter."""
207
- return self._formatter
580
+ return self._app.formatter
208
581
 
209
- # -- Public API --
582
+ @property
583
+ def agent_running(self) -> bool:
584
+ return self._app.agent_running
210
585
 
211
586
  def print_welcome(self, version: str = "0.1.0") -> None:
212
- """Print the welcome/banner message."""
213
- self._formatter.heading(f"RegCode - Minimalistic Coding Agent v{version}")
214
- self._formatter.info(
215
- "Type your message and press Enter. Agent will respond with full "
216
- "tool access."
217
- )
218
- self._formatter.info("Press Ctrl+C to exit.")
219
- self._stream.write("\n")
220
- self._formatter.flush()
587
+ self._app.print_welcome(version)
221
588
 
222
589
  def print_separator(self) -> None:
223
- """Print a visual separator between message rounds."""
224
- self._formatter.separator()
590
+ self._app.print_separator()
225
591
 
226
592
  def print_user_message(self, message: str) -> None:
227
- """Print a formatted user message."""
228
- self._formatter.user_message(message)
593
+ self._app.print_user_message(message)
229
594
 
230
595
  def print_assistant_message(self, message: str) -> None:
231
- """Print a formatted assistant message."""
232
- self._formatter.assistant_message(message)
596
+ self._app.print_assistant_message(message)
233
597
 
234
598
  def print_tool_call(self, tool_name: str, tool_args: dict | None = None) -> None:
235
- """Print a tool call notification."""
236
- self._formatter.tool_call(tool_name, tool_args)
237
-
238
- def print_tool_result(
239
- self, tool_name: str, result: str, error: bool = False
240
- ) -> None:
241
- """Print a tool result."""
242
- self._formatter.tool_result(tool_name, result, error)
599
+ self._app.print_tool_call(tool_name, tool_args)
243
600
 
244
- def handle_status(self, status, tool_call_info=None) -> None:
245
- """Status callback handler for the Agent.
601
+ def print_tool_result(self, tool_name, result, error=False):
602
+ self._app.print_tool_result(tool_name, result, error)
246
603
 
247
- This method is called by the agent's status_callback mechanism
248
- to report what the agent is currently doing.
249
- """
250
- if status.value == "tool_call" and tool_call_info is not None:
251
- self.print_tool_call(
252
- tool_call_info.tool_name,
253
- tool_call_info.tool_args,
254
- )
255
- elif status.value == "tool_result" and tool_call_info is not None:
256
- # Skip - tool results are printed via result_callback
257
- pass
258
- elif status.value == "budget_exhausted":
259
- self._formatter.status_update(status)
260
- elif status.value == "idle":
261
- self._formatter.status_update(status)
604
+ def handle_status(self, status, tool_call_info=None):
605
+ self._app.handle_status(status, tool_call_info)
262
606
 
263
- def handle_result(
264
- self,
265
- status,
266
- result_str: str,
267
- error: bool,
268
- tool_call: ToolCall | None = None,
269
- ) -> None:
270
- """Handle tool result callback.
271
- Tool results are NOT displayed - only tool calls are shown via status_callback.
272
- """
273
- pass # Silence tool results - user only needs to see tool being called
607
+ def handle_result(self, status, result_str, error, tool_call=None):
608
+ self._app.handle_result(status, result_str, error, tool_call)
274
609
 
275
610
  def print_text_chunk(self, chunk: str) -> None:
276
- """Print a text chunk from streaming output."""
277
- sys.stdout.write(chunk)
278
- sys.stdout.flush()
611
+ self._app.print_text_chunk(chunk)
279
612
 
280
613
  def get_user_input(self) -> str | None:
281
- """Get user input with visible prompt. click.prompt echoes the input."""
282
- try:
283
- msg = click.prompt("YOU", prompt_suffix="> ", show_default=False)
284
- except EOFError:
285
- return None
286
- return msg
614
+ return None
287
615
 
288
616
  def print_exit(self) -> None:
289
- """Print exit message."""
290
- self._formatter.info("Goodbye!")
617
+ self._app.print_exit()
291
618
 
292
619
  def print_round_complete(self) -> None:
293
- """Print a subtle separator after a complete round."""
294
- if self._formatter._force_color:
295
- self._stream.write(f"{self._formatter.colored(_DIM, '─' * 72)}\n")
296
- else:
297
- self._stream.write(f"{'─' * 72}\n")
298
- self._stream.flush()
620
+ self._app.print_round_complete()
299
621
 
300
622
  def print_interrupted(self) -> None:
301
- """Print interrupted message."""
302
- self._formatter.info("Interrupted by user. Goodbye!")
623
+ self._app.print_interrupted()
303
624
 
304
625
  def start_streaming(self) -> None:
305
- """Print a streaming indicator before streaming output."""
306
- self._streaming = True
307
- self._formatter.streaming_indicator()
308
- self._stream.flush()
626
+ self._app.start_streaming()
309
627
 
310
628
  def end_streaming(self) -> None:
311
- """Finalize streaming output with a newline."""
312
- self._streaming = False
313
- self._stream.write("\n")
314
- self._stream.flush()
629
+ self._app.end_streaming()
315
630
 
316
631
  def print_prompt(self) -> None:
317
- """Print the user input prompt."""
318
- if self._formatter._force_color:
319
- you_label = self._formatter.colored(_GREEN + _BOLD, "YOU")
320
- self._stream.write(f" {you_label} ")
321
- else:
322
- self._stream.write(" YOU ")
323
- self._stream.flush()
632
+ self._app.print_prompt()
324
633
 
325
634
  def clear(self) -> None:
326
- """Clear the terminal screen (optional, best-effort)."""
327
- try:
328
- self._stream.write("\033[2J\033[H")
329
- self._stream.flush()
330
- except (IOError, ValueError):
331
- pass
635
+ self._app.clear()
636
+
637
+ def set_context_progress(self, current: int, total: int) -> None:
638
+ self._app.set_context_progress(current, total)
639
+
640
+ def queue_message(self, message: str) -> None:
641
+ self._app.queue_message(message)
642
+
643
+ def get_queued_messages(self) -> list[str]:
644
+ return self._app.get_queued_messages()
645
+
646
+ def is_queue_empty(self) -> bool:
647
+ return self._app.is_queue_empty()
648
+
649
+ def set_agent_running(self, running: bool) -> None:
650
+ self._app.set_agent_running(running)
651
+
652
+ def start_input_thread(self) -> None:
653
+ self._app.start_input_thread()
654
+
655
+ def stop_input_thread(self) -> None:
656
+ self._app.stop_input_thread()
657
+
658
+ def run(self) -> None:
659
+ """Run the Textual app."""
660
+ self._app.run()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: regcode
3
- Version: 0.1.0
3
+ Version: 0.2.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
@@ -10,6 +10,7 @@ Requires-Dist: pydantic-settings>=2.14.2
10
10
  Requires-Dist: pytest>=8.0
11
11
  Requires-Dist: pyyaml>=6.0
12
12
  Requires-Dist: ruff>=0.8
13
+ Requires-Dist: textual>=8.2.7
13
14
  Provides-Extra: dev
14
15
  Description-Content-Type: text/markdown
15
16
 
@@ -1,18 +1,18 @@
1
1
  regcode/__init__.py,sha256=2Cv_d6ciJzl49HiCT7_xpCw0efN5gtoYM7_MPnYZRE8,168
2
- regcode/cli.py,sha256=xHt4m8e9p4bKfMDMAzJLf_q4JfkeGhi8kTXmCnbPg18,5354
2
+ regcode/cli.py,sha256=065RqyRFJSbiqpFae1fvy84ngto6KZw4tVksnypg6FA,4637
3
3
  regcode/config.py,sha256=c640TNa4-6AU0AK1iCPc9ESNacrTT5MG1w_zYwdzLDY,5583
4
4
  regcode/conversation_manager.py,sha256=7b1hB7881ydPkCvePJIZ-HgtUi95FglJ7MyFAYw2H1o,5636
5
- regcode/main.py,sha256=lE_IcHFnWF_8m8RvMB4YGbJ85G-Wk6hLeera0jaxiMs,37493
5
+ regcode/main.py,sha256=6MOYRZNnFuqE85TmxeqfwJgFweU9ISD3ZVLzYTdDPXU,40084
6
6
  regcode/monty_sandbox.py,sha256=UxTFmczQr5F4nlDe1gJ5Ul6GX_5KJkVYekVia-JKKvw,12930
7
7
  regcode/permissions.py,sha256=9R1dz6ypxBq_tNj-l4ixq_KJ-kAbXHarOey5EmnZzgU,784
8
8
  regcode/sandbox.py,sha256=v7oyl4PZVs4uMGGJRQC7YdQNLf7pbtRpVMXuNfnslJI,11414
9
- regcode/tui.py,sha256=EsH5OGoTu6R2yS1Hf9W5UOYW10Bg_oXClEzb2fhsN6o,11464
10
- regcode/tools/__init__.py,sha256=VG2pxPkVSRUaLZ-tgNTcvcJSXUOuVLeCT0yx7k1g2pQ,325
9
+ regcode/tui.py,sha256=RHVnmsaE7f32mvDmbKAo7YbBVBWRZ6Zm0ENihc0Pcgk,22949
10
+ regcode/tools/__init__.py,sha256=GQmpdlMT_xw3sKkK5hNKfB2IBQLUNAurV4HN6QWJRT0,348
11
11
  regcode/tools/base.py,sha256=D4oalxZb-cKvCMFeghyKSoLy_M7LUVPMeFTUPmrcuyY,3538
12
- regcode/tools/builtins.py,sha256=0f6ArIeUysL-8fwdtbNkKHlmebeC5OxYTtKRn-yuG64,30684
12
+ regcode/tools/builtins.py,sha256=-Q4OIW6WpnbdT6CxTi0rn98xxfKEZK3wsP4lzUgTASY,30920
13
13
  regcode/tools/registry.py,sha256=jrGtKLlRPgfFQFGlCAaxEfoR0ZXForQUmZexM5-HnEo,2638
14
14
  regcode/tools/review_notes.py,sha256=zdbKTKv2hlnfd7Q-F0xlg8n2IHt7h0KssS0OyyY2gcM,3743
15
- regcode-0.1.0.dist-info/METADATA,sha256=v8s5W2dnPIzYLbcuJUHyPg0e3GBI-qDad_EbU1kMofM,5039
16
- regcode-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
17
- regcode-0.1.0.dist-info/entry_points.txt,sha256=DzSdoP-DefYTNnnuwctq7neSx19XjfB78Jzk-c1UcsQ,44
18
- regcode-0.1.0.dist-info/RECORD,,
15
+ regcode-0.2.0.dist-info/METADATA,sha256=HCCJK8N1Bgrzp_biDrh2vGO4jus_tlmSdBq9R63LsLc,5069
16
+ regcode-0.2.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
17
+ regcode-0.2.0.dist-info/entry_points.txt,sha256=DzSdoP-DefYTNnnuwctq7neSx19XjfB78Jzk-c1UcsQ,44
18
+ regcode-0.2.0.dist-info/RECORD,,