k-cli-for-devs 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.
- k_cli/__init__.py +77 -0
- k_cli/agents/__init__.py +0 -0
- k_cli/agents/adversarial_swarm.py +338 -0
- k_cli/agents/agent_core.py +255 -0
- k_cli/agents/background_daemon.py +141 -0
- k_cli/agents/orchestrator.py +376 -0
- k_cli/agents/persona.py +649 -0
- k_cli/agents/scaffold_engine.py +121 -0
- k_cli/agents/strands_agent.py +832 -0
- k_cli/agents/subagents.py +1496 -0
- k_cli/cli.py +3297 -0
- k_cli/core/__init__.py +0 -0
- k_cli/core/airgap.py +95 -0
- k_cli/core/credentials.py +548 -0
- k_cli/core/intent_sensor.py +177 -0
- k_cli/core/llm_driver.py +1028 -0
- k_cli/core/model_manager.py +1109 -0
- k_cli/core/models_hub.py +913 -0
- k_cli/core/prompting.py +41 -0
- k_cli/core/sdk.py +322 -0
- k_cli/core/session.py +826 -0
- k_cli/core/smart_router.py +230 -0
- k_cli/core/storage_manager.py +176 -0
- k_cli/core/viewport_engine.py +117 -0
- k_cli/demo/demo_runner.py +579 -0
- k_cli/git/__init__.py +0 -0
- k_cli/git/ai_bisect.py +208 -0
- k_cli/git/conflict_resolver.py +1039 -0
- k_cli/git/git_guard.py +417 -0
- k_cli/git/patcher.py +1175 -0
- k_cli/git/repo_map.py +1780 -0
- k_cli/git/smart_git.py +928 -0
- k_cli/git/verifier.py +969 -0
- k_cli/github/__init__.py +0 -0
- k_cli/github/dedup_engine.py +787 -0
- k_cli/github/github_client.py +1702 -0
- k_cli/github/github_engine.py +641 -0
- k_cli/github/local_hub.py +209 -0
- k_cli/github/pr_watcher.py +129 -0
- k_cli/github/trending.py +205 -0
- k_cli/tools/__init__.py +0 -0
- k_cli/tools/audit.py +79 -0
- k_cli/tools/chaos_immunity.py +377 -0
- k_cli/tools/codebase_qa.py +106 -0
- k_cli/tools/command_runner.py +256 -0
- k_cli/tools/diagram_generator.py +547 -0
- k_cli/tools/doc_retriever.py +1332 -0
- k_cli/tools/feature.py +105 -0
- k_cli/tools/ghost_daemon.py +122 -0
- k_cli/tools/incident_triage.py +1365 -0
- k_cli/tools/mcp_client.py +1846 -0
- k_cli/tools/repo_gardener.py +142 -0
- k_cli/tools/rules.py +109 -0
- k_cli/tools/security.py +52 -0
- k_cli/tools/security_healer.py +999 -0
- k_cli/tools/synapse_graph.py +155 -0
- k_cli/tui/__init__.py +0 -0
- k_cli/tui/diff_viewer.py +223 -0
- k_cli/tui/tui.py +1145 -0
- k_cli/tui/tui_animations.py +648 -0
- k_cli/tui/tui_app.py +2788 -0
- k_cli/ui/__init__.py +10 -0
- k_cli/ui/simple_repl.py +315 -0
- k_cli/web/__init__.py +7 -0
- k_cli/web/server.py +624 -0
- k_cli/web/static/app.js +830 -0
- k_cli/web/static/index.html +495 -0
- k_cli/web/static/monitor.html +189 -0
- k_cli/web/static/style.css +838 -0
- k_cli_for_devs-1.0.0.dist-info/METADATA +461 -0
- k_cli_for_devs-1.0.0.dist-info/RECORD +75 -0
- k_cli_for_devs-1.0.0.dist-info/WHEEL +5 -0
- k_cli_for_devs-1.0.0.dist-info/entry_points.txt +2 -0
- k_cli_for_devs-1.0.0.dist-info/licenses/LICENSE +21 -0
- k_cli_for_devs-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tui_animations.py - Premium Cyberpunk & Neon Visual Experience Engine for K-CLI
|
|
3
|
+
Project Bankai Engine v1.0.0
|
|
4
|
+
|
|
5
|
+
Features:
|
|
6
|
+
1. Cyberpunk / Neon ASCII banners, logo splash with smooth multi-color gradient rendering.
|
|
7
|
+
2. Smooth animated spinners & loaders (Radar pulse, Quantum flux, Neon orbit, Cyber matrix cascade, Hex pulse).
|
|
8
|
+
3. Live Token Speedometer & Real-time Cost Ticker ($ USD calculation based on model token usage).
|
|
9
|
+
4. Dynamic Status Glow Badges (Git branch, Active Model, Verifier Status, MCP Server count, RAM RSS).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import math
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
import time
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from enum import Enum
|
|
20
|
+
from typing import Any, Dict, Generator, List, Optional, Tuple, Union
|
|
21
|
+
|
|
22
|
+
from rich.console import Console, RenderableType
|
|
23
|
+
from rich.panel import Panel
|
|
24
|
+
from rich.style import Style
|
|
25
|
+
from rich.table import Table
|
|
26
|
+
from rich.text import Text
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ==============================================================================
|
|
30
|
+
# 1. Cyberpunk Palettes & Gradient Color Interpolation Engine
|
|
31
|
+
# ==============================================================================
|
|
32
|
+
|
|
33
|
+
CYBER_PALETTES: Dict[str, List[str]] = {
|
|
34
|
+
"neon_cyan": ["#00f0ff", "#00b4d8", "#7000ff", "#d946ef", "#ff007f"],
|
|
35
|
+
"cyber_pink": ["#ff007f", "#ff0055", "#ff5500", "#ffe600"],
|
|
36
|
+
"matrix_green": ["#5af78e", "#00ff41", "#00e676", "#00c853", "#008f11"],
|
|
37
|
+
"quantum_purple": ["#b026ff", "#7000ff", "#00f0ff", "#ff007f"],
|
|
38
|
+
"synthwave": ["#ff71ce", "#01cdfe", "#05ffa1", "#b967ff", "#fffb96"],
|
|
39
|
+
"gold_amber": ["#ffe600", "#ffaa00", "#ff7700", "#ff3300"],
|
|
40
|
+
"laser_blue": ["#00ffff", "#0080ff", "#0000ff", "#8000ff"],
|
|
41
|
+
"blood_neon": ["#ff0055", "#ff3366", "#cc0033", "#990000"],
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def hex_to_rgb(hex_str: str) -> Tuple[int, int, int]:
|
|
46
|
+
"""Converts hex string (#RRGGBB or RRGGBB) to (r, g, b) tuple."""
|
|
47
|
+
clean = hex_str.lstrip("#")
|
|
48
|
+
if len(clean) == 3:
|
|
49
|
+
clean = "".join(c * 2 for c in clean)
|
|
50
|
+
if len(clean) != 6:
|
|
51
|
+
return 0, 240, 255
|
|
52
|
+
try:
|
|
53
|
+
r = int(clean[0:2], 16)
|
|
54
|
+
g = int(clean[2:4], 16)
|
|
55
|
+
b = int(clean[4:6], 16)
|
|
56
|
+
return r, g, b
|
|
57
|
+
except ValueError:
|
|
58
|
+
return 0, 240, 255
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def rgb_to_hex(r: int, g: int, b: int) -> str:
|
|
62
|
+
"""Converts RGB integers (0-255) to hex string (#RRGGBB)."""
|
|
63
|
+
r_clamped = max(0, min(255, int(r)))
|
|
64
|
+
g_clamped = max(0, min(255, int(g)))
|
|
65
|
+
b_clamped = max(0, min(255, int(b)))
|
|
66
|
+
return f"#{r_clamped:02x}{g_clamped:02x}{b_clamped:02x}"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def interpolate_color(hex1: str, hex2: str, factor: float) -> str:
|
|
70
|
+
"""Linearly interpolates between two hex colors by factor (0.0 to 1.0)."""
|
|
71
|
+
factor = max(0.0, min(1.0, factor))
|
|
72
|
+
r1, g1, b1 = hex_to_rgb(hex1)
|
|
73
|
+
r2, g2, b2 = hex_to_rgb(hex2)
|
|
74
|
+
r = r1 + (r2 - r1) * factor
|
|
75
|
+
g = g1 + (g2 - g1) * factor
|
|
76
|
+
b = b1 + (b2 - b1) * factor
|
|
77
|
+
return rgb_to_hex(int(r), int(g), int(b))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def generate_gradient_colors(palette: Union[str, List[str]], steps: int) -> List[str]:
|
|
81
|
+
"""
|
|
82
|
+
Generates a list of interpolated hex color codes of length `steps` across palette stops.
|
|
83
|
+
"""
|
|
84
|
+
if steps <= 0:
|
|
85
|
+
return []
|
|
86
|
+
if isinstance(palette, str):
|
|
87
|
+
colors = CYBER_PALETTES.get(palette, CYBER_PALETTES["neon_cyan"])
|
|
88
|
+
else:
|
|
89
|
+
colors = palette or CYBER_PALETTES["neon_cyan"]
|
|
90
|
+
|
|
91
|
+
if len(colors) == 1 or steps == 1:
|
|
92
|
+
return [colors[0]] * steps
|
|
93
|
+
|
|
94
|
+
result: List[str] = []
|
|
95
|
+
num_segments = len(colors) - 1
|
|
96
|
+
steps_per_segment = steps / num_segments
|
|
97
|
+
|
|
98
|
+
for i in range(steps):
|
|
99
|
+
segment_idx = min(int(i / steps_per_segment), num_segments - 1)
|
|
100
|
+
sub_factor = (i - segment_idx * steps_per_segment) / steps_per_segment
|
|
101
|
+
c = interpolate_color(colors[segment_idx], colors[segment_idx + 1], sub_factor)
|
|
102
|
+
result.append(c)
|
|
103
|
+
|
|
104
|
+
return result
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def apply_gradient_to_text(
|
|
108
|
+
text: str,
|
|
109
|
+
palette_name: str = "neon_cyan",
|
|
110
|
+
direction: str = "horizontal",
|
|
111
|
+
bold: bool = True,
|
|
112
|
+
) -> Text:
|
|
113
|
+
"""
|
|
114
|
+
Applies a smooth multi-color cyberpunk gradient to input string using Rich Text.
|
|
115
|
+
direction: 'horizontal', 'vertical', or 'diagonal'.
|
|
116
|
+
"""
|
|
117
|
+
lines = text.split("\n")
|
|
118
|
+
rich_text = Text()
|
|
119
|
+
|
|
120
|
+
if direction == "vertical":
|
|
121
|
+
colors = generate_gradient_colors(palette_name, max(1, len(lines)))
|
|
122
|
+
for i, line in enumerate(lines):
|
|
123
|
+
style = Style(color=colors[i], bold=bold)
|
|
124
|
+
rich_text.append(line + ("\n" if i < len(lines) - 1 else ""), style=style)
|
|
125
|
+
return rich_text
|
|
126
|
+
|
|
127
|
+
# Horizontal or diagonal per character
|
|
128
|
+
max_len = max(len(l) for l in lines) if lines else 1
|
|
129
|
+
total_cols = max(1, max_len)
|
|
130
|
+
|
|
131
|
+
for l_idx, line in enumerate(lines):
|
|
132
|
+
if not line:
|
|
133
|
+
if l_idx < len(lines) - 1:
|
|
134
|
+
rich_text.append("\n")
|
|
135
|
+
continue
|
|
136
|
+
|
|
137
|
+
colors = generate_gradient_colors(
|
|
138
|
+
palette_name,
|
|
139
|
+
total_cols if direction == "horizontal" else (total_cols + len(lines)),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
for c_idx, ch in enumerate(line):
|
|
143
|
+
idx = c_idx if direction == "horizontal" else (c_idx + l_idx)
|
|
144
|
+
color = colors[min(idx, len(colors) - 1)]
|
|
145
|
+
rich_text.append(ch, style=Style(color=color, bold=bold))
|
|
146
|
+
|
|
147
|
+
if l_idx < len(lines) - 1:
|
|
148
|
+
rich_text.append("\n")
|
|
149
|
+
|
|
150
|
+
return rich_text
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ==============================================================================
|
|
154
|
+
# 2. Cyberpunk ASCII Art Banners & Logo Splash
|
|
155
|
+
# ==============================================================================
|
|
156
|
+
|
|
157
|
+
CYBER_ASCII_BANNER_ART = r"""
|
|
158
|
+
██╗ ██╗ ██████╗██╗ ██╗
|
|
159
|
+
██║ ██╔╝ ██╔════╝██║ ██║
|
|
160
|
+
█████═╝ ██║ ██║ ██║
|
|
161
|
+
██╔═██╗ ██║ ██║ ██║
|
|
162
|
+
██║ ██╗ ╚██████╗███████╗██║
|
|
163
|
+
╚═╝ ╚═╝ ╚═════╝╚══════╝╚═╝
|
|
164
|
+
""".strip("\n")
|
|
165
|
+
|
|
166
|
+
CYBER_LOGO_COMPACT = r"""
|
|
167
|
+
[ ⚡ K - C L I // P R O J E C T B A N K A I ⚡ ]
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
CYBER_ASCII_SUBAGENTS = r"""
|
|
171
|
+
┌─────────┐ ┌─────────┐ ┌─────────┐
|
|
172
|
+
│ EXPLORE │ ──▶ │ REFACTOR│ ──▶ │ TEST & │
|
|
173
|
+
│ SWARM │ │ ENGINE │ │ VERIFY │
|
|
174
|
+
└─────────┘ └─────────┘ └─────────┘
|
|
175
|
+
""".strip("\n")
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def render_cyber_banner(
|
|
179
|
+
title: str = "K-CLI // AGENTIC WORKSTATION",
|
|
180
|
+
subtitle: str = "Compiler Guard • Verification-First Architecture",
|
|
181
|
+
palette: str = "neon_cyan",
|
|
182
|
+
border_style: str = "#00f0ff",
|
|
183
|
+
glitch_step: int = 0,
|
|
184
|
+
) -> Panel:
|
|
185
|
+
"""
|
|
186
|
+
Renders a glowing, gradient-rendered Cyberpunk banner with active status headers.
|
|
187
|
+
"""
|
|
188
|
+
art_text = CYBER_ASCII_BANNER_ART
|
|
189
|
+
if glitch_step > 0:
|
|
190
|
+
# Subtle glitch effect on glyphs
|
|
191
|
+
glitch_chars = ["⚡", "◈", "◇", "█", "░", "▒"]
|
|
192
|
+
art_lines = art_text.split("\n")
|
|
193
|
+
mod_lines = []
|
|
194
|
+
for line in art_lines:
|
|
195
|
+
if line.strip():
|
|
196
|
+
g_char = glitch_chars[glitch_step % len(glitch_chars)]
|
|
197
|
+
mod_lines.append(line.replace("═", g_char).replace("╗", "╝" if glitch_step % 2 == 0 else "╗"))
|
|
198
|
+
else:
|
|
199
|
+
mod_lines.append(line)
|
|
200
|
+
art_text = "\n".join(mod_lines)
|
|
201
|
+
|
|
202
|
+
gradient_art = apply_gradient_to_text(art_text, palette_name=palette, direction="horizontal")
|
|
203
|
+
|
|
204
|
+
# Build stylized subtitle and status line
|
|
205
|
+
table = Table(box=None, expand=True, padding=(0, 0))
|
|
206
|
+
table.add_column("Art", justify="center")
|
|
207
|
+
table.add_row(gradient_art)
|
|
208
|
+
table.add_row(Text(f"─── {title} ───", style="bold #00f0ff", justify="center"))
|
|
209
|
+
table.add_row(Text(subtitle, style="dim white", justify="center"))
|
|
210
|
+
|
|
211
|
+
return Panel(
|
|
212
|
+
table,
|
|
213
|
+
border_style=border_style,
|
|
214
|
+
title="[bold #00f0ff]◈ K-CLI CYBER WORKSTATION ◈[/bold #00f0ff]",
|
|
215
|
+
subtitle="[dim #7000ff]Bankai v1.0.0 • Online[/dim #7000ff]",
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def generate_splash_frames(steps: int = 8, palette: str = "neon_cyan") -> List[Panel]:
|
|
220
|
+
"""Generates animated splash frames for startup sequences or REPL transitions."""
|
|
221
|
+
frames: List[Panel] = []
|
|
222
|
+
palettes = list(CYBER_PALETTES.keys())
|
|
223
|
+
for s in range(steps):
|
|
224
|
+
cur_pal = palettes[s % len(palettes)] if palette == "rainbow" else palette
|
|
225
|
+
frame = render_cyber_banner(
|
|
226
|
+
title=f"K-CLI // SYSTEM BOOT [{s+1}/{steps}]",
|
|
227
|
+
subtitle="Ground-Truth Compiler Verification Active",
|
|
228
|
+
palette=cur_pal,
|
|
229
|
+
glitch_step=s if s % 2 == 1 else 0,
|
|
230
|
+
)
|
|
231
|
+
frames.append(frame)
|
|
232
|
+
return frames
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# ==============================================================================
|
|
236
|
+
# 3. Smooth Animated Spinners & Cyber Loaders
|
|
237
|
+
# ==============================================================================
|
|
238
|
+
|
|
239
|
+
class SpinnerType(str, Enum):
|
|
240
|
+
RADAR_PULSE = "radar_pulse"
|
|
241
|
+
QUANTUM_FLUX = "quantum_flux"
|
|
242
|
+
NEON_ORBIT = "neon_orbit"
|
|
243
|
+
CYBER_MATRIX = "cyber_matrix"
|
|
244
|
+
HEX_PULSE = "hex_pulse"
|
|
245
|
+
SYNTH_BARS = "synth_bars"
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
SPINNER_GLYPH_SETS: Dict[SpinnerType, List[str]] = {
|
|
249
|
+
SpinnerType.RADAR_PULSE: [
|
|
250
|
+
"📡 ⠋ [SCANNING »» ]",
|
|
251
|
+
"📡 ⠙ [SCANNING »»» ]",
|
|
252
|
+
"📡 ⠹ [SCANNING »»» ]",
|
|
253
|
+
"📡 ⠸ [LOCKED »»» ]",
|
|
254
|
+
"📡 ⠼ [PULSE •»» ]",
|
|
255
|
+
"📡 ⠴ [SWEEP ••» ]",
|
|
256
|
+
"📡 ⠦ [RANGE ••• ]",
|
|
257
|
+
"📡 ⠧ [ECHO »•• ]",
|
|
258
|
+
"📡 ⠇ [PING »»• ]",
|
|
259
|
+
"📡 ⠏ [TARGET »»» ]",
|
|
260
|
+
],
|
|
261
|
+
SpinnerType.QUANTUM_FLUX: [
|
|
262
|
+
"⟨ψ| ∿∿∿ |φ⟩",
|
|
263
|
+
"⟨.ψ| ∾∾∾ |φ.⟩",
|
|
264
|
+
"⟨..ψ| ⚡∿⚡ |φ..⟩",
|
|
265
|
+
"⟨...ψ| ✧∾✦ |φ...⟩",
|
|
266
|
+
"⟨....ψ| ░▒▓ |φ....⟩",
|
|
267
|
+
"⟨...ψ| ▒▓█ |φ...⟩",
|
|
268
|
+
"⟨..ψ| ▓██ |φ..⟩",
|
|
269
|
+
"⟨.ψ| █▓▒ |φ.⟩",
|
|
270
|
+
],
|
|
271
|
+
SpinnerType.NEON_ORBIT: [
|
|
272
|
+
"🪐 ⠋ ✦ Orbit Alpha",
|
|
273
|
+
"💫 ⠙ ✧ Orbit Beta",
|
|
274
|
+
"✨ ⠚ ✦ Orbit Gamma",
|
|
275
|
+
"🌟 ⠞ ✧ Orbit Delta",
|
|
276
|
+
"⚡ ⠖ ✦ Flux Sync",
|
|
277
|
+
"🔥 ⠦ ✧ Node Pulse",
|
|
278
|
+
"💎 ⠴ ✦ Core Cycle",
|
|
279
|
+
"🔮 ⠲ ✧ Energy Weave",
|
|
280
|
+
"🌀 ⠳ ✦ Vortex Gate",
|
|
281
|
+
"💠 ⠓ ✧ Matrix Mesh",
|
|
282
|
+
],
|
|
283
|
+
SpinnerType.CYBER_MATRIX: [
|
|
284
|
+
"1010110 ░ 010101",
|
|
285
|
+
"0110101 ▒ 101010",
|
|
286
|
+
"1101001 ▓ 010101",
|
|
287
|
+
"0011110 █ 110011",
|
|
288
|
+
"1010011 ▓ 001100",
|
|
289
|
+
"0101100 ▒ 111001",
|
|
290
|
+
"1110001 ░ 000111",
|
|
291
|
+
],
|
|
292
|
+
SpinnerType.HEX_PULSE: [
|
|
293
|
+
"⬡ ⬡ ⬡ [0x0000]",
|
|
294
|
+
"⬢ ⬡ ⬡ [0x00F0]",
|
|
295
|
+
"⬡ ⬢ ⬡ [0x0FF0]",
|
|
296
|
+
"⬡ ⬡ ⬢ [0xFFFF]",
|
|
297
|
+
"⬢ ⬢ ⬡ [0x7000]",
|
|
298
|
+
"⬡ ⬢ ⬢ [0xB026]",
|
|
299
|
+
"⬢ ⬢ ⬢ [0xBANK]",
|
|
300
|
+
],
|
|
301
|
+
SpinnerType.SYNTH_BARS: [
|
|
302
|
+
" ▃▄▅▆▇█ [FLUX 10%]",
|
|
303
|
+
"▃▄▅▆▇█▇ [FLUX 35%]",
|
|
304
|
+
"▄▅▆▇█▇▆ [FLUX 60%]",
|
|
305
|
+
"▅▆▇█▇▆▅ [FLUX 85%]",
|
|
306
|
+
"▆▇█▇▆▅▄ [FLUX 99%]",
|
|
307
|
+
"▇█▇▆▅▄▃ [FLUX 100%]",
|
|
308
|
+
"█▇▆▅▄▃ [FLUX SYNC]",
|
|
309
|
+
],
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
class AnimatedSpinner:
|
|
314
|
+
"""
|
|
315
|
+
Rich-compatible Cyberpunk animated spinner with custom neon glyph sets,
|
|
316
|
+
pulsating color cycles, and frame generator methods.
|
|
317
|
+
"""
|
|
318
|
+
|
|
319
|
+
def __init__(
|
|
320
|
+
self,
|
|
321
|
+
spinner_type: Union[SpinnerType, str] = SpinnerType.RADAR_PULSE,
|
|
322
|
+
label: str = "Processing...",
|
|
323
|
+
palette: str = "neon_cyan",
|
|
324
|
+
):
|
|
325
|
+
if isinstance(spinner_type, str):
|
|
326
|
+
try:
|
|
327
|
+
self.spinner_type = SpinnerType(spinner_type)
|
|
328
|
+
except ValueError:
|
|
329
|
+
self.spinner_type = SpinnerType.RADAR_PULSE
|
|
330
|
+
else:
|
|
331
|
+
self.spinner_type = spinner_type
|
|
332
|
+
|
|
333
|
+
self.label = label
|
|
334
|
+
self.palette = palette
|
|
335
|
+
self.frames = SPINNER_GLYPH_SETS.get(self.spinner_type, SPINNER_GLYPH_SETS[SpinnerType.RADAR_PULSE])
|
|
336
|
+
|
|
337
|
+
def get_frame(self, step: int) -> Text:
|
|
338
|
+
"""Returns stylized Text for the frame at given step index."""
|
|
339
|
+
raw_glyph = self.frames[step % len(self.frames)]
|
|
340
|
+
colors = generate_gradient_colors(self.palette, max(1, len(self.frames)))
|
|
341
|
+
color = colors[step % len(colors)]
|
|
342
|
+
|
|
343
|
+
text = Text()
|
|
344
|
+
text.append(raw_glyph, style=Style(color=color, bold=True))
|
|
345
|
+
text.append(f" {self.label}", style=Style(color="#e2e8f0", bold=False))
|
|
346
|
+
return text
|
|
347
|
+
|
|
348
|
+
def get_all_frames(self) -> List[str]:
|
|
349
|
+
"""Returns raw glyph frames."""
|
|
350
|
+
return list(self.frames)
|
|
351
|
+
|
|
352
|
+
def render(self, step: int) -> Text:
|
|
353
|
+
"""Alias for get_frame to allow direct Rich rendering."""
|
|
354
|
+
return self.get_frame(step)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
# ==============================================================================
|
|
358
|
+
# 4. Live Token Speedometer & Real-Time Cost Ticker ($ USD)
|
|
359
|
+
# ==============================================================================
|
|
360
|
+
|
|
361
|
+
# Pricing Catalog: (prompt_price_per_1m, completion_price_per_1m) in USD
|
|
362
|
+
MODEL_PRICING_CATALOG: Dict[str, Tuple[float, float]] = {
|
|
363
|
+
# Project Bankai Local SLMs & GGUF (100% Free / Local Compute)
|
|
364
|
+
"bankai-7b": (0.00, 0.00),
|
|
365
|
+
"bankai-14b": (0.00, 0.00),
|
|
366
|
+
"qwen2.5-coder:1.5b": (0.00, 0.00),
|
|
367
|
+
"qwen2.5-coder:7b": (0.00, 0.00),
|
|
368
|
+
"qwen2.5-coder:14b": (0.00, 0.00),
|
|
369
|
+
"local ollama": (0.00, 0.00),
|
|
370
|
+
"ollama": (0.00, 0.00),
|
|
371
|
+
# Cloud Models
|
|
372
|
+
"gemini": (0.075, 0.30),
|
|
373
|
+
"gemini-2.0-flash": (0.075, 0.30),
|
|
374
|
+
"gemini-2.0-pro": (1.25, 5.00),
|
|
375
|
+
"claude": (3.00, 15.00),
|
|
376
|
+
"claude-3-5-sonnet": (3.00, 15.00),
|
|
377
|
+
"claude-3-opus": (15.00, 75.00),
|
|
378
|
+
"gpt-4o": (2.50, 10.00),
|
|
379
|
+
"gpt-4o-mini": (0.15, 0.60),
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def calculate_token_cost(model_name: str, prompt_tokens: int, completion_tokens: int) -> float:
|
|
384
|
+
"""
|
|
385
|
+
Calculates USD cost for a given model and token usage.
|
|
386
|
+
Returns $0.00 for local SLMs/GGUFs.
|
|
387
|
+
"""
|
|
388
|
+
m_clean = (model_name or "bankai-7b").lower().strip()
|
|
389
|
+
prompt_rate, compl_rate = (0.00, 0.00)
|
|
390
|
+
|
|
391
|
+
for key, rates in MODEL_PRICING_CATALOG.items():
|
|
392
|
+
if key in m_clean or m_clean in key:
|
|
393
|
+
prompt_rate, compl_rate = rates
|
|
394
|
+
break
|
|
395
|
+
|
|
396
|
+
cost = (prompt_tokens / 1_000_000.0) * prompt_rate + (completion_tokens / 1_000_000.0) * compl_rate
|
|
397
|
+
return round(cost, 6)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
class TokenSpeedometer:
|
|
401
|
+
"""
|
|
402
|
+
Real-time Token Throughput Speedometer.
|
|
403
|
+
Tracks instantaneous tok/s, peak speed, rolling average, and renders visual RPM gauge.
|
|
404
|
+
"""
|
|
405
|
+
|
|
406
|
+
def __init__(self, target_max_speed: float = 200.0):
|
|
407
|
+
self.target_max_speed = max(10.0, target_max_speed)
|
|
408
|
+
self.token_history: List[Tuple[float, int]] = [] # (timestamp, delta_tokens)
|
|
409
|
+
self.total_tokens: int = 0
|
|
410
|
+
self.peak_speed: float = 0.0
|
|
411
|
+
self.start_time: float = time.time()
|
|
412
|
+
self.last_update_time: float = self.start_time
|
|
413
|
+
|
|
414
|
+
def record_tokens(self, count: int) -> None:
|
|
415
|
+
"""Records addition of generated tokens at current timestamp."""
|
|
416
|
+
now = time.time()
|
|
417
|
+
self.token_history.append((now, count))
|
|
418
|
+
self.total_tokens += count
|
|
419
|
+
self.last_update_time = now
|
|
420
|
+
|
|
421
|
+
# Prune history older than 5.0 seconds for rolling speed
|
|
422
|
+
cutoff = now - 5.0
|
|
423
|
+
self.token_history = [item for item in self.token_history if item[0] >= cutoff]
|
|
424
|
+
|
|
425
|
+
# Calculate current rolling speed and update peak
|
|
426
|
+
speed = self.get_current_speed()
|
|
427
|
+
if speed > self.peak_speed:
|
|
428
|
+
self.peak_speed = speed
|
|
429
|
+
|
|
430
|
+
def get_current_speed(self) -> float:
|
|
431
|
+
"""Calculates rolling tokens per second over recent window."""
|
|
432
|
+
if not self.token_history:
|
|
433
|
+
return 0.0
|
|
434
|
+
now = time.time()
|
|
435
|
+
window_start = self.token_history[0][0]
|
|
436
|
+
duration = max(0.05, now - window_start)
|
|
437
|
+
recent_tokens = sum(cnt for _, cnt in self.token_history)
|
|
438
|
+
return round(recent_tokens / duration, 1)
|
|
439
|
+
|
|
440
|
+
def get_average_speed(self) -> float:
|
|
441
|
+
"""Calculates cumulative average tokens per second since inception."""
|
|
442
|
+
total_time = max(0.05, time.time() - self.start_time)
|
|
443
|
+
return round(self.total_tokens / total_time, 1)
|
|
444
|
+
|
|
445
|
+
def render_gauge(self, width: int = 16, style: str = "neon") -> Text:
|
|
446
|
+
"""
|
|
447
|
+
Renders a cyber speedometer visual gauge bar with speed readout.
|
|
448
|
+
"""
|
|
449
|
+
speed = self.get_current_speed()
|
|
450
|
+
pct = min(1.0, speed / self.target_max_speed)
|
|
451
|
+
filled_bars = int(pct * width)
|
|
452
|
+
empty_bars = width - filled_bars
|
|
453
|
+
|
|
454
|
+
# Dynamic speed color tiers
|
|
455
|
+
if speed < 30.0:
|
|
456
|
+
bar_color = "#00f0ff"
|
|
457
|
+
elif speed < 80.0:
|
|
458
|
+
bar_color = "#5af78e"
|
|
459
|
+
elif speed < 140.0:
|
|
460
|
+
bar_color = "#ffe600"
|
|
461
|
+
else:
|
|
462
|
+
bar_color = "#ff007f"
|
|
463
|
+
|
|
464
|
+
gauge = Text()
|
|
465
|
+
gauge.append("⚡ ", style="bold #ffe600")
|
|
466
|
+
gauge.append(f"{speed:5.1f} tok/s ", style=f"bold {bar_color}")
|
|
467
|
+
gauge.append("[", style="dim white")
|
|
468
|
+
gauge.append("█" * filled_bars, style=f"bold {bar_color}")
|
|
469
|
+
gauge.append("░" * empty_bars, style="dim #1e293b")
|
|
470
|
+
gauge.append("]", style="dim white")
|
|
471
|
+
gauge.append(f" (peak: {self.peak_speed:.0f})", style="dim #94a3b8")
|
|
472
|
+
return gauge
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
class CostTicker:
|
|
476
|
+
"""
|
|
477
|
+
Real-time USD Cost Ticker tracking cumulative session spending by model.
|
|
478
|
+
"""
|
|
479
|
+
|
|
480
|
+
def __init__(self, active_model: str = "Bankai-7B"):
|
|
481
|
+
self.active_model = active_model
|
|
482
|
+
self.total_prompt_tokens: int = 0
|
|
483
|
+
self.total_completion_tokens: int = 0
|
|
484
|
+
self.model_breakdown: Dict[str, Tuple[int, int]] = {}
|
|
485
|
+
|
|
486
|
+
def record_usage(self, model_name: str, prompt_tokens: int, completion_tokens: int) -> None:
|
|
487
|
+
"""Records token usage for a model."""
|
|
488
|
+
self.active_model = model_name
|
|
489
|
+
self.total_prompt_tokens += prompt_tokens
|
|
490
|
+
self.total_completion_tokens += completion_tokens
|
|
491
|
+
|
|
492
|
+
prev_p, prev_c = self.model_breakdown.get(model_name, (0, 0))
|
|
493
|
+
self.model_breakdown[model_name] = (prev_p + prompt_tokens, prev_c + completion_tokens)
|
|
494
|
+
|
|
495
|
+
@property
|
|
496
|
+
def total_cost(self) -> float:
|
|
497
|
+
"""Calculates total cumulative USD cost across all recorded models."""
|
|
498
|
+
total = 0.0
|
|
499
|
+
for m_name, (p_cnt, c_cnt) in self.model_breakdown.items():
|
|
500
|
+
total += calculate_token_cost(m_name, p_cnt, c_cnt)
|
|
501
|
+
return round(total, 6)
|
|
502
|
+
|
|
503
|
+
def render_ticker(self, compact: bool = False) -> Text:
|
|
504
|
+
"""
|
|
505
|
+
Renders glowing USD cost ticker text.
|
|
506
|
+
"""
|
|
507
|
+
cost = self.total_cost
|
|
508
|
+
tot_tokens = self.total_prompt_tokens + self.total_completion_tokens
|
|
509
|
+
is_free = (cost == 0.0)
|
|
510
|
+
|
|
511
|
+
ticker = Text()
|
|
512
|
+
ticker.append("💰 ", style="bold #ffe600")
|
|
513
|
+
|
|
514
|
+
if is_free:
|
|
515
|
+
ticker.append("$0.00 USD (LOCAL FREE)", style="bold #5af78e")
|
|
516
|
+
else:
|
|
517
|
+
ticker.append(f"${cost:.5f} USD", style="bold #00f0ff")
|
|
518
|
+
|
|
519
|
+
if not compact:
|
|
520
|
+
ticker.append(f" │ 📊 {tot_tokens:,} tokens", style="dim white")
|
|
521
|
+
ticker.append(f" │ 🤖 {self.active_model}", style="dim #94a3b8")
|
|
522
|
+
|
|
523
|
+
return ticker
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
# ==============================================================================
|
|
527
|
+
# 5. Dynamic Status Glow Badges
|
|
528
|
+
# ==============================================================================
|
|
529
|
+
|
|
530
|
+
class GlowBadgeStatus(str, Enum):
|
|
531
|
+
ONLINE = "online"
|
|
532
|
+
ACTIVE = "active"
|
|
533
|
+
SUCCESS = "success"
|
|
534
|
+
WARNING = "warning"
|
|
535
|
+
ERROR = "error"
|
|
536
|
+
IDLE = "idle"
|
|
537
|
+
INFO = "info"
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
STATUS_COLORS: Dict[GlowBadgeStatus, Tuple[str, str, str]] = {
|
|
541
|
+
# (fg_color, bg_color, border_color)
|
|
542
|
+
GlowBadgeStatus.ONLINE: ("#00ff88", "#082d1c", "#00ff88"),
|
|
543
|
+
GlowBadgeStatus.ACTIVE: ("#00f0ff", "#092336", "#00f0ff"),
|
|
544
|
+
GlowBadgeStatus.SUCCESS: ("#5af78e", "#0a2612", "#5af78e"),
|
|
545
|
+
GlowBadgeStatus.WARNING: ("#ffe600", "#332b00", "#ffe600"),
|
|
546
|
+
GlowBadgeStatus.ERROR: ("#ff3366", "#360914", "#ff3366"),
|
|
547
|
+
GlowBadgeStatus.IDLE: ("#94a3b8", "#121a29", "#1e293b"),
|
|
548
|
+
GlowBadgeStatus.INFO: ("#b026ff", "#220d36", "#b026ff"),
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
@dataclass
|
|
553
|
+
class StatusGlowBadge:
|
|
554
|
+
"""
|
|
555
|
+
Dynamic neon glow badge representation for terminal HUDs and Textual headers.
|
|
556
|
+
"""
|
|
557
|
+
label: str
|
|
558
|
+
value: str
|
|
559
|
+
status: GlowBadgeStatus = GlowBadgeStatus.ACTIVE
|
|
560
|
+
icon: str = ""
|
|
561
|
+
glow_color: Optional[str] = None
|
|
562
|
+
|
|
563
|
+
def render(self) -> Text:
|
|
564
|
+
"""Renders stylized Rich Text badge with neon border brackets."""
|
|
565
|
+
fg, bg, default_glow = STATUS_COLORS.get(self.status, STATUS_COLORS[GlowBadgeStatus.ACTIVE])
|
|
566
|
+
color = self.glow_color or fg
|
|
567
|
+
|
|
568
|
+
badge = Text()
|
|
569
|
+
badge.append("[", style="dim #1e293b")
|
|
570
|
+
if self.icon:
|
|
571
|
+
badge.append(f"{self.icon} ", style=f"bold {color}")
|
|
572
|
+
badge.append(f"{self.label}: ", style="dim white")
|
|
573
|
+
badge.append(f"{self.value}", style=f"bold {color}")
|
|
574
|
+
badge.append("]", style="dim #1e293b")
|
|
575
|
+
return badge
|
|
576
|
+
|
|
577
|
+
def render_as_tag(self) -> str:
|
|
578
|
+
"""Returns string with Rich markup tags."""
|
|
579
|
+
fg, _, _ = STATUS_COLORS.get(self.status, STATUS_COLORS[GlowBadgeStatus.ACTIVE])
|
|
580
|
+
color = self.glow_color or fg
|
|
581
|
+
icon_str = f"{self.icon} " if self.icon else ""
|
|
582
|
+
return f"[{color}][bold]{icon_str}{self.label}: {self.value}[/bold][/{color}]"
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def create_branch_badge(branch_name: str = "main", is_dirty: bool = False) -> StatusGlowBadge:
|
|
586
|
+
"""Creates a Git Branch glow badge."""
|
|
587
|
+
val = f"{branch_name}{'*' if is_dirty else ''}"
|
|
588
|
+
stat = GlowBadgeStatus.WARNING if is_dirty else GlowBadgeStatus.ONLINE
|
|
589
|
+
return StatusGlowBadge(label="Git", value=val, status=stat, icon="🌿")
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def create_model_badge(model_name: str = "Bankai-7B", is_active: bool = True) -> StatusGlowBadge:
|
|
593
|
+
"""Creates an Active Model glow badge."""
|
|
594
|
+
stat = GlowBadgeStatus.ACTIVE if is_active else GlowBadgeStatus.IDLE
|
|
595
|
+
return StatusGlowBadge(label="Model", value=model_name, status=stat, icon="🤖")
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def create_verifier_badge(
|
|
599
|
+
status: str = "PASS",
|
|
600
|
+
pass_rate: float = 1.0,
|
|
601
|
+
attempts: int = 1,
|
|
602
|
+
) -> StatusGlowBadge:
|
|
603
|
+
"""Creates an AST/Compiler Verifier Status glow badge."""
|
|
604
|
+
if status.upper() in ("PASS", "VERIFIED", "OK"):
|
|
605
|
+
stat = GlowBadgeStatus.SUCCESS
|
|
606
|
+
icon = "🛡️"
|
|
607
|
+
val = f"VERIFIED ({int(pass_rate*100)}%)"
|
|
608
|
+
elif status.upper() in ("WARN", "RETRYING"):
|
|
609
|
+
stat = GlowBadgeStatus.WARNING
|
|
610
|
+
icon = "⚠️"
|
|
611
|
+
val = f"RETRY ({attempts})"
|
|
612
|
+
else:
|
|
613
|
+
stat = GlowBadgeStatus.ERROR
|
|
614
|
+
icon = "✘"
|
|
615
|
+
val = f"FAILED ({attempts})"
|
|
616
|
+
|
|
617
|
+
return StatusGlowBadge(label="Verifier", value=val, status=stat, icon=icon)
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def create_mcp_badge(server_count: int = 0, active_tools: int = 0) -> StatusGlowBadge:
|
|
621
|
+
"""Creates an MCP Server count and tool availability glow badge."""
|
|
622
|
+
stat = GlowBadgeStatus.INFO if server_count > 0 else GlowBadgeStatus.IDLE
|
|
623
|
+
val = f"{server_count} svr ({active_tools} tools)" if active_tools > 0 else f"{server_count} svr"
|
|
624
|
+
return StatusGlowBadge(label="MCP", value=val, status=stat, icon="🔌")
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def create_ram_badge(ram_mb: float = 0.0, max_ram_mb: float = 1024.0) -> StatusGlowBadge:
|
|
628
|
+
"""Creates a RAM Allocation RSS glow badge with budget threshold colors."""
|
|
629
|
+
pct = (ram_mb / max_ram_mb) * 100 if max_ram_mb > 0 else 0.0
|
|
630
|
+
if pct > 90.0:
|
|
631
|
+
stat = GlowBadgeStatus.ERROR
|
|
632
|
+
elif pct > 70.0:
|
|
633
|
+
stat = GlowBadgeStatus.WARNING
|
|
634
|
+
else:
|
|
635
|
+
stat = GlowBadgeStatus.ONLINE
|
|
636
|
+
|
|
637
|
+
val = f"{ram_mb:.1f}MB ({pct:.0f}%)"
|
|
638
|
+
return StatusGlowBadge(label="RAM", value=val, status=stat, icon="💾")
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def render_hud_status_bar(badges: List[StatusGlowBadge]) -> Text:
|
|
642
|
+
"""Combines multiple glow badges into a continuous horizontal HUD status bar."""
|
|
643
|
+
bar = Text()
|
|
644
|
+
for i, b in enumerate(badges):
|
|
645
|
+
bar.append_text(b.render())
|
|
646
|
+
if i < len(badges) - 1:
|
|
647
|
+
bar.append(" ")
|
|
648
|
+
return bar
|