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,725 @@
1
+ """
2
+ SuperQode Provider Connect Widget - Interactive provider/model picker.
3
+
4
+ Interactive connection flow with fuzzy search.
5
+
6
+ Features:
7
+ - Fuzzy search for providers
8
+ - Model picker with pricing info
9
+ - Recent history
10
+ - Favorites support
11
+
12
+ Usage:
13
+ :connect # Interactive picker
14
+ :connect anthropic # Pick provider, then model
15
+ :connect anthropic claude-sonnet-4 # Direct connect
16
+ :connect - # Switch to previous
17
+ :connect ! # Show history
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import os
23
+ from dataclasses import dataclass, field
24
+ from datetime import datetime
25
+ from pathlib import Path
26
+ from typing import Callable, Dict, List, Optional, Tuple, TYPE_CHECKING
27
+
28
+ from rich.text import Text
29
+
30
+ from textual.widgets import Static, Input, OptionList
31
+ from textual.containers import Container, Vertical, Horizontal
32
+ from textual.reactive import reactive
33
+ from textual.message import Message
34
+ from textual import on
35
+ from textual.binding import Binding
36
+
37
+ if TYPE_CHECKING:
38
+ from textual.app import App
39
+
40
+
41
+ # ============================================================================
42
+ # DESIGN
43
+ # ============================================================================
44
+
45
+ try:
46
+ from superqode.design_system import COLORS as SQ_COLORS
47
+ except ImportError:
48
+
49
+ class SQ_COLORS:
50
+ primary = "#7c3aed"
51
+ primary_light = "#a855f7"
52
+ success = "#10b981"
53
+ warning = "#f59e0b"
54
+ error = "#f43f5e"
55
+ info = "#06b6d4"
56
+ text_primary = "#fafafa"
57
+ text_secondary = "#e4e4e7"
58
+ text_muted = "#a1a1aa"
59
+ text_dim = "#71717a"
60
+ text_ghost = "#52525b"
61
+ bg_elevated = "#0a0a0a"
62
+ border_default = "#27272a"
63
+
64
+
65
+ # ============================================================================
66
+ # PROVIDER CONNECT WIDGET
67
+ # ============================================================================
68
+
69
+
70
+ class ProviderConnectWidget(Container):
71
+ """
72
+ Interactive provider connection widget with search.
73
+
74
+ Two-step flow:
75
+ 1. Search and select provider
76
+ 2. Search and select model
77
+ """
78
+
79
+ DEFAULT_CSS = """
80
+ ProviderConnectWidget {
81
+ height: auto;
82
+ max-height: 20;
83
+ background: #0a0a0a;
84
+ border: round #7c3aed;
85
+ padding: 1;
86
+ margin: 1 2;
87
+ }
88
+
89
+ ProviderConnectWidget .header {
90
+ height: 1;
91
+ color: #a855f7;
92
+ text-style: bold;
93
+ margin-bottom: 1;
94
+ }
95
+
96
+ ProviderConnectWidget Input {
97
+ width: 100%;
98
+ background: #050505;
99
+ border: solid #27272a;
100
+ margin-bottom: 1;
101
+ }
102
+
103
+ ProviderConnectWidget Input:focus {
104
+ border: solid #7c3aed;
105
+ }
106
+
107
+ ProviderConnectWidget OptionList {
108
+ height: auto;
109
+ max-height: 12;
110
+ background: #050505;
111
+ border: none;
112
+ }
113
+
114
+ ProviderConnectWidget OptionList > .option-list--option {
115
+ padding: 0 1;
116
+ }
117
+
118
+ ProviderConnectWidget OptionList > .option-list--option-highlighted {
119
+ background: #7c3aed40;
120
+ }
121
+
122
+ ProviderConnectWidget .hint {
123
+ height: 1;
124
+ color: #52525b;
125
+ margin-top: 1;
126
+ }
127
+ """
128
+
129
+ class ProviderSelected(Message):
130
+ """Posted when provider and model are selected."""
131
+
132
+ def __init__(self, provider_id: str, model: str, provider_name: str) -> None:
133
+ self.provider_id = provider_id
134
+ self.model = model
135
+ self.provider_name = provider_name
136
+ super().__init__()
137
+
138
+ class Cancelled(Message):
139
+ """Posted when selection is cancelled."""
140
+
141
+ pass
142
+
143
+ # State
144
+ step: reactive[str] = reactive("provider") # "provider" or "model"
145
+
146
+ def __init__(self, **kwargs):
147
+ super().__init__(**kwargs)
148
+ self._providers: List[Tuple[str, str, bool]] = [] # (id, name, configured)
149
+ self._models: List[str] = []
150
+ self._model_info: Dict = {} # Model ID -> ModelInfo
151
+ self._selected_provider: Optional[str] = None
152
+ self._selected_provider_name: str = ""
153
+ self._search_query: str = ""
154
+
155
+ def compose(self):
156
+ """Compose the widget."""
157
+ yield Static("◈ Connect to Provider", classes="header", id="connect-header")
158
+ yield Input(placeholder="Search providers...", id="connect-search")
159
+ yield OptionList(id="connect-options")
160
+ yield Static("Type number to select • Scroll with mouse • Esc Cancel", classes="hint")
161
+
162
+ def on_mount(self) -> None:
163
+ """Load providers on mount."""
164
+ self._load_providers()
165
+ self._update_options()
166
+ self.query_one("#connect-search", Input).focus()
167
+
168
+ def _load_providers(self) -> None:
169
+ """Load providers from registry."""
170
+ try:
171
+ from superqode.providers.registry import PROVIDERS, ProviderTier
172
+
173
+ self._providers = []
174
+
175
+ # Sort by tier then name
176
+ tier_order = {
177
+ ProviderTier.TIER1: 0,
178
+ ProviderTier.TIER2: 1,
179
+ ProviderTier.FREE: 2,
180
+ ProviderTier.LOCAL: 3,
181
+ }
182
+
183
+ sorted_providers = sorted(
184
+ PROVIDERS.items(), key=lambda x: (tier_order.get(x[1].tier, 99), x[1].name)
185
+ )
186
+
187
+ for provider_id, provider_def in sorted_providers:
188
+ # Check if configured
189
+ configured = False
190
+ if not provider_def.env_vars:
191
+ configured = True # Local provider
192
+ else:
193
+ for env_var in provider_def.env_vars:
194
+ if os.environ.get(env_var):
195
+ configured = True
196
+ break
197
+
198
+ self._providers.append((provider_id, provider_def.name, configured))
199
+
200
+ except Exception:
201
+ pass
202
+
203
+ def _update_options(self) -> None:
204
+ """Update option list based on current step and search."""
205
+ try:
206
+ options = self.query_one("#connect-options", OptionList)
207
+ options.clear_options()
208
+
209
+ if self.step == "provider":
210
+ self._update_provider_options(options)
211
+ else:
212
+ self._update_model_options(options)
213
+ except Exception:
214
+ pass
215
+
216
+ def _update_provider_options(self, options: OptionList) -> None:
217
+ """Update provider options with search filter."""
218
+ from superqode.utils.fuzzy import fuzzy_search
219
+
220
+ query = self._search_query.lower()
221
+
222
+ # Filter and score providers
223
+ if query:
224
+ matches = fuzzy_search.search(
225
+ query, [f"{p[0]} {p[1]}" for p in self._providers], max_results=15
226
+ )
227
+ matched_ids = {m.text.split()[0] for m in matches}
228
+ filtered = [p for p in self._providers if p[0] in matched_ids]
229
+ else:
230
+ filtered = self._providers[:15]
231
+
232
+ for provider_id, provider_name, configured in filtered:
233
+ status = "✓" if configured else "○"
234
+ status_style = SQ_COLORS.success if configured else SQ_COLORS.text_ghost
235
+
236
+ text = Text()
237
+ text.append(f"{status} ", style=status_style)
238
+ text.append(f"{provider_id}", style=SQ_COLORS.text_secondary)
239
+ text.append(f" {provider_name}", style=SQ_COLORS.text_dim)
240
+
241
+ options.add_option(text)
242
+
243
+ def _update_model_options(self, options: OptionList) -> None:
244
+ """Update model options with search filter, pricing info, and local/HF badges."""
245
+ from superqode.utils.fuzzy import fuzzy_search
246
+
247
+ query = self._search_query.lower()
248
+ model_info = getattr(self, "_model_info", {})
249
+ local_models = getattr(self, "_local_models", {}) # model_id -> LocalModel
250
+
251
+ if query:
252
+ matches = fuzzy_search.search(query, self._models, max_results=15)
253
+ filtered = [m.text for m in matches]
254
+ else:
255
+ filtered = self._models[:15]
256
+
257
+ for model_id in filtered:
258
+ text = Text()
259
+ text.append(" ", style="")
260
+
261
+ # Check if this is a local model
262
+ local_model = local_models.get(model_id)
263
+ if local_model:
264
+ # Local model display with running status and tool support
265
+ if local_model.running:
266
+ text.append("● ", style=SQ_COLORS.success)
267
+ else:
268
+ text.append("○ ", style=SQ_COLORS.text_ghost)
269
+
270
+ text.append(f"{local_model.name}", style=SQ_COLORS.text_secondary)
271
+ text.append(f"\n {model_id}", style=SQ_COLORS.text_dim)
272
+ text.append(f"\n ", style="")
273
+
274
+ # Size and quantization
275
+ if local_model.size_display != "unknown":
276
+ text.append(f"{local_model.size_display}", style=SQ_COLORS.info)
277
+ if local_model.quantization != "unknown":
278
+ text.append(f" • {local_model.quantization}", style=SQ_COLORS.text_ghost)
279
+
280
+ # Local model badges
281
+ badges = []
282
+ if local_model.running:
283
+ badges.append("[running]")
284
+ if local_model.supports_tools:
285
+ badges.append("🔧")
286
+ if local_model.supports_vision:
287
+ badges.append("👁️")
288
+ if badges:
289
+ text.append(f" {' '.join(badges)}", style=SQ_COLORS.success)
290
+ else:
291
+ # Regular model (cloud API)
292
+ info = model_info.get(model_id)
293
+ if info:
294
+ text.append(f"{info.name}", style=SQ_COLORS.text_secondary)
295
+ text.append(f"\n {model_id}", style=SQ_COLORS.text_dim)
296
+ text.append(f"\n ", style="")
297
+ text.append(f"{info.price_display}", style=SQ_COLORS.success)
298
+ text.append(f" • {info.context_display} ctx", style=SQ_COLORS.text_ghost)
299
+
300
+ # Capability badges
301
+ badges = []
302
+ if info.supports_tools:
303
+ badges.append("🔧")
304
+ if info.supports_vision:
305
+ badges.append("👁️")
306
+ if info.supports_reasoning:
307
+ badges.append("🧠")
308
+ if badges:
309
+ text.append(f" {' '.join(badges)}", style="")
310
+ else:
311
+ text.append(model_id, style=SQ_COLORS.text_secondary)
312
+
313
+ options.add_option(text)
314
+
315
+ def watch_step(self, step: str) -> None:
316
+ """Update UI when step changes."""
317
+ try:
318
+ header = self.query_one("#connect-header", Static)
319
+ search_input = self.query_one("#connect-search", Input)
320
+
321
+ if step == "provider":
322
+ header.update("◈ Connect to Provider")
323
+ search_input.placeholder = "Search providers..."
324
+ else:
325
+ header.update(f"◈ {self._selected_provider_name} - Select Model")
326
+ search_input.placeholder = "Search models..."
327
+
328
+ search_input.value = ""
329
+ self._search_query = ""
330
+ self._update_options()
331
+ search_input.focus()
332
+ except Exception:
333
+ pass
334
+
335
+ @on(Input.Changed, "#connect-search")
336
+ def _on_search_changed(self, event: Input.Changed) -> None:
337
+ """Handle search input change."""
338
+ self._search_query = event.value
339
+ self._update_options()
340
+
341
+ @on(Input.Submitted, "#connect-search")
342
+ def _on_search_submitted(self, event: Input.Submitted) -> None:
343
+ """Handle enter on search - select first option."""
344
+ try:
345
+ options = self.query_one("#connect-options", OptionList)
346
+ if options.option_count > 0:
347
+ options.highlighted = 0
348
+ self._select_current()
349
+ except Exception:
350
+ pass
351
+
352
+ @on(OptionList.OptionSelected)
353
+ def _on_option_selected(self, event: OptionList.OptionSelected) -> None:
354
+ """Handle option selection."""
355
+ event.stop()
356
+ self._select_at_index(event.option_index)
357
+
358
+ def _select_current(self) -> None:
359
+ """Select the currently highlighted option."""
360
+ try:
361
+ options = self.query_one("#connect-options", OptionList)
362
+ if options.highlighted is not None:
363
+ self._select_at_index(options.highlighted)
364
+ except Exception:
365
+ pass
366
+
367
+ def _select_at_index(self, index: int) -> None:
368
+ """Select option at index."""
369
+ if self.step == "provider":
370
+ self._select_provider(index)
371
+ else:
372
+ self._select_model(index)
373
+
374
+ def _select_provider(self, index: int) -> None:
375
+ """Handle provider selection."""
376
+ # Get filtered list
377
+ query = self._search_query.lower()
378
+ if query:
379
+ from superqode.utils.fuzzy import fuzzy_search
380
+
381
+ matches = fuzzy_search.search(
382
+ query, [f"{p[0]} {p[1]}" for p in self._providers], max_results=15
383
+ )
384
+ matched_ids = [m.text.split()[0] for m in matches]
385
+ filtered = [p for p in self._providers if p[0] in matched_ids]
386
+ else:
387
+ filtered = self._providers[:15]
388
+
389
+ if 0 <= index < len(filtered):
390
+ provider_id, provider_name, _ = filtered[index]
391
+ self._selected_provider = provider_id
392
+ self._selected_provider_name = provider_name
393
+
394
+ # Load models for this provider
395
+ self._load_models(provider_id)
396
+
397
+ # Switch to model selection
398
+ self.step = "model"
399
+
400
+ def _load_models(self, provider_id: str) -> None:
401
+ """Load models for a provider, with special handling for local providers and HuggingFace."""
402
+ try:
403
+ from superqode.providers.models import get_models_for_provider
404
+ from superqode.providers.registry import PROVIDERS, ProviderCategory
405
+
406
+ provider_def = PROVIDERS.get(provider_id)
407
+ self._local_models = {}
408
+
409
+ # Check if this is a local provider
410
+ if provider_def and provider_def.category == ProviderCategory.LOCAL:
411
+ # Load models from local provider
412
+ import asyncio
413
+
414
+ asyncio.get_event_loop().run_until_complete(self._load_local_models(provider_id))
415
+ return
416
+
417
+ # Check if this is HuggingFace
418
+ if provider_id == "huggingface":
419
+ self._load_hf_models()
420
+ return
421
+
422
+ # Try getting models from the database (includes live models.dev data)
423
+ db_models = get_models_for_provider(provider_id)
424
+
425
+ if db_models:
426
+ # Store both model IDs and their info
427
+ self._model_info = db_models
428
+ self._models = list(db_models.keys())
429
+ else:
430
+ # Fall back to registry example models
431
+ if provider_def:
432
+ self._models = list(provider_def.example_models)
433
+ self._model_info = {}
434
+ else:
435
+ self._models = []
436
+ self._model_info = {}
437
+ except Exception:
438
+ self._models = []
439
+ self._model_info = {}
440
+ self._local_models = {}
441
+
442
+ async def _load_local_models(self, provider_id: str) -> None:
443
+ """Load models from a local provider asynchronously."""
444
+ try:
445
+ from superqode.providers.local import (
446
+ OllamaClient,
447
+ LMStudioClient,
448
+ VLLMClient,
449
+ SGLangClient,
450
+ MLXClient,
451
+ TGIClient,
452
+ )
453
+
454
+ # Map provider ID to client class
455
+ client_map = {
456
+ "ollama": OllamaClient,
457
+ "lmstudio": LMStudioClient,
458
+ "vllm": VLLMClient,
459
+ "sglang": SGLangClient,
460
+ "mlx": MLXClient,
461
+ "tgi": TGIClient,
462
+ }
463
+
464
+ client_class = client_map.get(provider_id)
465
+ if not client_class:
466
+ self._models = []
467
+ self._model_info = {}
468
+ return
469
+
470
+ client = client_class()
471
+ if await client.is_available():
472
+ models = await client.list_models()
473
+
474
+ self._models = [m.id for m in models]
475
+ self._local_models = {m.id: m for m in models}
476
+ self._model_info = {}
477
+ else:
478
+ self._models = []
479
+ self._local_models = {}
480
+ self._model_info = {}
481
+
482
+ except Exception:
483
+ self._models = []
484
+ self._local_models = {}
485
+ self._model_info = {}
486
+
487
+ def _load_hf_models(self) -> None:
488
+ """Load recommended HuggingFace models."""
489
+ try:
490
+ from superqode.providers.huggingface import RECOMMENDED_MODELS
491
+
492
+ # Combine all recommended models
493
+ all_models = []
494
+ for category_models in RECOMMENDED_MODELS.values():
495
+ all_models.extend(category_models)
496
+
497
+ # Remove duplicates while preserving order
498
+ seen = set()
499
+ unique = []
500
+ for m in all_models:
501
+ if m not in seen:
502
+ seen.add(m)
503
+ unique.append(m)
504
+
505
+ self._models = unique
506
+ self._model_info = {}
507
+ self._local_models = {}
508
+
509
+ except Exception:
510
+ self._models = []
511
+ self._model_info = {}
512
+ self._local_models = {}
513
+
514
+ def _select_model(self, index: int) -> None:
515
+ """Handle model selection."""
516
+ query = self._search_query.lower()
517
+ if query:
518
+ from superqode.utils.fuzzy import fuzzy_search
519
+
520
+ matches = fuzzy_search.search(query, self._models, max_results=15)
521
+ filtered = [m.text for m in matches]
522
+ else:
523
+ filtered = self._models[:15]
524
+
525
+ if 0 <= index < len(filtered):
526
+ model = filtered[index]
527
+ self.post_message(
528
+ self.ProviderSelected(self._selected_provider, model, self._selected_provider_name)
529
+ )
530
+
531
+ def on_key(self, event) -> None:
532
+ """Handle key events."""
533
+ if event.key == "escape":
534
+ if self.step == "model":
535
+ # Go back to provider selection
536
+ self.step = "provider"
537
+ else:
538
+ self.post_message(self.Cancelled())
539
+ event.stop()
540
+ elif event.key == "up":
541
+ try:
542
+ self.query_one("#connect-options", OptionList).action_cursor_up()
543
+ except Exception:
544
+ pass
545
+ event.stop()
546
+ elif event.key == "down":
547
+ try:
548
+ self.query_one("#connect-options", OptionList).action_cursor_down()
549
+ except Exception:
550
+ pass
551
+ event.stop()
552
+ elif event.key == "enter":
553
+ self._select_current()
554
+ event.stop()
555
+
556
+ def set_provider(self, provider_id: str) -> None:
557
+ """Pre-select a provider and go to model selection."""
558
+ try:
559
+ from superqode.providers.registry import PROVIDERS
560
+
561
+ provider_def = PROVIDERS.get(provider_id)
562
+ if provider_def:
563
+ self._selected_provider = provider_id
564
+ self._selected_provider_name = provider_def.name
565
+ self._load_models(provider_id)
566
+ self.step = "model"
567
+ except Exception:
568
+ pass
569
+
570
+
571
+ # ============================================================================
572
+ # INLINE PROVIDER PICKER (for log display)
573
+ # ============================================================================
574
+
575
+
576
+ def render_provider_list(
577
+ providers: List[Tuple[str, str, bool]],
578
+ selected_index: int = -1,
579
+ max_items: int = 10,
580
+ ) -> Text:
581
+ """
582
+ Render a provider list for inline display.
583
+
584
+ Args:
585
+ providers: List of (id, name, configured) tuples
586
+ selected_index: Currently selected index (-1 for none)
587
+ max_items: Maximum items to show
588
+
589
+ Returns:
590
+ Rich Text object
591
+ """
592
+ text = Text()
593
+ text.append("◈ Select Provider\n\n", style=f"bold {SQ_COLORS.primary}")
594
+
595
+ for i, (provider_id, provider_name, configured) in enumerate(providers[:max_items]):
596
+ is_selected = i == selected_index
597
+
598
+ # Selection marker
599
+ if is_selected:
600
+ text.append("▸ ", style=f"bold {SQ_COLORS.primary}")
601
+ else:
602
+ text.append(" ", style="")
603
+
604
+ # Status indicator
605
+ status = "✓" if configured else "○"
606
+ status_style = SQ_COLORS.success if configured else SQ_COLORS.text_ghost
607
+ text.append(f"{status} ", style=status_style)
608
+
609
+ # Provider info
610
+ text.append(f"[{i + 1}] ", style=SQ_COLORS.text_dim)
611
+ text.append(
612
+ f"{provider_id}",
613
+ style=SQ_COLORS.text_secondary if is_selected else SQ_COLORS.text_muted,
614
+ )
615
+ text.append(f" {provider_name}\n", style=SQ_COLORS.text_dim)
616
+
617
+ if len(providers) > max_items:
618
+ text.append(f"\n ... and {len(providers) - max_items} more\n", style=SQ_COLORS.text_ghost)
619
+
620
+ text.append("\n Type number to select • Scroll to see more: ", style=SQ_COLORS.text_dim)
621
+
622
+ return text
623
+
624
+
625
+ def render_model_list(
626
+ provider_name: str,
627
+ models: List[str],
628
+ selected_index: int = -1,
629
+ max_items: int = 10,
630
+ model_info: Optional[Dict] = None,
631
+ local_models: Optional[Dict] = None,
632
+ ) -> Text:
633
+ """
634
+ Render a model list for inline display.
635
+
636
+ Args:
637
+ provider_name: Name of the provider
638
+ models: List of model IDs
639
+ selected_index: Currently selected index (-1 for none)
640
+ max_items: Maximum items to show
641
+ model_info: Optional dict of model_id -> ModelInfo for pricing/features
642
+ local_models: Optional dict of model_id -> LocalModel for local providers
643
+ """
644
+ text = Text()
645
+ text.append(f"◈ {provider_name} Models\n\n", style=f"bold {SQ_COLORS.primary}")
646
+
647
+ for i, model_id in enumerate(models[:max_items]):
648
+ is_selected = i == selected_index
649
+
650
+ if is_selected:
651
+ text.append("▸ ", style=f"bold {SQ_COLORS.primary}")
652
+ else:
653
+ text.append(" ", style="")
654
+
655
+ text.append(f"[{i + 1}] ", style=SQ_COLORS.text_dim)
656
+
657
+ # Check for local model first
658
+ local_model = local_models.get(model_id) if local_models else None
659
+ if local_model:
660
+ # Running status indicator
661
+ if local_model.running:
662
+ text.append("● ", style=SQ_COLORS.success)
663
+ else:
664
+ text.append("○ ", style=SQ_COLORS.text_ghost)
665
+
666
+ text.append(
667
+ f"{local_model.name}\n",
668
+ style=SQ_COLORS.text_secondary if is_selected else SQ_COLORS.text_muted,
669
+ )
670
+ text.append(f" {model_id}\n", style=SQ_COLORS.text_ghost)
671
+
672
+ # Local model details
673
+ details = []
674
+ if local_model.size_display != "unknown":
675
+ details.append(local_model.size_display)
676
+ if local_model.quantization != "unknown":
677
+ details.append(local_model.quantization)
678
+ if local_model.supports_tools:
679
+ details.append("🔧 tools")
680
+ if local_model.running:
681
+ details.append("[running]")
682
+
683
+ if details:
684
+ text.append(f" {' • '.join(details)}\n", style=SQ_COLORS.text_ghost)
685
+ else:
686
+ # Cloud model with pricing info
687
+ info = model_info.get(model_id) if model_info else None
688
+ if info:
689
+ text.append(
690
+ f"{info.name}\n",
691
+ style=SQ_COLORS.text_secondary if is_selected else SQ_COLORS.text_muted,
692
+ )
693
+ text.append(f" {model_id}\n", style=SQ_COLORS.text_ghost)
694
+ text.append(f" {info.price_display}", style=SQ_COLORS.success)
695
+ text.append(f" • {info.context_display} ctx", style=SQ_COLORS.text_ghost)
696
+
697
+ # Capability badges
698
+ badges = []
699
+ if info.supports_tools:
700
+ badges.append("🔧")
701
+ if info.supports_vision:
702
+ badges.append("👁️")
703
+ if badges:
704
+ text.append(f" {' '.join(badges)}", style="")
705
+ text.append("\n", style="")
706
+ else:
707
+ text.append(
708
+ f"{model_id}\n",
709
+ style=SQ_COLORS.text_secondary if is_selected else SQ_COLORS.text_muted,
710
+ )
711
+
712
+ text.append("\n Type number to select • Scroll to see more: ", style=SQ_COLORS.text_dim)
713
+
714
+ return text
715
+
716
+
717
+ # ============================================================================
718
+ # EXPORTS
719
+ # ============================================================================
720
+
721
+ __all__ = [
722
+ "ProviderConnectWidget",
723
+ "render_provider_list",
724
+ "render_model_list",
725
+ ]