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,445 @@
1
+ """
2
+ Mode Switcher Widget - Beautiful Mode Transitions.
3
+
4
+ Provides a polished UI for switching between different
5
+ SuperQode modes (home, QE, agent, etc.) with visual feedback.
6
+
7
+ Features:
8
+ - Animated transitions
9
+ - Visual mode indicators
10
+ - Quick keyboard shortcuts
11
+ - Recent modes history
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from dataclasses import dataclass
17
+ from datetime import datetime
18
+ from enum import Enum
19
+ from typing import Callable, Dict, List, Optional
20
+
21
+ from rich.console import RenderableType
22
+ from rich.panel import Panel
23
+ from rich.table import Table
24
+ from rich.text import Text
25
+ from textual.reactive import reactive
26
+ from textual.widgets import Static
27
+ from textual.containers import Container, Horizontal, Vertical
28
+ from textual.timer import Timer
29
+ from textual import events
30
+
31
+
32
+ class AppMode(Enum):
33
+ """Application modes."""
34
+
35
+ HOME = "home"
36
+ QE = "qe"
37
+ AGENT = "agent"
38
+ CHAT = "chat"
39
+ REVIEW = "review"
40
+ DEBUG = "debug"
41
+
42
+
43
+ @dataclass
44
+ class ModeInfo:
45
+ """Information about a mode."""
46
+
47
+ id: str
48
+ name: str
49
+ icon: str
50
+ color: str
51
+ description: str
52
+ shortcut: str = ""
53
+
54
+
55
+ # Mode definitions
56
+ MODES = {
57
+ AppMode.HOME: ModeInfo(
58
+ id="home",
59
+ name="Home",
60
+ icon="🏠",
61
+ color="#3b82f6",
62
+ description="Main dashboard and navigation",
63
+ shortcut="h",
64
+ ),
65
+ AppMode.QE: ModeInfo(
66
+ id="qe",
67
+ name="Quality Engineering",
68
+ icon="🔍",
69
+ color="#22c55e",
70
+ description="Run QE sessions with multi-agent analysis",
71
+ shortcut="q",
72
+ ),
73
+ AppMode.AGENT: ModeInfo(
74
+ id="agent",
75
+ name="Agent Mode",
76
+ icon="🤖",
77
+ color="#8b5cf6",
78
+ description="Direct interaction with coding agents",
79
+ shortcut="a",
80
+ ),
81
+ AppMode.CHAT: ModeInfo(
82
+ id="chat",
83
+ name="Chat",
84
+ icon="💬",
85
+ color="#06b6d4",
86
+ description="Conversational coding assistance",
87
+ shortcut="c",
88
+ ),
89
+ AppMode.REVIEW: ModeInfo(
90
+ id="review",
91
+ name="Code Review",
92
+ icon="📝",
93
+ color="#f59e0b",
94
+ description="Review and approve changes",
95
+ shortcut="r",
96
+ ),
97
+ AppMode.DEBUG: ModeInfo(
98
+ id="debug",
99
+ name="Debug",
100
+ icon="🐛",
101
+ color="#ef4444",
102
+ description="Debug and troubleshoot issues",
103
+ shortcut="d",
104
+ ),
105
+ }
106
+
107
+
108
+ class ModeTile(Static):
109
+ """Single mode tile in the switcher."""
110
+
111
+ DEFAULT_CSS = """
112
+ ModeTile {
113
+ width: 24;
114
+ height: 7;
115
+ border: solid #3f3f46;
116
+ padding: 0 1;
117
+ margin: 0 1;
118
+ }
119
+
120
+ ModeTile:hover {
121
+ border: solid #6b7280;
122
+ }
123
+
124
+ ModeTile.selected {
125
+ border: double #3b82f6;
126
+ }
127
+
128
+ ModeTile.current {
129
+ border: solid #22c55e;
130
+ }
131
+ """
132
+
133
+ selected: reactive[bool] = reactive(False)
134
+
135
+ def __init__(
136
+ self,
137
+ mode: AppMode,
138
+ is_current: bool = False,
139
+ on_select: Optional[Callable[[], None]] = None,
140
+ **kwargs,
141
+ ):
142
+ super().__init__(**kwargs)
143
+ self.mode = mode
144
+ self.info = MODES[mode]
145
+ self._is_current = is_current
146
+ self._on_select = on_select
147
+
148
+ if is_current:
149
+ self.add_class("current")
150
+
151
+ def on_click(self, event: events.Click) -> None:
152
+ """Handle click."""
153
+ if self._on_select:
154
+ self._on_select()
155
+
156
+ def watch_selected(self, selected: bool) -> None:
157
+ """React to selection changes."""
158
+ if selected:
159
+ self.add_class("selected")
160
+ else:
161
+ self.remove_class("selected")
162
+
163
+ def render(self) -> RenderableType:
164
+ """Render the mode tile."""
165
+ content = Text()
166
+
167
+ # Icon and name
168
+ content.append(f"\n {self.info.icon} ", style="")
169
+ content.append(f"{self.info.name}\n", style=f"bold {self.info.color}")
170
+
171
+ # Shortcut
172
+ if self.info.shortcut:
173
+ content.append(f" [{self.info.shortcut}]\n", style="#6b7280")
174
+
175
+ # Current indicator
176
+ if self._is_current:
177
+ content.append(" ● Current\n", style="bold #22c55e")
178
+ else:
179
+ content.append("\n", style="")
180
+
181
+ border_style = self.info.color if self.selected else "#3f3f46"
182
+ if self._is_current:
183
+ border_style = "#22c55e"
184
+
185
+ return Panel(
186
+ content,
187
+ border_style=border_style,
188
+ padding=(0, 0),
189
+ )
190
+
191
+
192
+ class ModeSwitcher(Container):
193
+ """
194
+ Mode switcher widget for navigating between app modes.
195
+
196
+ Shows all available modes in a grid with visual indicators
197
+ for the current mode and keyboard shortcuts.
198
+
199
+ Usage:
200
+ switcher = ModeSwitcher(
201
+ current_mode=AppMode.HOME,
202
+ on_mode_change=lambda mode: print(f"Switched to {mode}"),
203
+ )
204
+ """
205
+
206
+ DEFAULT_CSS = """
207
+ ModeSwitcher {
208
+ width: 100%;
209
+ height: auto;
210
+ align: center middle;
211
+ padding: 2;
212
+ }
213
+
214
+ ModeSwitcher .header {
215
+ width: 100%;
216
+ height: 3;
217
+ text-align: center;
218
+ margin-bottom: 2;
219
+ }
220
+
221
+ ModeSwitcher .modes-row {
222
+ width: auto;
223
+ height: auto;
224
+ align: center middle;
225
+ }
226
+
227
+ ModeSwitcher .footer {
228
+ width: 100%;
229
+ height: 2;
230
+ text-align: center;
231
+ margin-top: 2;
232
+ }
233
+ """
234
+
235
+ selected_index: reactive[int] = reactive(0)
236
+
237
+ def __init__(
238
+ self,
239
+ current_mode: AppMode = AppMode.HOME,
240
+ on_mode_change: Optional[Callable[[AppMode], None]] = None,
241
+ available_modes: Optional[List[AppMode]] = None,
242
+ **kwargs,
243
+ ):
244
+ super().__init__(**kwargs)
245
+ self.current_mode = current_mode
246
+ self._on_mode_change = on_mode_change
247
+ self.available_modes = available_modes or list(AppMode)
248
+ self._tiles: List[ModeTile] = []
249
+
250
+ def compose(self):
251
+ """Compose the switcher layout."""
252
+ # Header
253
+ yield Static(
254
+ "[bold #3b82f6]Switch Mode[/]\n[#6b7280]Select a mode or press its shortcut key[/]",
255
+ classes="header",
256
+ )
257
+
258
+ # Mode tiles
259
+ with Horizontal(classes="modes-row"):
260
+ for i, mode in enumerate(self.available_modes):
261
+ tile = ModeTile(
262
+ mode,
263
+ is_current=(mode == self.current_mode),
264
+ on_select=lambda m=mode: self._select_mode(m),
265
+ id=f"tile-{mode.value}",
266
+ )
267
+ self._tiles.append(tile)
268
+ yield tile
269
+
270
+ # Footer with shortcuts
271
+ shortcuts = " ".join(
272
+ f"[{MODES[m].shortcut}]{MODES[m].name[0]}"
273
+ for m in self.available_modes
274
+ if MODES[m].shortcut
275
+ )
276
+ yield Static(
277
+ f"[#6b7280]Shortcuts: {shortcuts} [Enter] Select [Esc] Cancel[/]",
278
+ classes="footer",
279
+ )
280
+
281
+ def on_mount(self) -> None:
282
+ """Initialize selection."""
283
+ self._update_selection()
284
+
285
+ def _update_selection(self) -> None:
286
+ """Update tile selection state."""
287
+ for i, tile in enumerate(self._tiles):
288
+ tile.selected = i == self.selected_index
289
+
290
+ def _select_mode(self, mode: AppMode) -> None:
291
+ """Select a mode."""
292
+ if self._on_mode_change:
293
+ self._on_mode_change(mode)
294
+
295
+ def on_key(self, event: events.Key) -> None:
296
+ """Handle keyboard navigation."""
297
+ # Arrow navigation
298
+ if event.key == "left":
299
+ self.selected_index = max(0, self.selected_index - 1)
300
+ self._update_selection()
301
+ event.prevent_default()
302
+
303
+ elif event.key == "right":
304
+ self.selected_index = min(len(self._tiles) - 1, self.selected_index + 1)
305
+ self._update_selection()
306
+ event.prevent_default()
307
+
308
+ elif event.key == "enter":
309
+ mode = self.available_modes[self.selected_index]
310
+ self._select_mode(mode)
311
+ event.prevent_default()
312
+
313
+ # Shortcut keys
314
+ else:
315
+ for mode in self.available_modes:
316
+ info = MODES[mode]
317
+ if event.key == info.shortcut:
318
+ self._select_mode(mode)
319
+ event.prevent_default()
320
+ return
321
+
322
+
323
+ class ModeIndicator(Static):
324
+ """
325
+ Compact mode indicator for status bar.
326
+
327
+ Shows current mode with icon and allows quick switching.
328
+ """
329
+
330
+ DEFAULT_CSS = """
331
+ ModeIndicator {
332
+ width: auto;
333
+ height: 1;
334
+ padding: 0 1;
335
+ }
336
+ """
337
+
338
+ def __init__(
339
+ self,
340
+ mode: AppMode = AppMode.HOME,
341
+ on_click_switch: Optional[Callable[[], None]] = None,
342
+ **kwargs,
343
+ ):
344
+ super().__init__(**kwargs)
345
+ self._mode = mode
346
+ self._on_click_switch = on_click_switch
347
+
348
+ @property
349
+ def mode(self) -> AppMode:
350
+ return self._mode
351
+
352
+ @mode.setter
353
+ def mode(self, value: AppMode) -> None:
354
+ self._mode = value
355
+ self.refresh()
356
+
357
+ def on_click(self, event: events.Click) -> None:
358
+ """Handle click to open switcher."""
359
+ if self._on_click_switch:
360
+ self._on_click_switch()
361
+
362
+ def render(self) -> RenderableType:
363
+ """Render the indicator."""
364
+ info = MODES[self._mode]
365
+
366
+ text = Text()
367
+ text.append(f"{info.icon} ", style=info.color)
368
+ text.append(info.name, style=f"bold {info.color}")
369
+
370
+ return text
371
+
372
+
373
+ class ModeTransition(Static):
374
+ """
375
+ Animated mode transition overlay.
376
+
377
+ Shows a brief animation when switching modes.
378
+ """
379
+
380
+ DEFAULT_CSS = """
381
+ ModeTransition {
382
+ width: 100%;
383
+ height: 100%;
384
+ align: center middle;
385
+ layer: overlay;
386
+ background: rgba(0, 0, 0, 0.8);
387
+ }
388
+ """
389
+
390
+ def __init__(
391
+ self,
392
+ from_mode: AppMode,
393
+ to_mode: AppMode,
394
+ on_complete: Optional[Callable[[], None]] = None,
395
+ **kwargs,
396
+ ):
397
+ super().__init__(**kwargs)
398
+ self.from_mode = from_mode
399
+ self.to_mode = to_mode
400
+ self._on_complete = on_complete
401
+ self._frame = 0
402
+ self._timer: Optional[Timer] = None
403
+
404
+ def on_mount(self) -> None:
405
+ """Start animation."""
406
+ self._timer = self.set_interval(0.1, self._tick)
407
+
408
+ def _tick(self) -> None:
409
+ """Animation tick."""
410
+ self._frame += 1
411
+ self.refresh()
412
+
413
+ if self._frame >= 10: # 1 second animation
414
+ if self._timer:
415
+ self._timer.stop()
416
+ if self._on_complete:
417
+ self._on_complete()
418
+ self.remove()
419
+
420
+ def render(self) -> RenderableType:
421
+ """Render the transition animation."""
422
+ from_info = MODES[self.from_mode]
423
+ to_info = MODES[self.to_mode]
424
+
425
+ content = Text()
426
+
427
+ # Fade out old mode, fade in new mode
428
+ if self._frame < 5:
429
+ # Show old mode fading
430
+ content.append(f"\n\n {from_info.icon} ", style=from_info.color)
431
+ content.append(f"{from_info.name}\n", style=f"bold {from_info.color}")
432
+ content.append(" → Switching...\n", style="#6b7280")
433
+ else:
434
+ # Show new mode appearing
435
+ content.append(f"\n\n {to_info.icon} ", style=to_info.color)
436
+ content.append(f"{to_info.name}\n", style=f"bold {to_info.color}")
437
+ content.append(" ✓ Ready\n", style="#22c55e")
438
+
439
+ return Panel(
440
+ content,
441
+ title="[bold #3b82f6]Mode Transition[/]",
442
+ border_style="#3f3f46",
443
+ width=40,
444
+ height=10,
445
+ )
@@ -0,0 +1,234 @@
1
+ """
2
+ Interactive Model Picker Widget for BYOK Model Selection.
3
+
4
+ Provides keyboard navigation (arrow keys, Enter) for selecting models.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Dict, List, Optional, Tuple, TYPE_CHECKING
10
+ from dataclasses import dataclass
11
+
12
+ from rich.text import Text
13
+ from textual.widgets import Static, Input, OptionList
14
+ from textual.containers import Container, Vertical
15
+ from textual.reactive import reactive
16
+ from textual.message import Message
17
+ from textual import on
18
+ from textual.binding import Binding
19
+
20
+ if TYPE_CHECKING:
21
+ from textual.app import App
22
+
23
+ try:
24
+ from superqode.design_system import COLORS as SQ_COLORS
25
+ except ImportError:
26
+
27
+ class SQ_COLORS:
28
+ primary = "#7c3aed"
29
+ success = "#10b981"
30
+ text_secondary = "#e4e4e7"
31
+ text_dim = "#71717a"
32
+ text_ghost = "#52525b"
33
+
34
+
35
+ @dataclass
36
+ class ModelOption:
37
+ """Model option data."""
38
+
39
+ id: str
40
+ name: str
41
+ price: str = ""
42
+ context: str = ""
43
+ capabilities: List[str] = None
44
+ is_latest: bool = False
45
+
46
+
47
+ class ModelPickerWidget(Container):
48
+ """
49
+ Interactive model picker with keyboard navigation.
50
+
51
+ Features:
52
+ - Arrow keys to navigate
53
+ - Enter to select
54
+ - Type to search/filter
55
+ - Visual highlighting
56
+ """
57
+
58
+ DEFAULT_CSS = """
59
+ ModelPickerWidget {
60
+ height: auto;
61
+ max-height: 20;
62
+ background: #0a0a0a;
63
+ border: round #7c3aed;
64
+ padding: 1;
65
+ margin: 1 2;
66
+ }
67
+
68
+ ModelPickerWidget .header {
69
+ height: 1;
70
+ color: #a855f7;
71
+ text-style: bold;
72
+ margin-bottom: 1;
73
+ }
74
+
75
+ ModelPickerWidget Input {
76
+ width: 100%;
77
+ background: #050505;
78
+ border: solid #27272a;
79
+ margin-bottom: 1;
80
+ }
81
+
82
+ ModelPickerWidget Input:focus {
83
+ border: solid #7c3aed;
84
+ }
85
+
86
+ ModelPickerWidget OptionList {
87
+ height: auto;
88
+ max-height: 12;
89
+ background: #050505;
90
+ border: none;
91
+ }
92
+
93
+ ModelPickerWidget OptionList > .option-list--option {
94
+ padding: 0 1;
95
+ }
96
+
97
+ ModelPickerWidget OptionList > .option-list--option-highlighted {
98
+ background: #7c3aed40;
99
+ }
100
+
101
+ ModelPickerWidget .hint {
102
+ height: 1;
103
+ color: #52525b;
104
+ margin-top: 1;
105
+ }
106
+ """
107
+
108
+ class ModelSelected(Message):
109
+ """Posted when a model is selected."""
110
+
111
+ def __init__(self, model_id: str) -> None:
112
+ self.model_id = model_id
113
+ super().__init__()
114
+
115
+ class Cancelled(Message):
116
+ """Posted when selection is cancelled."""
117
+
118
+ pass
119
+
120
+ def __init__(self, provider_name: str, models: List[ModelOption], **kwargs):
121
+ super().__init__(**kwargs)
122
+ self.provider_name = provider_name
123
+ self.models = models
124
+ self._filtered_models: List[ModelOption] = models
125
+ self._search_query: str = ""
126
+
127
+ def compose(self):
128
+ with Vertical():
129
+ yield Static(
130
+ f"◈ {self.provider_name} - Select Model", classes="header", id="picker-header"
131
+ )
132
+ yield Input(placeholder="Type to search models...", id="picker-search")
133
+ yield OptionList(id="picker-options")
134
+ yield Static("↑↓ Navigate Enter Select Esc Cancel", classes="hint", id="picker-hint")
135
+
136
+ def on_mount(self) -> None:
137
+ """Initialize the widget."""
138
+ self._update_options()
139
+ try:
140
+ self.query_one("#picker-search", Input).focus()
141
+ except Exception:
142
+ pass
143
+
144
+ def _update_options(self) -> None:
145
+ """Update option list based on search."""
146
+ try:
147
+ options = self.query_one("#picker-options", OptionList)
148
+ options.clear_options()
149
+
150
+ query = self._search_query.lower()
151
+
152
+ # Filter models
153
+ if query:
154
+ self._filtered_models = [
155
+ m for m in self.models if query in m.id.lower() or query in m.name.lower()
156
+ ]
157
+ else:
158
+ self._filtered_models = self.models
159
+
160
+ # Add options
161
+ for model in self._filtered_models[:20]: # Limit display
162
+ text = self._format_model_option(model)
163
+ options.add_option(text)
164
+ except Exception:
165
+ pass
166
+
167
+ def _format_model_option(self, model: ModelOption) -> Text:
168
+ """Format a model option for display."""
169
+ text = Text()
170
+
171
+ # Latest indicator
172
+ if model.is_latest:
173
+ text.append("⭐ ", style=SQ_COLORS.success)
174
+
175
+ # Model name
176
+ name_style = SQ_COLORS.success if model.is_latest else SQ_COLORS.text_secondary
177
+ text.append(f"{model.name:<30}", style=name_style)
178
+
179
+ # Price and context
180
+ if model.price:
181
+ text.append(f"{model.price:>12}", style=SQ_COLORS.success)
182
+ if model.context:
183
+ text.append(f" • {model.context:>6} ctx", style=SQ_COLORS.text_dim)
184
+
185
+ # Capabilities
186
+ if model.capabilities:
187
+ text.append(f" • {' '.join(model.capabilities)}", style=SQ_COLORS.text_ghost)
188
+
189
+ # Model ID on new line
190
+ text.append(f"\n {model.id}", style=SQ_COLORS.text_dim)
191
+
192
+ return text
193
+
194
+ @on(Input.Changed, "#picker-search")
195
+ def _on_search_changed(self, event: Input.Changed) -> None:
196
+ """Handle search input change."""
197
+ self._search_query = event.value
198
+ self._update_options()
199
+
200
+ @on(Input.Submitted, "#picker-search")
201
+ def _on_search_submitted(self, event: Input.Submitted) -> None:
202
+ """Handle enter on search - select first option."""
203
+ try:
204
+ options = self.query_one("#picker-options", OptionList)
205
+ if options.option_count > 0:
206
+ options.highlighted = 0
207
+ self._select_current()
208
+ except Exception:
209
+ pass
210
+
211
+ @on(OptionList.OptionSelected)
212
+ def _on_option_selected(self, event: OptionList.OptionSelected) -> None:
213
+ """Handle option selection."""
214
+ event.stop()
215
+ self._select_at_index(event.option_index)
216
+
217
+ def _select_current(self) -> None:
218
+ """Select the currently highlighted option."""
219
+ try:
220
+ options = self.query_one("#picker-options", OptionList)
221
+ if options.highlighted is not None:
222
+ self._select_at_index(options.highlighted)
223
+ except Exception:
224
+ pass
225
+
226
+ def _select_at_index(self, index: int) -> None:
227
+ """Select model at index."""
228
+ if 0 <= index < len(self._filtered_models):
229
+ model = self._filtered_models[index]
230
+ self.post_message(self.ModelSelected(model.id))
231
+
232
+ def action_cancel(self) -> None:
233
+ """Cancel selection."""
234
+ self.post_message(self.Cancelled())