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.
- superqode/__init__.py +33 -0
- superqode/acp/__init__.py +23 -0
- superqode/acp/client.py +913 -0
- superqode/acp/permission_screen.py +457 -0
- superqode/acp/types.py +480 -0
- superqode/acp_discovery.py +856 -0
- superqode/agent/__init__.py +22 -0
- superqode/agent/edit_strategies.py +334 -0
- superqode/agent/loop.py +892 -0
- superqode/agent/qe_report_templates.py +39 -0
- superqode/agent/system_prompts.py +353 -0
- superqode/agent_output.py +721 -0
- superqode/agent_stream.py +953 -0
- superqode/agents/__init__.py +59 -0
- superqode/agents/acp_registry.py +305 -0
- superqode/agents/client.py +249 -0
- superqode/agents/data/augmentcode.com.toml +51 -0
- superqode/agents/data/cagent.dev.toml +51 -0
- superqode/agents/data/claude.com.toml +60 -0
- superqode/agents/data/codeassistant.dev.toml +51 -0
- superqode/agents/data/codex.openai.com.toml +57 -0
- superqode/agents/data/fastagent.ai.toml +66 -0
- superqode/agents/data/geminicli.com.toml +77 -0
- superqode/agents/data/goose.block.xyz.toml +54 -0
- superqode/agents/data/junie.jetbrains.com.toml +56 -0
- superqode/agents/data/kimi.moonshot.cn.toml +57 -0
- superqode/agents/data/llmlingagent.dev.toml +51 -0
- superqode/agents/data/molt.bot.toml +49 -0
- superqode/agents/data/opencode.ai.toml +60 -0
- superqode/agents/data/stakpak.dev.toml +51 -0
- superqode/agents/data/vtcode.dev.toml +51 -0
- superqode/agents/discovery.py +266 -0
- superqode/agents/messaging.py +160 -0
- superqode/agents/persona.py +166 -0
- superqode/agents/registry.py +421 -0
- superqode/agents/schema.py +72 -0
- superqode/agents/unified.py +367 -0
- superqode/app/__init__.py +111 -0
- superqode/app/constants.py +314 -0
- superqode/app/css.py +366 -0
- superqode/app/models.py +118 -0
- superqode/app/suggester.py +125 -0
- superqode/app/widgets.py +1591 -0
- superqode/app_enhanced.py +399 -0
- superqode/app_main.py +17187 -0
- superqode/approval.py +312 -0
- superqode/atomic.py +296 -0
- superqode/commands/__init__.py +1 -0
- superqode/commands/acp.py +965 -0
- superqode/commands/agents.py +180 -0
- superqode/commands/auth.py +278 -0
- superqode/commands/config.py +374 -0
- superqode/commands/init.py +826 -0
- superqode/commands/providers.py +819 -0
- superqode/commands/qe.py +1145 -0
- superqode/commands/roles.py +380 -0
- superqode/commands/serve.py +172 -0
- superqode/commands/suggestions.py +127 -0
- superqode/commands/superqe.py +460 -0
- superqode/config/__init__.py +51 -0
- superqode/config/loader.py +812 -0
- superqode/config/schema.py +498 -0
- superqode/core/__init__.py +111 -0
- superqode/core/roles.py +281 -0
- superqode/danger.py +386 -0
- superqode/data/superqode-template.yaml +1522 -0
- superqode/design_system.py +1080 -0
- superqode/dialogs/__init__.py +6 -0
- superqode/dialogs/base.py +39 -0
- superqode/dialogs/model.py +130 -0
- superqode/dialogs/provider.py +870 -0
- superqode/diff_view.py +919 -0
- superqode/enterprise.py +21 -0
- superqode/evaluation/__init__.py +25 -0
- superqode/evaluation/adapters.py +93 -0
- superqode/evaluation/behaviors.py +89 -0
- superqode/evaluation/engine.py +209 -0
- superqode/evaluation/scenarios.py +96 -0
- superqode/execution/__init__.py +36 -0
- superqode/execution/linter.py +538 -0
- superqode/execution/modes.py +347 -0
- superqode/execution/resolver.py +283 -0
- superqode/execution/runner.py +642 -0
- superqode/file_explorer.py +811 -0
- superqode/file_viewer.py +471 -0
- superqode/flash.py +183 -0
- superqode/guidance/__init__.py +58 -0
- superqode/guidance/config.py +203 -0
- superqode/guidance/prompts.py +71 -0
- superqode/harness/__init__.py +54 -0
- superqode/harness/accelerator.py +291 -0
- superqode/harness/config.py +319 -0
- superqode/harness/validator.py +147 -0
- superqode/history.py +279 -0
- superqode/integrations/superopt_runner.py +124 -0
- superqode/logging/__init__.py +49 -0
- superqode/logging/adapters.py +219 -0
- superqode/logging/formatter.py +923 -0
- superqode/logging/integration.py +341 -0
- superqode/logging/sinks.py +170 -0
- superqode/logging/unified_log.py +417 -0
- superqode/lsp/__init__.py +26 -0
- superqode/lsp/client.py +544 -0
- superqode/main.py +1069 -0
- superqode/mcp/__init__.py +89 -0
- superqode/mcp/auth_storage.py +380 -0
- superqode/mcp/client.py +1236 -0
- superqode/mcp/config.py +319 -0
- superqode/mcp/integration.py +337 -0
- superqode/mcp/oauth.py +436 -0
- superqode/mcp/oauth_callback.py +385 -0
- superqode/mcp/types.py +290 -0
- superqode/memory/__init__.py +31 -0
- superqode/memory/feedback.py +342 -0
- superqode/memory/store.py +522 -0
- superqode/notifications.py +369 -0
- superqode/optimization/__init__.py +5 -0
- superqode/optimization/config.py +33 -0
- superqode/permissions/__init__.py +25 -0
- superqode/permissions/rules.py +488 -0
- superqode/plan.py +323 -0
- superqode/providers/__init__.py +33 -0
- superqode/providers/gateway/__init__.py +165 -0
- superqode/providers/gateway/base.py +228 -0
- superqode/providers/gateway/litellm_gateway.py +1170 -0
- superqode/providers/gateway/openresponses_gateway.py +436 -0
- superqode/providers/health.py +297 -0
- superqode/providers/huggingface/__init__.py +74 -0
- superqode/providers/huggingface/downloader.py +472 -0
- superqode/providers/huggingface/endpoints.py +442 -0
- superqode/providers/huggingface/hub.py +531 -0
- superqode/providers/huggingface/inference.py +394 -0
- superqode/providers/huggingface/transformers_runner.py +516 -0
- superqode/providers/local/__init__.py +100 -0
- superqode/providers/local/base.py +438 -0
- superqode/providers/local/discovery.py +418 -0
- superqode/providers/local/lmstudio.py +256 -0
- superqode/providers/local/mlx.py +457 -0
- superqode/providers/local/ollama.py +486 -0
- superqode/providers/local/sglang.py +268 -0
- superqode/providers/local/tgi.py +260 -0
- superqode/providers/local/tool_support.py +477 -0
- superqode/providers/local/vllm.py +258 -0
- superqode/providers/manager.py +1338 -0
- superqode/providers/models.py +1016 -0
- superqode/providers/models_dev.py +578 -0
- superqode/providers/openresponses/__init__.py +87 -0
- superqode/providers/openresponses/converters/__init__.py +17 -0
- superqode/providers/openresponses/converters/messages.py +343 -0
- superqode/providers/openresponses/converters/tools.py +268 -0
- superqode/providers/openresponses/schema/__init__.py +56 -0
- superqode/providers/openresponses/schema/models.py +585 -0
- superqode/providers/openresponses/streaming/__init__.py +5 -0
- superqode/providers/openresponses/streaming/parser.py +338 -0
- superqode/providers/openresponses/tools/__init__.py +21 -0
- superqode/providers/openresponses/tools/apply_patch.py +352 -0
- superqode/providers/openresponses/tools/code_interpreter.py +290 -0
- superqode/providers/openresponses/tools/file_search.py +333 -0
- superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
- superqode/providers/registry.py +716 -0
- superqode/providers/usage.py +332 -0
- superqode/pure_mode.py +384 -0
- superqode/qr/__init__.py +23 -0
- superqode/qr/dashboard.py +781 -0
- superqode/qr/generator.py +1018 -0
- superqode/qr/templates.py +135 -0
- superqode/safety/__init__.py +41 -0
- superqode/safety/sandbox.py +413 -0
- superqode/safety/warnings.py +256 -0
- superqode/server/__init__.py +33 -0
- superqode/server/lsp_server.py +775 -0
- superqode/server/web.py +250 -0
- superqode/session/__init__.py +25 -0
- superqode/session/persistence.py +580 -0
- superqode/session/sharing.py +477 -0
- superqode/session.py +475 -0
- superqode/sidebar.py +2991 -0
- superqode/stream_view.py +648 -0
- superqode/styles/__init__.py +3 -0
- superqode/superqe/__init__.py +184 -0
- superqode/superqe/acp_runner.py +1064 -0
- superqode/superqe/constitution/__init__.py +62 -0
- superqode/superqe/constitution/evaluator.py +308 -0
- superqode/superqe/constitution/loader.py +432 -0
- superqode/superqe/constitution/schema.py +250 -0
- superqode/superqe/events.py +591 -0
- superqode/superqe/frameworks/__init__.py +65 -0
- superqode/superqe/frameworks/base.py +234 -0
- superqode/superqe/frameworks/e2e.py +263 -0
- superqode/superqe/frameworks/executor.py +237 -0
- superqode/superqe/frameworks/javascript.py +409 -0
- superqode/superqe/frameworks/python.py +373 -0
- superqode/superqe/frameworks/registry.py +92 -0
- superqode/superqe/mcp_tools/__init__.py +47 -0
- superqode/superqe/mcp_tools/core_tools.py +418 -0
- superqode/superqe/mcp_tools/registry.py +230 -0
- superqode/superqe/mcp_tools/testing_tools.py +167 -0
- superqode/superqe/noise.py +89 -0
- superqode/superqe/orchestrator.py +778 -0
- superqode/superqe/roles.py +609 -0
- superqode/superqe/session.py +713 -0
- superqode/superqe/skills/__init__.py +57 -0
- superqode/superqe/skills/base.py +106 -0
- superqode/superqe/skills/core_skills.py +899 -0
- superqode/superqe/skills/registry.py +90 -0
- superqode/superqe/verifier.py +101 -0
- superqode/superqe_cli.py +76 -0
- superqode/tool_call.py +358 -0
- superqode/tools/__init__.py +93 -0
- superqode/tools/agent_tools.py +496 -0
- superqode/tools/base.py +324 -0
- superqode/tools/batch_tool.py +133 -0
- superqode/tools/diagnostics.py +311 -0
- superqode/tools/edit_tools.py +653 -0
- superqode/tools/enhanced_base.py +515 -0
- superqode/tools/file_tools.py +269 -0
- superqode/tools/file_tracking.py +45 -0
- superqode/tools/lsp_tools.py +610 -0
- superqode/tools/network_tools.py +350 -0
- superqode/tools/permissions.py +400 -0
- superqode/tools/question_tool.py +324 -0
- superqode/tools/search_tools.py +598 -0
- superqode/tools/shell_tools.py +259 -0
- superqode/tools/todo_tools.py +121 -0
- superqode/tools/validation.py +80 -0
- superqode/tools/web_tools.py +639 -0
- superqode/tui.py +1152 -0
- superqode/tui_integration.py +875 -0
- superqode/tui_widgets/__init__.py +27 -0
- superqode/tui_widgets/widgets/__init__.py +18 -0
- superqode/tui_widgets/widgets/progress.py +185 -0
- superqode/tui_widgets/widgets/tool_display.py +188 -0
- superqode/undo_manager.py +574 -0
- superqode/utils/__init__.py +5 -0
- superqode/utils/error_handling.py +323 -0
- superqode/utils/fuzzy.py +257 -0
- superqode/widgets/__init__.py +477 -0
- superqode/widgets/agent_collab.py +390 -0
- superqode/widgets/agent_store.py +936 -0
- superqode/widgets/agent_switcher.py +395 -0
- superqode/widgets/animation_manager.py +284 -0
- superqode/widgets/code_context.py +356 -0
- superqode/widgets/command_palette.py +412 -0
- superqode/widgets/connection_status.py +537 -0
- superqode/widgets/conversation_history.py +470 -0
- superqode/widgets/diff_indicator.py +155 -0
- superqode/widgets/enhanced_status_bar.py +385 -0
- superqode/widgets/enhanced_toast.py +476 -0
- superqode/widgets/file_browser.py +809 -0
- superqode/widgets/file_reference.py +585 -0
- superqode/widgets/issue_timeline.py +340 -0
- superqode/widgets/leader_key.py +264 -0
- superqode/widgets/mode_switcher.py +445 -0
- superqode/widgets/model_picker.py +234 -0
- superqode/widgets/permission_preview.py +1205 -0
- superqode/widgets/prompt.py +358 -0
- superqode/widgets/provider_connect.py +725 -0
- superqode/widgets/pty_shell.py +587 -0
- superqode/widgets/qe_dashboard.py +321 -0
- superqode/widgets/resizable_sidebar.py +377 -0
- superqode/widgets/response_changes.py +218 -0
- superqode/widgets/response_display.py +528 -0
- superqode/widgets/rich_tool_display.py +613 -0
- superqode/widgets/sidebar_panels.py +1180 -0
- superqode/widgets/slash_complete.py +356 -0
- superqode/widgets/split_view.py +612 -0
- superqode/widgets/status_bar.py +273 -0
- superqode/widgets/superqode_display.py +786 -0
- superqode/widgets/thinking_display.py +815 -0
- superqode/widgets/throbber.py +87 -0
- superqode/widgets/toast.py +206 -0
- superqode/widgets/unified_output.py +1073 -0
- superqode/workspace/__init__.py +75 -0
- superqode/workspace/artifacts.py +472 -0
- superqode/workspace/coordinator.py +353 -0
- superqode/workspace/diff_tracker.py +429 -0
- superqode/workspace/git_guard.py +373 -0
- superqode/workspace/git_snapshot.py +526 -0
- superqode/workspace/manager.py +750 -0
- superqode/workspace/snapshot.py +357 -0
- superqode/workspace/watcher.py +535 -0
- superqode/workspace/worktree.py +440 -0
- superqode-0.1.5.dist-info/METADATA +204 -0
- superqode-0.1.5.dist-info/RECORD +288 -0
- superqode-0.1.5.dist-info/WHEEL +5 -0
- superqode-0.1.5.dist-info/entry_points.txt +3 -0
- superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
- 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
|
|
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
|