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
k_cli/tui/tui.py
ADDED
|
@@ -0,0 +1,1145 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tui.py - Modern Terminal User Interface Architecture for K-CLI
|
|
3
|
+
Project Bankai Engine v1.0.0
|
|
4
|
+
|
|
5
|
+
Features:
|
|
6
|
+
1. Live token streaming with syntax highlighting, animated spinners, and real-time metrics.
|
|
7
|
+
2. Dynamic Status Bar displaying Active Model, Git Branch, Active Persona, RAM, Tokens, Cost Ticker, and Glow Badges.
|
|
8
|
+
3. Interactive slash commands (/model, /persona, /diff, /rollback, /help, /docs, /clear, /test, /banner, /tree).
|
|
9
|
+
4. Side-by-side and inline surgical diff visualization with instant preview cards.
|
|
10
|
+
5. High-speed prompt_toolkit interactive shell with auto-completion and toolbar.
|
|
11
|
+
6. Animated subagent execution trees with status glyphs and DAG hierarchy.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import functools
|
|
17
|
+
import os
|
|
18
|
+
import sys
|
|
19
|
+
import time
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any, Callable, Dict, Generator, List, Optional, Tuple, Union
|
|
22
|
+
|
|
23
|
+
from rich.console import Console, Group
|
|
24
|
+
from rich.live import Live
|
|
25
|
+
from rich.markdown import Markdown
|
|
26
|
+
from rich.panel import Panel
|
|
27
|
+
from rich.syntax import Syntax
|
|
28
|
+
from rich.table import Table
|
|
29
|
+
from rich.text import Text
|
|
30
|
+
from rich.tree import Tree
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
from prompt_toolkit import PromptSession
|
|
34
|
+
from prompt_toolkit.completion import Completer, Completion
|
|
35
|
+
from prompt_toolkit.formatted_text import HTML
|
|
36
|
+
from prompt_toolkit.history import InMemoryHistory
|
|
37
|
+
from prompt_toolkit.styles import Style as PTKStyle
|
|
38
|
+
HAS_PROMPT_TOOLKIT = True
|
|
39
|
+
except ImportError:
|
|
40
|
+
HAS_PROMPT_TOOLKIT = False
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
import psutil
|
|
44
|
+
from textual.app import App, ComposeResult
|
|
45
|
+
from textual.binding import Binding
|
|
46
|
+
from textual.containers import Container, Horizontal, Vertical, ScrollableContainer
|
|
47
|
+
from textual.widgets import Header, Footer, Input, Static, Button, RichLog, Label
|
|
48
|
+
HAS_TEXTUAL = True
|
|
49
|
+
except (ImportError, ModuleNotFoundError):
|
|
50
|
+
HAS_TEXTUAL = False
|
|
51
|
+
App = object # type: ignore
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
from k_cli.tui.diff_viewer import DiffVisualizer
|
|
55
|
+
from k_cli.agents.orchestrator import Persona
|
|
56
|
+
from k_cli.agents.persona import DomainPersona, PersonaProfile, PersonaRegistry
|
|
57
|
+
from k_cli.tui.tui_animations import (
|
|
58
|
+
AnimatedSpinner,
|
|
59
|
+
CostTicker,
|
|
60
|
+
GlowBadgeStatus,
|
|
61
|
+
SpinnerType,
|
|
62
|
+
StatusGlowBadge,
|
|
63
|
+
TokenSpeedometer,
|
|
64
|
+
apply_gradient_to_text,
|
|
65
|
+
calculate_token_cost,
|
|
66
|
+
create_branch_badge,
|
|
67
|
+
create_mcp_badge,
|
|
68
|
+
create_model_badge,
|
|
69
|
+
create_ram_badge,
|
|
70
|
+
create_verifier_badge,
|
|
71
|
+
generate_splash_frames,
|
|
72
|
+
render_cyber_banner,
|
|
73
|
+
render_hud_status_bar,
|
|
74
|
+
)
|
|
75
|
+
except (ModuleNotFoundError, ImportError):
|
|
76
|
+
try:
|
|
77
|
+
from diff_viewer import DiffVisualizer
|
|
78
|
+
from orchestrator import Persona
|
|
79
|
+
from persona import DomainPersona, PersonaProfile, PersonaRegistry
|
|
80
|
+
from tui_animations import (
|
|
81
|
+
AnimatedSpinner,
|
|
82
|
+
CostTicker,
|
|
83
|
+
GlowBadgeStatus,
|
|
84
|
+
SpinnerType,
|
|
85
|
+
StatusGlowBadge,
|
|
86
|
+
TokenSpeedometer,
|
|
87
|
+
apply_gradient_to_text,
|
|
88
|
+
calculate_token_cost,
|
|
89
|
+
create_branch_badge,
|
|
90
|
+
create_mcp_badge,
|
|
91
|
+
create_model_badge,
|
|
92
|
+
create_ram_badge,
|
|
93
|
+
create_verifier_badge,
|
|
94
|
+
generate_splash_frames,
|
|
95
|
+
render_cyber_banner,
|
|
96
|
+
render_hud_status_bar,
|
|
97
|
+
)
|
|
98
|
+
except (ModuleNotFoundError, ImportError):
|
|
99
|
+
DiffVisualizer = None
|
|
100
|
+
Persona = None
|
|
101
|
+
PersonaRegistry = None
|
|
102
|
+
AnimatedSpinner = None
|
|
103
|
+
TokenSpeedometer = None
|
|
104
|
+
CostTicker = None
|
|
105
|
+
render_cyber_banner = None
|
|
106
|
+
create_branch_badge = None
|
|
107
|
+
create_model_badge = None
|
|
108
|
+
create_verifier_badge = None
|
|
109
|
+
create_mcp_badge = None
|
|
110
|
+
create_ram_badge = None
|
|
111
|
+
render_hud_status_bar = None
|
|
112
|
+
apply_gradient_to_text = None
|
|
113
|
+
generate_splash_frames = None
|
|
114
|
+
calculate_token_cost = None
|
|
115
|
+
|
|
116
|
+
# Model Presets
|
|
117
|
+
MODEL_PRESETS: List[Dict[str, str]] = [
|
|
118
|
+
{"name": "Bankai-7B", "desc": "Project Bankai Flagship 7B Coder (Fast & Compiler-Grounded)", "type": "SLM"},
|
|
119
|
+
{"name": "Bankai-14B", "desc": "Project Bankai Flagship 14B Deep Reasoning Engine", "type": "SLM"},
|
|
120
|
+
{"name": "Gemini", "desc": "Gemini 2.0 Flash / Pro (Cloud Multi-Modal & High-Throughput)", "type": "Cloud"},
|
|
121
|
+
{"name": "Claude", "desc": "Claude 3.5 Sonnet (Advanced Agentic Architecture & Refactoring)", "type": "Cloud"},
|
|
122
|
+
{"name": "Local Ollama", "desc": "Local GGUF SLM (e.g. qwen2.5-coder:1.5b / deepseek)", "type": "Local"},
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
PERSONA_METADATA: Dict[str, Dict[str, str]] = {
|
|
126
|
+
"DEVOPS": {"color": "cyan", "icon": "☸", "desc": "Docker, Kubernetes, CI/CD, Terraform, Cloud Deployments"},
|
|
127
|
+
"SURGICAL DEBUGGER": {"color": "red", "icon": "🩺", "desc": "Root-cause analysis, minimal SEARCH/REPLACE diffs, zero regression"},
|
|
128
|
+
"SYSTEMS ARCHITECT": {"color": "magenta", "icon": "⚡", "desc": "C++23, Rust, Linux Kernel, Lock-free concurrency, Big-O proofs"},
|
|
129
|
+
"APPLICATION SECURITY ENGINEER": {"color": "red", "icon": "🛡️", "desc": "OWASP Top 10, HMAC, Auth middlewares, Constant-time crypto"},
|
|
130
|
+
"FRONTEND & FULLSTACK ENGINEER": {"color": "green", "icon": "🎨", "desc": "React, Vite, Next.js, CSS layout, accessibility"},
|
|
131
|
+
"DATABASE & QUERY OPTIMIZER": {"color": "yellow", "icon": "🗄️", "desc": "PostgreSQL, Redis, Spanner, SQL query optimization"},
|
|
132
|
+
"FULLSTACK AI SYSTEMS ENGINEER": {"color": "blue", "icon": "⚙", "desc": "Clean architecture, compiler-grounded verification"},
|
|
133
|
+
"RESEARCHER": {"color": "cyan", "icon": "🔍", "desc": "Extracts signatures, API dependencies, specifications"},
|
|
134
|
+
"ARCHITECT": {"color": "magenta", "icon": "📐", "desc": "Designs modular architecture & execution plan"},
|
|
135
|
+
"CODER": {"color": "green", "icon": "⚡", "desc": "Generates isolated, verified code implementation"},
|
|
136
|
+
"CRITIC": {"color": "yellow", "icon": "🛡️", "desc": "Audits safety, boundaries, memory & runtime limits"},
|
|
137
|
+
"DEBUGGER": {"color": "red", "icon": "🔧", "desc": "Analyzes compiler traces and applies surgical repairs"},
|
|
138
|
+
"AUTO": {"color": "bright_blue", "icon": "🔄", "desc": "Full sequential multi-persona pipeline"},
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@functools.lru_cache(maxsize=128)
|
|
143
|
+
def get_persona_style(persona_name: str) -> Tuple[str, str, str]:
|
|
144
|
+
"""Returns (color, icon, description) for a given persona."""
|
|
145
|
+
if not persona_name:
|
|
146
|
+
return "blue", "🤖", "AI Assistant Persona"
|
|
147
|
+
|
|
148
|
+
key = str(persona_name).upper().strip()
|
|
149
|
+
if key in PERSONA_METADATA:
|
|
150
|
+
p_v = PERSONA_METADATA[key]
|
|
151
|
+
return p_v["color"], p_v["icon"], p_v["desc"]
|
|
152
|
+
|
|
153
|
+
# Check PersonaRegistry first if available
|
|
154
|
+
if PersonaRegistry:
|
|
155
|
+
prof = PersonaRegistry.get(persona_name)
|
|
156
|
+
if prof is not None:
|
|
157
|
+
return prof.color, prof.icon, prof.description
|
|
158
|
+
|
|
159
|
+
for p_k, p_v in PERSONA_METADATA.items():
|
|
160
|
+
if p_k in key or key in p_k:
|
|
161
|
+
return p_v["color"], p_v["icon"], p_v["desc"]
|
|
162
|
+
return "blue", "🤖", "AI Assistant Persona"
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# ==============================================================================
|
|
166
|
+
# 1. Instant Diff Preview Card & Subagent Execution Tree Renderers
|
|
167
|
+
# ==============================================================================
|
|
168
|
+
|
|
169
|
+
def render_instant_diff_card(
|
|
170
|
+
diff_text: str = "",
|
|
171
|
+
file_path: str = "",
|
|
172
|
+
old_code: str = "",
|
|
173
|
+
new_code: str = "",
|
|
174
|
+
title: str = "Instant Surgical Diff Card",
|
|
175
|
+
) -> Panel:
|
|
176
|
+
"""
|
|
177
|
+
Renders a compact, glowing surgical diff preview card with line counts and syntax highlighting.
|
|
178
|
+
"""
|
|
179
|
+
if not diff_text.strip() and old_code and new_code:
|
|
180
|
+
# Generate simple unified diff preview from old and new code
|
|
181
|
+
import difflib
|
|
182
|
+
lines = difflib.unified_diff(
|
|
183
|
+
old_code.splitlines(keepends=True),
|
|
184
|
+
new_code.splitlines(keepends=True),
|
|
185
|
+
fromfile=f"a/{file_path or 'original.py'}",
|
|
186
|
+
tofile=f"b/{file_path or 'repaired.py'}",
|
|
187
|
+
)
|
|
188
|
+
diff_text = "".join(lines)
|
|
189
|
+
|
|
190
|
+
if not diff_text.strip():
|
|
191
|
+
return Panel(
|
|
192
|
+
Text("Working tree is clean — no diffs detected.", style="dim italic"),
|
|
193
|
+
title=f"[bold #00f0ff]{title}[/bold #00f0ff]",
|
|
194
|
+
border_style="#00f0ff",
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Count additions and deletions
|
|
198
|
+
additions = sum(1 for line in diff_text.splitlines() if line.startswith("+") and not line.startswith("+++"))
|
|
199
|
+
deletions = sum(1 for line in diff_text.splitlines() if line.startswith("-") and not line.startswith("---"))
|
|
200
|
+
|
|
201
|
+
header_text = Text()
|
|
202
|
+
if file_path:
|
|
203
|
+
header_text.append(f"📄 {file_path} │ ", style="bold white")
|
|
204
|
+
header_text.append(f"+{additions} ", style="bold #00ff88")
|
|
205
|
+
header_text.append(f"-{deletions} ", style="bold #ff3366")
|
|
206
|
+
header_text.append("lines changed", style="dim white")
|
|
207
|
+
|
|
208
|
+
# Style diff lines
|
|
209
|
+
formatted_body = Text()
|
|
210
|
+
for line in diff_text.splitlines()[:60]: # limit lines for card preview
|
|
211
|
+
if line.startswith("+++") or line.startswith("---"):
|
|
212
|
+
formatted_body.append(line + "\n", style="bold #ffe600")
|
|
213
|
+
elif line.startswith("@@"):
|
|
214
|
+
formatted_body.append(line + "\n", style="bold #b026ff")
|
|
215
|
+
elif line.startswith("+"):
|
|
216
|
+
formatted_body.append(line + "\n", style="bold #00ff88")
|
|
217
|
+
elif line.startswith("-"):
|
|
218
|
+
formatted_body.append(line + "\n", style="bold #ff3366")
|
|
219
|
+
else:
|
|
220
|
+
formatted_body.append(line + "\n", style="dim white")
|
|
221
|
+
|
|
222
|
+
card_content = Group(
|
|
223
|
+
header_text,
|
|
224
|
+
Text("─" * 40, style="dim #1e293b"),
|
|
225
|
+
formatted_body,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
return Panel(
|
|
229
|
+
card_content,
|
|
230
|
+
title=f"[bold #00f0ff]⚡ {title}[/bold #00f0ff]",
|
|
231
|
+
subtitle=f"[dim #5af78e]Surgical Patch Guard[/dim #5af78e]",
|
|
232
|
+
border_style="#00f0ff",
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def render_subagent_execution_tree(
|
|
237
|
+
tasks: List[Any],
|
|
238
|
+
title: str = "Subagent Swarm Execution Tree",
|
|
239
|
+
) -> Panel:
|
|
240
|
+
"""
|
|
241
|
+
Renders an animated hierarchical Rich Tree of subagent execution tasks,
|
|
242
|
+
displaying roles, status glyphs, elapsed durations, progress meters, and token metrics.
|
|
243
|
+
"""
|
|
244
|
+
root_label = Text(f"📦 {title} ({len(tasks)} Subagents)", style="bold #00f0ff")
|
|
245
|
+
tree = Tree(root_label)
|
|
246
|
+
|
|
247
|
+
role_glyphs = {
|
|
248
|
+
"EXPLORER": "🔍",
|
|
249
|
+
"RESEARCHER": "📚",
|
|
250
|
+
"REFACTORER": "🔨",
|
|
251
|
+
"CODER": "⚡",
|
|
252
|
+
"TESTER": "🧪",
|
|
253
|
+
"CRITIC": "🛡️",
|
|
254
|
+
"ARCHITECT": "📐",
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
status_styles = {
|
|
258
|
+
"COMPLETED": ("🟢", "#00ff88", "Done"),
|
|
259
|
+
"RUNNING": ("🟡", "#ffe600", "Running"),
|
|
260
|
+
"PENDING": ("🔵", "#00f0ff", "Queued"),
|
|
261
|
+
"FAILED": ("🔴", "#ff3366", "Failed"),
|
|
262
|
+
"CANCELLED": ("🚫", "#94a3b8", "Cancelled"),
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
for task in tasks:
|
|
266
|
+
# Extract attributes from SubagentTask object or dictionary
|
|
267
|
+
role_str = getattr(task, "role", "CODER")
|
|
268
|
+
if hasattr(role_str, "value"):
|
|
269
|
+
role_str = role_str.value
|
|
270
|
+
role_str = str(role_str).upper()
|
|
271
|
+
|
|
272
|
+
status_str = getattr(task, "status", "PENDING")
|
|
273
|
+
if hasattr(status_str, "value"):
|
|
274
|
+
status_str = status_str.value
|
|
275
|
+
status_str = str(status_str).upper()
|
|
276
|
+
|
|
277
|
+
name = getattr(task, "name", getattr(task, "task_id", "Subtask"))
|
|
278
|
+
prompt = getattr(task, "prompt", getattr(task, "status_message", ""))
|
|
279
|
+
duration = getattr(task, "duration_sec", 0.0)
|
|
280
|
+
tokens = getattr(task, "token_count", getattr(task, "tokens", 0))
|
|
281
|
+
|
|
282
|
+
glyph, color, st_text = status_styles.get(status_str, ("🔵", "#00f0ff", status_str))
|
|
283
|
+
r_glyph = role_glyphs.get(role_str, "🤖")
|
|
284
|
+
|
|
285
|
+
node_text = Text()
|
|
286
|
+
node_text.append(f"{glyph} {r_glyph} ", style=f"bold {color}")
|
|
287
|
+
node_text.append(f"[{role_str}] ", style="bold white")
|
|
288
|
+
node_text.append(f"{name} ", style=f"bold {color}")
|
|
289
|
+
node_text.append(f"({st_text})", style=f"dim {color}")
|
|
290
|
+
|
|
291
|
+
node = tree.add(node_text)
|
|
292
|
+
|
|
293
|
+
# Add detail leaves
|
|
294
|
+
if prompt:
|
|
295
|
+
node.add(Text(f"🎯 Objective: {str(prompt)[:80]}...", style="dim white"))
|
|
296
|
+
|
|
297
|
+
metrics_text = Text()
|
|
298
|
+
metrics_text.append("⚡ Metrics: ", style="dim #ffe600")
|
|
299
|
+
if duration > 0:
|
|
300
|
+
metrics_text.append(f"⏱️ {duration:.2f}s │ ", style="dim white")
|
|
301
|
+
if tokens > 0:
|
|
302
|
+
metrics_text.append(f"📊 {tokens} tokens │ ", style="dim white")
|
|
303
|
+
metrics_text.append("Verified Execution", style="dim #00ff88")
|
|
304
|
+
node.add(metrics_text)
|
|
305
|
+
|
|
306
|
+
return Panel(
|
|
307
|
+
tree,
|
|
308
|
+
title="[bold #00f0ff]◈ SWARM RADAR & EXECUTION TOPOLOGY ◈[/bold #00f0ff]",
|
|
309
|
+
border_style="#00f0ff",
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
# ==============================================================================
|
|
314
|
+
# 2. Enhanced Status Bar Manager
|
|
315
|
+
# ==============================================================================
|
|
316
|
+
|
|
317
|
+
class StatusBar:
|
|
318
|
+
"""Manages active session parameters and formats top/bottom status displays with glowing badges."""
|
|
319
|
+
|
|
320
|
+
def __init__(
|
|
321
|
+
self,
|
|
322
|
+
active_model: str = "Bankai-7B",
|
|
323
|
+
git_branch: str = "main",
|
|
324
|
+
active_persona: str = "AUTO",
|
|
325
|
+
ram_mb: float = 0.0,
|
|
326
|
+
max_ram_mb: float = 1024.0,
|
|
327
|
+
token_count: int = 0,
|
|
328
|
+
max_tokens: int = 4096,
|
|
329
|
+
context_files: Optional[List[str]] = None,
|
|
330
|
+
verifier_status: str = "VERIFIED",
|
|
331
|
+
mcp_server_count: int = 4,
|
|
332
|
+
):
|
|
333
|
+
self.active_model = active_model
|
|
334
|
+
self.git_branch = git_branch
|
|
335
|
+
self.active_persona = active_persona
|
|
336
|
+
self.ram_mb = ram_mb
|
|
337
|
+
self.max_ram_mb = max_ram_mb
|
|
338
|
+
self.token_count = token_count
|
|
339
|
+
self.max_tokens = max_tokens
|
|
340
|
+
self.context_files = context_files or []
|
|
341
|
+
self.verifier_status = verifier_status
|
|
342
|
+
self.mcp_server_count = mcp_server_count
|
|
343
|
+
self.speedometer = TokenSpeedometer() if TokenSpeedometer else None
|
|
344
|
+
self.cost_ticker = CostTicker(active_model=self.active_model) if CostTicker else None
|
|
345
|
+
|
|
346
|
+
def update_from_session(self, session: Any) -> None:
|
|
347
|
+
"""Syncs status bar properties from SessionManager status dict."""
|
|
348
|
+
if not session:
|
|
349
|
+
return
|
|
350
|
+
st = session.get_status() if hasattr(session, "get_status") else {}
|
|
351
|
+
self.active_model = st.get("model") or st.get("model_name") or self.active_model
|
|
352
|
+
self.git_branch = st.get("git_branch") or (session.get_git_branch() if hasattr(session, "get_git_branch") else self.git_branch)
|
|
353
|
+
self.active_persona = st.get("persona") or st.get("active_persona") or getattr(session, "active_persona", self.active_persona)
|
|
354
|
+
self.ram_mb = st.get("ram_mb", 0.0)
|
|
355
|
+
self.token_count = st.get("token_count", 0)
|
|
356
|
+
self.max_tokens = st.get("max_tokens", self.max_tokens)
|
|
357
|
+
self.context_files = st.get("context_files", [])
|
|
358
|
+
|
|
359
|
+
if self.cost_ticker:
|
|
360
|
+
self.cost_ticker.active_model = self.active_model
|
|
361
|
+
|
|
362
|
+
def get_prompt_toolkit_toolbar(self) -> HTML:
|
|
363
|
+
"""Returns stylized HTML for prompt_toolkit bottom toolbar."""
|
|
364
|
+
p_color, p_icon, _ = get_persona_style(self.active_persona)
|
|
365
|
+
ptk_color_map = {
|
|
366
|
+
"cyan": "#00d7ff",
|
|
367
|
+
"magenta": "#ff00d7",
|
|
368
|
+
"green": "#5af78e",
|
|
369
|
+
"yellow": "#f3e430",
|
|
370
|
+
"red": "#ff5c57",
|
|
371
|
+
"bright_blue": "#57c7ff",
|
|
372
|
+
"blue": "#57c7ff",
|
|
373
|
+
}
|
|
374
|
+
hex_p = ptk_color_map.get(p_color, "#57c7ff")
|
|
375
|
+
files_str = f"{len(self.context_files)} files" if self.context_files else "0 files"
|
|
376
|
+
cost_str = f"${self.cost_ticker.total_cost:.4f}" if self.cost_ticker else "$0.00"
|
|
377
|
+
|
|
378
|
+
return HTML(
|
|
379
|
+
f' <b>Model:</b> <style color="#00ffff">{self.active_model}</style> │ '
|
|
380
|
+
f'<b>Branch:</b> <style color="#5af78e">{self.git_branch}</style> │ '
|
|
381
|
+
f'<b>Persona:</b> <style color="{hex_p}">{p_icon} {self.active_persona}</style> │ '
|
|
382
|
+
f'<b>RAM:</b> <style color="#ffb86c">{self.ram_mb:.1f}/{self.max_ram_mb:.0f}MB</style> │ '
|
|
383
|
+
f'<b>Cost:</b> <style color="#ffe600">{cost_str}</style> │ '
|
|
384
|
+
f'<b>Context:</b> <style color="#8be9fd">{files_str}</style>'
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
def render_rich_panel(self) -> Panel:
|
|
388
|
+
"""Renders full diagnostic HUD panel with glow badges and speedometer."""
|
|
389
|
+
p_color, p_icon, p_desc = get_persona_style(self.active_persona)
|
|
390
|
+
|
|
391
|
+
# Generate badges
|
|
392
|
+
badges = []
|
|
393
|
+
if create_model_badge:
|
|
394
|
+
badges.append(create_model_badge(self.active_model, is_active=True))
|
|
395
|
+
if create_branch_badge:
|
|
396
|
+
badges.append(create_branch_badge(self.git_branch, is_dirty=False))
|
|
397
|
+
if create_verifier_badge:
|
|
398
|
+
badges.append(create_verifier_badge(self.verifier_status, pass_rate=1.0))
|
|
399
|
+
if create_mcp_badge:
|
|
400
|
+
badges.append(create_mcp_badge(self.mcp_server_count, active_tools=12))
|
|
401
|
+
if create_ram_badge:
|
|
402
|
+
badges.append(create_ram_badge(self.ram_mb, self.max_ram_mb))
|
|
403
|
+
|
|
404
|
+
hud_bar = render_hud_status_bar(badges) if render_hud_status_bar and badges else Text()
|
|
405
|
+
|
|
406
|
+
table = Table(box=None, expand=True, padding=(0, 1))
|
|
407
|
+
table.add_column("Parameter", style="bold cyan", width=24)
|
|
408
|
+
table.add_column("Value", style="bold white")
|
|
409
|
+
|
|
410
|
+
table.add_row("⚡ Active Model", f"[bold green]{self.active_model}[/bold green]")
|
|
411
|
+
table.add_row("🌿 Git Branch", f"[bold yellow]{self.git_branch}[/bold yellow]")
|
|
412
|
+
table.add_row(f"{p_icon} Active Persona", f"[{p_color}][bold]{self.active_persona}[/bold] - {p_desc}[/{p_color}]")
|
|
413
|
+
table.add_row("💾 RAM RSS Allocation", f"[bold magenta]{self.ram_mb:.2f} MB[/bold magenta] / {self.max_ram_mb:.0f} MB (Budget Limit)")
|
|
414
|
+
table.add_row("📊 Estimated Tokens", f"{self.token_count} / {self.max_tokens} max")
|
|
415
|
+
|
|
416
|
+
if self.cost_ticker:
|
|
417
|
+
ticker_text = self.cost_ticker.render_ticker()
|
|
418
|
+
table.add_row("💰 Real-Time Cost Ticker", ticker_text.markup if hasattr(ticker_text, "markup") else str(ticker_text))
|
|
419
|
+
|
|
420
|
+
if self.speedometer:
|
|
421
|
+
gauge_text = self.speedometer.render_gauge()
|
|
422
|
+
table.add_row("🏎️ Token Speedometer", gauge_text.markup if hasattr(gauge_text, "markup") else str(gauge_text))
|
|
423
|
+
|
|
424
|
+
files_text = ", ".join(self.context_files) if self.context_files else "[dim]None (use /add <file>)[/dim]"
|
|
425
|
+
table.add_row("📁 Tracked Files", files_text)
|
|
426
|
+
|
|
427
|
+
panel_content = Group(
|
|
428
|
+
hud_bar,
|
|
429
|
+
Text("─" * 60, style="dim #1e293b"),
|
|
430
|
+
table,
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
return Panel(
|
|
434
|
+
panel_content,
|
|
435
|
+
title="[bold #00f0ff]◈ K-CLI CYBER DIAGNOSTIC HUD ◈[/bold #00f0ff]",
|
|
436
|
+
subtitle="[dim #7000ff]Ground-Truth Verifier Active[/dim #7000ff]",
|
|
437
|
+
border_style="#00f0ff",
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
# ==============================================================================
|
|
442
|
+
# 3. Live Token Streaming Renderer with Cyber Animations
|
|
443
|
+
# ==============================================================================
|
|
444
|
+
|
|
445
|
+
class LiveStreamRenderer:
|
|
446
|
+
"""
|
|
447
|
+
Manages real-time token streaming with automatic code fence syntax highlighting,
|
|
448
|
+
animated cyberpunk spinners, live token speedometer, and glowing status badges.
|
|
449
|
+
"""
|
|
450
|
+
|
|
451
|
+
def __init__(self, console: Optional[Console] = None):
|
|
452
|
+
self.console = console or Console()
|
|
453
|
+
self.speedometer = TokenSpeedometer() if TokenSpeedometer else None
|
|
454
|
+
self.cost_ticker = CostTicker() if CostTicker else None
|
|
455
|
+
self.spinner = AnimatedSpinner(spinner_type=SpinnerType.QUANTUM_FLUX) if AnimatedSpinner else None
|
|
456
|
+
|
|
457
|
+
def stream_display(
|
|
458
|
+
self,
|
|
459
|
+
token_generator: Generator[str, None, Dict[str, Any]],
|
|
460
|
+
initial_persona: str = "RESEARCHER",
|
|
461
|
+
language: str = "python",
|
|
462
|
+
title: str = "Agent Execution",
|
|
463
|
+
model_name: str = "Bankai-7B",
|
|
464
|
+
) -> Dict[str, Any]:
|
|
465
|
+
"""
|
|
466
|
+
Consumes tokens from generator, dynamically highlighting syntax and updating Rich Live view
|
|
467
|
+
with real-time tok/s speedometer and animated cyberpunk spinners.
|
|
468
|
+
"""
|
|
469
|
+
current_persona = initial_persona
|
|
470
|
+
accumulated_text = ""
|
|
471
|
+
token_count = 0
|
|
472
|
+
start_time = time.time()
|
|
473
|
+
step_index = 0
|
|
474
|
+
|
|
475
|
+
if self.cost_ticker:
|
|
476
|
+
self.cost_ticker.active_model = model_name
|
|
477
|
+
|
|
478
|
+
def make_panel() -> Panel:
|
|
479
|
+
nonlocal step_index
|
|
480
|
+
step_index += 1
|
|
481
|
+
elapsed = max(0.001, time.time() - start_time)
|
|
482
|
+
speed = token_count / elapsed
|
|
483
|
+
p_color, p_icon, _ = get_persona_style(current_persona)
|
|
484
|
+
|
|
485
|
+
# Spinner frame
|
|
486
|
+
spinner_str = ""
|
|
487
|
+
if self.spinner:
|
|
488
|
+
spinner_str = f"{self.spinner.frames[step_index % len(self.spinner.frames)]} "
|
|
489
|
+
|
|
490
|
+
# Calculate cost ticker
|
|
491
|
+
cost_val = calculate_token_cost(model_name, 0, token_count) if calculate_token_cost else 0.0
|
|
492
|
+
cost_display = f"$0.00 (Local)" if cost_val == 0.0 else f"${cost_val:.5f}"
|
|
493
|
+
|
|
494
|
+
header = (
|
|
495
|
+
f"[{p_color}][bold]{p_icon} {current_persona}[/bold][/{p_color}] │ "
|
|
496
|
+
f"[bold #ffe600]{spinner_str}[/bold #ffe600] │ "
|
|
497
|
+
f"[bold #00f0ff]{speed:.1f} tok/s[/bold #00f0ff] │ "
|
|
498
|
+
f"[dim #5af78e]{token_count} tokens[/dim #5af78e] │ "
|
|
499
|
+
f"[bold #ffe600]{cost_display}[/bold #ffe600]"
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
if not accumulated_text.strip():
|
|
503
|
+
content = Text(f"⚡ Initializing {current_persona} pipeline stream...", style="dim italic")
|
|
504
|
+
elif "```" in accumulated_text:
|
|
505
|
+
# Detect and highlight code fences
|
|
506
|
+
try:
|
|
507
|
+
content = Markdown(accumulated_text)
|
|
508
|
+
except Exception:
|
|
509
|
+
content = Text(accumulated_text)
|
|
510
|
+
elif current_persona in ("CODER", "DEBUGGER"):
|
|
511
|
+
# Pure code output
|
|
512
|
+
try:
|
|
513
|
+
content = Syntax(accumulated_text, language, theme="monokai", line_numbers=True)
|
|
514
|
+
except Exception:
|
|
515
|
+
content = Text(accumulated_text, style="green")
|
|
516
|
+
else:
|
|
517
|
+
content = Text(accumulated_text, style="white")
|
|
518
|
+
|
|
519
|
+
return Panel(
|
|
520
|
+
content,
|
|
521
|
+
title=f"[bold {p_color}]{header}[/bold {p_color}]",
|
|
522
|
+
subtitle="[dim #7000ff]Streaming Live • Ground-Truth Guard[/dim #7000ff]",
|
|
523
|
+
border_style=p_color,
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
with Live(make_panel(), console=self.console, refresh_per_second=15, auto_refresh=True) as live:
|
|
527
|
+
for token in token_generator:
|
|
528
|
+
token_count += 1
|
|
529
|
+
accumulated_text += token
|
|
530
|
+
if self.speedometer:
|
|
531
|
+
self.speedometer.record_tokens(1)
|
|
532
|
+
live.update(make_panel())
|
|
533
|
+
|
|
534
|
+
elapsed_total = time.time() - start_time
|
|
535
|
+
final_cost = calculate_token_cost(model_name, 0, token_count) if calculate_token_cost else 0.0
|
|
536
|
+
|
|
537
|
+
if self.cost_ticker:
|
|
538
|
+
self.cost_ticker.record_usage(model_name, 0, token_count)
|
|
539
|
+
|
|
540
|
+
return {
|
|
541
|
+
"total_tokens": token_count,
|
|
542
|
+
"elapsed_seconds": elapsed_total,
|
|
543
|
+
"final_text": accumulated_text,
|
|
544
|
+
"speed_tok_s": round(token_count / max(0.001, elapsed_total), 1),
|
|
545
|
+
"cost_usd": final_cost,
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
# ==============================================================================
|
|
550
|
+
# 4. Prompt Toolkit Slash Command Completer
|
|
551
|
+
# ==============================================================================
|
|
552
|
+
|
|
553
|
+
if HAS_PROMPT_TOOLKIT:
|
|
554
|
+
class SlashCommandCompleter(Completer):
|
|
555
|
+
"""Auto-completes slash commands with rich descriptions in prompt_toolkit."""
|
|
556
|
+
|
|
557
|
+
COMMANDS = [
|
|
558
|
+
("/model", "Switch active model (Bankai-7B, Bankai-14B, Gemini, Claude, Local Ollama)"),
|
|
559
|
+
("/persona", "Switch active persona (RESEARCHER, ARCHITECT, CODER, CRITIC, DEBUGGER, AUTO)"),
|
|
560
|
+
("/diff", "View surgical / git diff (inline or side-by-side)"),
|
|
561
|
+
("/rollback", "Roll back last uncommitted edit via Git (alias /undo)"),
|
|
562
|
+
("/help", "Show all slash commands, shortcuts, and capabilities"),
|
|
563
|
+
("/docs", "Search DevDocs offline documentation index (alias /doc)"),
|
|
564
|
+
("/clear", "Reset conversation history and context files"),
|
|
565
|
+
("/test", "Run ground-truth compiler and pytest verification"),
|
|
566
|
+
("/banner", "Display cyberpunk animated gradient ASCII splash banner"),
|
|
567
|
+
("/tree", "Display live subagent execution tree and swarm metrics"),
|
|
568
|
+
("/speed", "Inspect real-time token throughput speedometer and cost ticker"),
|
|
569
|
+
("/add", "Add file to active session context"),
|
|
570
|
+
("/remove", "Remove file from active session context"),
|
|
571
|
+
("/undo", "Roll back last uncommitted edit via Git"),
|
|
572
|
+
("/status", "Display active model, context files, tokens, cost, and RAM"),
|
|
573
|
+
("/map", "Display workspace AST symbol repository map"),
|
|
574
|
+
("/exit", "Exit interactive session"),
|
|
575
|
+
("/quit", "Exit interactive session"),
|
|
576
|
+
]
|
|
577
|
+
|
|
578
|
+
MODEL_OPTIONS = [m["name"] for m in MODEL_PRESETS]
|
|
579
|
+
PERSONA_OPTIONS = ["AUTO", "RESEARCHER", "ARCHITECT", "CODER", "CRITIC", "DEBUGGER"]
|
|
580
|
+
DIFF_OPTIONS = ["inline", "side-by-side", "sbs", "card"]
|
|
581
|
+
|
|
582
|
+
def get_completions(self, document, complete_event):
|
|
583
|
+
text = document.text_before_cursor
|
|
584
|
+
if not text.startswith("/"):
|
|
585
|
+
return
|
|
586
|
+
|
|
587
|
+
parts = text.split()
|
|
588
|
+
if len(parts) == 1 and not text.endswith(" "):
|
|
589
|
+
# Completing slash command name
|
|
590
|
+
query = parts[0].lower()
|
|
591
|
+
for cmd, desc in self.COMMANDS:
|
|
592
|
+
if cmd.startswith(query):
|
|
593
|
+
yield Completion(cmd, start_position=-len(query), display=cmd, display_meta=desc)
|
|
594
|
+
elif len(parts) >= 1:
|
|
595
|
+
cmd = parts[0].lower()
|
|
596
|
+
sub_query = parts[1].lower() if len(parts) > 1 and not text.endswith(" ") else ""
|
|
597
|
+
start_pos = -len(sub_query) if sub_query else 0
|
|
598
|
+
|
|
599
|
+
if cmd == "/model":
|
|
600
|
+
for m in self.MODEL_OPTIONS:
|
|
601
|
+
if not sub_query or m.lower().startswith(sub_query):
|
|
602
|
+
yield Completion(m, start_position=start_pos, display=m)
|
|
603
|
+
elif cmd == "/persona":
|
|
604
|
+
for p in self.PERSONA_OPTIONS:
|
|
605
|
+
if not sub_query or p.lower().startswith(sub_query):
|
|
606
|
+
yield Completion(p, start_position=start_pos, display=p)
|
|
607
|
+
elif cmd == "/diff":
|
|
608
|
+
for d in self.DIFF_OPTIONS:
|
|
609
|
+
if not sub_query or d.lower().startswith(sub_query):
|
|
610
|
+
yield Completion(d, start_position=start_pos, display=d)
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
# ==============================================================================
|
|
614
|
+
# 5. Interactive Slash Commands Handler
|
|
615
|
+
# ==============================================================================
|
|
616
|
+
|
|
617
|
+
class SlashCommandHandler:
|
|
618
|
+
"""Handles and visually formats interactive slash commands."""
|
|
619
|
+
|
|
620
|
+
def __init__(self, session: Any, console: Optional[Console] = None):
|
|
621
|
+
self.session = session
|
|
622
|
+
self.console = console or Console()
|
|
623
|
+
self.diff_visualizer = DiffVisualizer(console=self.console) if DiffVisualizer else None
|
|
624
|
+
|
|
625
|
+
def handle(self, command_line: str) -> Tuple[bool, str]:
|
|
626
|
+
"""
|
|
627
|
+
Routes slash command and renders rich terminal output.
|
|
628
|
+
|
|
629
|
+
Returns:
|
|
630
|
+
Tuple[bool, str]: (should_continue, exit_signal_or_status)
|
|
631
|
+
"""
|
|
632
|
+
raw = command_line.strip()
|
|
633
|
+
if not raw.startswith("/"):
|
|
634
|
+
return True, ""
|
|
635
|
+
|
|
636
|
+
parts = raw[1:].split(None, 1)
|
|
637
|
+
cmd = parts[0].lower() if parts else ""
|
|
638
|
+
arg = parts[1].strip() if len(parts) > 1 else ""
|
|
639
|
+
|
|
640
|
+
# /exit, /quit
|
|
641
|
+
if cmd in ("exit", "quit", "q"):
|
|
642
|
+
self.console.print("[bold dim]Exiting K-CLI. Goodbye![/bold dim]")
|
|
643
|
+
return False, "EXIT"
|
|
644
|
+
|
|
645
|
+
# /help
|
|
646
|
+
if cmd in ("help", "?"):
|
|
647
|
+
self._render_help()
|
|
648
|
+
return True, "HELP_RENDERED"
|
|
649
|
+
|
|
650
|
+
# /banner, /splash
|
|
651
|
+
if cmd in ("banner", "splash"):
|
|
652
|
+
if render_cyber_banner:
|
|
653
|
+
self.console.print(render_cyber_banner(palette="neon_cyan"))
|
|
654
|
+
else:
|
|
655
|
+
self.console.print("[bold cyan]K-CLI Bankai Engine v1.0.0[/bold cyan]")
|
|
656
|
+
return True, "BANNER_RENDERED"
|
|
657
|
+
|
|
658
|
+
# /tree, /swarm
|
|
659
|
+
if cmd in ("tree", "swarm"):
|
|
660
|
+
self._handle_tree()
|
|
661
|
+
return True, "TREE_RENDERED"
|
|
662
|
+
|
|
663
|
+
# /speed, /cost
|
|
664
|
+
if cmd in ("speed", "cost", "ticker"):
|
|
665
|
+
self._handle_speed_and_cost()
|
|
666
|
+
return True, "SPEED_RENDERED"
|
|
667
|
+
|
|
668
|
+
# /model
|
|
669
|
+
if cmd == "model":
|
|
670
|
+
self._handle_model(arg)
|
|
671
|
+
return True, "MODEL_HANDLED"
|
|
672
|
+
|
|
673
|
+
# /persona
|
|
674
|
+
if cmd in ("persona", "role"):
|
|
675
|
+
self._handle_persona(arg)
|
|
676
|
+
return True, "PERSONA_HANDLED"
|
|
677
|
+
|
|
678
|
+
# /diff
|
|
679
|
+
if cmd == "diff":
|
|
680
|
+
self._handle_diff(arg)
|
|
681
|
+
return True, "DIFF_HANDLED"
|
|
682
|
+
|
|
683
|
+
# /rollback or /undo
|
|
684
|
+
if cmd in ("rollback", "undo"):
|
|
685
|
+
self._handle_rollback(arg)
|
|
686
|
+
return True, "ROLLBACK_HANDLED"
|
|
687
|
+
|
|
688
|
+
# /docs or /doc
|
|
689
|
+
if cmd in ("docs", "doc"):
|
|
690
|
+
self._handle_docs(arg)
|
|
691
|
+
return True, "DOCS_HANDLED"
|
|
692
|
+
|
|
693
|
+
# /clear
|
|
694
|
+
if cmd in ("clear", "cls"):
|
|
695
|
+
self.console.clear()
|
|
696
|
+
self.session.reset_context()
|
|
697
|
+
self.console.print("[bold green]✔ Screen, conversation history, and context files cleared.[/bold green]\n")
|
|
698
|
+
return True, "CLEARED"
|
|
699
|
+
|
|
700
|
+
# /test or /verify
|
|
701
|
+
if cmd in ("test", "verify"):
|
|
702
|
+
self._handle_test(arg)
|
|
703
|
+
return True, "TEST_HANDLED"
|
|
704
|
+
|
|
705
|
+
# /keys, /api, /vault
|
|
706
|
+
if cmd in ("keys", "api", "vault", "creds"):
|
|
707
|
+
self.console.print(Panel(
|
|
708
|
+
"[bold cyan]🔑 K-CLI Credentials Vault[/bold cyan]\n\n"
|
|
709
|
+
f"• GITHUB_TOKEN: {'[green]Configured[/green]' if os.environ.get('GITHUB_TOKEN') else '[red]Missing[/red]'}\n"
|
|
710
|
+
f"• GEMINI_API_KEY: {'[green]Configured[/green]' if os.environ.get('GEMINI_API_KEY') else '[red]Missing[/red]'}\n"
|
|
711
|
+
f"• ANTHROPIC_API_KEY: {'[green]Configured[/green]' if os.environ.get('ANTHROPIC_API_KEY') else '[red]Missing[/red]'}\n"
|
|
712
|
+
f"• OPENAI_API_KEY: {'[green]Configured[/green]' if os.environ.get('OPENAI_API_KEY') else '[red]Missing[/red]'}\n"
|
|
713
|
+
f"• DEEPSEEK_API_KEY: {'[green]Configured[/green]' if os.environ.get('DEEPSEEK_API_KEY') else '[red]Missing[/red]'}\n"
|
|
714
|
+
f"• GROQ_API_KEY: {'[green]Configured[/green]' if os.environ.get('GROQ_API_KEY') else '[red]Missing[/red]'}\n\n"
|
|
715
|
+
"[dim]To update credentials, run: [bold white]k-cli ui[/bold white] and press [bold white]Ctrl+A[/bold white], or export in your terminal.[/dim]",
|
|
716
|
+
title="Credentials Vault Status",
|
|
717
|
+
border_style="cyan",
|
|
718
|
+
))
|
|
719
|
+
return True, "KEYS_HANDLED"
|
|
720
|
+
|
|
721
|
+
# /conflict
|
|
722
|
+
if cmd in ("conflict", "conflicts"):
|
|
723
|
+
from k_cli.git.conflict_resolver import ConflictResolver
|
|
724
|
+
res = ConflictResolver().find_conflicts()
|
|
725
|
+
if not res:
|
|
726
|
+
self.console.print("[bold green]✔ Zero git merge conflicts in repository.[/bold green]")
|
|
727
|
+
else:
|
|
728
|
+
self.console.print(f"[bold yellow]⚠️ Found {len(res)} active merge conflicts in workspace.[/bold yellow]")
|
|
729
|
+
return True, "CONFLICT_HANDLED"
|
|
730
|
+
|
|
731
|
+
# /gh or /github
|
|
732
|
+
if cmd in ("gh", "github", "issues", "prs"):
|
|
733
|
+
from k_cli.github.github_engine import GitHubEngine
|
|
734
|
+
engine = GitHubEngine()
|
|
735
|
+
issues = engine.list_issues(limit=5)
|
|
736
|
+
self.console.print(f"[bold cyan]🐙 GitHub Issues ({len(issues)} listed):[/bold cyan]")
|
|
737
|
+
for i in issues:
|
|
738
|
+
self.console.print(f" #{i.number}: {i.title} (@{i.author})")
|
|
739
|
+
return True, "GH_HANDLED"
|
|
740
|
+
|
|
741
|
+
# /solve <issue_num>
|
|
742
|
+
if cmd in ("solve", "fix_issue") and arg:
|
|
743
|
+
try:
|
|
744
|
+
num = int(arg.strip("#"))
|
|
745
|
+
from k_cli.github.github_engine import GitHubEngine
|
|
746
|
+
from k_cli.git.verifier import Verifier
|
|
747
|
+
from k_cli.git.patcher import Patcher
|
|
748
|
+
self.console.print(f"[bold cyan]Autonomously investigating issue #{num}...[/bold cyan]")
|
|
749
|
+
res = GitHubEngine().solve_issue(issue_number=num, llm_driver=self.session.driver, verifier=Verifier(), patcher=Patcher(), auto_pr=True)
|
|
750
|
+
if res.success:
|
|
751
|
+
self.console.print(f"[bold green]✔ Issue #{num} solved! Branch: {res.branch_name}, PR: {res.pr_url}[/bold green]")
|
|
752
|
+
else:
|
|
753
|
+
self.console.print(f"[bold red]✘ Issue #{num} failed: {res.error_message}[/bold red]")
|
|
754
|
+
except ValueError:
|
|
755
|
+
self.console.print("[bold red]Please provide a numeric issue number: /solve <number>[/bold red]")
|
|
756
|
+
return True, "SOLVE_HANDLED"
|
|
757
|
+
|
|
758
|
+
# Delegate /add, /remove, /map, /status to SessionManager
|
|
759
|
+
handled, msg = self.session.handle_slash_command(raw)
|
|
760
|
+
if handled:
|
|
761
|
+
if cmd == "status":
|
|
762
|
+
status_bar = StatusBar()
|
|
763
|
+
status_bar.update_from_session(self.session)
|
|
764
|
+
self.console.print(status_bar.render_rich_panel())
|
|
765
|
+
elif cmd == "map" and self.session.repo_map:
|
|
766
|
+
map_str = self.session.repo_map.get_repo_map(max_tokens=400, focus_files=self.session.get_context_files())
|
|
767
|
+
if map_str.strip():
|
|
768
|
+
map_syn = Syntax(map_str, "python", theme="monokai", line_numbers=False)
|
|
769
|
+
self.console.print(Panel(map_syn, title="[bold magenta]AST Repository Codebase Map[/bold magenta]", border_style="magenta"))
|
|
770
|
+
else:
|
|
771
|
+
self.console.print("[yellow]Repository map is empty.[/yellow]")
|
|
772
|
+
else:
|
|
773
|
+
self.console.print(f"[bold cyan]{msg}[/bold cyan]")
|
|
774
|
+
return True, msg
|
|
775
|
+
|
|
776
|
+
self.console.print(f"[bold red]Unknown command:[/bold red] {raw}. Type [bold yellow]/help[/bold yellow] for available commands.")
|
|
777
|
+
return True, "UNKNOWN_COMMAND"
|
|
778
|
+
|
|
779
|
+
def _render_help(self) -> None:
|
|
780
|
+
table = Table(title="K-CLI Interactive Slash Commands", box=None, expand=True)
|
|
781
|
+
table.add_column("Command", style="bold cyan", width=18)
|
|
782
|
+
table.add_column("Arguments", style="bold yellow", width=16)
|
|
783
|
+
table.add_column("Description", style="white")
|
|
784
|
+
|
|
785
|
+
commands_info = [
|
|
786
|
+
("/model", "[name]", "Switch active model (Bankai-7B, Bankai-14B, Gemini, Claude, Local Ollama)"),
|
|
787
|
+
("/persona", "[name]", "Switch active persona (RESEARCHER, ARCHITECT, CODER, CRITIC, DEBUGGER, AUTO)"),
|
|
788
|
+
("/diff", "[mode]", "View surgical git diff (options: inline, side-by-side / sbs, card)"),
|
|
789
|
+
("/rollback", "[file]", "Roll back last uncommitted edit via Git (alias /undo)"),
|
|
790
|
+
("/docs", "<query>", "Search offline DevDocs SQLite database for API signatures"),
|
|
791
|
+
("/clear", "", "Clear terminal screen, conversation history, and context"),
|
|
792
|
+
("/test", "[file/code]", "Run ground-truth compiler / pytest verification"),
|
|
793
|
+
("/banner", "", "Display cyberpunk gradient ASCII splash banner"),
|
|
794
|
+
("/tree", "", "Display animated subagent execution tree & metrics"),
|
|
795
|
+
("/speed", "", "Inspect real-time token throughput and USD cost ticker"),
|
|
796
|
+
("/add", "<file>", "Add file to active session context"),
|
|
797
|
+
("/remove", "<file>", "Remove file from active session context"),
|
|
798
|
+
("/map", "", "Display AST codebase repository map"),
|
|
799
|
+
("/status", "", "Inspect model, token usage, and RAM budget diagnostics"),
|
|
800
|
+
("/help", "", "Show this help table"),
|
|
801
|
+
("/exit", "", "Exit interactive session (alias /quit)"),
|
|
802
|
+
]
|
|
803
|
+
|
|
804
|
+
for cmd, args, desc in commands_info:
|
|
805
|
+
table.add_row(cmd, args, desc)
|
|
806
|
+
|
|
807
|
+
self.console.print(Panel(table, title="[bold cyan]Command Reference[/bold cyan]", border_style="cyan"))
|
|
808
|
+
|
|
809
|
+
def _handle_tree(self) -> None:
|
|
810
|
+
"""Renders live subagent execution tree."""
|
|
811
|
+
tasks = []
|
|
812
|
+
if hasattr(self.session, "dispatcher") and self.session.dispatcher:
|
|
813
|
+
tasks = getattr(self.session.dispatcher, "last_tasks", [])
|
|
814
|
+
if not tasks:
|
|
815
|
+
# Generate sample execution topology
|
|
816
|
+
class MockTask:
|
|
817
|
+
def __init__(self, name, role, status, prompt, duration_sec=1.2, token_count=180):
|
|
818
|
+
self.name = name
|
|
819
|
+
self.role = role
|
|
820
|
+
self.status = status
|
|
821
|
+
self.prompt = prompt
|
|
822
|
+
self.duration_sec = duration_sec
|
|
823
|
+
self.token_count = token_count
|
|
824
|
+
|
|
825
|
+
tasks = [
|
|
826
|
+
MockTask("Inspect AST Map", "EXPLORER", "COMPLETED", "Scans workspace AST symbol definitions", 0.45, 120),
|
|
827
|
+
MockTask("DevDocs Reference Lookup", "RESEARCHER", "COMPLETED", "Queries offline signatures", 0.32, 95),
|
|
828
|
+
MockTask("Surgical Patch Generator", "REFACTORER", "RUNNING", "Synthesizes SEARCH/REPLACE diff", 1.15, 340),
|
|
829
|
+
MockTask("AST Compiler Guard", "TESTER", "PENDING", "Verifies zero syntax errors & tests", 0.0, 0),
|
|
830
|
+
]
|
|
831
|
+
|
|
832
|
+
self.console.print(render_subagent_execution_tree(tasks))
|
|
833
|
+
|
|
834
|
+
def _handle_speed_and_cost(self) -> None:
|
|
835
|
+
"""Renders token speedometer and cost ticker diagnostics."""
|
|
836
|
+
st = self.session.get_status() if hasattr(self.session, "get_status") else {}
|
|
837
|
+
m_name = st.get("model", "Bankai-7B")
|
|
838
|
+
t_count = st.get("token_count", 0)
|
|
839
|
+
|
|
840
|
+
speedometer = TokenSpeedometer()
|
|
841
|
+
speedometer.record_tokens(max(10, t_count))
|
|
842
|
+
|
|
843
|
+
cost_ticker = CostTicker(active_model=m_name)
|
|
844
|
+
cost_ticker.record_usage(m_name, max(0, t_count // 2), max(0, t_count // 2))
|
|
845
|
+
|
|
846
|
+
table = Table(box=None, expand=True)
|
|
847
|
+
table.add_column("Diagnostic Metric", style="bold cyan", width=26)
|
|
848
|
+
table.add_column("Real-Time Telemetry", style="bold white")
|
|
849
|
+
|
|
850
|
+
table.add_row("🏎️ Live Speedometer", speedometer.render_gauge())
|
|
851
|
+
table.add_row("💰 Cumulative Cost Ticker", cost_ticker.render_ticker())
|
|
852
|
+
table.add_row("🤖 Active Engine", f"[bold green]{m_name}[/bold green]")
|
|
853
|
+
table.add_row("💾 Memory Allocation", f"{st.get('ram_mb', 0.0):.1f} MB / 1024 MB")
|
|
854
|
+
|
|
855
|
+
self.console.print(Panel(table, title="[bold #00f0ff]◈ REAL-TIME SPEEDOMETER & COST TICKER ◈[/bold #00f0ff]", border_style="#00f0ff"))
|
|
856
|
+
|
|
857
|
+
def _handle_model(self, model_arg: str) -> None:
|
|
858
|
+
if not model_arg:
|
|
859
|
+
table = Table(title="Available AI Models", box=None, expand=True)
|
|
860
|
+
table.add_column("Preset", style="bold cyan", width=18)
|
|
861
|
+
table.add_column("Engine", style="bold magenta", width=10)
|
|
862
|
+
table.add_column("Description", style="white")
|
|
863
|
+
table.add_column("Active", style="bold green", width=8)
|
|
864
|
+
|
|
865
|
+
current = getattr(self.session, "model_name", "")
|
|
866
|
+
for p in MODEL_PRESETS:
|
|
867
|
+
is_active = "✔ YES" if p["name"].lower() == current.lower() or current.lower().startswith(p["name"].lower().split()[0]) else ""
|
|
868
|
+
table.add_row(p["name"], p["type"], p["desc"], is_active)
|
|
869
|
+
|
|
870
|
+
self.console.print(Panel(table, title="[bold cyan]Active & Available Models[/bold cyan]", border_style="cyan"))
|
|
871
|
+
self.console.print(f"[dim]Use [/dim][bold yellow]/model <name>[/bold yellow][dim] to switch models (e.g. [/dim][bold cyan]/model Bankai-14B[/bold cyan][dim]).[/dim]\n")
|
|
872
|
+
else:
|
|
873
|
+
self.session.set_model(model_arg)
|
|
874
|
+
self.console.print(f"[bold green]✔ Switched active model to:[/bold green] [bold cyan]{model_arg}[/bold cyan]")
|
|
875
|
+
|
|
876
|
+
def _handle_persona(self, persona_arg: str) -> None:
|
|
877
|
+
if not persona_arg:
|
|
878
|
+
table = Table(title="Dynamic Persona & Architecture State Machine", box=None, expand=True)
|
|
879
|
+
table.add_column("Persona", style="bold cyan", width=32)
|
|
880
|
+
table.add_column("Command", style="bold yellow", width=14)
|
|
881
|
+
table.add_column("Description", style="white")
|
|
882
|
+
table.add_column("Active", style="bold green", width=8)
|
|
883
|
+
|
|
884
|
+
current = getattr(self.session, "active_persona", "AUTO")
|
|
885
|
+
active_id = getattr(self.session.active_persona_profile, "id", "") if hasattr(self.session, "active_persona_profile") and self.session.active_persona_profile else ""
|
|
886
|
+
|
|
887
|
+
if PersonaRegistry:
|
|
888
|
+
for p in PersonaRegistry.list_personas():
|
|
889
|
+
is_active = "✔ YES" if (p.id == active_id or p.title.lower() == str(current).lower()) else ""
|
|
890
|
+
table.add_row(f"[{p.color}][bold]{p.icon} {p.title}[/bold][/{p.color}]", f"/{p.id}", p.description, is_active)
|
|
891
|
+
else:
|
|
892
|
+
valid_personas = ["AUTO", "RESEARCHER", "ARCHITECT", "CODER", "CRITIC", "DEBUGGER"]
|
|
893
|
+
for p in valid_personas:
|
|
894
|
+
color, icon, desc = get_persona_style(p)
|
|
895
|
+
is_active = "✔ YES" if p == current else ""
|
|
896
|
+
table.add_row(f"[{color}][bold]{icon} {p}[/bold][/{color}]", f"/{p.lower()}", desc, is_active)
|
|
897
|
+
|
|
898
|
+
self.console.print(Panel(table, title="[bold cyan]Dynamic Persona Profiles[/bold cyan]", border_style="cyan"))
|
|
899
|
+
self.console.print(f"[dim]Use [/dim][bold yellow]/persona <name>[/bold yellow][dim] to switch (e.g. [/dim][bold cyan]/persona devops[/bold cyan][dim] or [/dim][bold cyan]/persona debugger[/bold cyan][dim]).[/dim]\n")
|
|
900
|
+
else:
|
|
901
|
+
success, msg = self.session.set_persona(persona_arg)
|
|
902
|
+
if success:
|
|
903
|
+
self.console.print(f"[bold green]✔ {msg}[/bold green]")
|
|
904
|
+
else:
|
|
905
|
+
self.console.print(f"[bold red]{msg}[/bold red]")
|
|
906
|
+
|
|
907
|
+
def _handle_diff(self, mode_arg: str) -> None:
|
|
908
|
+
if not self.session.git_guard.is_git_repo():
|
|
909
|
+
self.console.print("[yellow]Not inside a Git repository. No diff available.[/yellow]")
|
|
910
|
+
return
|
|
911
|
+
|
|
912
|
+
diff_text = self.session.git_guard.get_diff()
|
|
913
|
+
if not diff_text.strip():
|
|
914
|
+
self.console.print("[dim]Working tree is clean; no uncommitted changes.[/dim]")
|
|
915
|
+
return
|
|
916
|
+
|
|
917
|
+
mode = (mode_arg or "").lower().strip()
|
|
918
|
+
if mode in ("card", "preview", "box"):
|
|
919
|
+
self.console.print(render_instant_diff_card(diff_text=diff_text, title="Working Tree Diff Preview"))
|
|
920
|
+
elif mode in ("sbs", "side-by-side", "2col", "side"):
|
|
921
|
+
if DiffVisualizer:
|
|
922
|
+
self.console.print(DiffVisualizer.render_inline_diff(diff_text, title="Git Working Tree Diff (Inline)"))
|
|
923
|
+
else:
|
|
924
|
+
self.console.print(render_instant_diff_card(diff_text=diff_text))
|
|
925
|
+
else:
|
|
926
|
+
if DiffVisualizer:
|
|
927
|
+
self.console.print(DiffVisualizer.render_inline_diff(diff_text, title="Git Working Tree Diff"))
|
|
928
|
+
else:
|
|
929
|
+
self.console.print(render_instant_diff_card(diff_text=diff_text))
|
|
930
|
+
|
|
931
|
+
def _handle_rollback(self, file_arg: str) -> None:
|
|
932
|
+
files = [file_arg] if file_arg else None
|
|
933
|
+
if not self.session.git_guard.is_git_repo():
|
|
934
|
+
self.console.print("[yellow]Not inside a Git repository; cannot rollback.[/yellow]")
|
|
935
|
+
return
|
|
936
|
+
|
|
937
|
+
success = self.session.git_guard.rollback(files=files)
|
|
938
|
+
if success:
|
|
939
|
+
target_str = f" for '{file_arg}'" if file_arg else ""
|
|
940
|
+
self.console.print(f"[bold green]✔ Successfully rolled back uncommitted changes{target_str}.[/bold green]")
|
|
941
|
+
else:
|
|
942
|
+
self.console.print("[bold red]✘ Rollback failed or no changes to revert.[/bold red]")
|
|
943
|
+
|
|
944
|
+
def _handle_docs(self, query: str) -> None:
|
|
945
|
+
if not query:
|
|
946
|
+
self.console.print("[yellow]Usage: /docs <query> (e.g. /docs json.loads)[/yellow]")
|
|
947
|
+
return
|
|
948
|
+
|
|
949
|
+
if not self.session.doc_retriever:
|
|
950
|
+
self.console.print("[yellow]DevDocs retriever not available.[/yellow]")
|
|
951
|
+
return
|
|
952
|
+
|
|
953
|
+
results = self.session.doc_retriever.search(query, limit=3, max_tokens=250)
|
|
954
|
+
if not results:
|
|
955
|
+
self.console.print(f"[yellow]No documentation found for '{query}'.[/yellow]")
|
|
956
|
+
return
|
|
957
|
+
|
|
958
|
+
self.console.print(f"[bold cyan]DevDocs search results for '{query}':[/bold cyan]\n")
|
|
959
|
+
for r in results:
|
|
960
|
+
name = r.get("name", "")
|
|
961
|
+
sig = r.get("signature", "")
|
|
962
|
+
doc_str = r.get("doc", "")
|
|
963
|
+
module = r.get("module", "")
|
|
964
|
+
content = f"[bold green]{sig}[/bold green]\n\n[dim]{doc_str}[/dim]"
|
|
965
|
+
self.console.print(Panel(content, title=f"Module: {module} | Symbol: {name}", border_style="cyan"))
|
|
966
|
+
|
|
967
|
+
def _handle_test(self, target_arg: str) -> None:
|
|
968
|
+
passed, summary = self.session.run_test(target_arg if target_arg else None)
|
|
969
|
+
if passed:
|
|
970
|
+
if create_verifier_badge:
|
|
971
|
+
badge = create_verifier_badge("PASS", pass_rate=1.0)
|
|
972
|
+
self.console.print(badge.render())
|
|
973
|
+
self.console.print(f"[bold green]{summary}[/bold green]")
|
|
974
|
+
else:
|
|
975
|
+
if create_verifier_badge:
|
|
976
|
+
badge = create_verifier_badge("FAIL", pass_rate=0.0)
|
|
977
|
+
self.console.print(badge.render())
|
|
978
|
+
self.console.print(f"[bold red]{summary}[/bold red]")
|
|
979
|
+
|
|
980
|
+
|
|
981
|
+
# ==============================================================================
|
|
982
|
+
# 6. Interactive Shell Engine
|
|
983
|
+
# ==============================================================================
|
|
984
|
+
|
|
985
|
+
class InteractiveShell:
|
|
986
|
+
"""Prompt-toolkit powered high-speed interactive shell for K-CLI."""
|
|
987
|
+
|
|
988
|
+
def __init__(
|
|
989
|
+
self,
|
|
990
|
+
session: Any,
|
|
991
|
+
console: Optional[Console] = None,
|
|
992
|
+
):
|
|
993
|
+
self.session = session
|
|
994
|
+
self.console = console or Console()
|
|
995
|
+
self.status_bar = StatusBar()
|
|
996
|
+
self.status_bar.update_from_session(self.session)
|
|
997
|
+
self.command_handler = SlashCommandHandler(session=self.session, console=self.console)
|
|
998
|
+
self.stream_renderer = LiveStreamRenderer(console=self.console)
|
|
999
|
+
|
|
1000
|
+
def run(self) -> None:
|
|
1001
|
+
"""Starts the interactive multi-turn REPL loop."""
|
|
1002
|
+
self.status_bar.update_from_session(self.session)
|
|
1003
|
+
|
|
1004
|
+
# Setup prompt_toolkit if available and interactive terminal
|
|
1005
|
+
prompt_session = None
|
|
1006
|
+
if HAS_PROMPT_TOOLKIT and sys.stdin.isatty():
|
|
1007
|
+
try:
|
|
1008
|
+
style = PTKStyle.from_dict({
|
|
1009
|
+
"prompt": "bold #00ffff",
|
|
1010
|
+
"arrow": "bold #5af78e",
|
|
1011
|
+
})
|
|
1012
|
+
prompt_session = PromptSession(
|
|
1013
|
+
completer=SlashCommandCompleter(),
|
|
1014
|
+
history=InMemoryHistory(),
|
|
1015
|
+
style=style,
|
|
1016
|
+
)
|
|
1017
|
+
except Exception:
|
|
1018
|
+
prompt_session = None
|
|
1019
|
+
|
|
1020
|
+
# Display startup banner
|
|
1021
|
+
if render_cyber_banner:
|
|
1022
|
+
self.console.print(render_cyber_banner(palette="neon_cyan"))
|
|
1023
|
+
|
|
1024
|
+
while True:
|
|
1025
|
+
try:
|
|
1026
|
+
self.status_bar.update_from_session(self.session)
|
|
1027
|
+
|
|
1028
|
+
if prompt_session:
|
|
1029
|
+
prompt_input = prompt_session.prompt(
|
|
1030
|
+
[("class:prompt", "K-CLI "), ("class:arrow", "❯ ")],
|
|
1031
|
+
bottom_toolbar=self.status_bar.get_prompt_toolkit_toolbar,
|
|
1032
|
+
).strip()
|
|
1033
|
+
else:
|
|
1034
|
+
prompt_input = self.console.input("[bold cyan]K-CLI [/bold cyan][bold green]❯ [/bold green]").strip()
|
|
1035
|
+
|
|
1036
|
+
if not prompt_input:
|
|
1037
|
+
continue
|
|
1038
|
+
|
|
1039
|
+
# 1. Handle Slash Commands
|
|
1040
|
+
if prompt_input.startswith("/"):
|
|
1041
|
+
cont, signal = self.command_handler.handle(prompt_input)
|
|
1042
|
+
if not cont or signal == "EXIT":
|
|
1043
|
+
break
|
|
1044
|
+
self.console.print()
|
|
1045
|
+
continue
|
|
1046
|
+
|
|
1047
|
+
# 2. Conversational greetings
|
|
1048
|
+
clean_lower = prompt_input.lower().strip()
|
|
1049
|
+
if clean_lower in ("yo", "hi", "hello", "hey", "sup", "howdy", "greetings"):
|
|
1050
|
+
self.console.print(Panel(
|
|
1051
|
+
"[bold green]Yo! I'm K-CLI — your universal, compiler-grounded AI coding assistant.[/bold green]\n\n"
|
|
1052
|
+
"[bold cyan]What you can do right now:[/bold cyan]\n"
|
|
1053
|
+
"• [bold]Write & Refactor Code[/bold]: Enter a coding task (e.g. [italic]write a function to parse jwt tokens[/italic]).\n"
|
|
1054
|
+
"• [bold]/model[/bold]: Switch active model (Bankai-7B, Bankai-14B, Gemini, Claude, Local Ollama).\n"
|
|
1055
|
+
"• [bold]/persona[/bold]: Switch active persona (RESEARCHER, ARCHITECT, CODER, CRITIC, DEBUGGER).\n"
|
|
1056
|
+
"• [bold]/add <file>[/bold]: Scope a file to active context for surgical edits.\n"
|
|
1057
|
+
"• [bold]/diff[/bold] & [bold]/rollback[/bold]: Review git diff or undo any modification instantly.\n"
|
|
1058
|
+
"• [bold]/docs <symbol>[/bold]: Instant SQLite FTS5 DevDocs lookup (e.g. [italic]/docs json.loads[/italic]).\n"
|
|
1059
|
+
"• [bold]/test [file][/bold]: Run ground-truth verification on file or tests.\n"
|
|
1060
|
+
"• [bold]/banner[/bold] & [bold]/tree[/bold]: Cyberpunk logo splash and subagent execution tree.\n"
|
|
1061
|
+
"• [bold]/status[/bold]: Inspect active model, tokens, branch, cost, and RAM diagnostics.",
|
|
1062
|
+
title="[bold cyan]K-CLI Assistant[/bold cyan]",
|
|
1063
|
+
border_style="cyan",
|
|
1064
|
+
))
|
|
1065
|
+
self.console.print("\n" + "─" * 60 + "\n")
|
|
1066
|
+
continue
|
|
1067
|
+
|
|
1068
|
+
# 3. Live Token Streaming & Pipeline Turn Execution
|
|
1069
|
+
self.console.print(f"\n[bold yellow]Agent Task:[/bold yellow] [italic]'{prompt_input}'[/italic]\n")
|
|
1070
|
+
|
|
1071
|
+
gen = self.session.process_turn(prompt_input)
|
|
1072
|
+
self.stream_renderer.stream_display(
|
|
1073
|
+
token_generator=gen,
|
|
1074
|
+
initial_persona=self.session.active_persona if self.session.active_persona != "AUTO" else "RESEARCHER",
|
|
1075
|
+
language="python",
|
|
1076
|
+
model_name=self.session.model_name,
|
|
1077
|
+
)
|
|
1078
|
+
|
|
1079
|
+
# 4. Result & Diff Presentation with Glowing Badges and Cards
|
|
1080
|
+
res = self.session.last_result or {}
|
|
1081
|
+
if res.get("success"):
|
|
1082
|
+
if create_verifier_badge:
|
|
1083
|
+
v_badge = create_verifier_badge("PASS", attempts=res.get("attempts", 1))
|
|
1084
|
+
self.console.print(v_badge.render())
|
|
1085
|
+
self.console.print(f"[bold green]✔ GROUND-TRUTH VERIFIED[/bold green] [dim](Attempts: {res.get('attempts', 1)} | RAM: {res.get('ram_mb', 0):.2f} MB)[/dim]\n")
|
|
1086
|
+
if res.get("code"):
|
|
1087
|
+
syntax = Syntax(res["code"], "python", theme="monokai", line_numbers=True)
|
|
1088
|
+
self.console.print(Panel(syntax, title="[bold green]Verified Implementation[/bold green]", border_style="green"))
|
|
1089
|
+
else:
|
|
1090
|
+
if create_verifier_badge:
|
|
1091
|
+
v_badge = create_verifier_badge("FAIL", attempts=res.get("attempts", 1))
|
|
1092
|
+
self.console.print(v_badge.render())
|
|
1093
|
+
self.console.print(f"[bold red]✘ VERIFICATION FAILED[/bold red] [dim](RAM: {res.get('ram_mb', 0):.2f} MB)[/dim]\n")
|
|
1094
|
+
if res.get("patch_error"):
|
|
1095
|
+
self.console.print(Panel(res["patch_error"], title="Patch Application Error", border_style="red"))
|
|
1096
|
+
elif res.get("code"):
|
|
1097
|
+
syntax = Syntax(res["code"], "python", theme="monokai", line_numbers=True)
|
|
1098
|
+
self.console.print(Panel(syntax, title="Unverified Candidate Code", border_style="yellow"))
|
|
1099
|
+
|
|
1100
|
+
# Check for git working tree diff preview card
|
|
1101
|
+
if hasattr(self.session, "git_guard") and self.session.git_guard.is_git_repo():
|
|
1102
|
+
diff_txt = self.session.git_guard.get_diff()
|
|
1103
|
+
if diff_txt.strip():
|
|
1104
|
+
self.console.print(render_instant_diff_card(diff_text=diff_txt, title="Working Tree Modification"))
|
|
1105
|
+
|
|
1106
|
+
self.console.print("\n" + "─" * 60 + "\n")
|
|
1107
|
+
|
|
1108
|
+
except (KeyboardInterrupt, EOFError):
|
|
1109
|
+
self.console.print("\n[bold dim]Exiting K-CLI. Goodbye![/bold dim]")
|
|
1110
|
+
break
|
|
1111
|
+
except Exception as e:
|
|
1112
|
+
self.console.print(f"[bold red]Error:[/bold red] {e}")
|
|
1113
|
+
|
|
1114
|
+
|
|
1115
|
+
# ==============================================================================
|
|
1116
|
+
# 7. Full-Screen Textual TUI (KCliApp fallback / delegation)
|
|
1117
|
+
# ==============================================================================
|
|
1118
|
+
|
|
1119
|
+
TUI_ASCII_BANNER = r"""[bold cyan]
|
|
1120
|
+
██╗ ██╗ ██████╗██╗ ██╗
|
|
1121
|
+
██║ ██╔╝ ██╔════╝██║ ██║
|
|
1122
|
+
█████═╝ ██║ ██║ ██║
|
|
1123
|
+
██╔═██╗ ██║ ██║ ██║
|
|
1124
|
+
██║ ██╗ ╚██████╗███████╗██║
|
|
1125
|
+
╚═╝ ╚═╝ ╚═════╝╚══════╝╚═╝
|
|
1126
|
+
[/bold cyan][bold bright_white]K-CLI AGENTIC WORKSTATION v1.0.0 | Verification-First Engine[/bold bright_white]"""
|
|
1127
|
+
|
|
1128
|
+
if HAS_TEXTUAL:
|
|
1129
|
+
try:
|
|
1130
|
+
from k_cli.tui.tui_app import KCliApp
|
|
1131
|
+
except (ImportError, ModuleNotFoundError):
|
|
1132
|
+
try:
|
|
1133
|
+
from tui_app import KCliApp
|
|
1134
|
+
except (ImportError, ModuleNotFoundError):
|
|
1135
|
+
class KCliApp(App): # type: ignore
|
|
1136
|
+
def compose(self) -> ComposeResult:
|
|
1137
|
+
yield Header()
|
|
1138
|
+
yield Static("K-CLI Bankai Workstation")
|
|
1139
|
+
yield Footer()
|
|
1140
|
+
else:
|
|
1141
|
+
class KCliApp:
|
|
1142
|
+
def __init__(self, *args, **kwargs):
|
|
1143
|
+
pass
|
|
1144
|
+
def run(self):
|
|
1145
|
+
print("Textual is not installed.")
|