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,321 @@
1
+ """
2
+ QE Dashboard Widget - Quality Metrics Visualization.
3
+
4
+ A SuperQode-original widget showing real-time quality metrics
5
+ during QE sessions. Displays coverage, complexity, tech debt,
6
+ and active analysis progress.
7
+
8
+ Design: Distinctive SuperQode visualization that doesn't copy
9
+ from other coding agent tools.
10
+ """
11
+
12
+ from dataclasses import dataclass, field
13
+ from datetime import datetime
14
+ from enum import Enum
15
+ from typing import List, Optional
16
+
17
+ from rich.console import RenderableType
18
+ from rich.panel import Panel
19
+ from rich.table import Table
20
+ from rich.text import Text
21
+ from rich.progress import Progress, BarColumn, TextColumn, SpinnerColumn
22
+ from textual.reactive import reactive
23
+ from textual.widgets import Static
24
+ from textual.timer import Timer
25
+
26
+
27
+ class MetricStatus(Enum):
28
+ """Status indicator for quality metrics."""
29
+
30
+ EXCELLENT = "excellent" # Green
31
+ GOOD = "good" # Blue
32
+ WARNING = "warning" # Yellow
33
+ CRITICAL = "critical" # Red
34
+ UNKNOWN = "unknown" # Gray
35
+
36
+
37
+ @dataclass
38
+ class QualityMetric:
39
+ """A single quality metric."""
40
+
41
+ name: str
42
+ value: float # 0.0 to 100.0
43
+ label: str = "" # e.g., "82%", "Low", "A"
44
+ status: MetricStatus = MetricStatus.UNKNOWN
45
+ trend: str = "" # "↑", "↓", "→"
46
+
47
+ @classmethod
48
+ def coverage(cls, value: float) -> "QualityMetric":
49
+ """Create a coverage metric."""
50
+ if value >= 80:
51
+ status = MetricStatus.EXCELLENT
52
+ elif value >= 60:
53
+ status = MetricStatus.GOOD
54
+ elif value >= 40:
55
+ status = MetricStatus.WARNING
56
+ else:
57
+ status = MetricStatus.CRITICAL
58
+ return cls("Coverage", value, f"{value:.0f}%", status)
59
+
60
+ @classmethod
61
+ def complexity(cls, value: float) -> "QualityMetric":
62
+ """Create a complexity metric (lower is better)."""
63
+ if value <= 10:
64
+ status, label = MetricStatus.EXCELLENT, "Low"
65
+ elif value <= 20:
66
+ status, label = MetricStatus.GOOD, "Medium"
67
+ elif value <= 30:
68
+ status, label = MetricStatus.WARNING, "High"
69
+ else:
70
+ status, label = MetricStatus.CRITICAL, "Very High"
71
+ return cls("Complexity", value, label, status)
72
+
73
+ @classmethod
74
+ def tech_debt(cls, value: float) -> "QualityMetric":
75
+ """Create a tech debt metric (lower is better)."""
76
+ if value <= 15:
77
+ status = MetricStatus.EXCELLENT
78
+ elif value <= 30:
79
+ status = MetricStatus.GOOD
80
+ elif value <= 50:
81
+ status = MetricStatus.WARNING
82
+ else:
83
+ status = MetricStatus.CRITICAL
84
+ return cls("Tech Debt", value, f"{value:.0f}%", status)
85
+
86
+ @classmethod
87
+ def test_health(cls, value: float) -> "QualityMetric":
88
+ """Create a test health metric."""
89
+ if value >= 90:
90
+ status = MetricStatus.EXCELLENT
91
+ elif value >= 75:
92
+ status = MetricStatus.GOOD
93
+ elif value >= 50:
94
+ status = MetricStatus.WARNING
95
+ else:
96
+ status = MetricStatus.CRITICAL
97
+ return cls("Test Health", value, f"{value:.0f}%", status)
98
+
99
+
100
+ @dataclass
101
+ class AnalysisTask:
102
+ """An active analysis task."""
103
+
104
+ file_path: str
105
+ description: str
106
+ progress: float = 0.0 # 0.0 to 1.0
107
+ started_at: datetime = field(default_factory=datetime.now)
108
+
109
+
110
+ # Color palette for SuperQode branding
111
+ METRIC_COLORS = {
112
+ MetricStatus.EXCELLENT: "#22c55e", # Green
113
+ MetricStatus.GOOD: "#3b82f6", # Blue
114
+ MetricStatus.WARNING: "#eab308", # Yellow
115
+ MetricStatus.CRITICAL: "#ef4444", # Red
116
+ MetricStatus.UNKNOWN: "#6b7280", # Gray
117
+ }
118
+
119
+
120
+ class QEDashboard(Static):
121
+ """Quality Engineering Dashboard Widget.
122
+
123
+ Displays real-time quality metrics and active analysis progress
124
+ in a compact, informative panel.
125
+
126
+ Usage:
127
+ dashboard = QEDashboard()
128
+ dashboard.update_metric(QualityMetric.coverage(82))
129
+ dashboard.set_active_analysis("src/api/handlers.py", "Checking boundaries...")
130
+ """
131
+
132
+ DEFAULT_CSS = """
133
+ QEDashboard {
134
+ height: auto;
135
+ border: solid #3f3f46;
136
+ padding: 0 1;
137
+ margin: 0 0 1 0;
138
+ }
139
+ """
140
+
141
+ # Reactive state
142
+ metrics: reactive[List[QualityMetric]] = reactive(list)
143
+ active_task: reactive[Optional[AnalysisTask]] = reactive(None)
144
+ is_analyzing: reactive[bool] = reactive(False)
145
+
146
+ def __init__(
147
+ self,
148
+ title: str = "Quality Pulse",
149
+ compact: bool = False,
150
+ **kwargs,
151
+ ):
152
+ super().__init__(**kwargs)
153
+ self.title = title
154
+ self.compact = compact
155
+ self._animation_frame = 0
156
+ self._timer: Optional[Timer] = None
157
+
158
+ # Default metrics
159
+ self._metrics: List[QualityMetric] = [
160
+ QualityMetric.coverage(0),
161
+ QualityMetric.complexity(0),
162
+ QualityMetric.tech_debt(0),
163
+ QualityMetric.test_health(0),
164
+ ]
165
+
166
+ def on_mount(self) -> None:
167
+ """Start animation timer when mounted."""
168
+ self._timer = self.set_interval(0.1, self._tick, pause=True)
169
+
170
+ def _tick(self) -> None:
171
+ """Animation tick."""
172
+ self._animation_frame += 1
173
+ if self.active_task:
174
+ self.refresh()
175
+
176
+ def update_metric(self, metric: QualityMetric) -> None:
177
+ """Update a specific metric by name."""
178
+ for i, m in enumerate(self._metrics):
179
+ if m.name == metric.name:
180
+ self._metrics[i] = metric
181
+ break
182
+ else:
183
+ self._metrics.append(metric)
184
+ self.refresh()
185
+
186
+ def set_metrics(
187
+ self,
188
+ coverage: Optional[float] = None,
189
+ complexity: Optional[float] = None,
190
+ tech_debt: Optional[float] = None,
191
+ test_health: Optional[float] = None,
192
+ ) -> None:
193
+ """Convenience method to set multiple metrics at once."""
194
+ if coverage is not None:
195
+ self.update_metric(QualityMetric.coverage(coverage))
196
+ if complexity is not None:
197
+ self.update_metric(QualityMetric.complexity(complexity))
198
+ if tech_debt is not None:
199
+ self.update_metric(QualityMetric.tech_debt(tech_debt))
200
+ if test_health is not None:
201
+ self.update_metric(QualityMetric.test_health(test_health))
202
+
203
+ def set_active_analysis(
204
+ self,
205
+ file_path: str,
206
+ description: str,
207
+ progress: float = 0.0,
208
+ ) -> None:
209
+ """Set the current active analysis task."""
210
+ self.active_task = AnalysisTask(
211
+ file_path=file_path,
212
+ description=description,
213
+ progress=progress,
214
+ )
215
+ self.is_analyzing = True
216
+ if self._timer:
217
+ self._timer.resume()
218
+ self.refresh()
219
+
220
+ def update_progress(self, progress: float, description: str = "") -> None:
221
+ """Update the progress of the active analysis."""
222
+ if self.active_task:
223
+ self.active_task.progress = progress
224
+ if description:
225
+ self.active_task.description = description
226
+ self.refresh()
227
+
228
+ def clear_analysis(self) -> None:
229
+ """Clear the active analysis task."""
230
+ self.active_task = None
231
+ self.is_analyzing = False
232
+ if self._timer:
233
+ self._timer.pause()
234
+ self.refresh()
235
+
236
+ def _render_progress_bar(self, value: float, width: int = 10) -> Text:
237
+ """Render a progress bar with Unicode blocks."""
238
+ filled = int(value / 100 * width)
239
+ empty = width - filled
240
+
241
+ bar = Text()
242
+ bar.append("█" * filled, style="bold #22c55e")
243
+ bar.append("░" * empty, style="#3f3f46")
244
+ return bar
245
+
246
+ def _render_metric(self, metric: QualityMetric) -> Text:
247
+ """Render a single metric."""
248
+ color = METRIC_COLORS[metric.status]
249
+
250
+ result = Text()
251
+ result.append(f"{metric.name}: ", style="#a1a1aa")
252
+ result.append(self._render_progress_bar(metric.value))
253
+ result.append(f" {metric.label}", style=f"bold {color}")
254
+
255
+ if metric.trend:
256
+ result.append(f" {metric.trend}", style="#6b7280")
257
+
258
+ return result
259
+
260
+ def _get_analysis_spinner(self) -> str:
261
+ """Get animated spinner character."""
262
+ spinners = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
263
+ return spinners[self._animation_frame % len(spinners)]
264
+
265
+ def render(self) -> RenderableType:
266
+ """Render the dashboard."""
267
+ content = Text()
268
+
269
+ # Metrics row (2x2 grid in compact mode)
270
+ if self.compact:
271
+ # Compact: 2 metrics per row
272
+ for i in range(0, len(self._metrics), 2):
273
+ row_metrics = self._metrics[i : i + 2]
274
+ for j, metric in enumerate(row_metrics):
275
+ content.append(self._render_metric(metric))
276
+ if j < len(row_metrics) - 1:
277
+ content.append(" ")
278
+ content.append("\n")
279
+ else:
280
+ # Full: All metrics visible
281
+ for metric in self._metrics:
282
+ content.append(self._render_metric(metric))
283
+ content.append("\n")
284
+
285
+ # Active analysis section
286
+ if self.active_task:
287
+ content.append("\n")
288
+
289
+ # File being analyzed
290
+ spinner = self._get_analysis_spinner()
291
+ content.append(f"{spinner} ", style="bold #3b82f6")
292
+ content.append("Active Analysis: ", style="#a1a1aa")
293
+
294
+ # Truncate path if too long
295
+ path = self.active_task.file_path
296
+ if len(path) > 35:
297
+ path = "..." + path[-32:]
298
+ content.append(path, style="bold #e2e8f0")
299
+ content.append("\n")
300
+
301
+ # Description
302
+ content.append(" ├─ ", style="#3f3f46")
303
+ content.append(self.active_task.description, style="#a1a1aa")
304
+ content.append("\n")
305
+
306
+ # Progress bar
307
+ progress_pct = int(self.active_task.progress * 100)
308
+ bar_width = 30
309
+ filled = int(self.active_task.progress * bar_width)
310
+
311
+ content.append(" └─ [", style="#3f3f46")
312
+ content.append("█" * filled, style="bold #3b82f6")
313
+ content.append("░" * (bar_width - filled), style="#27272a")
314
+ content.append(f"] {progress_pct}%", style="#3f3f46")
315
+
316
+ return Panel(
317
+ content,
318
+ title=f"[bold #3b82f6]{self.title}[/]",
319
+ border_style="#3f3f46",
320
+ padding=(0, 1),
321
+ )
@@ -0,0 +1,377 @@
1
+ """
2
+ SuperQode Resizable Sidebar - Draggable and Keyboard Resizing.
3
+
4
+ Provides a resizable sidebar container with:
5
+ - Draggable divider for mouse resize
6
+ - Keyboard shortcuts (Ctrl+[ / Ctrl+]) for resize
7
+ - Min/max width constraints
8
+ - Smooth resize animation
9
+
10
+ Usage:
11
+ from superqode.widgets.resizable_sidebar import (
12
+ ResizableDivider, ResizableSidebarContainer
13
+ )
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from typing import Callable, Optional
19
+
20
+ from textual.widgets import Static
21
+ from textual.containers import Container, Horizontal
22
+ from textual.reactive import reactive
23
+ from textual.events import MouseDown, MouseMove, MouseUp
24
+ from textual.binding import Binding
25
+ from textual.message import Message
26
+
27
+ from rich.text import Text
28
+
29
+
30
+ # ============================================================================
31
+ # DESIGN SYSTEM
32
+ # ============================================================================
33
+
34
+ try:
35
+ from superqode.design_system import COLORS as SQ_COLORS, GRADIENT_PURPLE
36
+ except ImportError:
37
+
38
+ class SQ_COLORS:
39
+ primary = "#7c3aed"
40
+ primary_light = "#a855f7"
41
+ text_primary = "#fafafa"
42
+ text_secondary = "#e4e4e7"
43
+ text_muted = "#a1a1aa"
44
+ text_dim = "#71717a"
45
+ text_ghost = "#52525b"
46
+ border_subtle = "#1a1a1a"
47
+ border_default = "#27272a"
48
+
49
+ GRADIENT_PURPLE = ["#6d28d9", "#7c3aed", "#8b5cf6", "#a855f7"]
50
+
51
+
52
+ # ============================================================================
53
+ # RESIZABLE DIVIDER
54
+ # ============================================================================
55
+
56
+
57
+ class ResizableDivider(Static):
58
+ """
59
+ Draggable divider for resizing adjacent panels.
60
+
61
+ SuperQode style: Minimal, purple highlight on hover/drag.
62
+ """
63
+
64
+ DEFAULT_CSS = """
65
+ ResizableDivider {
66
+ width: 1;
67
+ height: 100%;
68
+ background: #1a1a1a;
69
+ }
70
+
71
+ ResizableDivider:hover {
72
+ background: #7c3aed;
73
+ }
74
+
75
+ ResizableDivider.dragging {
76
+ background: #a855f7;
77
+ }
78
+ """
79
+
80
+ # Messages
81
+ class Resized(Message):
82
+ """Posted when divider is dragged."""
83
+
84
+ def __init__(self, delta_x: int, screen_x: int) -> None:
85
+ self.delta_x = delta_x
86
+ self.screen_x = screen_x
87
+ super().__init__()
88
+
89
+ class ResizeStart(Message):
90
+ """Posted when resize starts."""
91
+
92
+ pass
93
+
94
+ class ResizeEnd(Message):
95
+ """Posted when resize ends."""
96
+
97
+ pass
98
+
99
+ # State
100
+ dragging: reactive[bool] = reactive(False)
101
+
102
+ def __init__(self, **kwargs):
103
+ super().__init__("", **kwargs)
104
+ self._start_x: int = 0
105
+ self._last_x: int = 0
106
+
107
+ def watch_dragging(self, dragging: bool) -> None:
108
+ """Update visual state when dragging changes."""
109
+ if dragging:
110
+ self.add_class("dragging")
111
+ else:
112
+ self.remove_class("dragging")
113
+
114
+ def on_mouse_down(self, event: MouseDown) -> None:
115
+ """Start dragging."""
116
+ self.dragging = True
117
+ self._start_x = event.screen_x
118
+ self._last_x = event.screen_x
119
+ self.capture_mouse()
120
+ self.post_message(self.ResizeStart())
121
+ event.stop()
122
+
123
+ def on_mouse_move(self, event: MouseMove) -> None:
124
+ """Handle drag movement."""
125
+ if self.dragging:
126
+ delta = event.screen_x - self._last_x
127
+ if delta != 0:
128
+ self.post_message(self.Resized(delta, event.screen_x))
129
+ self._last_x = event.screen_x
130
+ event.stop()
131
+
132
+ def on_mouse_up(self, event: MouseUp) -> None:
133
+ """Stop dragging."""
134
+ if self.dragging:
135
+ self.dragging = False
136
+ self.release_mouse()
137
+ self.post_message(self.ResizeEnd())
138
+ event.stop()
139
+
140
+ def render(self) -> Text:
141
+ """Render the divider."""
142
+ # Just a vertical line character
143
+ return Text("│", style=SQ_COLORS.primary if self.dragging else SQ_COLORS.border_subtle)
144
+
145
+
146
+ # ============================================================================
147
+ # RESIZABLE SIDEBAR CONTAINER
148
+ # ============================================================================
149
+
150
+
151
+ class ResizableSidebarContainer(Container):
152
+ """
153
+ Container that wraps a sidebar and provides resize functionality.
154
+
155
+ Features:
156
+ - Drag-to-resize with ResizableDivider
157
+ - Keyboard shortcuts (Ctrl+[ / Ctrl+])
158
+ - Min/max width constraints
159
+ - Collapse/expand toggle
160
+ """
161
+
162
+ DEFAULT_CSS = """
163
+ ResizableSidebarContainer {
164
+ height: 100%;
165
+ layout: horizontal;
166
+ }
167
+
168
+ ResizableSidebarContainer #rsb-sidebar {
169
+ height: 100%;
170
+ background: #000000;
171
+ }
172
+
173
+ ResizableSidebarContainer #rsb-sidebar.collapsed {
174
+ width: 0;
175
+ display: none;
176
+ }
177
+
178
+ ResizableSidebarContainer #rsb-divider {
179
+ width: 1;
180
+ }
181
+
182
+ ResizableSidebarContainer #rsb-divider.hidden {
183
+ display: none;
184
+ }
185
+ """
186
+
187
+ BINDINGS = [
188
+ Binding("ctrl+[", "shrink_sidebar", "Shrink", show=False),
189
+ Binding("ctrl+]", "expand_sidebar", "Expand", show=False),
190
+ ]
191
+
192
+ # State
193
+ sidebar_width: reactive[int] = reactive(80)
194
+ sidebar_visible: reactive[bool] = reactive(True)
195
+
196
+ # Config
197
+ min_width: int = 30
198
+ max_width: int = 150
199
+ resize_step: int = 10
200
+
201
+ def __init__(
202
+ self,
203
+ sidebar_content: Container,
204
+ min_width: int = 30,
205
+ max_width: int = 150,
206
+ initial_width: int = 80,
207
+ **kwargs,
208
+ ):
209
+ super().__init__(**kwargs)
210
+ self._sidebar_content = sidebar_content
211
+ self.min_width = min_width
212
+ self.max_width = max_width
213
+ self.sidebar_width = initial_width
214
+
215
+ def compose(self):
216
+ """Compose the resizable sidebar."""
217
+ # Sidebar container
218
+ with Container(id="rsb-sidebar"):
219
+ yield self._sidebar_content
220
+
221
+ # Divider
222
+ yield ResizableDivider(id="rsb-divider")
223
+
224
+ def on_mount(self) -> None:
225
+ """Initialize sidebar width."""
226
+ self._update_sidebar_width()
227
+
228
+ def watch_sidebar_width(self, width: int) -> None:
229
+ """Update sidebar width when changed."""
230
+ self._update_sidebar_width()
231
+
232
+ def watch_sidebar_visible(self, visible: bool) -> None:
233
+ """Toggle sidebar visibility."""
234
+ try:
235
+ sidebar = self.query_one("#rsb-sidebar")
236
+ divider = self.query_one("#rsb-divider")
237
+
238
+ if visible:
239
+ sidebar.remove_class("collapsed")
240
+ divider.remove_class("hidden")
241
+ else:
242
+ sidebar.add_class("collapsed")
243
+ divider.add_class("hidden")
244
+ except Exception:
245
+ pass
246
+
247
+ def _update_sidebar_width(self) -> None:
248
+ """Apply the current width to the sidebar."""
249
+ try:
250
+ sidebar = self.query_one("#rsb-sidebar")
251
+ sidebar.styles.width = self.sidebar_width
252
+ except Exception:
253
+ pass
254
+
255
+ def on_resizable_divider_resized(self, event: ResizableDivider.Resized) -> None:
256
+ """Handle divider drag."""
257
+ new_width = self.sidebar_width + event.delta_x
258
+ new_width = max(self.min_width, min(self.max_width, new_width))
259
+ self.sidebar_width = new_width
260
+
261
+ def action_shrink_sidebar(self) -> None:
262
+ """Shrink sidebar by step size."""
263
+ new_width = self.sidebar_width - self.resize_step
264
+ self.sidebar_width = max(self.min_width, new_width)
265
+
266
+ def action_expand_sidebar(self) -> None:
267
+ """Expand sidebar by step size."""
268
+ new_width = self.sidebar_width + self.resize_step
269
+ self.sidebar_width = min(self.max_width, new_width)
270
+
271
+ def toggle_sidebar(self) -> None:
272
+ """Toggle sidebar visibility."""
273
+ self.sidebar_visible = not self.sidebar_visible
274
+
275
+ def set_width(self, width: int) -> None:
276
+ """Set sidebar width directly."""
277
+ self.sidebar_width = max(self.min_width, min(self.max_width, width))
278
+
279
+ def get_width(self) -> int:
280
+ """Get current sidebar width."""
281
+ return self.sidebar_width
282
+
283
+
284
+ # ============================================================================
285
+ # SIDEBAR TAB BAR
286
+ # ============================================================================
287
+
288
+
289
+ class SidebarTabBar(Static):
290
+ """
291
+ Tab bar for switching between sidebar panels.
292
+
293
+ SuperQode style: Minimal tabs with purple active indicator.
294
+ """
295
+
296
+ DEFAULT_CSS = """
297
+ SidebarTabBar {
298
+ height: 2;
299
+ background: #0a0a0a;
300
+ border-bottom: solid #1a1a1a;
301
+ padding: 0;
302
+ }
303
+ """
304
+
305
+ class TabSelected(Message):
306
+ """Posted when a tab is selected."""
307
+
308
+ def __init__(self, tab_id: str, index: int) -> None:
309
+ self.tab_id = tab_id
310
+ self.index = index
311
+ super().__init__()
312
+
313
+ active_tab: reactive[int] = reactive(0)
314
+
315
+ def __init__(self, tabs: list[str], **kwargs):
316
+ super().__init__("", **kwargs)
317
+ self._tabs = tabs
318
+
319
+ def watch_active_tab(self, index: int) -> None:
320
+ """Update display when active tab changes."""
321
+ self.refresh()
322
+ if 0 <= index < len(self._tabs):
323
+ self.post_message(self.TabSelected(self._tabs[index].lower(), index))
324
+
325
+ def select_tab(self, index: int) -> None:
326
+ """Select a tab by index."""
327
+ if 0 <= index < len(self._tabs):
328
+ self.active_tab = index
329
+
330
+ def select_tab_by_name(self, name: str) -> None:
331
+ """Select a tab by name."""
332
+ name_lower = name.lower()
333
+ for i, tab in enumerate(self._tabs):
334
+ if tab.lower() == name_lower:
335
+ self.active_tab = i
336
+ break
337
+
338
+ def render(self) -> Text:
339
+ """Render the tab bar."""
340
+ text = Text()
341
+
342
+ for i, tab in enumerate(self._tabs):
343
+ is_active = i == self.active_tab
344
+
345
+ # Tab separator
346
+ if i > 0:
347
+ text.append(" ", style="")
348
+
349
+ # Tab name (abbreviated for space)
350
+ short_name = tab[:3] if len(tab) > 4 else tab
351
+
352
+ if is_active:
353
+ text.append(f"[{short_name}]", style=f"bold {SQ_COLORS.primary}")
354
+ else:
355
+ text.append(f" {short_name} ", style=SQ_COLORS.text_dim)
356
+
357
+ return text
358
+
359
+ def on_click(self, event) -> None:
360
+ """Handle click to select tab."""
361
+ # Calculate which tab was clicked based on x position
362
+ # Each tab is roughly 5 characters wide
363
+ tab_width = 5
364
+ index = event.x // tab_width
365
+ if 0 <= index < len(self._tabs):
366
+ self.active_tab = index
367
+
368
+
369
+ # ============================================================================
370
+ # EXPORTS
371
+ # ============================================================================
372
+
373
+ __all__ = [
374
+ "ResizableDivider",
375
+ "ResizableSidebarContainer",
376
+ "SidebarTabBar",
377
+ ]