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