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,870 @@
1
+ """Provider selection dialog with keyboard navigation."""
2
+
3
+ import os
4
+ from typing import Optional, List
5
+ from prompt_toolkit import prompt
6
+ from prompt_toolkit.completion import Completer, Completion
7
+ from rich.console import Console
8
+ from rich.panel import Panel
9
+ from rich.table import Table
10
+ from rich.box import ROUNDED
11
+ from getpass import getpass
12
+
13
+ from superqode.providers import ProviderManager, ProviderInfo, ModelInfo
14
+ from superqode.providers.registry import get_free_providers, PROVIDERS
15
+
16
+ _console = Console()
17
+
18
+
19
+ class ProviderCompleter(Completer):
20
+ """Completer for provider selection."""
21
+
22
+ def __init__(self, providers: List[ProviderInfo]):
23
+ self.providers = providers
24
+
25
+ def get_completions(self, document, complete_event):
26
+ """Get completions for provider names."""
27
+ text = document.text_before_cursor.lower()
28
+ for idx, provider in enumerate(self.providers, 1):
29
+ if text in provider.name.lower() or text in provider.id.lower() or text == str(idx):
30
+ yield Completion(
31
+ provider.id,
32
+ start_position=-len(text),
33
+ display=f"{idx}. {provider.name}",
34
+ )
35
+
36
+
37
+ class CategoryCompleter(Completer):
38
+ """Completer for category selection."""
39
+
40
+ def __init__(self, categories: List[str]):
41
+ self.categories = categories
42
+
43
+ def get_completions(self, document, complete_event):
44
+ """Get completions for category names."""
45
+ text = document.text_before_cursor.lower()
46
+ for idx, category in enumerate(self.categories, 1):
47
+ display_name = category.replace("-", " ").title()
48
+ if text in display_name.lower() or text in category.lower() or text == str(idx):
49
+ yield Completion(
50
+ category,
51
+ start_position=-len(text),
52
+ display=f"{idx}. {display_name}",
53
+ )
54
+
55
+
56
+ class ModelCompleter(Completer):
57
+ """Completer for model selection."""
58
+
59
+ def __init__(self, models: List[ModelInfo]):
60
+ self.models = models
61
+
62
+ def get_completions(self, document, complete_event):
63
+ """Get completions for model names."""
64
+ text = document.text_before_cursor.lower()
65
+ for idx, model in enumerate(self.models, 1):
66
+ if text in model.name.lower() or text in model.id.lower() or text == str(idx):
67
+ yield Completion(
68
+ model.id,
69
+ start_position=-len(text),
70
+ display=f"{idx}. {model.name}",
71
+ )
72
+
73
+
74
+ class ProviderDialog:
75
+ """Dialog for selecting a provider with keyboard navigation."""
76
+
77
+ def __init__(self, manager: Optional[ProviderManager] = None):
78
+ self.manager = manager or ProviderManager()
79
+ self.selected_provider: Optional[ProviderInfo] = None
80
+
81
+ def show(self) -> Optional[str]:
82
+ """
83
+ Show the provider selection dialog.
84
+
85
+ Returns:
86
+ Selected provider ID, or None if cancelled
87
+ """
88
+ providers = self.manager.list_providers()
89
+
90
+ if not providers:
91
+ _console.print(
92
+ "[red]No providers available. Please configure at least one provider.[/red]"
93
+ )
94
+ return None
95
+
96
+ # Group providers
97
+ popular_providers = []
98
+ other_providers = []
99
+ chinese_providers = []
100
+
101
+ popular_ids = {"ollama", "anthropic", "github-copilot", "openai", "google", "openrouter"}
102
+ chinese_ids = {
103
+ "qwen",
104
+ "deepseek",
105
+ "zhipu",
106
+ "moonshot",
107
+ "minimax",
108
+ "baidu",
109
+ "tencent",
110
+ "doubao",
111
+ "01-ai",
112
+ }
113
+
114
+ for provider in providers:
115
+ if provider.id in popular_ids:
116
+ popular_providers.append(provider)
117
+ elif provider.id in chinese_ids:
118
+ chinese_providers.append(provider)
119
+ else:
120
+ other_providers.append(provider)
121
+
122
+ # Flatten for selection
123
+ all_providers = popular_providers + other_providers + chinese_providers
124
+
125
+ # Display provider selection
126
+ _console.print()
127
+ _console.print(
128
+ Panel.fit(
129
+ "[bold cyan]Select Provider[/bold cyan]\n[dim]Type number, provider name, or use Tab to autocomplete[/dim]",
130
+ border_style="bright_cyan",
131
+ )
132
+ )
133
+ _console.print()
134
+
135
+ # Create table for popular providers
136
+ if popular_providers:
137
+ _console.print("[bold bright_yellow]Popular Providers:[/bold bright_yellow]")
138
+ _console.print()
139
+ table = Table(show_header=True, header_style="bold magenta", box=None)
140
+ table.add_column("#", style="dim", width=3)
141
+ table.add_column("Provider", style="cyan", width=25)
142
+ table.add_column("Status", width=20)
143
+ table.add_column("Description", style="dim")
144
+
145
+ for idx, provider in enumerate(popular_providers, 1):
146
+ status = (
147
+ "[green]✓ Configured[/green]"
148
+ if provider.configured
149
+ else "[yellow]⚠ Needs API Key[/yellow]"
150
+ )
151
+ table.add_row(
152
+ str(idx),
153
+ provider.name,
154
+ status,
155
+ provider.description,
156
+ )
157
+
158
+ _console.print(table)
159
+ _console.print()
160
+
161
+ # Create table for other providers
162
+ if other_providers:
163
+ _console.print("[bold bright_cyan]Other Providers:[/bold bright_cyan]")
164
+ _console.print()
165
+ table = Table(show_header=True, header_style="bold magenta", box=None)
166
+ table.add_column("#", style="dim", width=3)
167
+ table.add_column("Provider", style="cyan", width=25)
168
+ table.add_column("Status", width=20)
169
+ table.add_column("Description", style="dim")
170
+
171
+ start_idx = len(popular_providers) + 1
172
+ for idx, provider in enumerate(other_providers, start_idx):
173
+ status = (
174
+ "[green]✓ Configured[/green]"
175
+ if provider.configured
176
+ else "[yellow]⚠ Needs API Key[/yellow]"
177
+ )
178
+ table.add_row(
179
+ str(idx),
180
+ provider.name,
181
+ status,
182
+ provider.description,
183
+ )
184
+
185
+ _console.print(table)
186
+ _console.print()
187
+
188
+ # Create table for Chinese providers
189
+ if chinese_providers:
190
+ _console.print("[bold bright_red]Chinese Providers:[/bold bright_red]")
191
+ _console.print()
192
+ table = Table(show_header=True, header_style="bold magenta", box=None)
193
+ table.add_column("#", style="dim", width=3)
194
+ table.add_column("Provider", style="cyan", width=25)
195
+ table.add_column("Status", width=20)
196
+ table.add_column("Description", style="dim")
197
+
198
+ start_idx = len(popular_providers) + len(other_providers) + 1
199
+ for idx, provider in enumerate(chinese_providers, start_idx):
200
+ status = (
201
+ "[green]✓ Configured[/green]"
202
+ if provider.configured
203
+ else "[yellow]⚠ Needs API Key[/yellow]"
204
+ )
205
+ table.add_row(
206
+ str(idx),
207
+ provider.name,
208
+ status,
209
+ provider.description,
210
+ )
211
+
212
+ _console.print(table)
213
+ _console.print()
214
+
215
+ # Get user selection with autocomplete
216
+ completer = ProviderCompleter(all_providers)
217
+
218
+ while True:
219
+ try:
220
+ choice = prompt(
221
+ "Select provider (number/name, Tab to autocomplete, 'q' to cancel): ",
222
+ completer=completer,
223
+ ).strip()
224
+
225
+ if choice.lower() in ("q", "quit", "exit", ""):
226
+ return None
227
+
228
+ # Try number selection
229
+ try:
230
+ idx = int(choice) - 1
231
+ if 0 <= idx < len(all_providers):
232
+ selected = all_providers[idx]
233
+ break
234
+ except ValueError:
235
+ pass
236
+
237
+ # Try name/ID selection
238
+ choice_lower = choice.lower()
239
+ for provider in all_providers:
240
+ if choice_lower == provider.id.lower() or choice_lower in provider.name.lower():
241
+ selected = provider
242
+ break
243
+ else:
244
+ _console.print(f"[red]Invalid selection: {choice}[/red]")
245
+ _console.print(
246
+ "[dim]Please enter a number, provider name, or use Tab for autocomplete.[/dim]"
247
+ )
248
+ continue
249
+
250
+ break
251
+
252
+ except KeyboardInterrupt:
253
+ return None
254
+
255
+ self.selected_provider = selected
256
+
257
+ # Show experimental warning for vLLM and SGLang
258
+ if selected.id in ("vllm", "sglang"):
259
+ _console.print()
260
+ _console.print(
261
+ Panel(
262
+ f"[yellow]⚠️ Experimental Provider Warning[/yellow]\n\n"
263
+ f"{selected.name} support is [bold yellow]EXPERIMENTAL[/bold yellow]. "
264
+ f"Features may be unstable and behavior may change.\n\n"
265
+ f"Please report any issues you encounter.",
266
+ border_style="yellow",
267
+ title="Experimental Feature",
268
+ )
269
+ )
270
+ _console.print()
271
+
272
+ # If not configured, prompt for API key
273
+ if not selected.configured and selected.requires_api_key:
274
+ api_key = self._prompt_api_key(selected)
275
+ if api_key is None:
276
+ return None
277
+ # Set the API key in environment for this session
278
+ env_var = self._get_env_var_for_provider(selected.id)
279
+ os.environ[env_var] = api_key
280
+ # Google - also set GEMINI_API_KEY for compatibility
281
+ if selected.id == "google":
282
+ os.environ["GEMINI_API_KEY"] = api_key
283
+ _console.print(f"[green]✓ API key set for {selected.name}[/green]")
284
+ # Re-check configuration
285
+ selected.configured = True
286
+
287
+ # Test connection
288
+ _console.print(f"\n[yellow]Testing connection to {selected.name}...[/yellow]")
289
+ success, error = self.manager.test_connection(selected.id)
290
+
291
+ if not success:
292
+ _console.print(f"[red]Connection failed: {error}[/red]")
293
+ if selected.requires_api_key:
294
+ env_var = self._get_env_var_for_provider(selected.id)
295
+ _console.print(f"\n[dim]To configure {selected.name}, set the API key:[/dim]")
296
+ _console.print(f"[dim] export {env_var}=your-key[/dim]")
297
+ _console.print(f"[dim] or[/dim]")
298
+ _console.print(f"[dim] export CODEOPTIX_LLM_API_KEY=your-key[/dim]")
299
+ return None
300
+
301
+ _console.print(f"[green]✓ Selected: {selected.name}[/green]")
302
+ return selected.id
303
+
304
+ def _prompt_api_key(self, provider: ProviderInfo) -> Optional[str]:
305
+ """Prompt user for API key (like OpenAI does - hidden input)."""
306
+ env_var = self._get_env_var_for_provider(provider.id)
307
+
308
+ _console.print()
309
+ _console.print(
310
+ Panel.fit(
311
+ f"[bold cyan]Configure {provider.name}[/bold cyan]\n\n"
312
+ f"[dim]Please enter your API key for {provider.name}.[/dim]\n"
313
+ f"[dim]This will be set for the current session only.[/dim]\n"
314
+ f"[dim]Environment variable: {env_var}[/dim]\n\n"
315
+ f"[yellow]Note: Your API key will be hidden for security (like password input).[/yellow]",
316
+ border_style="bright_cyan",
317
+ )
318
+ )
319
+ _console.print()
320
+
321
+ try:
322
+ # Use getpass for security (hides input like password)
323
+ api_key = getpass(f"Enter API key for {provider.name}: ").strip()
324
+
325
+ if not api_key:
326
+ _console.print("[yellow]No API key provided. Cancelled.[/yellow]")
327
+ return None
328
+
329
+ # Confirm the key (optional, but good UX)
330
+ api_key_confirm = getpass(f"Confirm API key: ").strip()
331
+
332
+ if api_key != api_key_confirm:
333
+ _console.print("[red]API keys do not match. Cancelled.[/red]")
334
+ return None
335
+
336
+ return api_key
337
+ except KeyboardInterrupt:
338
+ _console.print("\n[yellow]Cancelled.[/yellow]")
339
+ return None
340
+
341
+ def _get_env_var_for_provider(self, provider_id: str) -> str:
342
+ """Get environment variable name for a provider."""
343
+ env_var_mapping = {
344
+ # US/International Providers
345
+ "openai": "OPENAI_API_KEY",
346
+ "anthropic": "ANTHROPIC_API_KEY",
347
+ "google": "GOOGLE_API_KEY",
348
+ "xai": "XAI_API_KEY",
349
+ "groq": "GROQ_API_KEY",
350
+ "cerebras": "CEREBRAS_API_KEY",
351
+ "together": "TOGETHER_API_KEY",
352
+ "deepinfra": "DEEPINFRA_API_KEY",
353
+ "github-copilot": "GITHUB_TOKEN",
354
+ "openrouter": "OPENROUTER_API_KEY",
355
+ "perplexity": "PERPLEXITY_API_KEY",
356
+ "mistral": "MISTRAL_API_KEY",
357
+ "meta": "META_API_KEY",
358
+ "azure-openai": "AZURE_OPENAI_API_KEY",
359
+ "vertex-ai": "GOOGLE_APPLICATION_CREDENTIALS",
360
+ "openai-compatible": "OPENAI_COMPATIBLE_API_KEY",
361
+ # Chinese Providers
362
+ "qwen": "DASHSCOPE_API_KEY",
363
+ "deepseek": "DEEPSEEK_API_KEY",
364
+ "zhipu": "ZHIPU_API_KEY",
365
+ "moonshot": "MOONSHOT_API_KEY",
366
+ "minimax": "MINIMAX_API_KEY",
367
+ "baidu": "BAIDU_API_KEY",
368
+ "tencent": "TENCENT_API_KEY",
369
+ "doubao": "DOUBAO_API_KEY",
370
+ "01-ai": "ZEROONE_API_KEY",
371
+ # Legacy mappings for backward compatibility
372
+ "together-ai": "TOGETHER_API_KEY",
373
+ "google-vertex": "GOOGLE_APPLICATION_CREDENTIALS",
374
+ "azure": "AZURE_OPENAI_API_KEY",
375
+ "cohere": "COHERE_API_KEY",
376
+ "amazon-bedrock": "AWS_ACCESS_KEY_ID",
377
+ "gateway": "GATEWAY_API_KEY",
378
+ }
379
+ return env_var_mapping.get(provider_id, f"{provider_id.upper()}_API_KEY")
380
+
381
+
382
+ class ConnectDialog:
383
+ """Modal dialog for connecting to LLM providers with category selection."""
384
+
385
+ # Provider categories
386
+ CATEGORIES = {
387
+ "us-labs": {
388
+ "name": "[bright_blue]US Labs[/bright_blue]",
389
+ "description": "Premium models from leading US AI companies",
390
+ "providers": ["openai", "anthropic", "google", "xai", "amazon-bedrock"],
391
+ },
392
+ "china-labs": {
393
+ "name": "[bright_red]China Labs[/bright_red]",
394
+ "description": "Models from Chinese AI companies",
395
+ "providers": [
396
+ "deepseek",
397
+ "qwen",
398
+ "zhipu",
399
+ "moonshot",
400
+ "minimax",
401
+ "baidu",
402
+ "tencent",
403
+ "doubao",
404
+ ],
405
+ },
406
+ "other-labs": {
407
+ "name": "[bright_green]Other Labs[/bright_green]",
408
+ "description": "Labs from other countries with their own models",
409
+ "providers": ["mistral"],
410
+ },
411
+ "model-hosts": {
412
+ "name": "[bright_magenta]Model Hosts[/bright_magenta]",
413
+ "description": "Services hosting many open and proprietary models",
414
+ "providers": [
415
+ "openrouter",
416
+ "together",
417
+ "groq",
418
+ "fireworks",
419
+ "huggingface",
420
+ "cerebras",
421
+ "perplexity",
422
+ "cohere",
423
+ "opencode",
424
+ "github-copilot",
425
+ "azure",
426
+ "vertex",
427
+ "cloudflare",
428
+ ],
429
+ },
430
+ "local": {
431
+ "name": "[bright_cyan]Local & Self-Hosted[/bright_cyan]",
432
+ "description": "Local engines and OpenAI-compatible self-hosted endpoints",
433
+ "providers": [
434
+ "ollama",
435
+ "lmstudio",
436
+ "mlx",
437
+ "vllm",
438
+ "sglang",
439
+ "tgi",
440
+ "huggingface",
441
+ "openai-compatible",
442
+ ],
443
+ },
444
+ "free-models": {
445
+ "name": "[bright_yellow]🆓 Free Models[/bright_yellow]",
446
+ "description": "Providers offering free models or free tiers",
447
+ "providers": [], # Will be populated dynamically
448
+ },
449
+ }
450
+
451
+ def __init__(self, manager: Optional[ProviderManager] = None):
452
+ self.manager = manager or ProviderManager()
453
+ self.selected_provider: Optional[ProviderInfo] = None
454
+ self.selected_model: Optional[ModelInfo] = None
455
+ # Dynamically populate free-models category
456
+ self._populate_free_models_category()
457
+
458
+ def _populate_free_models_category(self):
459
+ """Dynamically populate the free-models category from registry."""
460
+ free_providers = get_free_providers()
461
+ # Get provider IDs that have free models
462
+ free_provider_ids = list(free_providers.keys())
463
+ # Update the category
464
+ if "free-models" in self.CATEGORIES:
465
+ self.CATEGORIES["free-models"]["providers"] = free_provider_ids
466
+
467
+ def show(self) -> Optional[tuple[str, str]]:
468
+ """
469
+ Show the connect dialog with category selection.
470
+
471
+ Returns:
472
+ Tuple of (provider_id, model_id) or None if cancelled
473
+ """
474
+ while True:
475
+ category = self._show_category_selection()
476
+ if category is None:
477
+ return None
478
+
479
+ provider_id = self._show_provider_selection(category)
480
+ if provider_id is None:
481
+ continue # Go back to category selection
482
+
483
+ model_id = self._show_model_selection(provider_id)
484
+ if model_id is None:
485
+ continue # Go back to provider selection
486
+
487
+ return (provider_id, model_id)
488
+
489
+ def _show_category_selection(self) -> Optional[str]:
490
+ """Show category selection modal."""
491
+ _console.print()
492
+ _console.print(
493
+ Panel.fit(
494
+ "[bold bright_blue]🔗 SuperQode Connect[/bold bright_blue]\n"
495
+ "[dim]Choose a category to browse available providers and models[/dim]",
496
+ border_style="bright_blue",
497
+ )
498
+ )
499
+ _console.print()
500
+
501
+ # Display categories
502
+ table = Table(show_header=True, header_style="bold magenta", box=ROUNDED)
503
+ table.add_column("#", style="dim cyan", width=3, justify="center")
504
+ table.add_column("Category", style="bold white", width=25)
505
+ table.add_column("Description", style="dim")
506
+ table.add_column("Providers", style="yellow", justify="center")
507
+
508
+ for idx, (category_id, category_info) in enumerate(self.CATEGORIES.items(), 1):
509
+ # Count configured providers in this category
510
+ providers = self.manager.list_providers()
511
+ configured_count = sum(
512
+ 1 for p in providers if p.id in category_info["providers"] and p.configured
513
+ )
514
+ total_count = len(category_info["providers"])
515
+
516
+ table.add_row(
517
+ str(idx),
518
+ category_info["name"],
519
+ category_info["description"],
520
+ f"{configured_count}/{total_count} configured",
521
+ )
522
+
523
+ _console.print(table)
524
+ _console.print()
525
+
526
+ # Get selection
527
+ while True:
528
+ try:
529
+ choice = prompt(
530
+ "Select category (number, 'q' to cancel): ",
531
+ completer=CategoryCompleter(list(self.CATEGORIES.keys())),
532
+ ).strip()
533
+
534
+ if choice.lower() in ("q", "quit", "exit", ""):
535
+ return None
536
+
537
+ # Try number selection
538
+ try:
539
+ idx = int(choice) - 1
540
+ if 0 <= idx < len(self.CATEGORIES):
541
+ return list(self.CATEGORIES.keys())[idx]
542
+ except ValueError:
543
+ pass
544
+
545
+ # Try name selection
546
+ choice_lower = choice.lower().replace(" ", "-")
547
+ if choice_lower in self.CATEGORIES:
548
+ return choice_lower
549
+
550
+ _console.print(f"[red]Invalid selection: {choice}[/red]")
551
+ _console.print("[dim]Please enter a number or category name.[/dim]")
552
+
553
+ except KeyboardInterrupt:
554
+ return None
555
+
556
+ def _show_provider_selection(self, category_id: str) -> Optional[str]:
557
+ """Show provider selection for the chosen category."""
558
+ category_info = self.CATEGORIES[category_id]
559
+ providers = self.manager.list_providers()
560
+
561
+ # Filter providers for this category
562
+ category_providers = [p for p in providers if p.id in category_info["providers"]]
563
+
564
+ if not category_providers:
565
+ _console.print(f"[red]No providers available in {category_info['name']}[/red]")
566
+ return None
567
+
568
+ _console.print()
569
+ _console.print(
570
+ Panel.fit(
571
+ f"[bold bright_green]{category_info['name']}[/bold bright_green]\n"
572
+ f"[dim]{category_info['description']}[/dim]",
573
+ border_style="bright_green",
574
+ )
575
+ )
576
+ _console.print()
577
+
578
+ # Display providers
579
+ table = Table(show_header=True, header_style="bold magenta", box=ROUNDED)
580
+ table.add_column("#", style="dim cyan", width=3, justify="center")
581
+ table.add_column("Provider", style="bold white", width=25)
582
+ table.add_column("Status", width=20)
583
+ table.add_column("Models", style="yellow", justify="center")
584
+ table.add_column("Description", style="dim")
585
+
586
+ # Get list of providers with free models for badge display
587
+ free_provider_ids = set(get_free_providers().keys())
588
+
589
+ for idx, provider in enumerate(category_providers, 1):
590
+ status = (
591
+ "[green]✓ Configured[/green]"
592
+ if provider.configured
593
+ else "[yellow]⚠ Needs Setup[/yellow]"
594
+ )
595
+ model_count = len(provider.models) if provider.models else 0
596
+
597
+ # Add free badge if provider offers free models
598
+ provider_name = provider.name
599
+ if provider.id in free_provider_ids:
600
+ provider_name = f"{provider.name} [bright_yellow]🆓 Free[/bright_yellow]"
601
+
602
+ table.add_row(str(idx), provider_name, status, str(model_count), provider.description)
603
+
604
+ _console.print(table)
605
+ _console.print()
606
+
607
+ # Get selection
608
+ while True:
609
+ try:
610
+ choice = prompt(
611
+ "Select provider (number/name, 'back' for categories, 'q' to cancel): ",
612
+ completer=ProviderCompleter(category_providers),
613
+ ).strip()
614
+
615
+ if choice.lower() in ("q", "quit", "exit", ""):
616
+ return None
617
+ if choice.lower() == "back":
618
+ return None # Go back to category selection
619
+
620
+ # Try number selection
621
+ try:
622
+ idx = int(choice) - 1
623
+ if 0 <= idx < len(category_providers):
624
+ selected = category_providers[idx]
625
+ break
626
+ except ValueError:
627
+ pass
628
+
629
+ # Try name/ID selection
630
+ choice_lower = choice.lower()
631
+ for provider in category_providers:
632
+ if choice_lower == provider.id.lower() or choice_lower in provider.name.lower():
633
+ selected = provider
634
+ break
635
+ else:
636
+ _console.print(f"[red]Invalid selection: {choice}[/red]")
637
+ continue
638
+
639
+ break
640
+
641
+ except KeyboardInterrupt:
642
+ return None
643
+
644
+ self.selected_provider = selected
645
+
646
+ # Show experimental warning for vLLM and SGLang
647
+ if selected.id in ("vllm", "sglang"):
648
+ _console.print()
649
+ _console.print(
650
+ Panel(
651
+ f"[yellow]⚠️ Experimental Provider Warning[/yellow]\n\n"
652
+ f"{selected.name} support is [bold yellow]EXPERIMENTAL[/bold yellow]. "
653
+ f"Features may be unstable and behavior may change.\n\n"
654
+ f"Please report any issues you encounter.",
655
+ border_style="yellow",
656
+ title="Experimental Feature",
657
+ )
658
+ )
659
+ _console.print()
660
+
661
+ # Show available models first (before asking for API keys)
662
+ models = self.manager.get_models(selected.id)
663
+ if models:
664
+ _console.print(f"\n[bold cyan]Available models for {selected.name}:[/bold cyan]")
665
+ for i, model in enumerate(models[:5], 1): # Show first 5 models
666
+ _console.print(f" {i}. {model.name}")
667
+ if len(models) > 5:
668
+ _console.print(f" ... and {len(models) - 5} more models")
669
+ _console.print()
670
+
671
+ # Handle configuration if needed
672
+ if not selected.configured and selected.requires_api_key:
673
+ _console.print(f"[yellow]⚠ {selected.name} requires API key configuration[/yellow]")
674
+ if not self._configure_provider(selected):
675
+ return None # Configuration failed
676
+
677
+ return selected.id
678
+
679
+ def _show_model_selection(self, provider_id: str) -> Optional[str]:
680
+ """Show model selection for the chosen provider."""
681
+ if not self.selected_provider:
682
+ return None
683
+
684
+ models = self.manager.get_models(provider_id)
685
+ if not models:
686
+ _console.print(
687
+ f"[yellow]No models available for {self.selected_provider.name}[/yellow]"
688
+ )
689
+ _console.print("[dim]This provider may require additional setup or API access.[/dim]")
690
+ return None
691
+
692
+ _console.print()
693
+ _console.print(
694
+ Panel.fit(
695
+ f"[bold bright_magenta]{self.selected_provider.name} Models[/bold bright_magenta]",
696
+ border_style="bright_magenta",
697
+ )
698
+ )
699
+ _console.print()
700
+
701
+ # Display models
702
+ table = Table(show_header=True, header_style="bold cyan", box=ROUNDED)
703
+ table.add_column("#", style="dim cyan", width=3, justify="center")
704
+ table.add_column("Model", style="bold white", width=30)
705
+ table.add_column("Context", style="yellow", justify="right", width=10)
706
+ table.add_column("Status", width=15)
707
+
708
+ for idx, model in enumerate(models[:20], 1): # Limit to first 20 models
709
+ # Model status (experimental, deprecated, etc.)
710
+ status = "[green]active[/green]"
711
+ if hasattr(model, "status") and model.status:
712
+ if model.status == "experimental":
713
+ status = "[yellow]experimental[/yellow]"
714
+ elif model.status == "deprecated":
715
+ status = "[red]deprecated[/red]"
716
+
717
+ table.add_row(
718
+ str(idx),
719
+ model.name,
720
+ f"{model.context_size:,}" if model.context_size else "unknown",
721
+ status,
722
+ )
723
+
724
+ _console.print(table)
725
+
726
+ if len(models) > 20:
727
+ _console.print(f"[dim]... and {len(models) - 20} more models[/dim]")
728
+
729
+ _console.print()
730
+
731
+ # Get selection
732
+ while True:
733
+ try:
734
+ choice = prompt(
735
+ "Select model (number/name, 'back' for providers, 'q' to cancel): ",
736
+ completer=ModelCompleter(models),
737
+ ).strip()
738
+
739
+ if choice.lower() in ("q", "quit", "exit", ""):
740
+ return None
741
+ if choice.lower() == "back":
742
+ return None # Go back to provider selection
743
+
744
+ # Try number selection
745
+ try:
746
+ idx = int(choice) - 1
747
+ if 0 <= idx < len(models):
748
+ selected = models[idx]
749
+ break
750
+ except ValueError:
751
+ pass
752
+
753
+ # Try name selection
754
+ choice_lower = choice.lower()
755
+ for model in models:
756
+ if choice_lower == model.id.lower() or choice_lower in model.name.lower():
757
+ selected = model
758
+ break
759
+ else:
760
+ _console.print(f"[red]Invalid selection: {choice}[/red]")
761
+ continue
762
+
763
+ break
764
+
765
+ except KeyboardInterrupt:
766
+ return None
767
+
768
+ self.selected_model = selected
769
+ return selected.id
770
+
771
+ def _configure_provider(self, provider: ProviderInfo) -> bool:
772
+ """Configure a provider that needs API key setup."""
773
+ api_key = self._prompt_api_key(provider)
774
+ if api_key is None:
775
+ return False
776
+
777
+ # Set the API key in environment
778
+ env_var = self._get_env_var_for_provider(provider.id)
779
+ os.environ[env_var] = api_key
780
+ # Google - also set GEMINI_API_KEY for compatibility
781
+ if provider.id == "google":
782
+ os.environ["GEMINI_API_KEY"] = api_key
783
+
784
+ # Test connection
785
+ _console.print(f"\n[yellow]Testing connection to {provider.name}...[/yellow]")
786
+ success, error = self.manager.test_connection(provider.id)
787
+
788
+ if not success:
789
+ _console.print(f"[red]Connection failed: {error}[/red]")
790
+ _console.print(f"\n[dim]To configure {provider.name} later, set:[/dim]")
791
+ _console.print(f"[dim] export {env_var}=your-key[/dim]")
792
+ return False
793
+
794
+ _console.print(f"[green]✓ Successfully configured {provider.name}[/green]")
795
+ provider.configured = True
796
+ return True
797
+
798
+ def _prompt_api_key(self, provider: ProviderInfo) -> Optional[str]:
799
+ """Prompt user for API key."""
800
+ env_var = self._get_env_var_for_provider(provider.id)
801
+
802
+ _console.print()
803
+ _console.print(
804
+ Panel.fit(
805
+ f"[bold cyan]🔑 Configure {provider.name}[/bold cyan]\n\n"
806
+ f"[dim]{provider.description}[/dim]\n\n"
807
+ f"[yellow]API Key Required[/yellow]\n"
808
+ f"[dim]Environment variable: {env_var}[/dim]\n\n"
809
+ f"[yellow]Note: Your API key will be hidden for security.[/yellow]",
810
+ border_style="bright_cyan",
811
+ )
812
+ )
813
+ _console.print()
814
+
815
+ try:
816
+ api_key = getpass(f"Enter API key for {provider.name}: ").strip()
817
+
818
+ if not api_key:
819
+ _console.print("[yellow]No API key provided. Cancelled.[/yellow]")
820
+ return None
821
+
822
+ # Optional confirmation
823
+ confirm = prompt("Confirm API key? (y/n): ", default="y").strip().lower()
824
+ if confirm not in ("y", "yes", ""):
825
+ return None
826
+
827
+ return api_key
828
+ except KeyboardInterrupt:
829
+ _console.print("\n[yellow]Cancelled.[/yellow]")
830
+ return None
831
+
832
+ def _get_env_var_for_provider(self, provider_id: str) -> str:
833
+ """Get environment variable name for a provider."""
834
+ env_var_mapping = {
835
+ # US/International Providers
836
+ "openai": "OPENAI_API_KEY",
837
+ "anthropic": "ANTHROPIC_API_KEY",
838
+ "google": "GOOGLE_API_KEY",
839
+ "xai": "XAI_API_KEY",
840
+ "groq": "GROQ_API_KEY",
841
+ "cerebras": "CEREBRAS_API_KEY",
842
+ "together": "TOGETHER_API_KEY",
843
+ "deepinfra": "DEEPINFRA_API_KEY",
844
+ "github-copilot": "GITHUB_TOKEN",
845
+ "openrouter": "OPENROUTER_API_KEY",
846
+ "perplexity": "PERPLEXITY_API_KEY",
847
+ "mistral": "MISTRAL_API_KEY",
848
+ "meta": "META_API_KEY",
849
+ "azure-openai": "AZURE_OPENAI_API_KEY",
850
+ "vertex-ai": "GOOGLE_APPLICATION_CREDENTIALS",
851
+ "openai-compatible": "OPENAI_COMPATIBLE_API_KEY",
852
+ # Chinese Providers
853
+ "qwen": "DASHSCOPE_API_KEY",
854
+ "deepseek": "DEEPSEEK_API_KEY",
855
+ "zhipu": "ZHIPU_API_KEY",
856
+ "moonshot": "MOONSHOT_API_KEY",
857
+ "minimax": "MINIMAX_API_KEY",
858
+ "baidu": "BAIDU_API_KEY",
859
+ "tencent": "TENCENT_API_KEY",
860
+ "doubao": "DOUBAO_API_KEY",
861
+ "01-ai": "ZEROONE_API_KEY",
862
+ # Legacy mappings for backward compatibility
863
+ "together-ai": "TOGETHER_API_KEY",
864
+ "google-vertex": "GOOGLE_APPLICATION_CREDENTIALS",
865
+ "azure": "AZURE_OPENAI_API_KEY",
866
+ "cohere": "COHERE_API_KEY",
867
+ "amazon-bedrock": "AWS_ACCESS_KEY_ID",
868
+ "gateway": "GATEWAY_API_KEY",
869
+ }
870
+ return env_var_mapping.get(provider_id, f"{provider_id.upper()}_API_KEY")