recursive-cleaner 0.7.1__py3-none-any.whl → 1.0.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.
- backends/__init__.py +2 -1
- backends/openai_backend.py +71 -0
- recursive_cleaner/__init__.py +5 -0
- recursive_cleaner/__main__.py +8 -0
- recursive_cleaner/apply.py +483 -0
- recursive_cleaner/cleaner.py +122 -29
- recursive_cleaner/cli.py +395 -0
- recursive_cleaner/tui.py +614 -0
- {recursive_cleaner-0.7.1.dist-info → recursive_cleaner-1.0.0.dist-info}/METADATA +119 -4
- {recursive_cleaner-0.7.1.dist-info → recursive_cleaner-1.0.0.dist-info}/RECORD +13 -7
- recursive_cleaner-1.0.0.dist-info/entry_points.txt +2 -0
- {recursive_cleaner-0.7.1.dist-info → recursive_cleaner-1.0.0.dist-info}/WHEEL +0 -0
- {recursive_cleaner-0.7.1.dist-info → recursive_cleaner-1.0.0.dist-info}/licenses/LICENSE +0 -0
recursive_cleaner/tui.py
ADDED
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
"""Rich TUI dashboard with Mission Control retro aesthetic."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
# Graceful import - TUI features only available when Rich is installed
|
|
8
|
+
try:
|
|
9
|
+
from rich.box import DOUBLE
|
|
10
|
+
from rich.console import Console, Group
|
|
11
|
+
from rich.layout import Layout
|
|
12
|
+
from rich.live import Live
|
|
13
|
+
from rich.panel import Panel
|
|
14
|
+
from rich.progress import BarColumn, Progress, TextColumn
|
|
15
|
+
from rich.table import Table
|
|
16
|
+
from rich.text import Text
|
|
17
|
+
|
|
18
|
+
HAS_RICH = True
|
|
19
|
+
except ImportError:
|
|
20
|
+
HAS_RICH = False
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# ASCII art banner - chunky block style
|
|
24
|
+
ASCII_BANNER = """
|
|
25
|
+
██████╗ ███████╗ ██████╗██╗ ██╗██████╗ ███████╗██╗██╗ ██╗███████╗
|
|
26
|
+
██╔══██╗██╔════╝██╔════╝██║ ██║██╔══██╗██╔════╝██║██║ ██║██╔════╝
|
|
27
|
+
██████╔╝█████╗ ██║ ██║ ██║██████╔╝███████╗██║██║ ██║█████╗
|
|
28
|
+
██╔══██╗██╔══╝ ██║ ██║ ██║██╔══██╗╚════██║██║╚██╗ ██╔╝██╔══╝
|
|
29
|
+
██║ ██║███████╗╚██████╗╚██████╔╝██║ ██║███████║██║ ╚████╔╝ ███████╗
|
|
30
|
+
╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═══╝ ╚══════╝
|
|
31
|
+
██████╗██╗ ███████╗ █████╗ ███╗ ██╗███████╗██████╗
|
|
32
|
+
██╔════╝██║ ██╔════╝██╔══██╗████╗ ██║██╔════╝██╔══██╗
|
|
33
|
+
██║ ██║ █████╗ ███████║██╔██╗ ██║█████╗ ██████╔╝
|
|
34
|
+
██║ ██║ ██╔══╝ ██╔══██║██║╚██╗██║██╔══╝ ██╔══██╗
|
|
35
|
+
╚██████╗███████╗███████╗██║ ██║██║ ╚████║███████╗██║ ██║
|
|
36
|
+
╚═════╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚═╝ ╚═╝
|
|
37
|
+
""".strip()
|
|
38
|
+
|
|
39
|
+
# Keep HEADER_TITLE for backwards compatibility with tests
|
|
40
|
+
HEADER_TITLE = "RECURSIVE CLEANER"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class FunctionInfo:
|
|
45
|
+
"""Info about a generated cleaning function."""
|
|
46
|
+
|
|
47
|
+
name: str
|
|
48
|
+
docstring: str
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class TUIState:
|
|
53
|
+
"""Dashboard display state."""
|
|
54
|
+
|
|
55
|
+
# Header
|
|
56
|
+
file_path: str
|
|
57
|
+
total_records: int
|
|
58
|
+
version: str = "0.8.0"
|
|
59
|
+
|
|
60
|
+
# Progress
|
|
61
|
+
current_chunk: int = 0
|
|
62
|
+
total_chunks: int = 0
|
|
63
|
+
current_iteration: int = 0
|
|
64
|
+
max_iterations: int = 5
|
|
65
|
+
|
|
66
|
+
# LLM Status
|
|
67
|
+
llm_status: Literal["idle", "calling"] = "idle"
|
|
68
|
+
|
|
69
|
+
# Functions
|
|
70
|
+
functions: list[FunctionInfo] = field(default_factory=list)
|
|
71
|
+
|
|
72
|
+
# Latency metrics
|
|
73
|
+
latency_last_ms: float = 0.0
|
|
74
|
+
latency_avg_ms: float = 0.0
|
|
75
|
+
latency_total_ms: float = 0.0
|
|
76
|
+
llm_call_count: int = 0
|
|
77
|
+
|
|
78
|
+
# Token estimation
|
|
79
|
+
tokens_in: int = 0
|
|
80
|
+
tokens_out: int = 0
|
|
81
|
+
|
|
82
|
+
# Transmission log
|
|
83
|
+
last_response: str = ""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class TUIRenderer:
|
|
87
|
+
"""
|
|
88
|
+
Rich-based terminal dashboard with Mission Control retro aesthetic.
|
|
89
|
+
|
|
90
|
+
Shows live updates during cleaning runs with:
|
|
91
|
+
- ASCII art banner header
|
|
92
|
+
- Mission timer and status indicator
|
|
93
|
+
- Progress bar and chunk/iteration counters
|
|
94
|
+
- List of generated functions with checkmarks
|
|
95
|
+
- Token estimation and latency metrics
|
|
96
|
+
- Transmission log showing latest LLM response
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
def __init__(self, file_path: str, total_chunks: int, total_records: int = 0):
|
|
100
|
+
"""
|
|
101
|
+
Initialize TUI renderer.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
file_path: Path to the data file being cleaned
|
|
105
|
+
total_chunks: Total number of chunks to process
|
|
106
|
+
total_records: Total number of records in the file
|
|
107
|
+
"""
|
|
108
|
+
self._state = TUIState(
|
|
109
|
+
file_path=file_path,
|
|
110
|
+
total_chunks=total_chunks,
|
|
111
|
+
total_records=total_records,
|
|
112
|
+
)
|
|
113
|
+
self._start_time = time.time()
|
|
114
|
+
self._layout = self._make_layout() if HAS_RICH else None
|
|
115
|
+
self._live: "Live | None" = None
|
|
116
|
+
self._console = Console() if HAS_RICH else None
|
|
117
|
+
|
|
118
|
+
def _make_layout(self) -> "Layout":
|
|
119
|
+
"""Create the dashboard layout structure.
|
|
120
|
+
|
|
121
|
+
Layout:
|
|
122
|
+
- header (size=5) - ASCII art banner "RECURSIVE CLEANER"
|
|
123
|
+
- status_bar (size=3) - MISSION | TIME | STATUS
|
|
124
|
+
- progress_bar (size=3) - CHUNK X/Y + progress bar
|
|
125
|
+
- body (size=computed) - Split horizontally, FIXED size to prevent infinite expansion
|
|
126
|
+
- left_panel - FUNCTIONS ACQUIRED, tokens, latency
|
|
127
|
+
- right_panel - Parsed transmission log
|
|
128
|
+
|
|
129
|
+
CRITICAL: Body uses fixed `size=` not `ratio=` to prevent panels from
|
|
130
|
+
expanding infinitely and pushing header off screen on large terminals.
|
|
131
|
+
Works on terminals as small as 80x24.
|
|
132
|
+
"""
|
|
133
|
+
if not HAS_RICH:
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
from rich.console import Console
|
|
137
|
+
|
|
138
|
+
console = Console()
|
|
139
|
+
term_height = console.height or 24 # Default to 24 if unknown
|
|
140
|
+
|
|
141
|
+
# Fixed heights for top sections
|
|
142
|
+
header_height = 14 # ASCII banner (12 lines + border)
|
|
143
|
+
status_height = 3
|
|
144
|
+
progress_height = 3
|
|
145
|
+
fixed_total = header_height + status_height + progress_height
|
|
146
|
+
|
|
147
|
+
# Body gets remaining space with a FIXED size (not ratio)
|
|
148
|
+
# Cap at 18 rows max to keep it tight
|
|
149
|
+
body_height = min(18, max(10, term_height - fixed_total - 2))
|
|
150
|
+
|
|
151
|
+
layout = Layout()
|
|
152
|
+
layout.split_column(
|
|
153
|
+
Layout(name="header", size=header_height),
|
|
154
|
+
Layout(name="status_bar", size=status_height),
|
|
155
|
+
Layout(name="progress_bar", size=progress_height),
|
|
156
|
+
Layout(name="body", size=body_height), # FIXED size, not ratio
|
|
157
|
+
)
|
|
158
|
+
layout["body"].split_row(
|
|
159
|
+
Layout(name="left_panel", ratio=1),
|
|
160
|
+
Layout(name="right_panel", ratio=1),
|
|
161
|
+
)
|
|
162
|
+
return layout
|
|
163
|
+
|
|
164
|
+
def start(self) -> None:
|
|
165
|
+
"""Start the live TUI display."""
|
|
166
|
+
if not HAS_RICH or self._layout is None:
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
self._start_time = time.time()
|
|
170
|
+
self._refresh()
|
|
171
|
+
self._live = Live(
|
|
172
|
+
self._layout,
|
|
173
|
+
console=self._console,
|
|
174
|
+
refresh_per_second=2,
|
|
175
|
+
vertical_overflow="crop",
|
|
176
|
+
)
|
|
177
|
+
self._live.start()
|
|
178
|
+
|
|
179
|
+
def stop(self) -> None:
|
|
180
|
+
"""Stop the live TUI display."""
|
|
181
|
+
if self._live:
|
|
182
|
+
self._live.stop()
|
|
183
|
+
self._live = None
|
|
184
|
+
|
|
185
|
+
def update_chunk(self, chunk_index: int, iteration: int, max_iterations: int) -> None:
|
|
186
|
+
"""
|
|
187
|
+
Update progress for current chunk and iteration.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
chunk_index: Current chunk index (0-based)
|
|
191
|
+
iteration: Current iteration within chunk (0-based)
|
|
192
|
+
max_iterations: Maximum iterations per chunk
|
|
193
|
+
"""
|
|
194
|
+
self._state.current_chunk = chunk_index + 1 # Convert to 1-based for display
|
|
195
|
+
self._state.current_iteration = iteration + 1
|
|
196
|
+
self._state.max_iterations = max_iterations
|
|
197
|
+
self._refresh()
|
|
198
|
+
|
|
199
|
+
def update_llm_status(self, status: Literal["calling", "idle"]) -> None:
|
|
200
|
+
"""
|
|
201
|
+
Update LLM call status.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
status: "calling" when LLM is being called, "idle" otherwise
|
|
205
|
+
"""
|
|
206
|
+
self._state.llm_status = status
|
|
207
|
+
self._refresh()
|
|
208
|
+
|
|
209
|
+
def add_function(self, name: str, docstring: str) -> None:
|
|
210
|
+
"""
|
|
211
|
+
Add a newly generated function to the display.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
name: Function name
|
|
215
|
+
docstring: Function docstring
|
|
216
|
+
"""
|
|
217
|
+
self._state.functions.append(FunctionInfo(name=name, docstring=docstring))
|
|
218
|
+
self._refresh()
|
|
219
|
+
|
|
220
|
+
def update_metrics(
|
|
221
|
+
self,
|
|
222
|
+
quality_delta: float,
|
|
223
|
+
latency_last: float,
|
|
224
|
+
latency_avg: float,
|
|
225
|
+
latency_total: float,
|
|
226
|
+
llm_calls: int,
|
|
227
|
+
) -> None:
|
|
228
|
+
"""
|
|
229
|
+
Update latency metrics.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
quality_delta: Quality improvement percentage (ignored, kept for compatibility)
|
|
233
|
+
latency_last: Last LLM call latency in ms
|
|
234
|
+
latency_avg: Average LLM call latency in ms
|
|
235
|
+
latency_total: Total LLM call time in ms
|
|
236
|
+
llm_calls: Total number of LLM calls
|
|
237
|
+
"""
|
|
238
|
+
self._state.latency_last_ms = latency_last
|
|
239
|
+
self._state.latency_avg_ms = latency_avg
|
|
240
|
+
self._state.latency_total_ms = latency_total
|
|
241
|
+
self._state.llm_call_count = llm_calls
|
|
242
|
+
self._refresh()
|
|
243
|
+
|
|
244
|
+
def update_tokens(self, prompt: str, response: str) -> None:
|
|
245
|
+
"""
|
|
246
|
+
Update token estimates.
|
|
247
|
+
|
|
248
|
+
Rough estimate: len(text) // 4
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
prompt: The prompt sent to the LLM
|
|
252
|
+
response: The response received from the LLM
|
|
253
|
+
"""
|
|
254
|
+
self._state.tokens_in += len(prompt) // 4
|
|
255
|
+
self._state.tokens_out += len(response) // 4
|
|
256
|
+
self._refresh()
|
|
257
|
+
|
|
258
|
+
def update_transmission(self, response: str) -> None:
|
|
259
|
+
"""
|
|
260
|
+
Update the transmission log with latest LLM response.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
response: The latest LLM response text
|
|
264
|
+
"""
|
|
265
|
+
self._state.last_response = response
|
|
266
|
+
self._refresh()
|
|
267
|
+
|
|
268
|
+
def _get_elapsed_time(self) -> str:
|
|
269
|
+
"""Get elapsed time as MM:SS string."""
|
|
270
|
+
elapsed = int(time.time() - self._start_time)
|
|
271
|
+
minutes = elapsed // 60
|
|
272
|
+
seconds = elapsed % 60
|
|
273
|
+
return f"{minutes:02d}:{seconds:02d}"
|
|
274
|
+
|
|
275
|
+
def show_complete(self, summary: dict) -> None:
|
|
276
|
+
"""
|
|
277
|
+
Show completion summary panel.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
summary: Dictionary with completion stats including:
|
|
281
|
+
- functions_count: Number of functions generated
|
|
282
|
+
- chunks_processed: Number of chunks processed
|
|
283
|
+
- latency_total_ms: Total LLM time in ms
|
|
284
|
+
- llm_calls: Number of LLM calls
|
|
285
|
+
- output_file: Path to output file
|
|
286
|
+
"""
|
|
287
|
+
if not HAS_RICH or self._layout is None:
|
|
288
|
+
return
|
|
289
|
+
|
|
290
|
+
# Build completion panel content
|
|
291
|
+
content = Table.grid(padding=(0, 2))
|
|
292
|
+
content.add_column(justify="left")
|
|
293
|
+
content.add_column(justify="left")
|
|
294
|
+
|
|
295
|
+
func_count = summary.get("functions_count", len(self._state.functions))
|
|
296
|
+
chunks = summary.get("chunks_processed", self._state.total_chunks)
|
|
297
|
+
elapsed = self._get_elapsed_time()
|
|
298
|
+
|
|
299
|
+
# Token stats
|
|
300
|
+
tokens_in_k = self._state.tokens_in / 1000
|
|
301
|
+
tokens_out_k = self._state.tokens_out / 1000
|
|
302
|
+
|
|
303
|
+
content.add_row(
|
|
304
|
+
Text("Functions Acquired:", style="bold"),
|
|
305
|
+
Text(str(func_count), style="green"),
|
|
306
|
+
)
|
|
307
|
+
content.add_row(
|
|
308
|
+
Text("Chunks Processed:", style="bold"),
|
|
309
|
+
Text(str(chunks)),
|
|
310
|
+
)
|
|
311
|
+
content.add_row(
|
|
312
|
+
Text("Total Time:", style="bold"),
|
|
313
|
+
Text(elapsed),
|
|
314
|
+
)
|
|
315
|
+
content.add_row(
|
|
316
|
+
Text("Tokens:", style="bold"),
|
|
317
|
+
Text(f"~{tokens_in_k:.1f}k in / ~{tokens_out_k:.1f}k out"),
|
|
318
|
+
)
|
|
319
|
+
content.add_row(Text(""), Text("")) # Spacer
|
|
320
|
+
content.add_row(
|
|
321
|
+
Text("Output:", style="bold"),
|
|
322
|
+
Text(summary.get("output_file", "cleaning_functions.py"), style="cyan"),
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
# Build the complete panel with box drawing
|
|
326
|
+
complete_panel = Panel(
|
|
327
|
+
content,
|
|
328
|
+
title="[bold green]MISSION COMPLETE[/bold green]",
|
|
329
|
+
border_style="green",
|
|
330
|
+
box=DOUBLE,
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
# Replace entire layout with completion panel
|
|
334
|
+
self._layout.split_column(
|
|
335
|
+
Layout(complete_panel, name="complete"),
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
if self._live:
|
|
339
|
+
self._live.update(self._layout)
|
|
340
|
+
|
|
341
|
+
def _refresh(self) -> None:
|
|
342
|
+
"""Refresh all panels with current state."""
|
|
343
|
+
if not HAS_RICH or self._layout is None:
|
|
344
|
+
return
|
|
345
|
+
|
|
346
|
+
self._refresh_header()
|
|
347
|
+
self._refresh_status_bar()
|
|
348
|
+
self._refresh_progress_bar()
|
|
349
|
+
self._refresh_left_panel()
|
|
350
|
+
self._refresh_right_panel()
|
|
351
|
+
|
|
352
|
+
if self._live:
|
|
353
|
+
self._live.update(self._layout)
|
|
354
|
+
|
|
355
|
+
def _refresh_header(self) -> None:
|
|
356
|
+
"""Refresh the header panel with ASCII art banner."""
|
|
357
|
+
if not HAS_RICH or self._layout is None:
|
|
358
|
+
return
|
|
359
|
+
|
|
360
|
+
banner_text = Text(ASCII_BANNER, style="bold cyan")
|
|
361
|
+
header_panel = Panel(
|
|
362
|
+
banner_text,
|
|
363
|
+
border_style="cyan",
|
|
364
|
+
box=DOUBLE,
|
|
365
|
+
padding=(0, 1),
|
|
366
|
+
)
|
|
367
|
+
self._layout["header"].update(header_panel)
|
|
368
|
+
|
|
369
|
+
def _refresh_status_bar(self) -> None:
|
|
370
|
+
"""Refresh the status bar with mission info, timer, and status."""
|
|
371
|
+
if not HAS_RICH or self._layout is None:
|
|
372
|
+
return
|
|
373
|
+
|
|
374
|
+
# Truncate file path if too long
|
|
375
|
+
file_path = self._state.file_path
|
|
376
|
+
if len(file_path) > 30:
|
|
377
|
+
file_path = "..." + file_path[-27:]
|
|
378
|
+
|
|
379
|
+
elapsed = self._get_elapsed_time()
|
|
380
|
+
|
|
381
|
+
# Status indicator
|
|
382
|
+
if self._state.llm_status == "calling":
|
|
383
|
+
status_text = Text("ACTIVE", style="bold green")
|
|
384
|
+
status_indicator = "\u25cf" # Filled circle
|
|
385
|
+
else:
|
|
386
|
+
status_text = Text("IDLE", style="dim")
|
|
387
|
+
status_indicator = "\u25cb" # Empty circle
|
|
388
|
+
|
|
389
|
+
# Build status bar content
|
|
390
|
+
status_table = Table.grid(padding=(0, 2), expand=True)
|
|
391
|
+
status_table.add_column(justify="left", ratio=2)
|
|
392
|
+
status_table.add_column(justify="center", ratio=1)
|
|
393
|
+
status_table.add_column(justify="right", ratio=1)
|
|
394
|
+
|
|
395
|
+
mission_text = Text()
|
|
396
|
+
mission_text.append("MISSION: ", style="bold")
|
|
397
|
+
mission_text.append(file_path, style="cyan")
|
|
398
|
+
|
|
399
|
+
time_text = Text()
|
|
400
|
+
time_text.append("TIME: ", style="bold")
|
|
401
|
+
time_text.append(elapsed, style="cyan")
|
|
402
|
+
|
|
403
|
+
status_combined = Text()
|
|
404
|
+
status_combined.append("STATUS: ", style="bold")
|
|
405
|
+
status_combined.append(f"{status_indicator} ", style="green" if self._state.llm_status == "calling" else "dim")
|
|
406
|
+
status_combined.append_text(status_text)
|
|
407
|
+
|
|
408
|
+
status_table.add_row(mission_text, time_text, status_combined)
|
|
409
|
+
|
|
410
|
+
status_panel = Panel(
|
|
411
|
+
status_table,
|
|
412
|
+
border_style="cyan",
|
|
413
|
+
box=DOUBLE,
|
|
414
|
+
padding=(0, 1),
|
|
415
|
+
)
|
|
416
|
+
self._layout["status_bar"].update(status_panel)
|
|
417
|
+
|
|
418
|
+
def _refresh_progress_bar(self) -> None:
|
|
419
|
+
"""Refresh the progress bar panel."""
|
|
420
|
+
if not HAS_RICH or self._layout is None:
|
|
421
|
+
return
|
|
422
|
+
|
|
423
|
+
# Calculate progress percentage
|
|
424
|
+
progress_pct = 0
|
|
425
|
+
if self._state.total_chunks > 0:
|
|
426
|
+
progress_pct = int((self._state.current_chunk / self._state.total_chunks) * 100)
|
|
427
|
+
|
|
428
|
+
# Build progress bar using Rich Progress
|
|
429
|
+
progress = Progress(
|
|
430
|
+
TextColumn("[bold cyan]\u25ba[/bold cyan]"),
|
|
431
|
+
TextColumn(f"CHUNK {self._state.current_chunk}/{self._state.total_chunks}"),
|
|
432
|
+
BarColumn(bar_width=30, complete_style="cyan", finished_style="green"),
|
|
433
|
+
TextColumn(f"{progress_pct}%"),
|
|
434
|
+
expand=False,
|
|
435
|
+
)
|
|
436
|
+
task = progress.add_task("", total=self._state.total_chunks, completed=self._state.current_chunk)
|
|
437
|
+
|
|
438
|
+
progress_panel = Panel(
|
|
439
|
+
progress,
|
|
440
|
+
border_style="cyan",
|
|
441
|
+
box=DOUBLE,
|
|
442
|
+
padding=(0, 1),
|
|
443
|
+
)
|
|
444
|
+
self._layout["progress_bar"].update(progress_panel)
|
|
445
|
+
|
|
446
|
+
def _refresh_left_panel(self) -> None:
|
|
447
|
+
"""Refresh the left panel with functions list and metrics."""
|
|
448
|
+
if not HAS_RICH or self._layout is None:
|
|
449
|
+
return
|
|
450
|
+
|
|
451
|
+
func_count = len(self._state.functions)
|
|
452
|
+
|
|
453
|
+
# Build function tree
|
|
454
|
+
content = Table.grid(padding=(0, 0))
|
|
455
|
+
content.add_column()
|
|
456
|
+
|
|
457
|
+
# Show max 6 functions with tree structure
|
|
458
|
+
max_display = 6
|
|
459
|
+
display_funcs = self._state.functions[-max_display:] if func_count > max_display else self._state.functions
|
|
460
|
+
|
|
461
|
+
for i, func in enumerate(display_funcs):
|
|
462
|
+
func_text = Text()
|
|
463
|
+
# Tree-style prefix
|
|
464
|
+
if i == len(display_funcs) - 1:
|
|
465
|
+
func_text.append("\u2514\u2500 ", style="dim cyan") # Corner
|
|
466
|
+
else:
|
|
467
|
+
func_text.append("\u251c\u2500 ", style="dim cyan") # Tee
|
|
468
|
+
|
|
469
|
+
func_text.append(func.name, style="bold")
|
|
470
|
+
func_text.append(" \u2713", style="green") # Checkmark
|
|
471
|
+
|
|
472
|
+
content.add_row(func_text)
|
|
473
|
+
|
|
474
|
+
# Show "+N more" if truncated
|
|
475
|
+
if func_count > max_display:
|
|
476
|
+
hidden_count = func_count - max_display
|
|
477
|
+
content.add_row(Text(f" (+{hidden_count} more)", style="dim italic"))
|
|
478
|
+
|
|
479
|
+
# Add spacing
|
|
480
|
+
content.add_row(Text(""))
|
|
481
|
+
|
|
482
|
+
# Token stats
|
|
483
|
+
tokens_in_k = self._state.tokens_in / 1000
|
|
484
|
+
tokens_out_k = self._state.tokens_out / 1000
|
|
485
|
+
tokens_text = Text()
|
|
486
|
+
tokens_text.append("TOKENS: ", style="bold")
|
|
487
|
+
tokens_text.append(f"~{tokens_in_k:.1f}k in / ~{tokens_out_k:.1f}k out", style="dim")
|
|
488
|
+
content.add_row(tokens_text)
|
|
489
|
+
|
|
490
|
+
# Latency stats
|
|
491
|
+
latency_text = Text()
|
|
492
|
+
latency_text.append("LATENCY: ", style="bold")
|
|
493
|
+
if self._state.llm_call_count > 0:
|
|
494
|
+
latency_text.append(f"{self._state.latency_last_ms:.1f}s", style="cyan")
|
|
495
|
+
latency_text.append(f" (avg {self._state.latency_avg_ms / 1000:.1f}s)", style="dim")
|
|
496
|
+
else:
|
|
497
|
+
latency_text.append("\u2014", style="dim") # Em dash
|
|
498
|
+
content.add_row(latency_text)
|
|
499
|
+
|
|
500
|
+
left_panel = Panel(
|
|
501
|
+
content,
|
|
502
|
+
title=f"[bold cyan]FUNCTIONS ACQUIRED [{func_count}][/bold cyan]",
|
|
503
|
+
border_style="cyan",
|
|
504
|
+
box=DOUBLE,
|
|
505
|
+
)
|
|
506
|
+
self._layout["left_panel"].update(left_panel)
|
|
507
|
+
|
|
508
|
+
def _colorize_transmission(self, response: str) -> "Text":
|
|
509
|
+
"""Parse LLM XML response into colorized Rich Text for transmission log.
|
|
510
|
+
|
|
511
|
+
Color scheme:
|
|
512
|
+
- Issues (solved): dim
|
|
513
|
+
- Issues (unsolved): bright_white with cycling accent (blue/magenta/cyan/yellow)
|
|
514
|
+
- Function names: green
|
|
515
|
+
- Docstrings: italic
|
|
516
|
+
- Status clean: green
|
|
517
|
+
- Status needs_more_work: yellow
|
|
518
|
+
|
|
519
|
+
Args:
|
|
520
|
+
response: Raw LLM response text (XML format)
|
|
521
|
+
|
|
522
|
+
Returns:
|
|
523
|
+
Rich Text object with colors applied.
|
|
524
|
+
"""
|
|
525
|
+
import re
|
|
526
|
+
|
|
527
|
+
ISSUE_COLORS = ["blue", "magenta", "cyan", "yellow"]
|
|
528
|
+
text = Text()
|
|
529
|
+
unsolved_index = 0
|
|
530
|
+
|
|
531
|
+
try:
|
|
532
|
+
# Find all issues
|
|
533
|
+
issue_pattern = r'<issue[^>]*id="(\d+)"[^>]*solved="(true|false)"[^>]*>([^<]+)</issue>'
|
|
534
|
+
issues = re.findall(issue_pattern, response, re.DOTALL)
|
|
535
|
+
|
|
536
|
+
if issues:
|
|
537
|
+
text.append("ISSUES DETECTED:\n", style="bold cyan")
|
|
538
|
+
for issue_id, solved, desc in issues[:8]: # Limit to 8 issues
|
|
539
|
+
desc_clean = desc.strip()[:40] # Truncate description
|
|
540
|
+
if solved == "true":
|
|
541
|
+
text.append(" \u2713 ", style="green")
|
|
542
|
+
text.append(f"{desc_clean}\n", style="dim")
|
|
543
|
+
else:
|
|
544
|
+
accent = ISSUE_COLORS[unsolved_index % len(ISSUE_COLORS)]
|
|
545
|
+
text.append(" \u2717 ", style=accent)
|
|
546
|
+
text.append(f"{desc_clean}\n", style="bright_white")
|
|
547
|
+
unsolved_index += 1
|
|
548
|
+
if len(issues) > 8:
|
|
549
|
+
text.append(f" (+{len(issues) - 8} more)\n", style="dim")
|
|
550
|
+
text.append("\n")
|
|
551
|
+
|
|
552
|
+
# Find function being generated
|
|
553
|
+
name_match = re.search(r'<name>([^<]+)</name>', response)
|
|
554
|
+
docstring_match = re.search(r'<docstring>([^<]+)</docstring>', response, re.DOTALL)
|
|
555
|
+
|
|
556
|
+
if name_match:
|
|
557
|
+
text.append("GENERATING: ", style="bold cyan")
|
|
558
|
+
text.append(f"{name_match.group(1).strip()}\n", style="green bold")
|
|
559
|
+
if docstring_match:
|
|
560
|
+
doc = docstring_match.group(1).strip()[:60]
|
|
561
|
+
text.append(f' "{doc}..."\n', style="italic")
|
|
562
|
+
text.append("\n")
|
|
563
|
+
|
|
564
|
+
# Find chunk status
|
|
565
|
+
status_match = re.search(r'<chunk_status>([^<]+)</chunk_status>', response)
|
|
566
|
+
if status_match:
|
|
567
|
+
status = status_match.group(1).strip()
|
|
568
|
+
text.append("STATUS: ", style="bold cyan")
|
|
569
|
+
if status == "clean":
|
|
570
|
+
text.append(status.upper(), style="green bold")
|
|
571
|
+
else:
|
|
572
|
+
text.append(status.upper().replace("_", " "), style="yellow bold")
|
|
573
|
+
|
|
574
|
+
if text.plain:
|
|
575
|
+
return text
|
|
576
|
+
except Exception:
|
|
577
|
+
pass
|
|
578
|
+
|
|
579
|
+
# Fallback: show truncated raw response
|
|
580
|
+
fallback = response[:500] + "..." if len(response) > 500 else response
|
|
581
|
+
return Text(fallback, style="dim cyan")
|
|
582
|
+
|
|
583
|
+
def _refresh_right_panel(self) -> None:
|
|
584
|
+
"""Refresh the right panel with colorized transmission log."""
|
|
585
|
+
if not HAS_RICH or self._layout is None:
|
|
586
|
+
return
|
|
587
|
+
|
|
588
|
+
# Get last response and colorize for display
|
|
589
|
+
response = self._state.last_response
|
|
590
|
+
if not response:
|
|
591
|
+
log_text = Text("(Awaiting transmission...)", style="dim cyan")
|
|
592
|
+
else:
|
|
593
|
+
log_text = self._colorize_transmission(response)
|
|
594
|
+
|
|
595
|
+
right_panel = Panel(
|
|
596
|
+
log_text,
|
|
597
|
+
title="[bold cyan]\u25c4\u25c4 TRANSMISSION LOG \u25ba\u25ba[/bold cyan]",
|
|
598
|
+
border_style="cyan",
|
|
599
|
+
box=DOUBLE,
|
|
600
|
+
)
|
|
601
|
+
self._layout["right_panel"].update(right_panel)
|
|
602
|
+
|
|
603
|
+
# Legacy method stubs for backwards compatibility
|
|
604
|
+
def _refresh_progress(self) -> None:
|
|
605
|
+
"""Legacy method - calls _refresh_progress_bar."""
|
|
606
|
+
self._refresh_progress_bar()
|
|
607
|
+
|
|
608
|
+
def _refresh_functions(self) -> None:
|
|
609
|
+
"""Legacy method - calls _refresh_left_panel."""
|
|
610
|
+
self._refresh_left_panel()
|
|
611
|
+
|
|
612
|
+
def _refresh_footer(self) -> None:
|
|
613
|
+
"""Legacy method - no longer used but kept for compatibility."""
|
|
614
|
+
pass
|