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.
- superqode/__init__.py +33 -0
- superqode/acp/__init__.py +23 -0
- superqode/acp/client.py +913 -0
- superqode/acp/permission_screen.py +457 -0
- superqode/acp/types.py +480 -0
- superqode/acp_discovery.py +856 -0
- superqode/agent/__init__.py +22 -0
- superqode/agent/edit_strategies.py +334 -0
- superqode/agent/loop.py +892 -0
- superqode/agent/qe_report_templates.py +39 -0
- superqode/agent/system_prompts.py +353 -0
- superqode/agent_output.py +721 -0
- superqode/agent_stream.py +953 -0
- superqode/agents/__init__.py +59 -0
- superqode/agents/acp_registry.py +305 -0
- superqode/agents/client.py +249 -0
- superqode/agents/data/augmentcode.com.toml +51 -0
- superqode/agents/data/cagent.dev.toml +51 -0
- superqode/agents/data/claude.com.toml +60 -0
- superqode/agents/data/codeassistant.dev.toml +51 -0
- superqode/agents/data/codex.openai.com.toml +57 -0
- superqode/agents/data/fastagent.ai.toml +66 -0
- superqode/agents/data/geminicli.com.toml +77 -0
- superqode/agents/data/goose.block.xyz.toml +54 -0
- superqode/agents/data/junie.jetbrains.com.toml +56 -0
- superqode/agents/data/kimi.moonshot.cn.toml +57 -0
- superqode/agents/data/llmlingagent.dev.toml +51 -0
- superqode/agents/data/molt.bot.toml +49 -0
- superqode/agents/data/opencode.ai.toml +60 -0
- superqode/agents/data/stakpak.dev.toml +51 -0
- superqode/agents/data/vtcode.dev.toml +51 -0
- superqode/agents/discovery.py +266 -0
- superqode/agents/messaging.py +160 -0
- superqode/agents/persona.py +166 -0
- superqode/agents/registry.py +421 -0
- superqode/agents/schema.py +72 -0
- superqode/agents/unified.py +367 -0
- superqode/app/__init__.py +111 -0
- superqode/app/constants.py +314 -0
- superqode/app/css.py +366 -0
- superqode/app/models.py +118 -0
- superqode/app/suggester.py +125 -0
- superqode/app/widgets.py +1591 -0
- superqode/app_enhanced.py +399 -0
- superqode/app_main.py +17187 -0
- superqode/approval.py +312 -0
- superqode/atomic.py +296 -0
- superqode/commands/__init__.py +1 -0
- superqode/commands/acp.py +965 -0
- superqode/commands/agents.py +180 -0
- superqode/commands/auth.py +278 -0
- superqode/commands/config.py +374 -0
- superqode/commands/init.py +826 -0
- superqode/commands/providers.py +819 -0
- superqode/commands/qe.py +1145 -0
- superqode/commands/roles.py +380 -0
- superqode/commands/serve.py +172 -0
- superqode/commands/suggestions.py +127 -0
- superqode/commands/superqe.py +460 -0
- superqode/config/__init__.py +51 -0
- superqode/config/loader.py +812 -0
- superqode/config/schema.py +498 -0
- superqode/core/__init__.py +111 -0
- superqode/core/roles.py +281 -0
- superqode/danger.py +386 -0
- superqode/data/superqode-template.yaml +1522 -0
- superqode/design_system.py +1080 -0
- superqode/dialogs/__init__.py +6 -0
- superqode/dialogs/base.py +39 -0
- superqode/dialogs/model.py +130 -0
- superqode/dialogs/provider.py +870 -0
- superqode/diff_view.py +919 -0
- superqode/enterprise.py +21 -0
- superqode/evaluation/__init__.py +25 -0
- superqode/evaluation/adapters.py +93 -0
- superqode/evaluation/behaviors.py +89 -0
- superqode/evaluation/engine.py +209 -0
- superqode/evaluation/scenarios.py +96 -0
- superqode/execution/__init__.py +36 -0
- superqode/execution/linter.py +538 -0
- superqode/execution/modes.py +347 -0
- superqode/execution/resolver.py +283 -0
- superqode/execution/runner.py +642 -0
- superqode/file_explorer.py +811 -0
- superqode/file_viewer.py +471 -0
- superqode/flash.py +183 -0
- superqode/guidance/__init__.py +58 -0
- superqode/guidance/config.py +203 -0
- superqode/guidance/prompts.py +71 -0
- superqode/harness/__init__.py +54 -0
- superqode/harness/accelerator.py +291 -0
- superqode/harness/config.py +319 -0
- superqode/harness/validator.py +147 -0
- superqode/history.py +279 -0
- superqode/integrations/superopt_runner.py +124 -0
- superqode/logging/__init__.py +49 -0
- superqode/logging/adapters.py +219 -0
- superqode/logging/formatter.py +923 -0
- superqode/logging/integration.py +341 -0
- superqode/logging/sinks.py +170 -0
- superqode/logging/unified_log.py +417 -0
- superqode/lsp/__init__.py +26 -0
- superqode/lsp/client.py +544 -0
- superqode/main.py +1069 -0
- superqode/mcp/__init__.py +89 -0
- superqode/mcp/auth_storage.py +380 -0
- superqode/mcp/client.py +1236 -0
- superqode/mcp/config.py +319 -0
- superqode/mcp/integration.py +337 -0
- superqode/mcp/oauth.py +436 -0
- superqode/mcp/oauth_callback.py +385 -0
- superqode/mcp/types.py +290 -0
- superqode/memory/__init__.py +31 -0
- superqode/memory/feedback.py +342 -0
- superqode/memory/store.py +522 -0
- superqode/notifications.py +369 -0
- superqode/optimization/__init__.py +5 -0
- superqode/optimization/config.py +33 -0
- superqode/permissions/__init__.py +25 -0
- superqode/permissions/rules.py +488 -0
- superqode/plan.py +323 -0
- superqode/providers/__init__.py +33 -0
- superqode/providers/gateway/__init__.py +165 -0
- superqode/providers/gateway/base.py +228 -0
- superqode/providers/gateway/litellm_gateway.py +1170 -0
- superqode/providers/gateway/openresponses_gateway.py +436 -0
- superqode/providers/health.py +297 -0
- superqode/providers/huggingface/__init__.py +74 -0
- superqode/providers/huggingface/downloader.py +472 -0
- superqode/providers/huggingface/endpoints.py +442 -0
- superqode/providers/huggingface/hub.py +531 -0
- superqode/providers/huggingface/inference.py +394 -0
- superqode/providers/huggingface/transformers_runner.py +516 -0
- superqode/providers/local/__init__.py +100 -0
- superqode/providers/local/base.py +438 -0
- superqode/providers/local/discovery.py +418 -0
- superqode/providers/local/lmstudio.py +256 -0
- superqode/providers/local/mlx.py +457 -0
- superqode/providers/local/ollama.py +486 -0
- superqode/providers/local/sglang.py +268 -0
- superqode/providers/local/tgi.py +260 -0
- superqode/providers/local/tool_support.py +477 -0
- superqode/providers/local/vllm.py +258 -0
- superqode/providers/manager.py +1338 -0
- superqode/providers/models.py +1016 -0
- superqode/providers/models_dev.py +578 -0
- superqode/providers/openresponses/__init__.py +87 -0
- superqode/providers/openresponses/converters/__init__.py +17 -0
- superqode/providers/openresponses/converters/messages.py +343 -0
- superqode/providers/openresponses/converters/tools.py +268 -0
- superqode/providers/openresponses/schema/__init__.py +56 -0
- superqode/providers/openresponses/schema/models.py +585 -0
- superqode/providers/openresponses/streaming/__init__.py +5 -0
- superqode/providers/openresponses/streaming/parser.py +338 -0
- superqode/providers/openresponses/tools/__init__.py +21 -0
- superqode/providers/openresponses/tools/apply_patch.py +352 -0
- superqode/providers/openresponses/tools/code_interpreter.py +290 -0
- superqode/providers/openresponses/tools/file_search.py +333 -0
- superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
- superqode/providers/registry.py +716 -0
- superqode/providers/usage.py +332 -0
- superqode/pure_mode.py +384 -0
- superqode/qr/__init__.py +23 -0
- superqode/qr/dashboard.py +781 -0
- superqode/qr/generator.py +1018 -0
- superqode/qr/templates.py +135 -0
- superqode/safety/__init__.py +41 -0
- superqode/safety/sandbox.py +413 -0
- superqode/safety/warnings.py +256 -0
- superqode/server/__init__.py +33 -0
- superqode/server/lsp_server.py +775 -0
- superqode/server/web.py +250 -0
- superqode/session/__init__.py +25 -0
- superqode/session/persistence.py +580 -0
- superqode/session/sharing.py +477 -0
- superqode/session.py +475 -0
- superqode/sidebar.py +2991 -0
- superqode/stream_view.py +648 -0
- superqode/styles/__init__.py +3 -0
- superqode/superqe/__init__.py +184 -0
- superqode/superqe/acp_runner.py +1064 -0
- superqode/superqe/constitution/__init__.py +62 -0
- superqode/superqe/constitution/evaluator.py +308 -0
- superqode/superqe/constitution/loader.py +432 -0
- superqode/superqe/constitution/schema.py +250 -0
- superqode/superqe/events.py +591 -0
- superqode/superqe/frameworks/__init__.py +65 -0
- superqode/superqe/frameworks/base.py +234 -0
- superqode/superqe/frameworks/e2e.py +263 -0
- superqode/superqe/frameworks/executor.py +237 -0
- superqode/superqe/frameworks/javascript.py +409 -0
- superqode/superqe/frameworks/python.py +373 -0
- superqode/superqe/frameworks/registry.py +92 -0
- superqode/superqe/mcp_tools/__init__.py +47 -0
- superqode/superqe/mcp_tools/core_tools.py +418 -0
- superqode/superqe/mcp_tools/registry.py +230 -0
- superqode/superqe/mcp_tools/testing_tools.py +167 -0
- superqode/superqe/noise.py +89 -0
- superqode/superqe/orchestrator.py +778 -0
- superqode/superqe/roles.py +609 -0
- superqode/superqe/session.py +713 -0
- superqode/superqe/skills/__init__.py +57 -0
- superqode/superqe/skills/base.py +106 -0
- superqode/superqe/skills/core_skills.py +899 -0
- superqode/superqe/skills/registry.py +90 -0
- superqode/superqe/verifier.py +101 -0
- superqode/superqe_cli.py +76 -0
- superqode/tool_call.py +358 -0
- superqode/tools/__init__.py +93 -0
- superqode/tools/agent_tools.py +496 -0
- superqode/tools/base.py +324 -0
- superqode/tools/batch_tool.py +133 -0
- superqode/tools/diagnostics.py +311 -0
- superqode/tools/edit_tools.py +653 -0
- superqode/tools/enhanced_base.py +515 -0
- superqode/tools/file_tools.py +269 -0
- superqode/tools/file_tracking.py +45 -0
- superqode/tools/lsp_tools.py +610 -0
- superqode/tools/network_tools.py +350 -0
- superqode/tools/permissions.py +400 -0
- superqode/tools/question_tool.py +324 -0
- superqode/tools/search_tools.py +598 -0
- superqode/tools/shell_tools.py +259 -0
- superqode/tools/todo_tools.py +121 -0
- superqode/tools/validation.py +80 -0
- superqode/tools/web_tools.py +639 -0
- superqode/tui.py +1152 -0
- superqode/tui_integration.py +875 -0
- superqode/tui_widgets/__init__.py +27 -0
- superqode/tui_widgets/widgets/__init__.py +18 -0
- superqode/tui_widgets/widgets/progress.py +185 -0
- superqode/tui_widgets/widgets/tool_display.py +188 -0
- superqode/undo_manager.py +574 -0
- superqode/utils/__init__.py +5 -0
- superqode/utils/error_handling.py +323 -0
- superqode/utils/fuzzy.py +257 -0
- superqode/widgets/__init__.py +477 -0
- superqode/widgets/agent_collab.py +390 -0
- superqode/widgets/agent_store.py +936 -0
- superqode/widgets/agent_switcher.py +395 -0
- superqode/widgets/animation_manager.py +284 -0
- superqode/widgets/code_context.py +356 -0
- superqode/widgets/command_palette.py +412 -0
- superqode/widgets/connection_status.py +537 -0
- superqode/widgets/conversation_history.py +470 -0
- superqode/widgets/diff_indicator.py +155 -0
- superqode/widgets/enhanced_status_bar.py +385 -0
- superqode/widgets/enhanced_toast.py +476 -0
- superqode/widgets/file_browser.py +809 -0
- superqode/widgets/file_reference.py +585 -0
- superqode/widgets/issue_timeline.py +340 -0
- superqode/widgets/leader_key.py +264 -0
- superqode/widgets/mode_switcher.py +445 -0
- superqode/widgets/model_picker.py +234 -0
- superqode/widgets/permission_preview.py +1205 -0
- superqode/widgets/prompt.py +358 -0
- superqode/widgets/provider_connect.py +725 -0
- superqode/widgets/pty_shell.py +587 -0
- superqode/widgets/qe_dashboard.py +321 -0
- superqode/widgets/resizable_sidebar.py +377 -0
- superqode/widgets/response_changes.py +218 -0
- superqode/widgets/response_display.py +528 -0
- superqode/widgets/rich_tool_display.py +613 -0
- superqode/widgets/sidebar_panels.py +1180 -0
- superqode/widgets/slash_complete.py +356 -0
- superqode/widgets/split_view.py +612 -0
- superqode/widgets/status_bar.py +273 -0
- superqode/widgets/superqode_display.py +786 -0
- superqode/widgets/thinking_display.py +815 -0
- superqode/widgets/throbber.py +87 -0
- superqode/widgets/toast.py +206 -0
- superqode/widgets/unified_output.py +1073 -0
- superqode/workspace/__init__.py +75 -0
- superqode/workspace/artifacts.py +472 -0
- superqode/workspace/coordinator.py +353 -0
- superqode/workspace/diff_tracker.py +429 -0
- superqode/workspace/git_guard.py +373 -0
- superqode/workspace/git_snapshot.py +526 -0
- superqode/workspace/manager.py +750 -0
- superqode/workspace/snapshot.py +357 -0
- superqode/workspace/watcher.py +535 -0
- superqode/workspace/worktree.py +440 -0
- superqode-0.1.5.dist-info/METADATA +204 -0
- superqode-0.1.5.dist-info/RECORD +288 -0
- superqode-0.1.5.dist-info/WHEEL +5 -0
- superqode-0.1.5.dist-info/entry_points.txt +3 -0
- superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
- 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
|
+
]
|