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,612 @@
1
+ """
2
+ SuperQode Split View - Code + Chat Side by Side.
3
+
4
+ Provides a resizable split view for showing code/files alongside
5
+ the chat conversation. Essential for a full coding agent experience.
6
+
7
+ Features:
8
+ - Draggable divider
9
+ - Keyboard shortcuts for resizing
10
+ - Tab support for multiple files
11
+ - Syntax highlighting
12
+ - Line numbers
13
+
14
+ Usage:
15
+ from superqode.widgets.split_view import SplitView
16
+
17
+ split = SplitView()
18
+ split.open_file("src/main.py")
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from dataclasses import dataclass, field
24
+ from pathlib import Path
25
+ from typing import Callable, Dict, List, Optional, TYPE_CHECKING
26
+
27
+ from rich.text import Text
28
+ from rich.syntax import Syntax
29
+
30
+ from textual.widgets import Static, TextArea
31
+ from textual.containers import Container, Horizontal, Vertical, ScrollableContainer
32
+ from textual.reactive import reactive
33
+ from textual.events import MouseMove, MouseDown, MouseUp
34
+ from textual import on
35
+
36
+ if TYPE_CHECKING:
37
+ from textual.app import App
38
+
39
+
40
+ # ============================================================================
41
+ # IMPORTS
42
+ # ============================================================================
43
+
44
+ try:
45
+ from superqode.design_system import COLORS, GRADIENT_PURPLE, SUPERQODE_ICONS
46
+ except ImportError:
47
+
48
+ class COLORS:
49
+ primary = "#7c3aed"
50
+ primary_light = "#a855f7"
51
+ secondary = "#ec4899"
52
+ success = "#10b981"
53
+ error = "#f43f5e"
54
+ text_primary = "#fafafa"
55
+ text_secondary = "#e4e4e7"
56
+ text_muted = "#a1a1aa"
57
+ text_dim = "#71717a"
58
+ text_ghost = "#52525b"
59
+ bg_surface = "#050505"
60
+ border_subtle = "#1a1a1a"
61
+ code_bg = "#0c0c0c"
62
+
63
+ SUPERQODE_ICONS = {}
64
+
65
+
66
+ # ============================================================================
67
+ # FILE TAB
68
+ # ============================================================================
69
+
70
+
71
+ @dataclass
72
+ class FileTab:
73
+ """Information about an open file tab."""
74
+
75
+ path: str
76
+ name: str
77
+ language: str = ""
78
+ content: str = ""
79
+ modified: bool = False
80
+ scroll_pos: int = 0
81
+ cursor_line: int = 0
82
+
83
+
84
+ class TabBar(Static):
85
+ """
86
+ Tab bar for open files.
87
+
88
+ SuperQode style: Minimal tabs with close buttons.
89
+ """
90
+
91
+ DEFAULT_CSS = """
92
+ TabBar {
93
+ height: 1;
94
+ background: #0a0a0a;
95
+ border-bottom: solid #1a1a1a;
96
+ }
97
+ """
98
+
99
+ def __init__(self, **kwargs):
100
+ super().__init__("", **kwargs)
101
+ self._tabs: List[FileTab] = []
102
+ self._active: int = -1
103
+ self._on_select: Optional[Callable[[int], None]] = None
104
+ self._on_close: Optional[Callable[[int], None]] = None
105
+
106
+ def add_tab(self, tab: FileTab) -> int:
107
+ """Add a new tab."""
108
+ self._tabs.append(tab)
109
+ self._active = len(self._tabs) - 1
110
+ self.refresh()
111
+ return self._active
112
+
113
+ def remove_tab(self, index: int) -> None:
114
+ """Remove a tab."""
115
+ if 0 <= index < len(self._tabs):
116
+ self._tabs.pop(index)
117
+ if self._active >= len(self._tabs):
118
+ self._active = len(self._tabs) - 1
119
+ self.refresh()
120
+
121
+ def select_tab(self, index: int) -> None:
122
+ """Select a tab."""
123
+ if 0 <= index < len(self._tabs):
124
+ self._active = index
125
+ self.refresh()
126
+ if self._on_select:
127
+ self._on_select(index)
128
+
129
+ def get_active_tab(self) -> Optional[FileTab]:
130
+ """Get the active tab."""
131
+ if 0 <= self._active < len(self._tabs):
132
+ return self._tabs[self._active]
133
+ return None
134
+
135
+ def render(self) -> Text:
136
+ """Render the tab bar."""
137
+ text = Text()
138
+
139
+ if not self._tabs:
140
+ text.append(" No files open", style=COLORS.text_dim)
141
+ return text
142
+
143
+ for i, tab in enumerate(self._tabs):
144
+ is_active = i == self._active
145
+
146
+ # Tab indicator
147
+ if is_active:
148
+ text.append("▸ ", style=f"bold {COLORS.primary}")
149
+ else:
150
+ text.append(" ", style="")
151
+
152
+ # File name
153
+ style = COLORS.text_primary if is_active else COLORS.text_muted
154
+ text.append(tab.name, style=style)
155
+
156
+ # Modified indicator
157
+ if tab.modified:
158
+ text.append(" ●", style=COLORS.warning)
159
+
160
+ text.append(" ", style="")
161
+
162
+ return text
163
+
164
+
165
+ # ============================================================================
166
+ # CODE VIEWER
167
+ # ============================================================================
168
+
169
+
170
+ class CodeViewer(ScrollableContainer):
171
+ """
172
+ Code viewer with syntax highlighting and line numbers.
173
+
174
+ SuperQode style: Clean, minimal, focused on code.
175
+ """
176
+
177
+ DEFAULT_CSS = """
178
+ CodeViewer {
179
+ background: #0c0c0c;
180
+ padding: 0;
181
+ }
182
+
183
+ CodeViewer .code-content {
184
+ width: 100%;
185
+ }
186
+
187
+ CodeViewer .line-numbers {
188
+ width: 5;
189
+ background: #0a0a0a;
190
+ border-right: solid #1a1a1a;
191
+ padding-right: 1;
192
+ }
193
+ """
194
+
195
+ def __init__(self, **kwargs):
196
+ super().__init__(**kwargs)
197
+ self._content = ""
198
+ self._language = "text"
199
+ self._highlight_lines: List[int] = []
200
+
201
+ def compose(self):
202
+ """Compose the viewer."""
203
+ with Horizontal():
204
+ yield Static("", id="line-numbers", classes="line-numbers")
205
+ yield Static("", id="code-content", classes="code-content")
206
+
207
+ def set_content(self, content: str, language: str = "text") -> None:
208
+ """Set the code content."""
209
+ self._content = content
210
+ self._language = language
211
+ self._render()
212
+
213
+ def highlight_lines(self, lines: List[int]) -> None:
214
+ """Highlight specific lines."""
215
+ self._highlight_lines = lines
216
+ self._render()
217
+
218
+ def _render(self) -> None:
219
+ """Render the code."""
220
+ try:
221
+ lines = self._content.split("\n")
222
+
223
+ # Line numbers
224
+ ln_text = Text()
225
+ for i, _ in enumerate(lines, 1):
226
+ style = COLORS.primary if i in self._highlight_lines else COLORS.text_ghost
227
+ ln_text.append(f"{i:4} \n", style=style)
228
+
229
+ self.query_one("#line-numbers", Static).update(ln_text)
230
+
231
+ # Code content with syntax highlighting
232
+ try:
233
+ syntax = Syntax(
234
+ self._content,
235
+ self._language,
236
+ theme="monokai",
237
+ line_numbers=False,
238
+ word_wrap=False,
239
+ )
240
+ self.query_one("#code-content", Static).update(syntax)
241
+ except Exception:
242
+ self.query_one("#code-content", Static).update(
243
+ Text(self._content, style=COLORS.text_secondary)
244
+ )
245
+ except Exception:
246
+ pass
247
+
248
+
249
+ # ============================================================================
250
+ # SPLIT DIVIDER
251
+ # ============================================================================
252
+
253
+
254
+ class SplitDivider(Static):
255
+ """
256
+ Draggable divider for split view.
257
+
258
+ SuperQode style: Minimal, changes color on hover.
259
+ """
260
+
261
+ DEFAULT_CSS = """
262
+ SplitDivider {
263
+ width: 1;
264
+ background: #1a1a1a;
265
+ }
266
+
267
+ SplitDivider:hover {
268
+ background: #7c3aed;
269
+ }
270
+
271
+ SplitDivider.dragging {
272
+ background: #a855f7;
273
+ }
274
+ """
275
+
276
+ dragging: reactive[bool] = reactive(False)
277
+
278
+ def __init__(self, **kwargs):
279
+ super().__init__("", **kwargs)
280
+ self._on_drag: Optional[Callable[[int], None]] = None
281
+
282
+ def on_mouse_down(self, event: MouseDown) -> None:
283
+ """Start dragging."""
284
+ self.dragging = True
285
+ self.add_class("dragging")
286
+ self.capture_mouse()
287
+
288
+ def on_mouse_up(self, event: MouseUp) -> None:
289
+ """Stop dragging."""
290
+ self.dragging = False
291
+ self.remove_class("dragging")
292
+ self.release_mouse()
293
+
294
+ def on_mouse_move(self, event: MouseMove) -> None:
295
+ """Handle drag."""
296
+ if self.dragging and self._on_drag:
297
+ self._on_drag(event.screen_x)
298
+
299
+ def render(self) -> Text:
300
+ """Render the divider."""
301
+ # Just a vertical line
302
+ return Text("│", style=COLORS.border_subtle if not self.dragging else COLORS.primary)
303
+
304
+
305
+ # ============================================================================
306
+ # SPLIT VIEW
307
+ # ============================================================================
308
+
309
+
310
+ class SplitView(Container):
311
+ """
312
+ Split view container with code viewer and chat.
313
+
314
+ Layout:
315
+ ┌────────────────────┬─┬────────────────────┐
316
+ │ [Tab Bar] │ │ │
317
+ ├────────────────────┤ │ │
318
+ │ │D│ Chat/Content │
319
+ │ Code Viewer │I│ │
320
+ │ │V│ │
321
+ │ │ │ │
322
+ └────────────────────┴─┴────────────────────┘
323
+ """
324
+
325
+ DEFAULT_CSS = """
326
+ SplitView {
327
+ height: 100%;
328
+ width: 100%;
329
+ }
330
+
331
+ SplitView #split-main {
332
+ height: 100%;
333
+ }
334
+
335
+ SplitView #split-left {
336
+ width: 50%;
337
+ background: #050505;
338
+ }
339
+
340
+ SplitView #split-right {
341
+ width: 1fr;
342
+ }
343
+
344
+ SplitView #split-left.collapsed {
345
+ width: 0;
346
+ display: none;
347
+ }
348
+
349
+ SplitView #code-header {
350
+ height: 2;
351
+ background: #0a0a0a;
352
+ border-bottom: solid #1a1a1a;
353
+ padding: 0 1;
354
+ }
355
+ """
356
+
357
+ # State
358
+ split_visible: reactive[bool] = reactive(False)
359
+ split_position: reactive[int] = reactive(50) # Percentage
360
+
361
+ def __init__(self, **kwargs):
362
+ super().__init__(**kwargs)
363
+ self._tabs: List[FileTab] = []
364
+ self._active_tab: int = -1
365
+ self._on_file_select: Optional[Callable[[str], None]] = None
366
+
367
+ def compose(self):
368
+ """Compose the split view."""
369
+ with Horizontal(id="split-main"):
370
+ # Left side - Code
371
+ with Vertical(id="split-left", classes="collapsed"):
372
+ yield Static(self._render_header(), id="code-header")
373
+ yield TabBar(id="tab-bar")
374
+ yield CodeViewer(id="code-viewer")
375
+
376
+ # Divider
377
+ yield SplitDivider(id="split-divider")
378
+
379
+ # Right side - Chat (content provided by parent)
380
+ yield Container(id="split-right")
381
+
382
+ def _render_header(self) -> Text:
383
+ """Render the code header."""
384
+ text = Text()
385
+ text.append("◇ ", style=f"bold {COLORS.primary}")
386
+ text.append("Code", style=COLORS.text_secondary)
387
+ text.append(" [Ctrl+\\ to close]", style=COLORS.text_ghost)
388
+ return text
389
+
390
+ def on_mount(self) -> None:
391
+ """Set up event handlers."""
392
+ try:
393
+ divider = self.query_one("#split-divider", SplitDivider)
394
+ divider._on_drag = self._handle_drag
395
+
396
+ tab_bar = self.query_one("#tab-bar", TabBar)
397
+ tab_bar._on_select = self._handle_tab_select
398
+ tab_bar._on_close = self._handle_tab_close
399
+ except Exception:
400
+ pass
401
+
402
+ def _handle_drag(self, x: int) -> None:
403
+ """Handle divider drag."""
404
+ # Calculate percentage
405
+ width = self.size.width
406
+ if width > 0:
407
+ pct = int((x / width) * 100)
408
+ pct = max(20, min(80, pct)) # Clamp between 20-80%
409
+ self.split_position = pct
410
+ self._update_widths()
411
+
412
+ def _update_widths(self) -> None:
413
+ """Update split widths."""
414
+ try:
415
+ left = self.query_one("#split-left")
416
+ left.styles.width = f"{self.split_position}%"
417
+ except Exception:
418
+ pass
419
+
420
+ def _handle_tab_select(self, index: int) -> None:
421
+ """Handle tab selection."""
422
+ if 0 <= index < len(self._tabs):
423
+ self._active_tab = index
424
+ tab = self._tabs[index]
425
+ self._show_content(tab)
426
+
427
+ def _handle_tab_close(self, index: int) -> None:
428
+ """Handle tab close."""
429
+ if 0 <= index < len(self._tabs):
430
+ self._tabs.pop(index)
431
+ if self._active_tab >= len(self._tabs):
432
+ self._active_tab = len(self._tabs) - 1
433
+
434
+ if self._tabs and self._active_tab >= 0:
435
+ self._show_content(self._tabs[self._active_tab])
436
+ else:
437
+ self.hide_split()
438
+
439
+ def _show_content(self, tab: FileTab) -> None:
440
+ """Show file content."""
441
+ try:
442
+ viewer = self.query_one("#code-viewer", CodeViewer)
443
+ viewer.set_content(tab.content, tab.language)
444
+ except Exception:
445
+ pass
446
+
447
+ def show_split(self) -> None:
448
+ """Show the split view."""
449
+ self.split_visible = True
450
+ try:
451
+ left = self.query_one("#split-left")
452
+ left.remove_class("collapsed")
453
+ except Exception:
454
+ pass
455
+
456
+ def hide_split(self) -> None:
457
+ """Hide the split view."""
458
+ self.split_visible = False
459
+ try:
460
+ left = self.query_one("#split-left")
461
+ left.add_class("collapsed")
462
+ except Exception:
463
+ pass
464
+
465
+ def toggle_split(self) -> None:
466
+ """Toggle the split view."""
467
+ if self.split_visible:
468
+ self.hide_split()
469
+ else:
470
+ self.show_split()
471
+
472
+ def open_file(self, path: str) -> bool:
473
+ """
474
+ Open a file in the split view.
475
+
476
+ Returns True if successful.
477
+ """
478
+ try:
479
+ file_path = Path(path)
480
+ if not file_path.exists():
481
+ return False
482
+
483
+ # Check if already open
484
+ for i, tab in enumerate(self._tabs):
485
+ if tab.path == str(file_path):
486
+ self._active_tab = i
487
+ try:
488
+ tab_bar = self.query_one("#tab-bar", TabBar)
489
+ tab_bar.select_tab(i)
490
+ except Exception:
491
+ pass
492
+ self._show_content(tab)
493
+ self.show_split()
494
+ return True
495
+
496
+ # Read file
497
+ content = file_path.read_text(encoding="utf-8", errors="ignore")
498
+
499
+ # Detect language
500
+ language = self._detect_language(file_path)
501
+
502
+ # Create tab
503
+ tab = FileTab(
504
+ path=str(file_path),
505
+ name=file_path.name,
506
+ language=language,
507
+ content=content,
508
+ )
509
+
510
+ self._tabs.append(tab)
511
+ self._active_tab = len(self._tabs) - 1
512
+
513
+ try:
514
+ tab_bar = self.query_one("#tab-bar", TabBar)
515
+ tab_bar.add_tab(tab)
516
+ except Exception:
517
+ pass
518
+
519
+ self._show_content(tab)
520
+ self.show_split()
521
+ return True
522
+
523
+ except Exception:
524
+ return False
525
+
526
+ def _detect_language(self, path: Path) -> str:
527
+ """Detect language from file extension."""
528
+ ext_map = {
529
+ ".py": "python",
530
+ ".js": "javascript",
531
+ ".ts": "typescript",
532
+ ".jsx": "jsx",
533
+ ".tsx": "tsx",
534
+ ".html": "html",
535
+ ".css": "css",
536
+ ".json": "json",
537
+ ".yaml": "yaml",
538
+ ".yml": "yaml",
539
+ ".md": "markdown",
540
+ ".rs": "rust",
541
+ ".go": "go",
542
+ ".java": "java",
543
+ ".rb": "ruby",
544
+ ".sh": "bash",
545
+ ".sql": "sql",
546
+ ".c": "c",
547
+ ".cpp": "cpp",
548
+ ".h": "c",
549
+ ".hpp": "cpp",
550
+ }
551
+ return ext_map.get(path.suffix.lower(), "text")
552
+
553
+ def show_diff(
554
+ self,
555
+ file_path: str,
556
+ old_content: str,
557
+ new_content: str,
558
+ ) -> None:
559
+ """
560
+ Show a diff in the split view.
561
+ """
562
+ # Create unified diff
563
+ import difflib
564
+
565
+ old_lines = old_content.splitlines(keepends=True)
566
+ new_lines = new_content.splitlines(keepends=True)
567
+
568
+ diff = difflib.unified_diff(
569
+ old_lines,
570
+ new_lines,
571
+ fromfile=f"a/{file_path}",
572
+ tofile=f"b/{file_path}",
573
+ )
574
+
575
+ diff_text = "".join(diff)
576
+
577
+ # Create a diff tab
578
+ tab = FileTab(
579
+ path=f"diff:{file_path}",
580
+ name=f"Δ {Path(file_path).name}",
581
+ language="diff",
582
+ content=diff_text,
583
+ )
584
+
585
+ self._tabs.append(tab)
586
+ self._active_tab = len(self._tabs) - 1
587
+
588
+ try:
589
+ tab_bar = self.query_one("#tab-bar", TabBar)
590
+ tab_bar.add_tab(tab)
591
+ except Exception:
592
+ pass
593
+
594
+ self._show_content(tab)
595
+ self.show_split()
596
+
597
+ def get_right_container(self) -> Container:
598
+ """Get the right side container for adding chat content."""
599
+ return self.query_one("#split-right", Container)
600
+
601
+
602
+ # ============================================================================
603
+ # EXPORTS
604
+ # ============================================================================
605
+
606
+ __all__ = [
607
+ "FileTab",
608
+ "TabBar",
609
+ "CodeViewer",
610
+ "SplitDivider",
611
+ "SplitView",
612
+ ]