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
@@ -0,0 +1,537 @@
1
+ """
2
+ Connection Status Widget - ACP/BYOK Agent Connection Display.
3
+
4
+ Shows the current connection status to coding agents:
5
+ - ACP connections (OpenCode, Claude Code, etc.)
6
+ - BYOK (Bring Your Own Key) provider connections
7
+ - Connection health and latency
8
+ - Model information and token usage
9
+ - Cost tracking
10
+
11
+ Makes it clear what agent/model you're connected to.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import asyncio
17
+ from dataclasses import dataclass, field
18
+ from datetime import datetime
19
+ from enum import Enum
20
+ from typing import Any, Callable, Dict, List, Optional
21
+
22
+ from rich.console import RenderableType
23
+ from rich.panel import Panel
24
+ from rich.table import Table
25
+ from rich.text import Text
26
+ from rich.box import ROUNDED, SIMPLE
27
+ from textual.reactive import reactive
28
+ from textual.widgets import Static
29
+ from textual.containers import Container, Horizontal, Vertical
30
+ from textual.timer import Timer
31
+ from textual.message import Message
32
+ from textual import events
33
+
34
+
35
+ class ConnectionType(Enum):
36
+ """Type of agent connection."""
37
+
38
+ ACP = "acp" # Agent Client Protocol
39
+ BYOK = "byok" # Bring Your Own Key (direct API)
40
+ MCP = "mcp" # Model Context Protocol
41
+ LOCAL = "local" # Local model
42
+
43
+
44
+ class ConnectionState(Enum):
45
+ """State of the connection."""
46
+
47
+ DISCONNECTED = "disconnected"
48
+ CONNECTING = "connecting"
49
+ CONNECTED = "connected"
50
+ ERROR = "error"
51
+ BUSY = "busy" # Agent is processing
52
+
53
+
54
+ @dataclass
55
+ class TokenUsage:
56
+ """Token usage tracking."""
57
+
58
+ prompt_tokens: int = 0
59
+ completion_tokens: int = 0
60
+ total_tokens: int = 0
61
+
62
+ # Cost tracking (if available)
63
+ input_cost: float = 0.0
64
+ output_cost: float = 0.0
65
+ total_cost: float = 0.0
66
+
67
+
68
+ @dataclass
69
+ class ConnectionInfo:
70
+ """Information about the current connection."""
71
+
72
+ connection_type: ConnectionType
73
+ state: ConnectionState = ConnectionState.DISCONNECTED
74
+
75
+ # Agent info
76
+ agent_name: str = ""
77
+ agent_version: str = ""
78
+ agent_command: str = "" # For ACP
79
+
80
+ # Model info
81
+ model_name: str = ""
82
+ provider: str = "" # anthropic, openai, etc.
83
+
84
+ # Session info
85
+ session_id: str = ""
86
+ connected_at: Optional[datetime] = None
87
+
88
+ # Stats
89
+ messages_sent: int = 0
90
+ messages_received: int = 0
91
+ tool_calls: int = 0
92
+ token_usage: TokenUsage = field(default_factory=TokenUsage)
93
+
94
+ # Health
95
+ latency_ms: Optional[float] = None
96
+ last_error: str = ""
97
+
98
+
99
+ # Provider styling
100
+ PROVIDER_STYLES = {
101
+ "anthropic": {"icon": "🧠", "color": "#d4a27f", "name": "Anthropic"},
102
+ "openai": {"icon": "🤖", "color": "#10a37f", "name": "OpenAI"},
103
+ "google": {"icon": "🔮", "color": "#4285f4", "name": "Google"},
104
+ "mistral": {"icon": "🌊", "color": "#ff7000", "name": "Mistral"},
105
+ "groq": {"icon": "⚡", "color": "#f55036", "name": "Groq"},
106
+ "ollama": {"icon": "🦙", "color": "#ffffff", "name": "Ollama"},
107
+ "opencode": {"icon": "💻", "color": "#3b82f6", "name": "OpenCode"},
108
+ "toad": {"icon": "🐸", "color": "#22c55e", "name": "Toad"},
109
+ "cursor": {"icon": "✨", "color": "#a855f7", "name": "Cursor"},
110
+ }
111
+
112
+ STATE_STYLES = {
113
+ ConnectionState.DISCONNECTED: {"icon": "○", "color": "#52525b"},
114
+ ConnectionState.CONNECTING: {"icon": "◐", "color": "#fbbf24"},
115
+ ConnectionState.CONNECTED: {"icon": "●", "color": "#22c55e"},
116
+ ConnectionState.ERROR: {"icon": "✗", "color": "#ef4444"},
117
+ ConnectionState.BUSY: {"icon": "◑", "color": "#3b82f6"},
118
+ }
119
+
120
+ TYPE_STYLES = {
121
+ ConnectionType.ACP: {"icon": "🔌", "label": "ACP"},
122
+ ConnectionType.BYOK: {"icon": "🔑", "label": "BYOK"},
123
+ ConnectionType.MCP: {"icon": "🔗", "label": "MCP"},
124
+ ConnectionType.LOCAL: {"icon": "💻", "label": "Local"},
125
+ }
126
+
127
+
128
+ class ConnectionIndicator(Static):
129
+ """
130
+ Compact connection indicator for status bar.
131
+
132
+ Shows:
133
+ - Connection state icon
134
+ - Agent/model name
135
+ - Provider badge
136
+ """
137
+
138
+ DEFAULT_CSS = """
139
+ ConnectionIndicator {
140
+ width: auto;
141
+ height: 1;
142
+ padding: 0 1;
143
+ }
144
+ """
145
+
146
+ def __init__(self, **kwargs):
147
+ super().__init__(**kwargs)
148
+ self._info: Optional[ConnectionInfo] = None
149
+ self._frame = 0
150
+
151
+ def set_connection(self, info: ConnectionInfo) -> None:
152
+ """Set connection info."""
153
+ self._info = info
154
+ self.refresh()
155
+
156
+ def animate(self) -> None:
157
+ """Advance animation frame."""
158
+ self._frame += 1
159
+ if self._info and self._info.state in (ConnectionState.CONNECTING, ConnectionState.BUSY):
160
+ self.refresh()
161
+
162
+ def render(self) -> Text:
163
+ text = Text()
164
+
165
+ if not self._info:
166
+ text.append("○ ", style="#52525b")
167
+ text.append("Not connected", style="#52525b")
168
+ return text
169
+
170
+ info = self._info
171
+ state_style = STATE_STYLES.get(info.state, STATE_STYLES[ConnectionState.DISCONNECTED])
172
+
173
+ # Animated state icon for connecting/busy
174
+ if info.state in (ConnectionState.CONNECTING, ConnectionState.BUSY):
175
+ icons = ["◐", "◓", "◑", "◒"]
176
+ icon = icons[self._frame % len(icons)]
177
+ else:
178
+ icon = state_style["icon"]
179
+
180
+ text.append(f"{icon} ", style=f"bold {state_style['color']}")
181
+
182
+ # Provider icon and name
183
+ provider_style = PROVIDER_STYLES.get(
184
+ info.provider.lower(), {"icon": "🤖", "color": "#a1a1aa", "name": info.provider}
185
+ )
186
+
187
+ if info.agent_name:
188
+ text.append(f"{provider_style['icon']} ", style=provider_style["color"])
189
+ text.append(info.agent_name, style=provider_style["color"])
190
+ elif info.model_name:
191
+ text.append(f"{provider_style['icon']} ", style=provider_style["color"])
192
+ text.append(info.model_name, style=provider_style["color"])
193
+ else:
194
+ text.append(info.state.value.title(), style="#a1a1aa")
195
+
196
+ # Connection type badge
197
+ type_style = TYPE_STYLES.get(info.connection_type, TYPE_STYLES[ConnectionType.BYOK])
198
+ text.append(f" [{type_style['label']}]", style="#6b7280")
199
+
200
+ return text
201
+
202
+
203
+ class ConnectionPanel(Container):
204
+ """
205
+ Full connection status panel.
206
+
207
+ Shows detailed connection information including:
208
+ - Agent/model info
209
+ - Session stats
210
+ - Token usage and cost
211
+ - Connection health
212
+ """
213
+
214
+ DEFAULT_CSS = """
215
+ ConnectionPanel {
216
+ height: auto;
217
+ border: solid #27272a;
218
+ background: #0a0a0a;
219
+ padding: 1;
220
+ margin: 0 0 1 0;
221
+ }
222
+
223
+ ConnectionPanel.connected {
224
+ border: solid #22c55e;
225
+ }
226
+
227
+ ConnectionPanel.error {
228
+ border: solid #ef4444;
229
+ }
230
+
231
+ ConnectionPanel .panel-header {
232
+ height: 2;
233
+ margin-bottom: 1;
234
+ }
235
+
236
+ ConnectionPanel .panel-content {
237
+ height: auto;
238
+ }
239
+
240
+ ConnectionPanel .panel-stats {
241
+ height: auto;
242
+ margin-top: 1;
243
+ }
244
+ """
245
+
246
+ def __init__(self, **kwargs):
247
+ super().__init__(**kwargs)
248
+ self._info: Optional[ConnectionInfo] = None
249
+
250
+ def set_connection(self, info: ConnectionInfo) -> None:
251
+ """Set connection info."""
252
+ self._info = info
253
+
254
+ # Update CSS class
255
+ self.remove_class("connected", "error")
256
+ if info.state == ConnectionState.CONNECTED:
257
+ self.add_class("connected")
258
+ elif info.state == ConnectionState.ERROR:
259
+ self.add_class("error")
260
+
261
+ self._update_display()
262
+
263
+ def _update_display(self) -> None:
264
+ """Update the display."""
265
+ try:
266
+ header = self.query_one(".panel-header", Static)
267
+ content = self.query_one(".panel-content", Static)
268
+ stats = self.query_one(".panel-stats", Static)
269
+ except Exception:
270
+ return
271
+
272
+ if not self._info:
273
+ header.update(Text("No connection", style="#52525b"))
274
+ content.update("")
275
+ stats.update("")
276
+ return
277
+
278
+ info = self._info
279
+
280
+ # Header
281
+ header_text = Text()
282
+ state_style = STATE_STYLES.get(info.state, STATE_STYLES[ConnectionState.DISCONNECTED])
283
+ type_style = TYPE_STYLES.get(info.connection_type, TYPE_STYLES[ConnectionType.BYOK])
284
+
285
+ header_text.append(f"{type_style['icon']} ", style="#6b7280")
286
+ header_text.append(f"{type_style['label']} Connection", style="bold #e4e4e7")
287
+ header_text.append(f" {state_style['icon']} ", style=state_style["color"])
288
+ header_text.append(info.state.value.title(), style=state_style["color"])
289
+
290
+ header.update(header_text)
291
+
292
+ # Content - connection details
293
+ content_text = Text()
294
+
295
+ if info.agent_name:
296
+ provider_style = PROVIDER_STYLES.get(
297
+ info.provider.lower(), {"icon": "🤖", "color": "#a1a1aa"}
298
+ )
299
+ content_text.append(f" {provider_style['icon']} Agent: ", style="#6b7280")
300
+ content_text.append(f"{info.agent_name}", style=provider_style["color"])
301
+ if info.agent_version:
302
+ content_text.append(f" v{info.agent_version}", style="#52525b")
303
+ content_text.append("\n")
304
+
305
+ if info.model_name:
306
+ content_text.append(" 🧠 Model: ", style="#6b7280")
307
+ content_text.append(f"{info.model_name}", style="#e4e4e7")
308
+ content_text.append("\n")
309
+
310
+ if info.provider:
311
+ content_text.append(" 🏢 Provider: ", style="#6b7280")
312
+ content_text.append(f"{info.provider.title()}", style="#a1a1aa")
313
+ content_text.append("\n")
314
+
315
+ if info.session_id:
316
+ content_text.append(" 📋 Session: ", style="#6b7280")
317
+ content_text.append(f"{info.session_id[:12]}...", style="#52525b")
318
+ content_text.append("\n")
319
+
320
+ if info.connected_at:
321
+ duration = (datetime.now() - info.connected_at).total_seconds()
322
+ if duration < 60:
323
+ dur_str = f"{duration:.0f}s"
324
+ elif duration < 3600:
325
+ dur_str = f"{duration / 60:.0f}m"
326
+ else:
327
+ dur_str = f"{duration / 3600:.1f}h"
328
+ content_text.append(" ⏱️ Connected: ", style="#6b7280")
329
+ content_text.append(dur_str, style="#a1a1aa")
330
+ content_text.append("\n")
331
+
332
+ if info.last_error:
333
+ content_text.append(" ❌ Error: ", style="#ef4444")
334
+ content_text.append(info.last_error[:50], style="#ef4444")
335
+ content_text.append("\n")
336
+
337
+ content.update(content_text)
338
+
339
+ # Stats
340
+ stats_text = Text()
341
+ stats_text.append(" ─────────────────────────────\n", style="#27272a")
342
+
343
+ # Message counts
344
+ stats_text.append(" 💬 ", style="#6b7280")
345
+ stats_text.append(f"{info.messages_sent}↑ ", style="#3b82f6")
346
+ stats_text.append(f"{info.messages_received}↓ ", style="#22c55e")
347
+
348
+ # Tool calls
349
+ stats_text.append(" │ 🔧 ", style="#27272a")
350
+ stats_text.append(f"{info.tool_calls}", style="#f59e0b")
351
+
352
+ # Token usage
353
+ if info.token_usage.total_tokens > 0:
354
+ stats_text.append("\n 📊 Tokens: ", style="#6b7280")
355
+ stats_text.append(f"{info.token_usage.prompt_tokens:,}", style="#3b82f6")
356
+ stats_text.append(" → ", style="#52525b")
357
+ stats_text.append(f"{info.token_usage.completion_tokens:,}", style="#22c55e")
358
+ stats_text.append(f" ({info.token_usage.total_tokens:,} total)", style="#52525b")
359
+
360
+ # Cost
361
+ if info.token_usage.total_cost > 0:
362
+ stats_text.append("\n 💰 Cost: ", style="#6b7280")
363
+ stats_text.append(f"${info.token_usage.total_cost:.4f}", style="#fbbf24")
364
+
365
+ # Latency
366
+ if info.latency_ms is not None:
367
+ stats_text.append("\n 📶 Latency: ", style="#6b7280")
368
+ latency_color = (
369
+ "#22c55e"
370
+ if info.latency_ms < 200
371
+ else "#f59e0b"
372
+ if info.latency_ms < 500
373
+ else "#ef4444"
374
+ )
375
+ stats_text.append(f"{info.latency_ms:.0f}ms", style=latency_color)
376
+
377
+ stats.update(stats_text)
378
+
379
+ def compose(self):
380
+ """Compose the panel."""
381
+ yield Static("", classes="panel-header")
382
+ yield Static("", classes="panel-content")
383
+ yield Static("", classes="panel-stats")
384
+
385
+
386
+ class ModelChanged(Message):
387
+ """
388
+ Message posted when user changes the model.
389
+
390
+ When a user selects a new model, this message is posted to inform
391
+ the parent app that it should reset the session with the new model.
392
+ """
393
+
394
+ def __init__(self, model_info: Dict[str, str]) -> None:
395
+ """
396
+ Initialize ModelChanged message.
397
+
398
+ Args:
399
+ model_info: Dictionary with model details:
400
+ - name: Display name of the model
401
+ - provider: Provider name (anthropic, openai, etc.)
402
+ - id: Model identifier for API calls
403
+ """
404
+ super().__init__()
405
+ self.model_info = model_info
406
+ self.model_name = model_info.get("name", "")
407
+ self.model_id = model_info.get("id", "")
408
+ self.provider = model_info.get("provider", "")
409
+
410
+
411
+ class ModelSelector(Container):
412
+ """
413
+ Model selection widget.
414
+
415
+ Allows switching between available models/providers.
416
+ Posts ModelChanged message when user selects a new model.
417
+ """
418
+
419
+ DEFAULT_CSS = """
420
+ ModelSelector {
421
+ height: auto;
422
+ border: solid #27272a;
423
+ background: #0a0a0a;
424
+ padding: 1;
425
+ }
426
+
427
+ ModelSelector .selector-header {
428
+ height: 1;
429
+ margin-bottom: 1;
430
+ }
431
+
432
+ ModelSelector .model-list {
433
+ height: auto;
434
+ }
435
+
436
+ ModelSelector .model-item {
437
+ height: 1;
438
+ padding: 0 1;
439
+ }
440
+
441
+ ModelSelector .model-item:hover {
442
+ background: #1a1a1a;
443
+ }
444
+
445
+ ModelSelector .model-item.selected {
446
+ background: #1a1a2a;
447
+ border-left: tall #3b82f6;
448
+ }
449
+ """
450
+
451
+ selected_index: reactive[int] = reactive(0)
452
+
453
+ def __init__(
454
+ self,
455
+ models: List[Dict[str, str]],
456
+ on_select: Optional[Callable[[Dict[str, str]], None]] = None,
457
+ **kwargs,
458
+ ):
459
+ super().__init__(**kwargs)
460
+ self.models = models # [{"name": "...", "provider": "...", "id": "..."}]
461
+ self._on_select = on_select
462
+ self._current_model_id: Optional[str] = None
463
+
464
+ def on_key(self, event: events.Key) -> None:
465
+ """Handle key events."""
466
+ if event.key == "up":
467
+ self.selected_index = max(0, self.selected_index - 1)
468
+ event.prevent_default()
469
+ elif event.key == "down":
470
+ self.selected_index = min(len(self.models) - 1, self.selected_index + 1)
471
+ event.prevent_default()
472
+ elif event.key == "enter":
473
+ if self.models:
474
+ selected_model = self.models[self.selected_index]
475
+ new_model_id = selected_model.get("id", "")
476
+
477
+ # Only trigger if model actually changed
478
+ if new_model_id != self._current_model_id:
479
+ self._current_model_id = new_model_id
480
+
481
+ # Post ModelChanged message for parent app to handle
482
+ self.post_message(ModelChanged(selected_model))
483
+
484
+ # Also call callback if provided
485
+ if self._on_select:
486
+ self._on_select(selected_model)
487
+
488
+ event.prevent_default()
489
+
490
+ def set_current_model(self, model_id: str) -> None:
491
+ """Set the current model ID (to detect actual changes)."""
492
+ self._current_model_id = model_id
493
+
494
+ # Update selected index to match
495
+ for i, model in enumerate(self.models):
496
+ if model.get("id") == model_id:
497
+ self.selected_index = i
498
+ break
499
+
500
+ def watch_selected_index(self, index: int) -> None:
501
+ """React to selection changes."""
502
+ self._update_display()
503
+
504
+ def _update_display(self) -> None:
505
+ """Update the display."""
506
+ try:
507
+ model_list = self.query_one(".model-list", Container)
508
+ except Exception:
509
+ return
510
+
511
+ model_list.remove_children()
512
+
513
+ for i, model in enumerate(self.models):
514
+ provider_style = PROVIDER_STYLES.get(
515
+ model.get("provider", "").lower(), {"icon": "🤖", "color": "#a1a1aa"}
516
+ )
517
+
518
+ text = Text()
519
+ text.append(f" {provider_style['icon']} ", style=provider_style["color"])
520
+ text.append(model.get("name", "Unknown"), style="#e4e4e7")
521
+ text.append(f" ({model.get('provider', '')})", style="#6b7280")
522
+
523
+ item = Static(text, classes="model-item")
524
+ if i == self.selected_index:
525
+ item.add_class("selected")
526
+
527
+ model_list.mount(item)
528
+
529
+ def compose(self):
530
+ """Compose the selector."""
531
+ yield Static(Text("🧠 Select Model", style="bold #e4e4e7"), classes="selector-header")
532
+ with Container(classes="model-list"):
533
+ pass
534
+
535
+ def on_mount(self) -> None:
536
+ """Initialize display."""
537
+ self._update_display()