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.
Files changed (75) hide show
  1. k_cli/__init__.py +77 -0
  2. k_cli/agents/__init__.py +0 -0
  3. k_cli/agents/adversarial_swarm.py +338 -0
  4. k_cli/agents/agent_core.py +255 -0
  5. k_cli/agents/background_daemon.py +141 -0
  6. k_cli/agents/orchestrator.py +376 -0
  7. k_cli/agents/persona.py +649 -0
  8. k_cli/agents/scaffold_engine.py +121 -0
  9. k_cli/agents/strands_agent.py +832 -0
  10. k_cli/agents/subagents.py +1496 -0
  11. k_cli/cli.py +3297 -0
  12. k_cli/core/__init__.py +0 -0
  13. k_cli/core/airgap.py +95 -0
  14. k_cli/core/credentials.py +548 -0
  15. k_cli/core/intent_sensor.py +177 -0
  16. k_cli/core/llm_driver.py +1028 -0
  17. k_cli/core/model_manager.py +1109 -0
  18. k_cli/core/models_hub.py +913 -0
  19. k_cli/core/prompting.py +41 -0
  20. k_cli/core/sdk.py +322 -0
  21. k_cli/core/session.py +826 -0
  22. k_cli/core/smart_router.py +230 -0
  23. k_cli/core/storage_manager.py +176 -0
  24. k_cli/core/viewport_engine.py +117 -0
  25. k_cli/demo/demo_runner.py +579 -0
  26. k_cli/git/__init__.py +0 -0
  27. k_cli/git/ai_bisect.py +208 -0
  28. k_cli/git/conflict_resolver.py +1039 -0
  29. k_cli/git/git_guard.py +417 -0
  30. k_cli/git/patcher.py +1175 -0
  31. k_cli/git/repo_map.py +1780 -0
  32. k_cli/git/smart_git.py +928 -0
  33. k_cli/git/verifier.py +969 -0
  34. k_cli/github/__init__.py +0 -0
  35. k_cli/github/dedup_engine.py +787 -0
  36. k_cli/github/github_client.py +1702 -0
  37. k_cli/github/github_engine.py +641 -0
  38. k_cli/github/local_hub.py +209 -0
  39. k_cli/github/pr_watcher.py +129 -0
  40. k_cli/github/trending.py +205 -0
  41. k_cli/tools/__init__.py +0 -0
  42. k_cli/tools/audit.py +79 -0
  43. k_cli/tools/chaos_immunity.py +377 -0
  44. k_cli/tools/codebase_qa.py +106 -0
  45. k_cli/tools/command_runner.py +256 -0
  46. k_cli/tools/diagram_generator.py +547 -0
  47. k_cli/tools/doc_retriever.py +1332 -0
  48. k_cli/tools/feature.py +105 -0
  49. k_cli/tools/ghost_daemon.py +122 -0
  50. k_cli/tools/incident_triage.py +1365 -0
  51. k_cli/tools/mcp_client.py +1846 -0
  52. k_cli/tools/repo_gardener.py +142 -0
  53. k_cli/tools/rules.py +109 -0
  54. k_cli/tools/security.py +52 -0
  55. k_cli/tools/security_healer.py +999 -0
  56. k_cli/tools/synapse_graph.py +155 -0
  57. k_cli/tui/__init__.py +0 -0
  58. k_cli/tui/diff_viewer.py +223 -0
  59. k_cli/tui/tui.py +1145 -0
  60. k_cli/tui/tui_animations.py +648 -0
  61. k_cli/tui/tui_app.py +2788 -0
  62. k_cli/ui/__init__.py +10 -0
  63. k_cli/ui/simple_repl.py +315 -0
  64. k_cli/web/__init__.py +7 -0
  65. k_cli/web/server.py +624 -0
  66. k_cli/web/static/app.js +830 -0
  67. k_cli/web/static/index.html +495 -0
  68. k_cli/web/static/monitor.html +189 -0
  69. k_cli/web/static/style.css +838 -0
  70. k_cli_for_devs-1.0.0.dist-info/METADATA +461 -0
  71. k_cli_for_devs-1.0.0.dist-info/RECORD +75 -0
  72. k_cli_for_devs-1.0.0.dist-info/WHEEL +5 -0
  73. k_cli_for_devs-1.0.0.dist-info/entry_points.txt +2 -0
  74. k_cli_for_devs-1.0.0.dist-info/licenses/LICENSE +21 -0
  75. 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.")