localcoder 0.1.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.
- localcoder/__init__.py +2 -0
- localcoder/__main__.py +2 -0
- localcoder/agent.py +35 -0
- localcoder/backends.py +2470 -0
- localcoder/bench.py +335 -0
- localcoder/cli.py +827 -0
- localcoder/gemma4coder_display.py +583 -0
- localcoder/setup.py +321 -0
- localcoder/tui.py +276 -0
- localcoder/voice.py +187 -0
- localcoder-0.1.0.dist-info/METADATA +187 -0
- localcoder-0.1.0.dist-info/RECORD +15 -0
- localcoder-0.1.0.dist-info/WHEEL +4 -0
- localcoder-0.1.0.dist-info/entry_points.txt +2 -0
- localcoder-0.1.0.dist-info/licenses/LICENSE +4 -0
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
"""
|
|
2
|
+
gemma4coder_display.py — Memory-safe animation and display system for gemma4coder.
|
|
3
|
+
|
|
4
|
+
Uses rich.live for all animations (no background threads).
|
|
5
|
+
All functions are standalone and can be dropped into the main script.
|
|
6
|
+
|
|
7
|
+
Requirements: Python 3.10+, rich >= 13.0
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import time
|
|
11
|
+
from contextlib import contextmanager
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
from rich.live import Live
|
|
16
|
+
from rich.panel import Panel
|
|
17
|
+
from rich.progress import (
|
|
18
|
+
BarColumn,
|
|
19
|
+
Progress,
|
|
20
|
+
SpinnerColumn,
|
|
21
|
+
TaskID,
|
|
22
|
+
TextColumn,
|
|
23
|
+
)
|
|
24
|
+
from rich.spinner import Spinner
|
|
25
|
+
from rich.style import Style
|
|
26
|
+
from rich.syntax import Syntax
|
|
27
|
+
from rich.table import Table
|
|
28
|
+
from rich.text import Text
|
|
29
|
+
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
# 1. THINKING SPINNER — shows during LLM inference
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
class ThinkingSpinner:
|
|
35
|
+
"""Single-line spinner that shows elapsed time, token count, and tok/s.
|
|
36
|
+
|
|
37
|
+
Usage:
|
|
38
|
+
spinner = ThinkingSpinner(console)
|
|
39
|
+
spinner.start()
|
|
40
|
+
# ... in your streaming loop ...
|
|
41
|
+
spinner.update(tokens=42, tps=18.5)
|
|
42
|
+
# ... when done ...
|
|
43
|
+
spinner.stop(total_tokens=120, tps=22.0, elapsed=5.3)
|
|
44
|
+
|
|
45
|
+
Or as a context manager:
|
|
46
|
+
with ThinkingSpinner(console) as sp:
|
|
47
|
+
sp.update(tokens=10)
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(self, console: Console) -> None:
|
|
51
|
+
self._console = console
|
|
52
|
+
self._live: Optional[Live] = None
|
|
53
|
+
self._start_time: float = 0.0
|
|
54
|
+
self._tokens: int = 0
|
|
55
|
+
self._tps: float = 0.0
|
|
56
|
+
|
|
57
|
+
# -- rendering ----------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
def _render(self) -> Text:
|
|
60
|
+
elapsed = time.time() - self._start_time
|
|
61
|
+
if elapsed < 60:
|
|
62
|
+
time_str = f"{elapsed:.0f}s"
|
|
63
|
+
else:
|
|
64
|
+
m, s = divmod(elapsed, 60)
|
|
65
|
+
time_str = f"{m:.0f}m {s:.0f}s"
|
|
66
|
+
|
|
67
|
+
line = Text()
|
|
68
|
+
line.append(" ")
|
|
69
|
+
# spinner glyph — cycle through 4 frames based on elapsed
|
|
70
|
+
frames = "◐◓◑◒"
|
|
71
|
+
frame = frames[int(elapsed * 4) % len(frames)]
|
|
72
|
+
line.append(frame, style="bold magenta")
|
|
73
|
+
line.append(" ")
|
|
74
|
+
line.append("Thinking…", style="italic magenta")
|
|
75
|
+
line.append(f" {time_str}", style="dim")
|
|
76
|
+
|
|
77
|
+
if self._tokens > 0:
|
|
78
|
+
line.append(f" ↓ {self._tokens} tokens", style="dim")
|
|
79
|
+
if self._tps > 0:
|
|
80
|
+
line.append(f" {self._tps:.0f} tok/s", style="dim cyan")
|
|
81
|
+
|
|
82
|
+
return line
|
|
83
|
+
|
|
84
|
+
# -- public API ---------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
def start(self) -> None:
|
|
87
|
+
self._start_time = time.time()
|
|
88
|
+
self._tokens = 0
|
|
89
|
+
self._tps = 0.0
|
|
90
|
+
self._live = Live(
|
|
91
|
+
self._render(),
|
|
92
|
+
console=self._console,
|
|
93
|
+
refresh_per_second=4,
|
|
94
|
+
transient=True,
|
|
95
|
+
)
|
|
96
|
+
self._live.start()
|
|
97
|
+
|
|
98
|
+
def update(self, tokens: int = 0, tps: float = 0.0) -> None:
|
|
99
|
+
"""Update token count and speed. Call as often as you like."""
|
|
100
|
+
if tokens > 0:
|
|
101
|
+
self._tokens = tokens
|
|
102
|
+
if tps > 0:
|
|
103
|
+
self._tps = tps
|
|
104
|
+
if self._live is not None:
|
|
105
|
+
self._live.update(self._render())
|
|
106
|
+
|
|
107
|
+
def stop(
|
|
108
|
+
self,
|
|
109
|
+
total_tokens: int = 0,
|
|
110
|
+
tps: float = 0.0,
|
|
111
|
+
elapsed: Optional[float] = None,
|
|
112
|
+
) -> None:
|
|
113
|
+
"""Stop the spinner and print a final summary line."""
|
|
114
|
+
try:
|
|
115
|
+
if self._live is not None:
|
|
116
|
+
self._live.stop()
|
|
117
|
+
self._live = None
|
|
118
|
+
except Exception:
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
if elapsed is None:
|
|
122
|
+
elapsed = time.time() - self._start_time
|
|
123
|
+
if elapsed < 60:
|
|
124
|
+
t = f"{elapsed:.0f}s"
|
|
125
|
+
else:
|
|
126
|
+
m, s = divmod(elapsed, 60)
|
|
127
|
+
t = f"{m:.0f}m {s:.0f}s"
|
|
128
|
+
|
|
129
|
+
tok = total_tokens or self._tokens
|
|
130
|
+
speed = tps or self._tps
|
|
131
|
+
self._console.print(
|
|
132
|
+
f"\n [dim]✦ {t} · {tok} tokens · {speed:.0f} tok/s[/]"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def __enter__(self) -> "ThinkingSpinner":
|
|
136
|
+
self.start()
|
|
137
|
+
return self
|
|
138
|
+
|
|
139
|
+
def __exit__(self, *exc) -> None:
|
|
140
|
+
try:
|
|
141
|
+
if self._live is not None:
|
|
142
|
+
self._live.stop()
|
|
143
|
+
self._live = None
|
|
144
|
+
except Exception:
|
|
145
|
+
pass
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# ---------------------------------------------------------------------------
|
|
149
|
+
# 2. STARTUP ANIMATION — gradient banner + connect/ready transition
|
|
150
|
+
# ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
_GRADIENT_COLORS = [
|
|
153
|
+
"#ff6ec7", "#d16bff", "#9b6bff", "#6b8bff", "#6bcfff",
|
|
154
|
+
"#6bffcf", "#6bff8b", "#b5ff6b", "#ffef6b", "#ffb86b",
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _gradient_text(text: str, colors: list[str]) -> Text:
|
|
159
|
+
"""Apply a gradient across characters of *text*."""
|
|
160
|
+
result = Text()
|
|
161
|
+
n = max(len(colors), 1)
|
|
162
|
+
for i, ch in enumerate(text):
|
|
163
|
+
color = colors[i % n]
|
|
164
|
+
result.append(ch, style=Style(color=color, bold=True))
|
|
165
|
+
return result
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def show_startup_animation(console: Console, backend_info: dict) -> None:
|
|
169
|
+
"""Elegant startup: gradient banner -> connecting -> ready.
|
|
170
|
+
|
|
171
|
+
Total animation time < 500 ms. Uses Live for smooth updates.
|
|
172
|
+
"""
|
|
173
|
+
banner_text = "gemma4coder"
|
|
174
|
+
model_name = backend_info.get("model_name", "unknown")
|
|
175
|
+
backend = backend_info.get("backend", "unknown")
|
|
176
|
+
ctx = backend_info.get("ctx", "")
|
|
177
|
+
quant = backend_info.get("quant", "")
|
|
178
|
+
size = backend_info.get("size", "")
|
|
179
|
+
|
|
180
|
+
model_label = f"Gemma 4 {size}" if size else model_name
|
|
181
|
+
if quant:
|
|
182
|
+
model_label += f" {quant}"
|
|
183
|
+
|
|
184
|
+
# Phase 1: gradient banner + "connecting..."
|
|
185
|
+
def _frame_connecting() -> Panel:
|
|
186
|
+
title = Text()
|
|
187
|
+
title.append("◆ ", style="bold magenta")
|
|
188
|
+
title.append_text(_gradient_text(banner_text, _GRADIENT_COLORS))
|
|
189
|
+
|
|
190
|
+
inner = Text()
|
|
191
|
+
inner.append(f" {model_label} ", style="bold white on rgb(60,20,80)")
|
|
192
|
+
inner.append(" ", style="dim")
|
|
193
|
+
inner.append("connecting…", style="italic yellow")
|
|
194
|
+
|
|
195
|
+
return Panel(
|
|
196
|
+
inner,
|
|
197
|
+
title=title,
|
|
198
|
+
title_align="left",
|
|
199
|
+
border_style="magenta",
|
|
200
|
+
padding=(0, 1),
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# Phase 2: gradient banner + ready info
|
|
204
|
+
def _frame_ready() -> Panel:
|
|
205
|
+
title = Text()
|
|
206
|
+
title.append("◆ ", style="bold magenta")
|
|
207
|
+
title.append_text(_gradient_text(banner_text, _GRADIENT_COLORS))
|
|
208
|
+
|
|
209
|
+
inner = Text()
|
|
210
|
+
inner.append(f" {model_label} ", style="bold white on rgb(60,20,80)")
|
|
211
|
+
inner.append(" ", style="dim")
|
|
212
|
+
inner.append(backend, style="green")
|
|
213
|
+
inner.append(" (local)", style="dim green")
|
|
214
|
+
if ctx:
|
|
215
|
+
inner.append(" ", style="dim")
|
|
216
|
+
inner.append(ctx, style="bold green")
|
|
217
|
+
inner.append(" ", style="dim")
|
|
218
|
+
inner.append("$0.00", style="bold green")
|
|
219
|
+
|
|
220
|
+
return Panel(
|
|
221
|
+
inner,
|
|
222
|
+
title=title,
|
|
223
|
+
title_align="left",
|
|
224
|
+
border_style="magenta",
|
|
225
|
+
padding=(0, 1),
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
console.print()
|
|
229
|
+
try:
|
|
230
|
+
with Live(
|
|
231
|
+
_frame_connecting(),
|
|
232
|
+
console=console,
|
|
233
|
+
refresh_per_second=10,
|
|
234
|
+
transient=True,
|
|
235
|
+
) as live:
|
|
236
|
+
time.sleep(0.25)
|
|
237
|
+
live.update(_frame_ready())
|
|
238
|
+
time.sleep(0.15)
|
|
239
|
+
except KeyboardInterrupt:
|
|
240
|
+
pass
|
|
241
|
+
|
|
242
|
+
# Print the final frame permanently
|
|
243
|
+
console.print(_frame_ready())
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
# ---------------------------------------------------------------------------
|
|
247
|
+
# 3. TOOL CALL ANIMATIONS — single-line, non-blocking
|
|
248
|
+
# ---------------------------------------------------------------------------
|
|
249
|
+
|
|
250
|
+
def show_tool_animation(
|
|
251
|
+
console: Console,
|
|
252
|
+
tool_name: str,
|
|
253
|
+
args: dict,
|
|
254
|
+
) -> None:
|
|
255
|
+
"""Display a tool call indicator. Single line, returns immediately.
|
|
256
|
+
|
|
257
|
+
Supported tools: bash, write_file, read_file, edit_file, web_search, fetch_url.
|
|
258
|
+
"""
|
|
259
|
+
if tool_name == "bash":
|
|
260
|
+
cmd = args.get("command", "")[:120]
|
|
261
|
+
# Brief syntax-highlighted command display
|
|
262
|
+
try:
|
|
263
|
+
syn = Syntax(
|
|
264
|
+
f"$ {cmd}",
|
|
265
|
+
"bash",
|
|
266
|
+
theme="monokai",
|
|
267
|
+
line_numbers=False,
|
|
268
|
+
word_wrap=True,
|
|
269
|
+
)
|
|
270
|
+
console.print(
|
|
271
|
+
Panel(
|
|
272
|
+
syn,
|
|
273
|
+
title="[bold yellow]⚙ bash[/]",
|
|
274
|
+
title_align="left",
|
|
275
|
+
border_style="yellow",
|
|
276
|
+
padding=(0, 1),
|
|
277
|
+
)
|
|
278
|
+
)
|
|
279
|
+
except Exception:
|
|
280
|
+
console.print(
|
|
281
|
+
Panel(
|
|
282
|
+
Text(f"$ {cmd}", style="cyan"),
|
|
283
|
+
title="[bold yellow]⚙ bash[/]",
|
|
284
|
+
title_align="left",
|
|
285
|
+
border_style="yellow",
|
|
286
|
+
padding=(0, 1),
|
|
287
|
+
)
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
elif tool_name == "write_file":
|
|
291
|
+
path = args.get("path", "?")
|
|
292
|
+
sz = len(args.get("content", ""))
|
|
293
|
+
line = Text()
|
|
294
|
+
line.append(" ← ", style="bold green")
|
|
295
|
+
line.append("Writing ", style="green")
|
|
296
|
+
line.append(path, style="bold white")
|
|
297
|
+
line.append(f" ({sz} chars)", style="dim")
|
|
298
|
+
console.print(line)
|
|
299
|
+
|
|
300
|
+
elif tool_name == "read_file":
|
|
301
|
+
path = args.get("path", "?")
|
|
302
|
+
line = Text()
|
|
303
|
+
line.append(" → ", style="bold blue")
|
|
304
|
+
line.append("Reading ", style="blue")
|
|
305
|
+
line.append(path, style="bold white")
|
|
306
|
+
console.print(line)
|
|
307
|
+
|
|
308
|
+
elif tool_name == "edit_file":
|
|
309
|
+
path = args.get("path", "?")
|
|
310
|
+
line = Text()
|
|
311
|
+
line.append(" ← ", style="bold green")
|
|
312
|
+
line.append("Editing ", style="green")
|
|
313
|
+
line.append(path, style="bold white")
|
|
314
|
+
console.print(line)
|
|
315
|
+
|
|
316
|
+
elif tool_name == "web_search":
|
|
317
|
+
query = args.get("query", "")
|
|
318
|
+
line = Text()
|
|
319
|
+
line.append(" 🔍 ", style="bold magenta")
|
|
320
|
+
line.append("Searching ", style="magenta")
|
|
321
|
+
line.append(f'"{query}"', style="bold white")
|
|
322
|
+
line.append(" ···", style="dim magenta")
|
|
323
|
+
console.print(line)
|
|
324
|
+
|
|
325
|
+
elif tool_name == "fetch_url":
|
|
326
|
+
url = args.get("url", "")[:80]
|
|
327
|
+
line = Text()
|
|
328
|
+
line.append(" 🌐 ", style="bold blue")
|
|
329
|
+
line.append("Fetching ", style="blue")
|
|
330
|
+
line.append(url, style="dim")
|
|
331
|
+
console.print(line)
|
|
332
|
+
|
|
333
|
+
else:
|
|
334
|
+
console.print(f" [yellow]⚡ {tool_name}[/]")
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
@contextmanager
|
|
338
|
+
def tool_running_indicator(console: Console, tool_name: str):
|
|
339
|
+
"""Context manager: shows a brief 'running...' indicator while a tool executes.
|
|
340
|
+
|
|
341
|
+
Usage:
|
|
342
|
+
with tool_running_indicator(console, "bash"):
|
|
343
|
+
result = exec_tool("bash", args)
|
|
344
|
+
"""
|
|
345
|
+
labels = {
|
|
346
|
+
"bash": "running…",
|
|
347
|
+
"write_file": "writing…",
|
|
348
|
+
"read_file": "reading…",
|
|
349
|
+
"edit_file": "editing…",
|
|
350
|
+
"web_search": "searching…",
|
|
351
|
+
"fetch_url": "fetching…",
|
|
352
|
+
}
|
|
353
|
+
label = labels.get(tool_name, "running…")
|
|
354
|
+
|
|
355
|
+
spinner_text = Text()
|
|
356
|
+
spinner_text.append(" ")
|
|
357
|
+
spinner_text.append_text(Spinner("dots").render(time.time()))
|
|
358
|
+
spinner_text.append(f" {label}", style="dim italic")
|
|
359
|
+
|
|
360
|
+
live = Live(
|
|
361
|
+
spinner_text,
|
|
362
|
+
console=console,
|
|
363
|
+
refresh_per_second=8,
|
|
364
|
+
transient=True,
|
|
365
|
+
)
|
|
366
|
+
try:
|
|
367
|
+
live.start()
|
|
368
|
+
yield
|
|
369
|
+
finally:
|
|
370
|
+
try:
|
|
371
|
+
live.stop()
|
|
372
|
+
except Exception:
|
|
373
|
+
pass
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
# ---------------------------------------------------------------------------
|
|
377
|
+
# 4. TOKEN STREAM DISPLAY — "generating..." then full markdown render
|
|
378
|
+
# ---------------------------------------------------------------------------
|
|
379
|
+
|
|
380
|
+
@contextmanager
|
|
381
|
+
def generating_indicator(console: Console):
|
|
382
|
+
"""Shows a live 'generating...' indicator. Stop it, then render markdown.
|
|
383
|
+
|
|
384
|
+
Usage:
|
|
385
|
+
with generating_indicator(console):
|
|
386
|
+
response = chat_api(messages)
|
|
387
|
+
show_response(console, response_text)
|
|
388
|
+
"""
|
|
389
|
+
line = Text()
|
|
390
|
+
line.append(" ")
|
|
391
|
+
line.append("◐", style="bold magenta")
|
|
392
|
+
line.append(" generating…", style="italic dim")
|
|
393
|
+
|
|
394
|
+
live = Live(
|
|
395
|
+
line,
|
|
396
|
+
console=console,
|
|
397
|
+
refresh_per_second=4,
|
|
398
|
+
transient=True,
|
|
399
|
+
)
|
|
400
|
+
try:
|
|
401
|
+
live.start()
|
|
402
|
+
yield live
|
|
403
|
+
finally:
|
|
404
|
+
try:
|
|
405
|
+
live.stop()
|
|
406
|
+
except Exception:
|
|
407
|
+
pass
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
# ---------------------------------------------------------------------------
|
|
411
|
+
# 5. CONTEXT USAGE BAR
|
|
412
|
+
# ---------------------------------------------------------------------------
|
|
413
|
+
|
|
414
|
+
def context_usage_bar(
|
|
415
|
+
console: Console,
|
|
416
|
+
used_tokens: int,
|
|
417
|
+
max_tokens: int,
|
|
418
|
+
) -> None:
|
|
419
|
+
"""Print a thin context-usage bar. Green <50%, yellow 50-80%, red >80%.
|
|
420
|
+
|
|
421
|
+
Example output: ▰▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱ 4.2K / 128K tokens
|
|
422
|
+
"""
|
|
423
|
+
if max_tokens <= 0:
|
|
424
|
+
return
|
|
425
|
+
|
|
426
|
+
ratio = min(used_tokens / max_tokens, 1.0)
|
|
427
|
+
pct = ratio * 100
|
|
428
|
+
|
|
429
|
+
if pct < 50:
|
|
430
|
+
bar_style = "green"
|
|
431
|
+
elif pct < 80:
|
|
432
|
+
bar_style = "yellow"
|
|
433
|
+
else:
|
|
434
|
+
bar_style = "red"
|
|
435
|
+
|
|
436
|
+
# 30-char bar
|
|
437
|
+
bar_width = 30
|
|
438
|
+
filled = int(ratio * bar_width)
|
|
439
|
+
empty = bar_width - filled
|
|
440
|
+
|
|
441
|
+
bar = Text()
|
|
442
|
+
bar.append(" ")
|
|
443
|
+
bar.append("▰" * filled, style=bar_style)
|
|
444
|
+
bar.append("▱" * empty, style="dim")
|
|
445
|
+
bar.append(" ", style="dim")
|
|
446
|
+
|
|
447
|
+
# Format token counts: 4200 -> "4.2K", 128000 -> "128K"
|
|
448
|
+
def _fmt(n: int) -> str:
|
|
449
|
+
if n >= 1000:
|
|
450
|
+
k = n / 1000
|
|
451
|
+
return f"{k:.1f}K" if k < 100 else f"{k:.0f}K"
|
|
452
|
+
return str(n)
|
|
453
|
+
|
|
454
|
+
bar.append(f"{_fmt(used_tokens)} / {_fmt(max_tokens)} tokens", style="dim")
|
|
455
|
+
bar.append(f" ({pct:.0f}%)", style=f"dim {bar_style}")
|
|
456
|
+
|
|
457
|
+
console.print(bar)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def context_usage_bar_compact(
|
|
461
|
+
used_tokens: int,
|
|
462
|
+
max_tokens: int,
|
|
463
|
+
) -> Text:
|
|
464
|
+
"""Return a Text renderable (for embedding in panels/toolbars)."""
|
|
465
|
+
if max_tokens <= 0:
|
|
466
|
+
return Text("? / ? tokens", style="dim")
|
|
467
|
+
|
|
468
|
+
ratio = min(used_tokens / max_tokens, 1.0)
|
|
469
|
+
pct = ratio * 100
|
|
470
|
+
|
|
471
|
+
if pct < 50:
|
|
472
|
+
bar_style = "green"
|
|
473
|
+
elif pct < 80:
|
|
474
|
+
bar_style = "yellow"
|
|
475
|
+
else:
|
|
476
|
+
bar_style = "red"
|
|
477
|
+
|
|
478
|
+
bar_width = 15
|
|
479
|
+
filled = int(ratio * bar_width)
|
|
480
|
+
empty = bar_width - filled
|
|
481
|
+
|
|
482
|
+
def _fmt(n: int) -> str:
|
|
483
|
+
if n >= 1000:
|
|
484
|
+
k = n / 1000
|
|
485
|
+
return f"{k:.1f}K" if k < 100 else f"{k:.0f}K"
|
|
486
|
+
return str(n)
|
|
487
|
+
|
|
488
|
+
result = Text()
|
|
489
|
+
result.append("▰" * filled, style=bar_style)
|
|
490
|
+
result.append("▱" * empty, style="dim")
|
|
491
|
+
result.append(f" {_fmt(used_tokens)}/{_fmt(max_tokens)}", style="dim")
|
|
492
|
+
return result
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
# ---------------------------------------------------------------------------
|
|
496
|
+
# 6. CONVENIENCE: drop-in replacements for existing gemma4coder functions
|
|
497
|
+
# ---------------------------------------------------------------------------
|
|
498
|
+
|
|
499
|
+
def print_thinking_live(
|
|
500
|
+
console: Console,
|
|
501
|
+
tokens: int = 0,
|
|
502
|
+
tps: float = 0.0,
|
|
503
|
+
start_time: float = 0.0,
|
|
504
|
+
) -> Text:
|
|
505
|
+
"""Return a renderable for the thinking state (for use with Live)."""
|
|
506
|
+
elapsed = time.time() - start_time if start_time else 0
|
|
507
|
+
if elapsed < 60:
|
|
508
|
+
time_str = f"{elapsed:.0f}s"
|
|
509
|
+
else:
|
|
510
|
+
m, s = divmod(elapsed, 60)
|
|
511
|
+
time_str = f"{m:.0f}m {s:.0f}s"
|
|
512
|
+
|
|
513
|
+
frames = "◐◓◑◒"
|
|
514
|
+
frame = frames[int(elapsed * 4) % len(frames)]
|
|
515
|
+
|
|
516
|
+
line = Text()
|
|
517
|
+
line.append(" ")
|
|
518
|
+
line.append(frame, style="bold magenta")
|
|
519
|
+
line.append(" ")
|
|
520
|
+
line.append("Thinking…", style="italic magenta")
|
|
521
|
+
line.append(f" {time_str}", style="dim")
|
|
522
|
+
if tokens > 0:
|
|
523
|
+
line.append(f" ↓ {tokens} tokens", style="dim")
|
|
524
|
+
if tps > 0:
|
|
525
|
+
line.append(f" {tps:.0f} tok/s", style="dim cyan")
|
|
526
|
+
return line
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
# ---------------------------------------------------------------------------
|
|
530
|
+
# DEMO / SELF-TEST
|
|
531
|
+
# ---------------------------------------------------------------------------
|
|
532
|
+
|
|
533
|
+
def _demo() -> None:
|
|
534
|
+
"""Run a visual demo of all components."""
|
|
535
|
+
c = Console()
|
|
536
|
+
|
|
537
|
+
c.print("\n[bold underline]1. Startup Animation[/]\n")
|
|
538
|
+
show_startup_animation(c, {
|
|
539
|
+
"backend": "llama.cpp",
|
|
540
|
+
"model_name": "gemma-4-26B-A4B-it-UD-Q3_K_XL",
|
|
541
|
+
"quant": "Q3_K_XL",
|
|
542
|
+
"size": "26B",
|
|
543
|
+
"ctx": "32K",
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
c.print("\n[bold underline]2. Thinking Spinner[/]\n")
|
|
547
|
+
with ThinkingSpinner(c) as sp:
|
|
548
|
+
for i in range(12):
|
|
549
|
+
time.sleep(0.15)
|
|
550
|
+
sp.update(tokens=i * 8, tps=18.0 + i * 0.5)
|
|
551
|
+
sp.stop(total_tokens=96, tps=24.0)
|
|
552
|
+
|
|
553
|
+
c.print("\n[bold underline]3. Tool Call Animations[/]\n")
|
|
554
|
+
show_tool_animation(c, "bash", {"command": "find . -name '*.py' | head -20"})
|
|
555
|
+
show_tool_animation(c, "write_file", {"path": "src/app.py", "content": "x" * 1420})
|
|
556
|
+
show_tool_animation(c, "read_file", {"path": "package.json"})
|
|
557
|
+
show_tool_animation(c, "edit_file", {"path": "main.rs"})
|
|
558
|
+
show_tool_animation(c, "web_search", {"query": "rust async trait 2025"})
|
|
559
|
+
show_tool_animation(c, "fetch_url", {"url": "https://docs.rs/tokio/latest/tokio/"})
|
|
560
|
+
|
|
561
|
+
c.print("\n[bold underline]4. Tool Running Indicator[/]\n")
|
|
562
|
+
with tool_running_indicator(c, "bash"):
|
|
563
|
+
time.sleep(0.6)
|
|
564
|
+
c.print(" [dim]done[/]")
|
|
565
|
+
|
|
566
|
+
c.print("\n[bold underline]5. Generating Indicator[/]\n")
|
|
567
|
+
with generating_indicator(c):
|
|
568
|
+
time.sleep(0.8)
|
|
569
|
+
c.print(" [dim](response would render here)[/]")
|
|
570
|
+
|
|
571
|
+
c.print("\n[bold underline]6. Context Usage Bar[/]\n")
|
|
572
|
+
context_usage_bar(c, 2100, 131072) # green
|
|
573
|
+
context_usage_bar(c, 78000, 131072) # yellow
|
|
574
|
+
context_usage_bar(c, 118000, 131072) # red
|
|
575
|
+
|
|
576
|
+
c.print("\n[bold underline]7. Compact Bar (for toolbar)[/]\n")
|
|
577
|
+
c.print(Text(" "), context_usage_bar_compact(4200, 131072))
|
|
578
|
+
|
|
579
|
+
c.print()
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
if __name__ == "__main__":
|
|
583
|
+
_demo()
|