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,395 @@
|
|
|
1
|
+
"""Agent switcher modal widget (Ctrl+A) - Redesigned for accessibility."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
import uuid
|
|
7
|
+
|
|
8
|
+
from textual import on
|
|
9
|
+
from textual.app import ComposeResult
|
|
10
|
+
from textual.containers import Vertical, VerticalScroll, Horizontal
|
|
11
|
+
from textual.message import Message
|
|
12
|
+
from textual.reactive import reactive
|
|
13
|
+
from textual.widget import Widget
|
|
14
|
+
from textual.widgets import Static
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class AgentInfo:
|
|
19
|
+
"""Information about an agent."""
|
|
20
|
+
|
|
21
|
+
identity: str
|
|
22
|
+
name: str
|
|
23
|
+
short_name: str
|
|
24
|
+
description: str
|
|
25
|
+
installed: bool = False
|
|
26
|
+
connected: bool = False
|
|
27
|
+
agent_type: str = "coding"
|
|
28
|
+
provider: str = "" # e.g., "OpenCode", "Gemini", "OpenAI"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AgentItem(Widget):
|
|
32
|
+
"""A single agent item in the switcher - high contrast design."""
|
|
33
|
+
|
|
34
|
+
DEFAULT_CSS = """
|
|
35
|
+
AgentItem {
|
|
36
|
+
height: auto;
|
|
37
|
+
min-height: 5;
|
|
38
|
+
padding: 1;
|
|
39
|
+
margin: 0 0 1 0;
|
|
40
|
+
background: #1a1a1a;
|
|
41
|
+
border: solid #444444;
|
|
42
|
+
layout: vertical;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
AgentItem:hover {
|
|
46
|
+
border: solid #00ffff;
|
|
47
|
+
background: #2a2a2a;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
AgentItem.selected {
|
|
51
|
+
border: double #00ff00;
|
|
52
|
+
background: #002200;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
AgentItem.connected {
|
|
56
|
+
border: solid #00ff00;
|
|
57
|
+
background: #002200;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
AgentItem .agent-header {
|
|
61
|
+
height: 1;
|
|
62
|
+
width: 100%;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
AgentItem .status-icon {
|
|
66
|
+
width: 4;
|
|
67
|
+
color: #ffffff;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
AgentItem .agent-name {
|
|
71
|
+
text-style: bold;
|
|
72
|
+
color: #ffffff;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
AgentItem .agent-status-text {
|
|
76
|
+
dock: right;
|
|
77
|
+
color: #00ff00;
|
|
78
|
+
text-style: bold;
|
|
79
|
+
padding-right: 1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
AgentItem .agent-provider {
|
|
83
|
+
color: #00ffff;
|
|
84
|
+
text-style: bold;
|
|
85
|
+
padding-left: 4;
|
|
86
|
+
height: 1;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
AgentItem .agent-description {
|
|
90
|
+
color: #aaaaaa;
|
|
91
|
+
padding-left: 4;
|
|
92
|
+
height: 1;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
AgentItem .agent-type {
|
|
96
|
+
color: #888888;
|
|
97
|
+
padding-left: 4;
|
|
98
|
+
text-style: italic;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
AgentItem.not-installed .agent-name {
|
|
102
|
+
color: #888888;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
AgentItem.not-installed .agent-status-text {
|
|
106
|
+
color: #ffaa00;
|
|
107
|
+
}
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
class Selected(Message):
|
|
111
|
+
"""Message sent when agent is selected."""
|
|
112
|
+
|
|
113
|
+
def __init__(self, agent: AgentInfo) -> None:
|
|
114
|
+
self.agent = agent
|
|
115
|
+
super().__init__()
|
|
116
|
+
|
|
117
|
+
selected: reactive[bool] = reactive(False)
|
|
118
|
+
|
|
119
|
+
def __init__(self, agent: AgentInfo, **kwargs) -> None:
|
|
120
|
+
super().__init__(**kwargs)
|
|
121
|
+
self.agent = agent
|
|
122
|
+
|
|
123
|
+
def compose(self) -> ComposeResult:
|
|
124
|
+
# Header row with icon, name, and status
|
|
125
|
+
with Horizontal(classes="agent-header"):
|
|
126
|
+
# Status icon - clear and visible
|
|
127
|
+
if self.agent.connected:
|
|
128
|
+
icon = "🟢"
|
|
129
|
+
status_text = "CONNECTED"
|
|
130
|
+
elif self.agent.installed:
|
|
131
|
+
icon = "✅"
|
|
132
|
+
status_text = "READY"
|
|
133
|
+
else:
|
|
134
|
+
icon = "📦"
|
|
135
|
+
status_text = "INSTALL"
|
|
136
|
+
|
|
137
|
+
yield Static(icon, classes="status-icon")
|
|
138
|
+
yield Static(f"{self.agent.name}", classes="agent-name")
|
|
139
|
+
yield Static(status_text, classes="agent-status-text")
|
|
140
|
+
|
|
141
|
+
# Provider/Coding Agent info
|
|
142
|
+
provider_text = self.agent.provider or self.agent.short_name.upper()
|
|
143
|
+
yield Static(f"🤖 Coding Agent: {provider_text}", classes="agent-provider")
|
|
144
|
+
|
|
145
|
+
# Description
|
|
146
|
+
desc = (
|
|
147
|
+
self.agent.description[:55] + "..."
|
|
148
|
+
if len(self.agent.description) > 55
|
|
149
|
+
else self.agent.description
|
|
150
|
+
)
|
|
151
|
+
yield Static(desc if desc else "No description", classes="agent-description")
|
|
152
|
+
|
|
153
|
+
# Agent type
|
|
154
|
+
yield Static(f"Type: {self.agent.agent_type}", classes="agent-type")
|
|
155
|
+
|
|
156
|
+
def on_mount(self) -> None:
|
|
157
|
+
"""Set classes on mount."""
|
|
158
|
+
self.set_class(self.agent.connected, "connected")
|
|
159
|
+
self.set_class(not self.agent.installed, "not-installed")
|
|
160
|
+
|
|
161
|
+
def watch_selected(self, selected: bool) -> None:
|
|
162
|
+
self.set_class(selected, "selected")
|
|
163
|
+
|
|
164
|
+
def on_click(self) -> None:
|
|
165
|
+
self.post_message(self.Selected(self.agent))
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class AgentSwitcher(Widget):
|
|
169
|
+
"""
|
|
170
|
+
Quick agent switcher modal (Ctrl+A) - High contrast, accessible design.
|
|
171
|
+
|
|
172
|
+
Shows available agents with their status and allows quick switching.
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
DEFAULT_CSS = """
|
|
176
|
+
AgentSwitcher {
|
|
177
|
+
layer: overlay;
|
|
178
|
+
align: center middle;
|
|
179
|
+
width: 75;
|
|
180
|
+
height: auto;
|
|
181
|
+
max-height: 28;
|
|
182
|
+
background: #000000;
|
|
183
|
+
border: double #00ffff;
|
|
184
|
+
display: none;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
AgentSwitcher.visible {
|
|
188
|
+
display: block;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
AgentSwitcher #switcher-title-bar {
|
|
192
|
+
height: 3;
|
|
193
|
+
background: #001a33;
|
|
194
|
+
padding: 1;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
AgentSwitcher #switcher-title {
|
|
198
|
+
text-style: bold;
|
|
199
|
+
color: #00ffff;
|
|
200
|
+
text-align: center;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
AgentSwitcher #switcher-subtitle {
|
|
204
|
+
color: #888888;
|
|
205
|
+
text-align: center;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
AgentSwitcher #agent-list {
|
|
209
|
+
height: auto;
|
|
210
|
+
max-height: 21;
|
|
211
|
+
padding: 1;
|
|
212
|
+
background: #0a0a0a;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
AgentSwitcher #switcher-footer {
|
|
216
|
+
height: 2;
|
|
217
|
+
background: #1a1a1a;
|
|
218
|
+
color: #00ffff;
|
|
219
|
+
padding: 0 1;
|
|
220
|
+
border-top: solid #333333;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
AgentSwitcher #footer-hints {
|
|
224
|
+
text-align: center;
|
|
225
|
+
color: #00ff00;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
AgentSwitcher .empty-message {
|
|
229
|
+
padding: 2;
|
|
230
|
+
color: #ffff00;
|
|
231
|
+
text-style: bold;
|
|
232
|
+
text-align: center;
|
|
233
|
+
background: #1a1a00;
|
|
234
|
+
border: solid #ffff00;
|
|
235
|
+
margin: 1;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
AgentSwitcher .agent-count {
|
|
239
|
+
dock: right;
|
|
240
|
+
color: #00ff00;
|
|
241
|
+
}
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
class AgentSelected(Message):
|
|
245
|
+
"""Message sent when an agent is selected."""
|
|
246
|
+
|
|
247
|
+
def __init__(self, agent: AgentInfo) -> None:
|
|
248
|
+
self.agent = agent
|
|
249
|
+
super().__init__()
|
|
250
|
+
|
|
251
|
+
class Dismissed(Message):
|
|
252
|
+
"""Message sent when switcher is dismissed."""
|
|
253
|
+
|
|
254
|
+
# State
|
|
255
|
+
is_visible: reactive[bool] = reactive(False)
|
|
256
|
+
selected_index: reactive[int] = reactive(0)
|
|
257
|
+
|
|
258
|
+
def __init__(self, agents: list[AgentInfo] | None = None, **kwargs) -> None:
|
|
259
|
+
super().__init__(**kwargs)
|
|
260
|
+
self.agents: list[AgentInfo] = agents or []
|
|
261
|
+
self._render_id = "" # Unique ID for each render
|
|
262
|
+
|
|
263
|
+
def compose(self) -> ComposeResult:
|
|
264
|
+
with Vertical(id="switcher-title-bar"):
|
|
265
|
+
yield Static("🤖 AGENT SWITCHER", id="switcher-title")
|
|
266
|
+
yield Static("Select a coding agent to connect", id="switcher-subtitle")
|
|
267
|
+
yield VerticalScroll(id="agent-list")
|
|
268
|
+
with Vertical(id="switcher-footer"):
|
|
269
|
+
yield Static("↑↓ Navigate │ Enter Connect │ Esc Close", id="footer-hints")
|
|
270
|
+
|
|
271
|
+
def show(self, agents: list[AgentInfo] | None = None) -> None:
|
|
272
|
+
"""Show agent switcher."""
|
|
273
|
+
if agents is not None:
|
|
274
|
+
self.agents = agents
|
|
275
|
+
self.selected_index = 0
|
|
276
|
+
self.is_visible = True
|
|
277
|
+
self.add_class("visible")
|
|
278
|
+
self._render_agents()
|
|
279
|
+
self.focus()
|
|
280
|
+
|
|
281
|
+
def hide(self) -> None:
|
|
282
|
+
"""Hide agent switcher."""
|
|
283
|
+
self.is_visible = False
|
|
284
|
+
self.remove_class("visible")
|
|
285
|
+
self.post_message(self.Dismissed())
|
|
286
|
+
|
|
287
|
+
def toggle(self, agents: list[AgentInfo] | None = None) -> None:
|
|
288
|
+
"""Toggle visibility."""
|
|
289
|
+
if self.is_visible:
|
|
290
|
+
self.hide()
|
|
291
|
+
else:
|
|
292
|
+
self.show(agents)
|
|
293
|
+
|
|
294
|
+
def _render_agents(self) -> None:
|
|
295
|
+
"""Render the agent list with clear visibility."""
|
|
296
|
+
# Generate unique ID for this render to avoid duplicate widget IDs
|
|
297
|
+
self._render_id = uuid.uuid4().hex[:8]
|
|
298
|
+
|
|
299
|
+
container = self.query_one("#agent-list", VerticalScroll)
|
|
300
|
+
container.remove_children()
|
|
301
|
+
|
|
302
|
+
if not self.agents:
|
|
303
|
+
container.mount(
|
|
304
|
+
Static(
|
|
305
|
+
"No agents found!\nUse /store to browse and install agents.",
|
|
306
|
+
classes="empty-message",
|
|
307
|
+
)
|
|
308
|
+
)
|
|
309
|
+
return
|
|
310
|
+
|
|
311
|
+
# Sort: connected first, then installed, then others
|
|
312
|
+
sorted_agents = sorted(
|
|
313
|
+
self.agents,
|
|
314
|
+
key=lambda a: (not a.connected, not a.installed, a.name),
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
for i, agent in enumerate(sorted_agents):
|
|
318
|
+
# Use unique render_id + index to ensure unique widget IDs
|
|
319
|
+
item = AgentItem(agent, id=f"agent-{self._render_id}-{i}")
|
|
320
|
+
item.selected = i == self.selected_index
|
|
321
|
+
container.mount(item)
|
|
322
|
+
|
|
323
|
+
# Update title to show count
|
|
324
|
+
title = self.query_one("#switcher-title", Static)
|
|
325
|
+
installed = sum(1 for a in self.agents if a.installed)
|
|
326
|
+
connected = sum(1 for a in self.agents if a.connected)
|
|
327
|
+
title.update(f"🤖 CODING AGENTS ({installed} installed, {connected} connected)")
|
|
328
|
+
|
|
329
|
+
def _update_selection(self) -> None:
|
|
330
|
+
"""Update visual selection state."""
|
|
331
|
+
items = list(self.query("#agent-list AgentItem"))
|
|
332
|
+
for i, item in enumerate(items):
|
|
333
|
+
if isinstance(item, AgentItem):
|
|
334
|
+
item.selected = i == self.selected_index
|
|
335
|
+
|
|
336
|
+
def move_selection(self, delta: int) -> None:
|
|
337
|
+
"""Move selection up or down."""
|
|
338
|
+
if not self.agents:
|
|
339
|
+
return
|
|
340
|
+
new_index = (self.selected_index + delta) % len(self.agents)
|
|
341
|
+
self.selected_index = new_index
|
|
342
|
+
self._update_selection()
|
|
343
|
+
|
|
344
|
+
# Scroll to make selection visible
|
|
345
|
+
try:
|
|
346
|
+
container = self.query_one("#agent-list", VerticalScroll)
|
|
347
|
+
items = list(self.query("#agent-list AgentItem"))
|
|
348
|
+
if 0 <= self.selected_index < len(items):
|
|
349
|
+
container.scroll_visible(items[self.selected_index])
|
|
350
|
+
except Exception:
|
|
351
|
+
pass
|
|
352
|
+
|
|
353
|
+
def select_current(self) -> AgentInfo | None:
|
|
354
|
+
"""Select the current agent."""
|
|
355
|
+
if self.agents and 0 <= self.selected_index < len(self.agents):
|
|
356
|
+
# Get sorted agents (same order as rendered)
|
|
357
|
+
sorted_agents = sorted(
|
|
358
|
+
self.agents,
|
|
359
|
+
key=lambda a: (not a.connected, not a.installed, a.name),
|
|
360
|
+
)
|
|
361
|
+
agent = sorted_agents[self.selected_index]
|
|
362
|
+
self.post_message(self.AgentSelected(agent))
|
|
363
|
+
self.hide()
|
|
364
|
+
return agent
|
|
365
|
+
return None
|
|
366
|
+
|
|
367
|
+
def on_key(self, event) -> None:
|
|
368
|
+
"""Handle key events."""
|
|
369
|
+
if not self.is_visible:
|
|
370
|
+
return
|
|
371
|
+
|
|
372
|
+
if event.key == "escape":
|
|
373
|
+
self.hide()
|
|
374
|
+
event.stop()
|
|
375
|
+
elif event.key == "up":
|
|
376
|
+
self.move_selection(-1)
|
|
377
|
+
event.stop()
|
|
378
|
+
elif event.key == "down":
|
|
379
|
+
self.move_selection(1)
|
|
380
|
+
event.stop()
|
|
381
|
+
elif event.key == "enter":
|
|
382
|
+
self.select_current()
|
|
383
|
+
event.stop()
|
|
384
|
+
|
|
385
|
+
@on(AgentItem.Selected)
|
|
386
|
+
def on_agent_item_selected(self, event: AgentItem.Selected) -> None:
|
|
387
|
+
"""Handle agent selection via click."""
|
|
388
|
+
self.post_message(self.AgentSelected(event.agent))
|
|
389
|
+
self.hide()
|
|
390
|
+
|
|
391
|
+
def update_agents(self, agents: list[AgentInfo]) -> None:
|
|
392
|
+
"""Update the agent list."""
|
|
393
|
+
self.agents = agents
|
|
394
|
+
if self.is_visible:
|
|
395
|
+
self._render_agents()
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Animation Manager - Performance-Conscious Animation Control.
|
|
3
|
+
|
|
4
|
+
Provides centralized control for TUI animations to prevent
|
|
5
|
+
performance issues from multiple concurrent animations.
|
|
6
|
+
|
|
7
|
+
Features:
|
|
8
|
+
- Global frame rate limiting
|
|
9
|
+
- Focus-aware animation pausing
|
|
10
|
+
- Low-power mode support
|
|
11
|
+
- Batched updates for multiple animated widgets
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import time
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from typing import Callable, Dict, List, Optional, Set
|
|
17
|
+
from weakref import WeakSet
|
|
18
|
+
|
|
19
|
+
from textual.app import App
|
|
20
|
+
from textual.timer import Timer
|
|
21
|
+
from textual.widget import Widget
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class AnimationConfig:
|
|
26
|
+
"""Configuration for animation behavior."""
|
|
27
|
+
|
|
28
|
+
max_fps: int = 10 # Maximum frames per second
|
|
29
|
+
pause_on_blur: bool = True # Pause when app loses focus
|
|
30
|
+
low_power_mode: bool = False # Reduce animations for battery
|
|
31
|
+
batch_updates: bool = True # Batch widget updates together
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def frame_interval(self) -> float:
|
|
35
|
+
"""Get interval between frames in seconds."""
|
|
36
|
+
return 1.0 / self.max_fps
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class AnimationManager:
|
|
40
|
+
"""Central manager for TUI animations.
|
|
41
|
+
|
|
42
|
+
Controls animation timing across multiple widgets to prevent
|
|
43
|
+
performance degradation from too many concurrent animations.
|
|
44
|
+
|
|
45
|
+
Usage:
|
|
46
|
+
# In your App class
|
|
47
|
+
class MyApp(App):
|
|
48
|
+
def on_mount(self):
|
|
49
|
+
self.animation_manager = AnimationManager(self)
|
|
50
|
+
self.animation_manager.start()
|
|
51
|
+
|
|
52
|
+
# In animated widgets
|
|
53
|
+
class MyWidget(Static):
|
|
54
|
+
def on_mount(self):
|
|
55
|
+
app = self.app
|
|
56
|
+
if hasattr(app, 'animation_manager'):
|
|
57
|
+
app.animation_manager.register(self, self._animate)
|
|
58
|
+
|
|
59
|
+
def _animate(self, frame: int) -> None:
|
|
60
|
+
# Update animation state
|
|
61
|
+
self.refresh()
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
app: App,
|
|
67
|
+
config: Optional[AnimationConfig] = None,
|
|
68
|
+
):
|
|
69
|
+
self.app = app
|
|
70
|
+
self.config = config or AnimationConfig()
|
|
71
|
+
|
|
72
|
+
# Registered widgets and their callbacks
|
|
73
|
+
self._widgets: WeakSet[Widget] = WeakSet()
|
|
74
|
+
self._callbacks: Dict[int, Callable[[int], None]] = {}
|
|
75
|
+
|
|
76
|
+
# State
|
|
77
|
+
self._timer: Optional[Timer] = None
|
|
78
|
+
self._frame: int = 0
|
|
79
|
+
self._running: bool = False
|
|
80
|
+
self._focused: bool = True
|
|
81
|
+
self._last_tick: float = 0.0
|
|
82
|
+
|
|
83
|
+
# Batching
|
|
84
|
+
self._pending_refreshes: Set[Widget] = set()
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def is_running(self) -> bool:
|
|
88
|
+
"""Check if animation manager is running."""
|
|
89
|
+
return self._running
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def current_frame(self) -> int:
|
|
93
|
+
"""Get the current animation frame number."""
|
|
94
|
+
return self._frame
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def should_animate(self) -> bool:
|
|
98
|
+
"""Check if animations should run right now."""
|
|
99
|
+
if self.config.low_power_mode:
|
|
100
|
+
return False
|
|
101
|
+
if self.config.pause_on_blur and not self._focused:
|
|
102
|
+
return False
|
|
103
|
+
return self._running
|
|
104
|
+
|
|
105
|
+
def register(
|
|
106
|
+
self,
|
|
107
|
+
widget: Widget,
|
|
108
|
+
callback: Callable[[int], None],
|
|
109
|
+
) -> None:
|
|
110
|
+
"""Register a widget for animation updates.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
widget: The widget to animate
|
|
114
|
+
callback: Called each frame with frame number
|
|
115
|
+
"""
|
|
116
|
+
self._widgets.add(widget)
|
|
117
|
+
self._callbacks[id(widget)] = callback
|
|
118
|
+
|
|
119
|
+
def unregister(self, widget: Widget) -> None:
|
|
120
|
+
"""Unregister a widget from animation updates."""
|
|
121
|
+
self._widgets.discard(widget)
|
|
122
|
+
self._callbacks.pop(id(widget), None)
|
|
123
|
+
self._pending_refreshes.discard(widget)
|
|
124
|
+
|
|
125
|
+
def start(self) -> None:
|
|
126
|
+
"""Start the animation manager."""
|
|
127
|
+
if self._running:
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
self._running = True
|
|
131
|
+
self._last_tick = time.monotonic()
|
|
132
|
+
|
|
133
|
+
# Create timer for animation ticks
|
|
134
|
+
self._timer = self.app.set_interval(
|
|
135
|
+
self.config.frame_interval,
|
|
136
|
+
self._tick,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
def stop(self) -> None:
|
|
140
|
+
"""Stop the animation manager."""
|
|
141
|
+
self._running = False
|
|
142
|
+
if self._timer:
|
|
143
|
+
self._timer.stop()
|
|
144
|
+
self._timer = None
|
|
145
|
+
|
|
146
|
+
def pause(self) -> None:
|
|
147
|
+
"""Pause animations (e.g., when app loses focus)."""
|
|
148
|
+
self._focused = False
|
|
149
|
+
|
|
150
|
+
def resume(self) -> None:
|
|
151
|
+
"""Resume animations (e.g., when app gains focus)."""
|
|
152
|
+
self._focused = True
|
|
153
|
+
|
|
154
|
+
def set_low_power(self, enabled: bool) -> None:
|
|
155
|
+
"""Enable/disable low power mode."""
|
|
156
|
+
self.config.low_power_mode = enabled
|
|
157
|
+
|
|
158
|
+
def request_refresh(self, widget: Widget) -> None:
|
|
159
|
+
"""Request a refresh for a widget (batched if enabled)."""
|
|
160
|
+
if self.config.batch_updates:
|
|
161
|
+
self._pending_refreshes.add(widget)
|
|
162
|
+
else:
|
|
163
|
+
widget.refresh()
|
|
164
|
+
|
|
165
|
+
def _tick(self) -> None:
|
|
166
|
+
"""Called each animation frame."""
|
|
167
|
+
if not self.should_animate:
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
self._frame += 1
|
|
171
|
+
current_time = time.monotonic()
|
|
172
|
+
|
|
173
|
+
# Call all registered callbacks
|
|
174
|
+
dead_widgets = []
|
|
175
|
+
for widget in list(self._widgets):
|
|
176
|
+
if not widget.is_attached:
|
|
177
|
+
dead_widgets.append(widget)
|
|
178
|
+
continue
|
|
179
|
+
|
|
180
|
+
callback = self._callbacks.get(id(widget))
|
|
181
|
+
if callback:
|
|
182
|
+
try:
|
|
183
|
+
callback(self._frame)
|
|
184
|
+
except Exception:
|
|
185
|
+
# Don't let one widget break others
|
|
186
|
+
dead_widgets.append(widget)
|
|
187
|
+
|
|
188
|
+
# Cleanup dead widgets
|
|
189
|
+
for widget in dead_widgets:
|
|
190
|
+
self.unregister(widget)
|
|
191
|
+
|
|
192
|
+
# Flush batched refreshes
|
|
193
|
+
if self.config.batch_updates and self._pending_refreshes:
|
|
194
|
+
for widget in self._pending_refreshes:
|
|
195
|
+
if widget.is_attached:
|
|
196
|
+
widget.refresh()
|
|
197
|
+
self._pending_refreshes.clear()
|
|
198
|
+
|
|
199
|
+
self._last_tick = current_time
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class AnimatedWidget(Widget):
|
|
203
|
+
"""Base class for widgets that participate in managed animations.
|
|
204
|
+
|
|
205
|
+
Automatically registers with the app's AnimationManager if present.
|
|
206
|
+
|
|
207
|
+
Subclasses should override `on_animation_frame()` to update state.
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
def on_mount(self) -> None:
|
|
211
|
+
"""Register with animation manager when mounted."""
|
|
212
|
+
super().on_mount()
|
|
213
|
+
self._register_animation()
|
|
214
|
+
|
|
215
|
+
def on_unmount(self) -> None:
|
|
216
|
+
"""Unregister from animation manager when unmounted."""
|
|
217
|
+
self._unregister_animation()
|
|
218
|
+
super().on_unmount()
|
|
219
|
+
|
|
220
|
+
def _register_animation(self) -> None:
|
|
221
|
+
"""Register with the app's animation manager."""
|
|
222
|
+
app = self.app
|
|
223
|
+
if hasattr(app, "animation_manager"):
|
|
224
|
+
app.animation_manager.register(self, self.on_animation_frame)
|
|
225
|
+
|
|
226
|
+
def _unregister_animation(self) -> None:
|
|
227
|
+
"""Unregister from the app's animation manager."""
|
|
228
|
+
app = self.app
|
|
229
|
+
if hasattr(app, "animation_manager"):
|
|
230
|
+
app.animation_manager.unregister(self)
|
|
231
|
+
|
|
232
|
+
def on_animation_frame(self, frame: int) -> None:
|
|
233
|
+
"""Called each animation frame.
|
|
234
|
+
|
|
235
|
+
Override this method to update animation state.
|
|
236
|
+
Call self.refresh() when visual update is needed.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
frame: The current frame number
|
|
240
|
+
"""
|
|
241
|
+
pass
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class ThrottledRefreshMixin:
|
|
245
|
+
"""Mixin that provides throttled refresh capability.
|
|
246
|
+
|
|
247
|
+
Use this to prevent excessive refreshes in widgets that
|
|
248
|
+
receive rapid updates.
|
|
249
|
+
|
|
250
|
+
Usage:
|
|
251
|
+
class MyWidget(ThrottledRefreshMixin, Static):
|
|
252
|
+
def update_content(self, data):
|
|
253
|
+
self._data = data
|
|
254
|
+
self.throttled_refresh()
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
_throttle_interval: float = 0.05 # 50ms minimum between refreshes
|
|
258
|
+
_last_refresh: float = 0.0
|
|
259
|
+
_refresh_pending: bool = False
|
|
260
|
+
_refresh_timer: Optional[Timer] = None
|
|
261
|
+
|
|
262
|
+
def throttled_refresh(self) -> None:
|
|
263
|
+
"""Request a throttled refresh."""
|
|
264
|
+
current = time.monotonic()
|
|
265
|
+
elapsed = current - self._last_refresh
|
|
266
|
+
|
|
267
|
+
if elapsed >= self._throttle_interval:
|
|
268
|
+
# Enough time has passed, refresh immediately
|
|
269
|
+
self._last_refresh = current
|
|
270
|
+
self.refresh()
|
|
271
|
+
elif not self._refresh_pending:
|
|
272
|
+
# Schedule a refresh for later
|
|
273
|
+
self._refresh_pending = True
|
|
274
|
+
remaining = self._throttle_interval - elapsed
|
|
275
|
+
|
|
276
|
+
# Use call_later if available (Textual widget)
|
|
277
|
+
if hasattr(self, "call_later"):
|
|
278
|
+
self.call_later(self._do_throttled_refresh, delay=remaining)
|
|
279
|
+
|
|
280
|
+
def _do_throttled_refresh(self) -> None:
|
|
281
|
+
"""Execute the pending throttled refresh."""
|
|
282
|
+
self._refresh_pending = False
|
|
283
|
+
self._last_refresh = time.monotonic()
|
|
284
|
+
self.refresh()
|