superqode 0.1.5__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 (288) hide show
  1. superqode/__init__.py +33 -0
  2. superqode/acp/__init__.py +23 -0
  3. superqode/acp/client.py +913 -0
  4. superqode/acp/permission_screen.py +457 -0
  5. superqode/acp/types.py +480 -0
  6. superqode/acp_discovery.py +856 -0
  7. superqode/agent/__init__.py +22 -0
  8. superqode/agent/edit_strategies.py +334 -0
  9. superqode/agent/loop.py +892 -0
  10. superqode/agent/qe_report_templates.py +39 -0
  11. superqode/agent/system_prompts.py +353 -0
  12. superqode/agent_output.py +721 -0
  13. superqode/agent_stream.py +953 -0
  14. superqode/agents/__init__.py +59 -0
  15. superqode/agents/acp_registry.py +305 -0
  16. superqode/agents/client.py +249 -0
  17. superqode/agents/data/augmentcode.com.toml +51 -0
  18. superqode/agents/data/cagent.dev.toml +51 -0
  19. superqode/agents/data/claude.com.toml +60 -0
  20. superqode/agents/data/codeassistant.dev.toml +51 -0
  21. superqode/agents/data/codex.openai.com.toml +57 -0
  22. superqode/agents/data/fastagent.ai.toml +66 -0
  23. superqode/agents/data/geminicli.com.toml +77 -0
  24. superqode/agents/data/goose.block.xyz.toml +54 -0
  25. superqode/agents/data/junie.jetbrains.com.toml +56 -0
  26. superqode/agents/data/kimi.moonshot.cn.toml +57 -0
  27. superqode/agents/data/llmlingagent.dev.toml +51 -0
  28. superqode/agents/data/molt.bot.toml +49 -0
  29. superqode/agents/data/opencode.ai.toml +60 -0
  30. superqode/agents/data/stakpak.dev.toml +51 -0
  31. superqode/agents/data/vtcode.dev.toml +51 -0
  32. superqode/agents/discovery.py +266 -0
  33. superqode/agents/messaging.py +160 -0
  34. superqode/agents/persona.py +166 -0
  35. superqode/agents/registry.py +421 -0
  36. superqode/agents/schema.py +72 -0
  37. superqode/agents/unified.py +367 -0
  38. superqode/app/__init__.py +111 -0
  39. superqode/app/constants.py +314 -0
  40. superqode/app/css.py +366 -0
  41. superqode/app/models.py +118 -0
  42. superqode/app/suggester.py +125 -0
  43. superqode/app/widgets.py +1591 -0
  44. superqode/app_enhanced.py +399 -0
  45. superqode/app_main.py +17187 -0
  46. superqode/approval.py +312 -0
  47. superqode/atomic.py +296 -0
  48. superqode/commands/__init__.py +1 -0
  49. superqode/commands/acp.py +965 -0
  50. superqode/commands/agents.py +180 -0
  51. superqode/commands/auth.py +278 -0
  52. superqode/commands/config.py +374 -0
  53. superqode/commands/init.py +826 -0
  54. superqode/commands/providers.py +819 -0
  55. superqode/commands/qe.py +1145 -0
  56. superqode/commands/roles.py +380 -0
  57. superqode/commands/serve.py +172 -0
  58. superqode/commands/suggestions.py +127 -0
  59. superqode/commands/superqe.py +460 -0
  60. superqode/config/__init__.py +51 -0
  61. superqode/config/loader.py +812 -0
  62. superqode/config/schema.py +498 -0
  63. superqode/core/__init__.py +111 -0
  64. superqode/core/roles.py +281 -0
  65. superqode/danger.py +386 -0
  66. superqode/data/superqode-template.yaml +1522 -0
  67. superqode/design_system.py +1080 -0
  68. superqode/dialogs/__init__.py +6 -0
  69. superqode/dialogs/base.py +39 -0
  70. superqode/dialogs/model.py +130 -0
  71. superqode/dialogs/provider.py +870 -0
  72. superqode/diff_view.py +919 -0
  73. superqode/enterprise.py +21 -0
  74. superqode/evaluation/__init__.py +25 -0
  75. superqode/evaluation/adapters.py +93 -0
  76. superqode/evaluation/behaviors.py +89 -0
  77. superqode/evaluation/engine.py +209 -0
  78. superqode/evaluation/scenarios.py +96 -0
  79. superqode/execution/__init__.py +36 -0
  80. superqode/execution/linter.py +538 -0
  81. superqode/execution/modes.py +347 -0
  82. superqode/execution/resolver.py +283 -0
  83. superqode/execution/runner.py +642 -0
  84. superqode/file_explorer.py +811 -0
  85. superqode/file_viewer.py +471 -0
  86. superqode/flash.py +183 -0
  87. superqode/guidance/__init__.py +58 -0
  88. superqode/guidance/config.py +203 -0
  89. superqode/guidance/prompts.py +71 -0
  90. superqode/harness/__init__.py +54 -0
  91. superqode/harness/accelerator.py +291 -0
  92. superqode/harness/config.py +319 -0
  93. superqode/harness/validator.py +147 -0
  94. superqode/history.py +279 -0
  95. superqode/integrations/superopt_runner.py +124 -0
  96. superqode/logging/__init__.py +49 -0
  97. superqode/logging/adapters.py +219 -0
  98. superqode/logging/formatter.py +923 -0
  99. superqode/logging/integration.py +341 -0
  100. superqode/logging/sinks.py +170 -0
  101. superqode/logging/unified_log.py +417 -0
  102. superqode/lsp/__init__.py +26 -0
  103. superqode/lsp/client.py +544 -0
  104. superqode/main.py +1069 -0
  105. superqode/mcp/__init__.py +89 -0
  106. superqode/mcp/auth_storage.py +380 -0
  107. superqode/mcp/client.py +1236 -0
  108. superqode/mcp/config.py +319 -0
  109. superqode/mcp/integration.py +337 -0
  110. superqode/mcp/oauth.py +436 -0
  111. superqode/mcp/oauth_callback.py +385 -0
  112. superqode/mcp/types.py +290 -0
  113. superqode/memory/__init__.py +31 -0
  114. superqode/memory/feedback.py +342 -0
  115. superqode/memory/store.py +522 -0
  116. superqode/notifications.py +369 -0
  117. superqode/optimization/__init__.py +5 -0
  118. superqode/optimization/config.py +33 -0
  119. superqode/permissions/__init__.py +25 -0
  120. superqode/permissions/rules.py +488 -0
  121. superqode/plan.py +323 -0
  122. superqode/providers/__init__.py +33 -0
  123. superqode/providers/gateway/__init__.py +165 -0
  124. superqode/providers/gateway/base.py +228 -0
  125. superqode/providers/gateway/litellm_gateway.py +1170 -0
  126. superqode/providers/gateway/openresponses_gateway.py +436 -0
  127. superqode/providers/health.py +297 -0
  128. superqode/providers/huggingface/__init__.py +74 -0
  129. superqode/providers/huggingface/downloader.py +472 -0
  130. superqode/providers/huggingface/endpoints.py +442 -0
  131. superqode/providers/huggingface/hub.py +531 -0
  132. superqode/providers/huggingface/inference.py +394 -0
  133. superqode/providers/huggingface/transformers_runner.py +516 -0
  134. superqode/providers/local/__init__.py +100 -0
  135. superqode/providers/local/base.py +438 -0
  136. superqode/providers/local/discovery.py +418 -0
  137. superqode/providers/local/lmstudio.py +256 -0
  138. superqode/providers/local/mlx.py +457 -0
  139. superqode/providers/local/ollama.py +486 -0
  140. superqode/providers/local/sglang.py +268 -0
  141. superqode/providers/local/tgi.py +260 -0
  142. superqode/providers/local/tool_support.py +477 -0
  143. superqode/providers/local/vllm.py +258 -0
  144. superqode/providers/manager.py +1338 -0
  145. superqode/providers/models.py +1016 -0
  146. superqode/providers/models_dev.py +578 -0
  147. superqode/providers/openresponses/__init__.py +87 -0
  148. superqode/providers/openresponses/converters/__init__.py +17 -0
  149. superqode/providers/openresponses/converters/messages.py +343 -0
  150. superqode/providers/openresponses/converters/tools.py +268 -0
  151. superqode/providers/openresponses/schema/__init__.py +56 -0
  152. superqode/providers/openresponses/schema/models.py +585 -0
  153. superqode/providers/openresponses/streaming/__init__.py +5 -0
  154. superqode/providers/openresponses/streaming/parser.py +338 -0
  155. superqode/providers/openresponses/tools/__init__.py +21 -0
  156. superqode/providers/openresponses/tools/apply_patch.py +352 -0
  157. superqode/providers/openresponses/tools/code_interpreter.py +290 -0
  158. superqode/providers/openresponses/tools/file_search.py +333 -0
  159. superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
  160. superqode/providers/registry.py +716 -0
  161. superqode/providers/usage.py +332 -0
  162. superqode/pure_mode.py +384 -0
  163. superqode/qr/__init__.py +23 -0
  164. superqode/qr/dashboard.py +781 -0
  165. superqode/qr/generator.py +1018 -0
  166. superqode/qr/templates.py +135 -0
  167. superqode/safety/__init__.py +41 -0
  168. superqode/safety/sandbox.py +413 -0
  169. superqode/safety/warnings.py +256 -0
  170. superqode/server/__init__.py +33 -0
  171. superqode/server/lsp_server.py +775 -0
  172. superqode/server/web.py +250 -0
  173. superqode/session/__init__.py +25 -0
  174. superqode/session/persistence.py +580 -0
  175. superqode/session/sharing.py +477 -0
  176. superqode/session.py +475 -0
  177. superqode/sidebar.py +2991 -0
  178. superqode/stream_view.py +648 -0
  179. superqode/styles/__init__.py +3 -0
  180. superqode/superqe/__init__.py +184 -0
  181. superqode/superqe/acp_runner.py +1064 -0
  182. superqode/superqe/constitution/__init__.py +62 -0
  183. superqode/superqe/constitution/evaluator.py +308 -0
  184. superqode/superqe/constitution/loader.py +432 -0
  185. superqode/superqe/constitution/schema.py +250 -0
  186. superqode/superqe/events.py +591 -0
  187. superqode/superqe/frameworks/__init__.py +65 -0
  188. superqode/superqe/frameworks/base.py +234 -0
  189. superqode/superqe/frameworks/e2e.py +263 -0
  190. superqode/superqe/frameworks/executor.py +237 -0
  191. superqode/superqe/frameworks/javascript.py +409 -0
  192. superqode/superqe/frameworks/python.py +373 -0
  193. superqode/superqe/frameworks/registry.py +92 -0
  194. superqode/superqe/mcp_tools/__init__.py +47 -0
  195. superqode/superqe/mcp_tools/core_tools.py +418 -0
  196. superqode/superqe/mcp_tools/registry.py +230 -0
  197. superqode/superqe/mcp_tools/testing_tools.py +167 -0
  198. superqode/superqe/noise.py +89 -0
  199. superqode/superqe/orchestrator.py +778 -0
  200. superqode/superqe/roles.py +609 -0
  201. superqode/superqe/session.py +713 -0
  202. superqode/superqe/skills/__init__.py +57 -0
  203. superqode/superqe/skills/base.py +106 -0
  204. superqode/superqe/skills/core_skills.py +899 -0
  205. superqode/superqe/skills/registry.py +90 -0
  206. superqode/superqe/verifier.py +101 -0
  207. superqode/superqe_cli.py +76 -0
  208. superqode/tool_call.py +358 -0
  209. superqode/tools/__init__.py +93 -0
  210. superqode/tools/agent_tools.py +496 -0
  211. superqode/tools/base.py +324 -0
  212. superqode/tools/batch_tool.py +133 -0
  213. superqode/tools/diagnostics.py +311 -0
  214. superqode/tools/edit_tools.py +653 -0
  215. superqode/tools/enhanced_base.py +515 -0
  216. superqode/tools/file_tools.py +269 -0
  217. superqode/tools/file_tracking.py +45 -0
  218. superqode/tools/lsp_tools.py +610 -0
  219. superqode/tools/network_tools.py +350 -0
  220. superqode/tools/permissions.py +400 -0
  221. superqode/tools/question_tool.py +324 -0
  222. superqode/tools/search_tools.py +598 -0
  223. superqode/tools/shell_tools.py +259 -0
  224. superqode/tools/todo_tools.py +121 -0
  225. superqode/tools/validation.py +80 -0
  226. superqode/tools/web_tools.py +639 -0
  227. superqode/tui.py +1152 -0
  228. superqode/tui_integration.py +875 -0
  229. superqode/tui_widgets/__init__.py +27 -0
  230. superqode/tui_widgets/widgets/__init__.py +18 -0
  231. superqode/tui_widgets/widgets/progress.py +185 -0
  232. superqode/tui_widgets/widgets/tool_display.py +188 -0
  233. superqode/undo_manager.py +574 -0
  234. superqode/utils/__init__.py +5 -0
  235. superqode/utils/error_handling.py +323 -0
  236. superqode/utils/fuzzy.py +257 -0
  237. superqode/widgets/__init__.py +477 -0
  238. superqode/widgets/agent_collab.py +390 -0
  239. superqode/widgets/agent_store.py +936 -0
  240. superqode/widgets/agent_switcher.py +395 -0
  241. superqode/widgets/animation_manager.py +284 -0
  242. superqode/widgets/code_context.py +356 -0
  243. superqode/widgets/command_palette.py +412 -0
  244. superqode/widgets/connection_status.py +537 -0
  245. superqode/widgets/conversation_history.py +470 -0
  246. superqode/widgets/diff_indicator.py +155 -0
  247. superqode/widgets/enhanced_status_bar.py +385 -0
  248. superqode/widgets/enhanced_toast.py +476 -0
  249. superqode/widgets/file_browser.py +809 -0
  250. superqode/widgets/file_reference.py +585 -0
  251. superqode/widgets/issue_timeline.py +340 -0
  252. superqode/widgets/leader_key.py +264 -0
  253. superqode/widgets/mode_switcher.py +445 -0
  254. superqode/widgets/model_picker.py +234 -0
  255. superqode/widgets/permission_preview.py +1205 -0
  256. superqode/widgets/prompt.py +358 -0
  257. superqode/widgets/provider_connect.py +725 -0
  258. superqode/widgets/pty_shell.py +587 -0
  259. superqode/widgets/qe_dashboard.py +321 -0
  260. superqode/widgets/resizable_sidebar.py +377 -0
  261. superqode/widgets/response_changes.py +218 -0
  262. superqode/widgets/response_display.py +528 -0
  263. superqode/widgets/rich_tool_display.py +613 -0
  264. superqode/widgets/sidebar_panels.py +1180 -0
  265. superqode/widgets/slash_complete.py +356 -0
  266. superqode/widgets/split_view.py +612 -0
  267. superqode/widgets/status_bar.py +273 -0
  268. superqode/widgets/superqode_display.py +786 -0
  269. superqode/widgets/thinking_display.py +815 -0
  270. superqode/widgets/throbber.py +87 -0
  271. superqode/widgets/toast.py +206 -0
  272. superqode/widgets/unified_output.py +1073 -0
  273. superqode/workspace/__init__.py +75 -0
  274. superqode/workspace/artifacts.py +472 -0
  275. superqode/workspace/coordinator.py +353 -0
  276. superqode/workspace/diff_tracker.py +429 -0
  277. superqode/workspace/git_guard.py +373 -0
  278. superqode/workspace/git_snapshot.py +526 -0
  279. superqode/workspace/manager.py +750 -0
  280. superqode/workspace/snapshot.py +357 -0
  281. superqode/workspace/watcher.py +535 -0
  282. superqode/workspace/worktree.py +440 -0
  283. superqode-0.1.5.dist-info/METADATA +204 -0
  284. superqode-0.1.5.dist-info/RECORD +288 -0
  285. superqode-0.1.5.dist-info/WHEEL +5 -0
  286. superqode-0.1.5.dist-info/entry_points.txt +3 -0
  287. superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
  288. superqode-0.1.5.dist-info/top_level.txt +1 -0
superqode/tui.py ADDED
@@ -0,0 +1,1152 @@
1
+ """
2
+ SuperQode TUI - Clean, Professional Developer Experience
3
+ Using Rich + prompt_toolkit for a polished CLI interface.
4
+
5
+ Features:
6
+ - Beautiful welcome screen with ASCII art
7
+ - Clean, focused prompt box with clear input area
8
+ - Smooth thinking animations
9
+ - Syntax-highlighted responses
10
+ - Professional exit/disconnect messages
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import os
16
+ import re
17
+ import sys
18
+ import time
19
+ import random
20
+ import shutil
21
+ import textwrap
22
+ import threading
23
+ from pathlib import Path
24
+ from typing import Optional, Callable, List, Dict, Any
25
+ from dataclasses import dataclass
26
+
27
+ from rich.console import Console, Group
28
+ from rich.panel import Panel
29
+ from rich.table import Table
30
+ from rich.text import Text
31
+ from rich.live import Live
32
+ from rich.spinner import Spinner
33
+ from rich.status import Status
34
+ from rich.syntax import Syntax
35
+ from rich.markdown import Markdown
36
+ from rich.align import Align
37
+ from rich.box import ROUNDED, DOUBLE, SIMPLE, HEAVY, MINIMAL, Box
38
+ from rich.rule import Rule
39
+ from rich.columns import Columns
40
+ from rich.style import Style
41
+ from rich.padding import Padding
42
+
43
+ from prompt_toolkit import PromptSession
44
+ from prompt_toolkit.completion import Completer, Completion
45
+ from prompt_toolkit.history import InMemoryHistory, FileHistory
46
+ from prompt_toolkit.styles import Style as PTStyle
47
+ from prompt_toolkit.key_binding import KeyBindings
48
+ from prompt_toolkit.keys import Keys
49
+ from prompt_toolkit.formatted_text import HTML
50
+
51
+
52
+ # ============================================================================
53
+ # TEAM CONFIGURATION READER
54
+ # ============================================================================
55
+
56
+
57
+ @dataclass
58
+ class TeamRole:
59
+ """Represents a configured team role."""
60
+
61
+ mode: str
62
+ role: str
63
+ description: str
64
+ model: str
65
+ provider: str
66
+ coding_agent: str
67
+ enabled: bool
68
+ job_description: str = ""
69
+ execution_mode: str = "acp" # "acp", "byok", or "local"
70
+ agent: str = "" # Agent ID for ACP mode
71
+
72
+ @property
73
+ def command(self) -> str:
74
+ return f":{self.mode} {self.role}"
75
+
76
+ @property
77
+ def display_name(self) -> str:
78
+ return f"{self.mode.upper()}.{self.role}"
79
+
80
+ @property
81
+ def exec_mode_display(self) -> str:
82
+ """Get display string for execution mode."""
83
+ if self.execution_mode == "acp":
84
+ return f"ACP•{self.agent or self.coding_agent}"
85
+ else:
86
+ return f"BYOK•{self.provider}"
87
+
88
+
89
+ @dataclass
90
+ class TeamConfig:
91
+ """Team configuration loaded from YAML."""
92
+
93
+ team_name: str
94
+ description: str
95
+ roles: List[TeamRole]
96
+
97
+ @property
98
+ def enabled_roles(self) -> List[TeamRole]:
99
+ return [r for r in self.roles if r.enabled]
100
+
101
+ @property
102
+ def enabled_count(self) -> int:
103
+ return len(self.enabled_roles)
104
+
105
+ def get_roles_by_mode(self, mode: str) -> List[TeamRole]:
106
+ return [r for r in self.roles if r.mode == mode]
107
+
108
+ def get_enabled_roles_by_mode(self, mode: str) -> List[TeamRole]:
109
+ return [r for r in self.enabled_roles if r.mode == mode]
110
+
111
+
112
+ def load_team_config() -> TeamConfig:
113
+ """Load team configuration from superqode.yaml."""
114
+ try:
115
+ from superqode.config import load_config
116
+
117
+ config = load_config()
118
+
119
+ team_name = "Development Team"
120
+ description = "AI-powered software development team"
121
+
122
+ if hasattr(config, "superqode") and config.superqode:
123
+ team_name = getattr(config.superqode, "team_name", team_name)
124
+ description = getattr(config.superqode, "description", description)
125
+
126
+ roles = []
127
+
128
+ if hasattr(config, "team") and config.team and hasattr(config.team, "modes"):
129
+ for mode_name, mode_config in config.team.modes.items():
130
+ if hasattr(mode_config, "roles") and mode_config.roles:
131
+ for role_name, role_config in mode_config.roles.items():
132
+ enabled = getattr(role_config, "enabled", True)
133
+
134
+ # Get execution mode (explicit or inferred)
135
+ exec_mode = getattr(role_config, "mode", "")
136
+ agent_id = getattr(role_config, "agent", "")
137
+ coding_agent = getattr(role_config, "coding_agent", "opencode")
138
+
139
+ # Infer execution mode if not explicit
140
+ if not exec_mode:
141
+ if agent_id or (
142
+ coding_agent
143
+ and coding_agent not in ("superqode", "superqode", "byok")
144
+ ):
145
+ exec_mode = "acp"
146
+ else:
147
+ exec_mode = "byok"
148
+
149
+ # Get model from agent_config if ACP mode
150
+ model = getattr(role_config, "model", "")
151
+ provider = getattr(role_config, "provider", "")
152
+
153
+ agent_config = getattr(role_config, "agent_config", None)
154
+ if agent_config:
155
+ if not model:
156
+ model = getattr(agent_config, "model", "glm-4.7")
157
+ if not provider:
158
+ provider = getattr(agent_config, "provider", "")
159
+
160
+ roles.append(
161
+ TeamRole(
162
+ mode=mode_name,
163
+ role=role_name,
164
+ description=getattr(role_config, "description", ""),
165
+ model=model or "glm-4.7",
166
+ provider=provider or "opencode",
167
+ coding_agent=coding_agent,
168
+ enabled=enabled,
169
+ job_description=getattr(role_config, "job_description", ""),
170
+ execution_mode=exec_mode,
171
+ agent=agent_id or coding_agent,
172
+ )
173
+ )
174
+
175
+ return TeamConfig(team_name=team_name, description=description, roles=roles)
176
+
177
+ except Exception:
178
+ return TeamConfig(
179
+ team_name="Development Team",
180
+ description="AI-powered software development team",
181
+ roles=[
182
+ TeamRole(
183
+ "dev",
184
+ "fullstack",
185
+ "Full-stack development",
186
+ "glm-4.7",
187
+ "opencode",
188
+ "opencode",
189
+ True,
190
+ "",
191
+ "acp",
192
+ "opencode",
193
+ ),
194
+ TeamRole(
195
+ "qe",
196
+ "fullstack",
197
+ "Full-stack QE",
198
+ "grok-code",
199
+ "opencode",
200
+ "opencode",
201
+ True,
202
+ "",
203
+ "acp",
204
+ "opencode",
205
+ ),
206
+ TeamRole(
207
+ "devops",
208
+ "fullstack",
209
+ "Full-stack DevOps",
210
+ "glm-4.7",
211
+ "opencode",
212
+ "opencode",
213
+ True,
214
+ "",
215
+ "acp",
216
+ "opencode",
217
+ ),
218
+ ],
219
+ )
220
+
221
+
222
+ # ============================================================================
223
+ # ASCII ART LOGO
224
+ # ============================================================================
225
+
226
+ SUPERQODE_ASCII = """
227
+ [bold #a855f7] ____ _ _ ____ _____ ____ ___ ___ ____ _____[/]
228
+ [bold #c084fc]/ ___|| | | | _ \\| ____| _ \\ / _ \\ / _ \\| _ \\| ____|[/]
229
+ [bold #ec4899]\\___ \\| | | | |_) | _| | |_) | | | || | | | | | | _| [/]
230
+ [bold #f97316] ___) | |_| | __/| |___| _ <| |_| || |_| | |_| | |___ [/]
231
+ [bold #fb923c]|____/ \\___/|_| |_____|_| \\_\\\\__\\_\\ \\___/|____/|_____|[/]
232
+ """
233
+
234
+ # Compact logo for smaller terminals
235
+ SUPERQODE_ASCII_COMPACT = """[bold bright_cyan]SUPERQODE[/]"""
236
+
237
+
238
+ # ============================================================================
239
+ # EMOJI & CONSTANTS
240
+ # ============================================================================
241
+
242
+ EMOJI = {
243
+ "brain": "🧠",
244
+ "rocket": "🚀",
245
+ "sparkles": "✨",
246
+ "lightning": "⚡",
247
+ "star": "⭐",
248
+ "fire": "🔥",
249
+ "gem": "💎",
250
+ "robot": "🤖",
251
+ "laptop": "💻",
252
+ "test_tube": "🧪",
253
+ "wrench": "🔧",
254
+ "house": "🏠",
255
+ "link": "🔗",
256
+ "folder": "📁",
257
+ "check": "✅",
258
+ "cross": "❌",
259
+ "warning": "⚠️",
260
+ "info": "ℹ️",
261
+ "gear": "⚙️",
262
+ "search": "🔍",
263
+ "thought": "💭",
264
+ "writing": "🖋️",
265
+ "tools": "🛠️",
266
+ "package": "📦",
267
+ "wave": "👋",
268
+ "point_right": "👉",
269
+ "bulb": "💡",
270
+ "zap": "⚡",
271
+ "target": "🎯",
272
+ "trophy": "🏆",
273
+ "magic": "🪄",
274
+ "crystal": "🔮",
275
+ "hourglass": "⏳",
276
+ "clock": "🕐",
277
+ "green_circle": "🟢",
278
+ "yellow_circle": "🟡",
279
+ "blue_circle": "🔵",
280
+ "white_circle": "⚪",
281
+ "plug": "🔌",
282
+ "key": "🔑",
283
+ "book": "📖",
284
+ "globe": "🌐",
285
+ "heart": "❤️",
286
+ "thumbs_up": "👍",
287
+ "eyes": "👀",
288
+ "speech": "💬",
289
+ "terminal": "▶",
290
+ "prompt": "❯",
291
+ "arrow": "→",
292
+ "dot": "●",
293
+ }
294
+
295
+ # Thinking messages with emojis
296
+ THINKING_MESSAGES = [
297
+ ("Analyzing your request", "brain"),
298
+ ("Understanding context", "search"),
299
+ ("Thinking deeply", "thought"),
300
+ ("Processing information", "gear"),
301
+ ("Reading codebase", "book"),
302
+ ("Exploring files", "folder"),
303
+ ("Formulating response", "writing"),
304
+ ("Crafting solution", "tools"),
305
+ ("Connecting the dots", "link"),
306
+ ("Almost there", "rocket"),
307
+ ]
308
+
309
+
310
+ # ============================================================================
311
+ # OUTPUT FILTERING
312
+ # ============================================================================
313
+
314
+
315
+ class OutputFilter:
316
+ """Filter agent output to show only the response."""
317
+
318
+ TOOL_OPERATIONS = [
319
+ "Read",
320
+ "Write",
321
+ "Edit",
322
+ "Bash",
323
+ "Grep",
324
+ "Glob",
325
+ "Search",
326
+ "List",
327
+ "Task",
328
+ "TodoWrite",
329
+ "WebFetch",
330
+ "WebSearch",
331
+ "LSP",
332
+ "NotebookEdit",
333
+ ]
334
+
335
+ def __init__(self):
336
+ self.ansi_pattern = re.compile(r"\x1b\[[0-9;]*m|\[\d+(?:;\d+)*m")
337
+
338
+ def filter(self, text: str) -> str:
339
+ """Filter out file operations from agent output."""
340
+ if not text:
341
+ return text
342
+
343
+ lines = text.split("\n")
344
+ filtered = []
345
+
346
+ for line in lines:
347
+ clean = self.ansi_pattern.sub("", line).strip()
348
+
349
+ # Skip tool operation lines
350
+ should_skip = False
351
+ if clean.startswith("|"):
352
+ after_pipe = clean[1:].strip()
353
+ for op in self.TOOL_OPERATIONS:
354
+ if after_pipe.startswith(op) and (
355
+ len(after_pipe) == len(op) or after_pipe[len(op)] in " \t"
356
+ ):
357
+ should_skip = True
358
+ break
359
+
360
+ if not should_skip:
361
+ filtered.append(line)
362
+
363
+ result = "\n".join(filtered)
364
+ result = re.sub(r"\n{3,}", "\n\n", result)
365
+ return result.strip()
366
+
367
+
368
+ output_filter = OutputFilter()
369
+
370
+
371
+ # ============================================================================
372
+ # THINKING ANIMATION - Clean, Professional Spinner
373
+ # ============================================================================
374
+
375
+
376
+ class ThinkingSpinner:
377
+ """
378
+ Clean thinking animation using Rich Status.
379
+ Shows a professional spinner with elapsed time.
380
+ """
381
+
382
+ def __init__(self, console: Console, message: str = "Thinking..."):
383
+ self.console = console
384
+ self.initial_message = message
385
+ self._status: Optional[Status] = None
386
+ self._start_time = 0.0
387
+ self._running = False
388
+ self._thread: Optional[threading.Thread] = None
389
+ self._msg_index = 0
390
+ self._last_msg_change = 0.0
391
+
392
+ def _get_status_text(self) -> str:
393
+ """Generate the status text with emoji and time."""
394
+ elapsed = time.time() - self._start_time
395
+
396
+ # Change message every 3 seconds
397
+ if time.time() - self._last_msg_change > 3.0:
398
+ self._msg_index = (self._msg_index + 1) % len(THINKING_MESSAGES)
399
+ self._last_msg_change = time.time()
400
+
401
+ msg_text, emoji_key = THINKING_MESSAGES[self._msg_index]
402
+ emoji = EMOJI.get(emoji_key, EMOJI["brain"])
403
+
404
+ return f" {emoji} [bold cyan]{msg_text}[/bold cyan] [dim]({elapsed:.1f}s)[/dim]"
405
+
406
+ def _update_loop(self):
407
+ """Background thread to update status text."""
408
+ while self._running and self._status:
409
+ try:
410
+ self._status.update(self._get_status_text())
411
+ time.sleep(0.1)
412
+ except Exception:
413
+ break
414
+
415
+ def __enter__(self):
416
+ """Start the animation."""
417
+ self._start_time = time.time()
418
+ self._last_msg_change = time.time()
419
+ self._msg_index = random.randint(0, len(THINKING_MESSAGES) - 1)
420
+ self._running = True
421
+
422
+ # Create Rich Status with spinner
423
+ self._status = self.console.status(
424
+ self._get_status_text(),
425
+ spinner="dots",
426
+ spinner_style="bright_cyan",
427
+ )
428
+ self._status.__enter__()
429
+
430
+ # Start update thread
431
+ self._thread = threading.Thread(target=self._update_loop, daemon=True)
432
+ self._thread.start()
433
+
434
+ return self
435
+
436
+ def __exit__(self, *args):
437
+ """Stop the animation and show completion."""
438
+ self._running = False
439
+
440
+ if self._thread:
441
+ self._thread.join(timeout=0.5)
442
+
443
+ if self._status:
444
+ self._status.__exit__(*args)
445
+
446
+ elapsed = time.time() - self._start_time
447
+ self.console.print(
448
+ f" [bold green]✓[/bold green] [green]Complete[/green] [dim]({elapsed:.1f}s)[/dim]"
449
+ )
450
+
451
+
452
+ # ============================================================================
453
+ # RESPONSE PANEL - Clean Code Display
454
+ # ============================================================================
455
+
456
+
457
+ def _strip_markdown(text: str) -> str:
458
+ """Strip markdown formatting from text for clean display."""
459
+ # Strip bold: **text** or __text__
460
+ text = re.sub(r"\*\*(.+?)\*\*", r"\1", text)
461
+ text = re.sub(r"__(.+?)__", r"\1", text)
462
+
463
+ # Strip italic: *text* or _text_
464
+ text = re.sub(r"(?<![*\w])\*([^*]+?)\*(?![*\w])", r"\1", text)
465
+ text = re.sub(r"(?<![_\w])_([^_]+?)_(?![_\w])", r"\1", text)
466
+
467
+ # Strip strikethrough: ~~text~~
468
+ text = re.sub(r"~~(.+?)~~", r"\1", text)
469
+
470
+ # Strip inline code: `code`
471
+ text = re.sub(r"`([^`]+?)`", r"\1", text)
472
+
473
+ # Strip links: [text](url) -> text
474
+ text = re.sub(r"\[([^\]]+?)\]\([^)]+?\)", r"\1", text)
475
+
476
+ # Strip images: ![alt](url) -> alt
477
+ text = re.sub(r"!\[([^\]]*?)\]\([^)]+?\)", r"\1", text)
478
+
479
+ # Strip blockquotes: > text -> text
480
+ text = re.sub(r"^>\s*", "", text, flags=re.MULTILINE)
481
+
482
+ # Strip headers: # text -> text (keep the text)
483
+ text = re.sub(r"^#{1,6}\s+", "", text, flags=re.MULTILINE)
484
+
485
+ return text
486
+
487
+
488
+ class ResponsePanel:
489
+ """Display agent responses with syntax highlighting."""
490
+
491
+ def __init__(self, console: Console):
492
+ self.console = console
493
+ self.filter = output_filter
494
+
495
+ def display(self, content: str, title: str = "Response", agent_name: str = ""):
496
+ """Display a response with code highlighting."""
497
+ filtered = self.filter.filter(content)
498
+
499
+ if not filtered.strip():
500
+ return
501
+
502
+ rendered = self._render_content(filtered)
503
+
504
+ # Create header with agent name
505
+ if agent_name:
506
+ header = f"[bold bright_cyan]{EMOJI['robot']} {agent_name}[/bold bright_cyan]"
507
+ else:
508
+ header = f"[bold bright_cyan]{title}[/bold bright_cyan]"
509
+
510
+ panel = Panel(
511
+ rendered,
512
+ title=header,
513
+ title_align="left",
514
+ border_style="bright_blue",
515
+ box=ROUNDED,
516
+ padding=(1, 2),
517
+ )
518
+
519
+ self.console.print()
520
+ self.console.print(panel)
521
+
522
+ def _render_content(self, content: str) -> Group:
523
+ """Render content with syntax-highlighted code blocks - no raw markdown."""
524
+ # Get terminal width for wrapping
525
+ term_width = shutil.get_terminal_size().columns
526
+ wrap_width = min(term_width - 10, 100) # Conservative width
527
+
528
+ # Pattern to match code blocks with optional language
529
+ code_pattern = r"```(\w*)\n?(.*?)```"
530
+ parts = []
531
+ last_end = 0
532
+
533
+ for match in re.finditer(code_pattern, content, re.DOTALL):
534
+ # Add text before code block
535
+ if match.start() > last_end:
536
+ text = content[last_end : match.start()]
537
+ if text.strip():
538
+ # Strip markdown and wrap text properly
539
+ clean_text = _strip_markdown(text.strip())
540
+ wrapped = textwrap.fill(clean_text, width=wrap_width)
541
+ parts.append(Text(wrapped))
542
+
543
+ lang = match.group(1) or "text"
544
+ code = match.group(2)
545
+
546
+ # Language mapping
547
+ lang_map = {
548
+ "py": "python",
549
+ "js": "javascript",
550
+ "ts": "typescript",
551
+ "sh": "bash",
552
+ "yml": "yaml",
553
+ }
554
+ lang = lang_map.get(lang.lower(), lang) if lang else "text"
555
+
556
+ # Create syntax highlighted code
557
+ if code.strip():
558
+ syntax = Syntax(
559
+ code.strip(),
560
+ lang,
561
+ theme="monokai",
562
+ line_numbers=True,
563
+ word_wrap=True,
564
+ background_color="#000000",
565
+ )
566
+ parts.append(Text()) # Spacing
567
+ parts.append(syntax)
568
+ parts.append(Text()) # Spacing
569
+
570
+ last_end = match.end()
571
+
572
+ # Add remaining text after last code block
573
+ if last_end < len(content):
574
+ remaining = content[last_end:]
575
+ if remaining.strip():
576
+ # Strip markdown and wrap
577
+ clean_text = _strip_markdown(remaining.strip())
578
+ wrapped = textwrap.fill(clean_text, width=wrap_width)
579
+ parts.append(Text(wrapped))
580
+
581
+ return Group(*parts) if parts else Group(Text(_strip_markdown(content)))
582
+
583
+
584
+ # ============================================================================
585
+ # WELCOME SCREEN - Professional Landing Page
586
+ # ============================================================================
587
+
588
+
589
+ def print_welcome(console: Console, team_config: Optional[TeamConfig] = None):
590
+ """Print a beautiful, professional welcome screen."""
591
+ console.clear()
592
+
593
+ if team_config is None:
594
+ team_config = load_team_config()
595
+
596
+ # Get terminal width for responsive layout
597
+ term_width = shutil.get_terminal_size().columns
598
+
599
+ # ASCII Logo (use compact for narrow terminals)
600
+ console.print()
601
+ if term_width >= 80:
602
+ console.print(SUPERQODE_ASCII)
603
+ else:
604
+ console.print(SUPERQODE_ASCII_COMPACT)
605
+
606
+ # Tagline
607
+ console.print(
608
+ Align.center(
609
+ f"[bold white]{team_config.team_name}[/bold white] [dim]•[/dim] [dim]{team_config.description}[/dim]"
610
+ )
611
+ )
612
+ console.print()
613
+
614
+ # Separator
615
+ console.print(Rule(style="bright_magenta"))
616
+ console.print()
617
+
618
+ # Quick start section in a clean grid
619
+ enabled = team_config.enabled_roles
620
+
621
+ if enabled:
622
+ # Create a nice table for available agents
623
+ table = Table(
624
+ show_header=False,
625
+ box=None,
626
+ padding=(0, 2),
627
+ expand=False,
628
+ )
629
+ table.add_column("Icon", style="bold", width=3)
630
+ table.add_column("Command", style="bold yellow", width=18)
631
+ table.add_column("Mode", style="bold", width=14)
632
+ table.add_column("Description", style="white")
633
+ table.add_column("Model", style="dim cyan", width=15)
634
+
635
+ mode_icons = {"dev": "💻", "qe": "🧪", "devops": "⚙️"}
636
+
637
+ for role in enabled[:5]:
638
+ icon = mode_icons.get(role.mode, "🔧")
639
+
640
+ # Execution mode badge
641
+ if role.execution_mode == "acp":
642
+ exec_badge = f"[blue]ACP[/blue]•{role.agent[:8]}"
643
+ else:
644
+ exec_badge = f"[green]BYOK[/green]•{role.provider[:6]}"
645
+
646
+ table.add_row(
647
+ icon,
648
+ role.command,
649
+ exec_badge,
650
+ role.description[:30] + "..." if len(role.description) > 30 else role.description,
651
+ role.model[:12],
652
+ )
653
+
654
+ console.print(Align.center(table))
655
+
656
+ if len(enabled) > 5:
657
+ console.print(
658
+ Align.center(
659
+ f"[dim]... and {len(enabled) - 5} more roles (use :roles to see all)[/dim]"
660
+ )
661
+ )
662
+
663
+ console.print()
664
+ console.print(Rule(style="dim cyan"))
665
+ console.print()
666
+
667
+ # Quick commands hint
668
+ hints = Text()
669
+ hints.append(" Quick Start: ", style="bold white")
670
+ hints.append("🏠 :home", style="bold yellow")
671
+ hints.append(" • ", style="dim")
672
+ hints.append("🚀 :i", style="bold yellow")
673
+ hints.append(" • ", style="dim")
674
+ hints.append("📚 :s", style="bold yellow")
675
+ hints.append(" • ", style="dim")
676
+ hints.append("🔌 :c", style="bold yellow")
677
+ hints.append(" • ", style="dim")
678
+ hints.append("👋 :q", style="bold yellow")
679
+ hints.append(" exit", style="dim")
680
+
681
+ console.print()
682
+ console.print(Align.center(hints))
683
+ console.print()
684
+
685
+
686
+ def print_roles(console: Console, team_config: Optional[TeamConfig] = None):
687
+ """Print all available roles in a clean format."""
688
+ if team_config is None:
689
+ team_config = load_team_config()
690
+
691
+ console.print()
692
+
693
+ # Header
694
+ header = Text()
695
+ header.append(f"{EMOJI['robot']} ", style="bold")
696
+ header.append(team_config.team_name, style="bold white")
697
+ header.append(" - Available Roles", style="dim")
698
+ console.print(Align.center(header))
699
+ console.print()
700
+
701
+ # Legend
702
+ console.print(
703
+ Align.center(
704
+ "[dim]Execution Modes:[/dim] [blue]ACP[/blue] = Coding Agent [green]BYOK[/green] = Direct LLM API"
705
+ )
706
+ )
707
+ console.print()
708
+
709
+ mode_info = {
710
+ "dev": (EMOJI["laptop"], "Development", "bright_green"),
711
+ "qa": (EMOJI["test_tube"], "Quality Assurance", "bright_yellow"),
712
+ "devops": (EMOJI["gear"], "DevOps", "bright_blue"),
713
+ }
714
+
715
+ for mode in ["dev", "qe", "devops"]:
716
+ roles = team_config.get_roles_by_mode(mode)
717
+ if not roles:
718
+ continue
719
+
720
+ icon, title, color = mode_info.get(mode, (EMOJI["wrench"], mode.upper(), "white"))
721
+
722
+ # Mode header
723
+ console.print(f" [bold {color}]{icon} {title}[/bold {color}]")
724
+
725
+ # Roles table
726
+ for role in roles:
727
+ status = (
728
+ f"[green]{EMOJI['green_circle']}[/green]"
729
+ if role.enabled
730
+ else f"[dim]{EMOJI['white_circle']}[/dim]"
731
+ )
732
+ desc = role.description[:35] + "..." if len(role.description) > 35 else role.description
733
+
734
+ # Execution mode badge
735
+ if role.execution_mode == "acp":
736
+ exec_badge = f"[blue]ACP[/blue]•{role.agent[:8]:<8}"
737
+ else:
738
+ exec_badge = f"[green]BYOK[/green]•{role.provider[:6]:<6}"
739
+
740
+ console.print(
741
+ f" {status} [yellow]{role.command:<18}[/yellow] "
742
+ f"{exec_badge} "
743
+ f"[dim cyan]{role.model:<12}[/dim cyan] "
744
+ f"[dim]{desc}[/dim]"
745
+ )
746
+ console.print()
747
+
748
+ # Footer
749
+ total = len(team_config.roles)
750
+ enabled = team_config.enabled_count
751
+ console.print(
752
+ f" [dim]{EMOJI['bulb']} {enabled}/{total} roles enabled. Edit superqode.yaml to configure.[/dim]"
753
+ )
754
+ console.print()
755
+
756
+ # Commands hint
757
+ console.print(
758
+ f" [dim]Commands:[/dim] [yellow]:agents connect[/yellow] (ACP) [yellow]:providers use[/yellow] (BYOK)"
759
+ )
760
+ console.print()
761
+
762
+
763
+ # ============================================================================
764
+ # DISCONNECT & EXIT MESSAGES
765
+ # ============================================================================
766
+
767
+
768
+ def print_disconnect_message(console: Console, agent_name: str = "Agent"):
769
+ """Print a clean disconnect message."""
770
+ console.print()
771
+
772
+ content = Text()
773
+ content.append(f"{EMOJI['wave']} ", style="bold")
774
+ content.append("Disconnected from ", style="white")
775
+ content.append(agent_name, style="bold cyan")
776
+
777
+ panel = Panel(
778
+ content,
779
+ border_style="cyan",
780
+ box=ROUNDED,
781
+ padding=(0, 2),
782
+ )
783
+ console.print(panel)
784
+ console.print()
785
+
786
+
787
+ def print_exit_message(console: Console):
788
+ """Print a clean exit message."""
789
+ console.print()
790
+
791
+ content = Text()
792
+ content.append(f"{EMOJI['wave']} ", style="bold")
793
+ content.append("Thanks for using ", style="white")
794
+ content.append("SuperQode", style="bold bright_cyan")
795
+ content.append("!", style="white")
796
+
797
+ panel = Panel(
798
+ content,
799
+ border_style="bright_cyan",
800
+ box=ROUNDED,
801
+ padding=(0, 2),
802
+ )
803
+ console.print(panel)
804
+ console.print()
805
+
806
+
807
+ # ============================================================================
808
+ # COMMAND COMPLETER
809
+ # ============================================================================
810
+
811
+
812
+ class SuperQodeCompleter(Completer):
813
+ """Command completer with dynamic role loading."""
814
+
815
+ def __init__(self):
816
+ self.base_commands = [
817
+ (":roles", "List all available roles"),
818
+ (":agents", "List available ACP agents"),
819
+ (":agents store", "Browse agent store"),
820
+ (":agents connect", "Connect to an ACP agent (full coding capabilities)"),
821
+ (":providers", "List available BYOK providers"),
822
+ (":providers list", "List all BYOK providers"),
823
+ (":providers use", "Use a BYOK provider (direct LLM API)"),
824
+ (":disconnect", "Disconnect from agent/provider"),
825
+ (":home", "Return to home screen"),
826
+ (":files", "Show project files"),
827
+ (":find", "Fuzzy search files"),
828
+ (":recent", "Show recent files"),
829
+ (":bookmark", "Manage bookmarks"),
830
+ (":handoff", "Hand off to another role"),
831
+ (":context", "Show work context"),
832
+ (":approve", "Approve work"),
833
+ (":help", "Show help"),
834
+ (":h", "Alias for :help"),
835
+ (":init", "Initialize SuperQode configuration"),
836
+ (":i", "Alias for :init"),
837
+ (":sidebar", "Show/hide sidebar"),
838
+ (":s", "Alias for :sidebar"),
839
+ (":connect", "Connect to an agent or provider"),
840
+ (":c", "Alias for :connect"),
841
+ (":clear", "Clear screen"),
842
+ (":exit", "Exit SuperQode"),
843
+ (":quit", "Exit SuperQode"),
844
+ (":q", "Alias for :exit"),
845
+ ]
846
+ self._role_commands: Optional[List[tuple]] = None
847
+
848
+ @property
849
+ def commands(self) -> List[tuple]:
850
+ if self._role_commands is None:
851
+ self._load_role_commands()
852
+ return self._role_commands + self.base_commands
853
+
854
+ def _load_role_commands(self):
855
+ self._role_commands = []
856
+ try:
857
+ team_config = load_team_config()
858
+ for role in team_config.roles:
859
+ # Show execution mode in description
860
+ mode_badge = "ACP" if role.execution_mode == "acp" else "BYOK"
861
+ desc = f"[{mode_badge}] {role.description} ({role.model})"
862
+ if not role.enabled:
863
+ desc += " [disabled]"
864
+ self._role_commands.append((role.command, desc))
865
+ except Exception:
866
+ self._role_commands = [
867
+ (":qe fullstack", "[ACP] Full-stack QE"),
868
+ (":qe api_tester", "[ACP] API Tester"),
869
+ ]
870
+
871
+ def get_completions(self, document, complete_event):
872
+ text = document.text_before_cursor.lower()
873
+ if not text.startswith(":"):
874
+ return
875
+
876
+ for cmd, desc in self.commands:
877
+ if cmd.lower().startswith(text):
878
+ yield Completion(cmd, start_position=-len(text), display=cmd, display_meta=desc)
879
+
880
+
881
+ # Alias for backward compatibility
882
+ SuperQodeCompleter = SuperQodeCompleter
883
+
884
+
885
+ # ============================================================================
886
+ # ENHANCED PROMPT - Centered, Fully Visible Input Box
887
+ # ============================================================================
888
+
889
+
890
+ class EnhancedPrompt:
891
+ """
892
+ Clean, professional prompt centered on screen.
893
+
894
+ Features:
895
+ - Centered prompt box that's always fully visible
896
+ - Mode indicator (HOME, DEV, QA, etc.)
897
+ - Agent connection status
898
+ - Tab completion for commands
899
+ - History navigation
900
+ - Footer hints always visible
901
+ """
902
+
903
+ # prompt_toolkit style
904
+ STYLE = PTStyle.from_dict(
905
+ {
906
+ "prompt": "bold ansicyan",
907
+ "mode": "bold ansigreen",
908
+ "arrow": "bold ansiwhite",
909
+ "input": "ansiwhite",
910
+ "completion-menu": "bg:ansiblack ansigreen",
911
+ "completion-menu.completion": "bg:ansiblack ansiwhite",
912
+ "completion-menu.completion.current": "bg:ansicyan ansiblack bold",
913
+ "completion-menu.meta": "bg:ansiblack ansigray",
914
+ "completion-menu.meta.current": "bg:ansicyan ansiblack",
915
+ }
916
+ )
917
+
918
+ def __init__(self, history_file: Optional[Path] = None):
919
+ # Setup history
920
+ if history_file:
921
+ history_file.parent.mkdir(parents=True, exist_ok=True)
922
+ self.history = FileHistory(str(history_file))
923
+ else:
924
+ self.history = InMemoryHistory()
925
+
926
+ # Key bindings
927
+ self.bindings = KeyBindings()
928
+
929
+ @self.bindings.add(Keys.ControlC)
930
+ def _(event):
931
+ event.app.exit(exception=KeyboardInterrupt())
932
+
933
+ @self.bindings.add(Keys.ControlD)
934
+ def _(event):
935
+ event.app.exit(exception=EOFError())
936
+
937
+ # Create session
938
+ self.session = PromptSession(
939
+ history=self.history,
940
+ completer=SuperQodeCompleter(),
941
+ style=self.STYLE,
942
+ key_bindings=self.bindings,
943
+ complete_while_typing=True,
944
+ enable_history_search=True,
945
+ )
946
+
947
+ # State
948
+ self.mode = "HOME"
949
+ self.connected = False
950
+ self.agent_name = ""
951
+ self.execution_mode = "" # "acp" or "byok"
952
+ self.console = Console()
953
+
954
+ def _get_mode_info(self) -> tuple:
955
+ """Get mode icon, text, and color."""
956
+ if self.connected and self.agent_name:
957
+ # Show execution mode when connected
958
+ if self.execution_mode == "acp":
959
+ return EMOJI["link"], f"ACP • {self.agent_name.upper()}", "bright_blue"
960
+ elif self.execution_mode == "byok":
961
+ return EMOJI["zap"], f"BYOK • {self.agent_name.upper()}", "bright_green"
962
+ else:
963
+ return EMOJI["link"], self.agent_name.upper(), "bright_magenta"
964
+
965
+ mode_map = {
966
+ "HOME": (EMOJI["house"], "HOME", "bright_cyan"),
967
+ "DEV": (EMOJI["laptop"], "DEV", "bright_green"),
968
+ "QA": (EMOJI["test_tube"], "QA", "bright_yellow"),
969
+ "DEVOPS": (EMOJI["gear"], "DEVOPS", "bright_blue"),
970
+ }
971
+
972
+ base = self.mode.split(".")[0].upper() if "." in self.mode else self.mode.upper()
973
+ return mode_map.get(base, (EMOJI["wrench"], self.mode.upper(), "white"))
974
+
975
+ def _get_box_width(self) -> int:
976
+ """Get the prompt box width based on terminal size."""
977
+ term_width = shutil.get_terminal_size().columns
978
+ return min(term_width - 4, 70)
979
+
980
+ def prompt(self, clear_screen: bool = False) -> str:
981
+ """Show clean prompt with mode badge and get input.
982
+
983
+ Args:
984
+ clear_screen: If True, clears screen before showing prompt.
985
+ """
986
+ if clear_screen:
987
+ self.console.clear()
988
+
989
+ icon, mode_text, color = self._get_mode_info()
990
+
991
+ # REMOVED EXTRA PRINT HERE to move badge fully up
992
+
993
+ # Mode badge with extra text for HOME
994
+ if mode_text == "HOME":
995
+ display_text = f"{icon} {mode_text} [dim]ready to code[/dim]"
996
+ else:
997
+ display_text = f"{icon} {mode_text}"
998
+
999
+ self.console.print(f"[bold {color} reverse] {display_text} [/]")
1000
+
1001
+ # Get input with simple prompt
1002
+ try:
1003
+ # Add a small prefix to the prompt to give it some horizontal breathing room
1004
+ result = self.session.prompt("❯ ")
1005
+ except (KeyboardInterrupt, EOFError):
1006
+ self.console.print()
1007
+ raise
1008
+
1009
+ # Footer hints after input - reduced space to match badge-prompt gap
1010
+ self.console.print()
1011
+
1012
+ hints = (
1013
+ f" [bright_cyan]🏠 :home[/] [dim]•[/] "
1014
+ f"[bright_yellow]❓ :h[/] [dim][:help][/] [dim]•[/] "
1015
+ f"[bright_magenta]🚀 :i[/] [dim][:init][/] [dim]•[/] "
1016
+ f"[bright_blue]📚 :s[/] [dim][:sidebar][/] [dim]•[/] "
1017
+ f"[bright_green]🔌 :c[/] [dim][:connect][/] [dim]•[/] "
1018
+ f"[bright_red]👋 :q[/] [dim][:quit][/]"
1019
+ )
1020
+ self.console.print(hints)
1021
+
1022
+ return result
1023
+
1024
+ def set_mode(self, mode: str):
1025
+ """Set the current mode."""
1026
+ self.mode = mode
1027
+
1028
+ def set_connected(self, agent_name: str, connected: bool = True, execution_mode: str = "acp"):
1029
+ """Set connection state.
1030
+
1031
+ Args:
1032
+ agent_name: Name of the agent or provider
1033
+ connected: Whether connected or not
1034
+ execution_mode: "acp" for coding agent, "byok" for direct LLM
1035
+ """
1036
+ self.agent_name = agent_name
1037
+ self.connected = connected
1038
+ self.execution_mode = execution_mode
1039
+
1040
+
1041
+ # ============================================================================
1042
+ # MAIN TUI CLASS - Unified Interface
1043
+ # ============================================================================
1044
+
1045
+
1046
+ class SuperQodeUI:
1047
+ """Main TUI controller combining all components."""
1048
+
1049
+ def __init__(self):
1050
+ self.console = Console()
1051
+ self.prompt = EnhancedPrompt(history_file=Path.home() / ".superqode" / "history")
1052
+ self.response_panel = ResponsePanel(self.console)
1053
+ self.output_filter = output_filter
1054
+ self._team_config: Optional[TeamConfig] = None
1055
+
1056
+ @property
1057
+ def team_config(self) -> TeamConfig:
1058
+ if self._team_config is None:
1059
+ self._team_config = load_team_config()
1060
+ return self._team_config
1061
+
1062
+ def reload_config(self):
1063
+ """Reload team configuration."""
1064
+ self._team_config = load_team_config()
1065
+
1066
+ def print_welcome(self):
1067
+ """Print the welcome screen."""
1068
+ print_welcome(self.console, self.team_config)
1069
+
1070
+ def print_roles(self):
1071
+ """Print all available roles."""
1072
+ print_roles(self.console, self.team_config)
1073
+
1074
+ def get_input(self, clear_screen: bool = False) -> str:
1075
+ """Get user input with the enhanced prompt.
1076
+
1077
+ Args:
1078
+ clear_screen: If True, clears screen before showing prompt.
1079
+ """
1080
+ return self.prompt.prompt(clear_screen=clear_screen)
1081
+
1082
+ def wait_for_keypress(self):
1083
+ """Wait for user to press Enter before continuing."""
1084
+ self.console.print()
1085
+ self.console.print("[dim] Press Enter to continue...[/dim]", end="")
1086
+ input()
1087
+
1088
+ def set_mode(self, mode: str):
1089
+ """Set the current mode."""
1090
+ self.prompt.set_mode(mode)
1091
+
1092
+ def set_agent(self, name: str, connected: bool = False, execution_mode: str = "acp"):
1093
+ """Set agent connection state.
1094
+
1095
+ Args:
1096
+ name: Name of the agent or provider
1097
+ connected: Whether connected or not
1098
+ execution_mode: "acp" for coding agent, "byok" for direct LLM
1099
+ """
1100
+ self.prompt.set_connected(name, connected, execution_mode)
1101
+
1102
+ def show_thinking(self, message: str = "Thinking..."):
1103
+ """Show thinking animation (context manager)."""
1104
+ return ThinkingSpinner(self.console, message)
1105
+
1106
+ def display_response(self, content: str, agent_name: str = "Agent"):
1107
+ """Display an agent response."""
1108
+ self.response_panel.display(content, agent_name=agent_name)
1109
+
1110
+ def filter_output(self, text: str) -> str:
1111
+ """Filter agent output."""
1112
+ return self.output_filter.filter(text)
1113
+
1114
+ def print(self, *args, **kwargs):
1115
+ """Print to console."""
1116
+ self.console.print(*args, **kwargs)
1117
+
1118
+ def clear(self):
1119
+ """Clear the console."""
1120
+ self.console.clear()
1121
+
1122
+ def rule(self, title: str = "", style: str = "dim"):
1123
+ """Print a horizontal rule."""
1124
+ self.console.print(Rule(title=title, style=style))
1125
+
1126
+ def print_disconnect(self, agent_name: str = "Agent"):
1127
+ """Print disconnect message."""
1128
+ print_disconnect_message(self.console, agent_name)
1129
+
1130
+ def print_exit(self):
1131
+ """Print exit message."""
1132
+ print_exit_message(self.console)
1133
+
1134
+ def print_error(self, message: str):
1135
+ """Print an error message."""
1136
+ self.console.print(f" [bold red]✗[/bold red] [red]{message}[/red]")
1137
+
1138
+ def print_success(self, message: str):
1139
+ """Print a success message."""
1140
+ self.console.print(f" [bold green]✓[/bold green] [green]{message}[/green]")
1141
+
1142
+ def print_info(self, message: str):
1143
+ """Print an info message."""
1144
+ self.console.print(f" [bold blue]ℹ[/bold blue] [blue]{message}[/blue]")
1145
+
1146
+ def print_warning(self, message: str):
1147
+ """Print a warning message."""
1148
+ self.console.print(f" [bold yellow]⚠[/bold yellow] [yellow]{message}[/yellow]")
1149
+
1150
+
1151
+ # Alias for backward compatibility
1152
+ SuperQodeTUI = SuperQodeUI