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,537 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Connection Status Widget - ACP/BYOK Agent Connection Display.
|
|
3
|
+
|
|
4
|
+
Shows the current connection status to coding agents:
|
|
5
|
+
- ACP connections (OpenCode, Claude Code, etc.)
|
|
6
|
+
- BYOK (Bring Your Own Key) provider connections
|
|
7
|
+
- Connection health and latency
|
|
8
|
+
- Model information and token usage
|
|
9
|
+
- Cost tracking
|
|
10
|
+
|
|
11
|
+
Makes it clear what agent/model you're connected to.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
from enum import Enum
|
|
20
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
21
|
+
|
|
22
|
+
from rich.console import RenderableType
|
|
23
|
+
from rich.panel import Panel
|
|
24
|
+
from rich.table import Table
|
|
25
|
+
from rich.text import Text
|
|
26
|
+
from rich.box import ROUNDED, SIMPLE
|
|
27
|
+
from textual.reactive import reactive
|
|
28
|
+
from textual.widgets import Static
|
|
29
|
+
from textual.containers import Container, Horizontal, Vertical
|
|
30
|
+
from textual.timer import Timer
|
|
31
|
+
from textual.message import Message
|
|
32
|
+
from textual import events
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ConnectionType(Enum):
|
|
36
|
+
"""Type of agent connection."""
|
|
37
|
+
|
|
38
|
+
ACP = "acp" # Agent Client Protocol
|
|
39
|
+
BYOK = "byok" # Bring Your Own Key (direct API)
|
|
40
|
+
MCP = "mcp" # Model Context Protocol
|
|
41
|
+
LOCAL = "local" # Local model
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ConnectionState(Enum):
|
|
45
|
+
"""State of the connection."""
|
|
46
|
+
|
|
47
|
+
DISCONNECTED = "disconnected"
|
|
48
|
+
CONNECTING = "connecting"
|
|
49
|
+
CONNECTED = "connected"
|
|
50
|
+
ERROR = "error"
|
|
51
|
+
BUSY = "busy" # Agent is processing
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class TokenUsage:
|
|
56
|
+
"""Token usage tracking."""
|
|
57
|
+
|
|
58
|
+
prompt_tokens: int = 0
|
|
59
|
+
completion_tokens: int = 0
|
|
60
|
+
total_tokens: int = 0
|
|
61
|
+
|
|
62
|
+
# Cost tracking (if available)
|
|
63
|
+
input_cost: float = 0.0
|
|
64
|
+
output_cost: float = 0.0
|
|
65
|
+
total_cost: float = 0.0
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class ConnectionInfo:
|
|
70
|
+
"""Information about the current connection."""
|
|
71
|
+
|
|
72
|
+
connection_type: ConnectionType
|
|
73
|
+
state: ConnectionState = ConnectionState.DISCONNECTED
|
|
74
|
+
|
|
75
|
+
# Agent info
|
|
76
|
+
agent_name: str = ""
|
|
77
|
+
agent_version: str = ""
|
|
78
|
+
agent_command: str = "" # For ACP
|
|
79
|
+
|
|
80
|
+
# Model info
|
|
81
|
+
model_name: str = ""
|
|
82
|
+
provider: str = "" # anthropic, openai, etc.
|
|
83
|
+
|
|
84
|
+
# Session info
|
|
85
|
+
session_id: str = ""
|
|
86
|
+
connected_at: Optional[datetime] = None
|
|
87
|
+
|
|
88
|
+
# Stats
|
|
89
|
+
messages_sent: int = 0
|
|
90
|
+
messages_received: int = 0
|
|
91
|
+
tool_calls: int = 0
|
|
92
|
+
token_usage: TokenUsage = field(default_factory=TokenUsage)
|
|
93
|
+
|
|
94
|
+
# Health
|
|
95
|
+
latency_ms: Optional[float] = None
|
|
96
|
+
last_error: str = ""
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# Provider styling
|
|
100
|
+
PROVIDER_STYLES = {
|
|
101
|
+
"anthropic": {"icon": "🧠", "color": "#d4a27f", "name": "Anthropic"},
|
|
102
|
+
"openai": {"icon": "🤖", "color": "#10a37f", "name": "OpenAI"},
|
|
103
|
+
"google": {"icon": "🔮", "color": "#4285f4", "name": "Google"},
|
|
104
|
+
"mistral": {"icon": "🌊", "color": "#ff7000", "name": "Mistral"},
|
|
105
|
+
"groq": {"icon": "⚡", "color": "#f55036", "name": "Groq"},
|
|
106
|
+
"ollama": {"icon": "🦙", "color": "#ffffff", "name": "Ollama"},
|
|
107
|
+
"opencode": {"icon": "💻", "color": "#3b82f6", "name": "OpenCode"},
|
|
108
|
+
"toad": {"icon": "🐸", "color": "#22c55e", "name": "Toad"},
|
|
109
|
+
"cursor": {"icon": "✨", "color": "#a855f7", "name": "Cursor"},
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
STATE_STYLES = {
|
|
113
|
+
ConnectionState.DISCONNECTED: {"icon": "○", "color": "#52525b"},
|
|
114
|
+
ConnectionState.CONNECTING: {"icon": "◐", "color": "#fbbf24"},
|
|
115
|
+
ConnectionState.CONNECTED: {"icon": "●", "color": "#22c55e"},
|
|
116
|
+
ConnectionState.ERROR: {"icon": "✗", "color": "#ef4444"},
|
|
117
|
+
ConnectionState.BUSY: {"icon": "◑", "color": "#3b82f6"},
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
TYPE_STYLES = {
|
|
121
|
+
ConnectionType.ACP: {"icon": "🔌", "label": "ACP"},
|
|
122
|
+
ConnectionType.BYOK: {"icon": "🔑", "label": "BYOK"},
|
|
123
|
+
ConnectionType.MCP: {"icon": "🔗", "label": "MCP"},
|
|
124
|
+
ConnectionType.LOCAL: {"icon": "💻", "label": "Local"},
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class ConnectionIndicator(Static):
|
|
129
|
+
"""
|
|
130
|
+
Compact connection indicator for status bar.
|
|
131
|
+
|
|
132
|
+
Shows:
|
|
133
|
+
- Connection state icon
|
|
134
|
+
- Agent/model name
|
|
135
|
+
- Provider badge
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
DEFAULT_CSS = """
|
|
139
|
+
ConnectionIndicator {
|
|
140
|
+
width: auto;
|
|
141
|
+
height: 1;
|
|
142
|
+
padding: 0 1;
|
|
143
|
+
}
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
def __init__(self, **kwargs):
|
|
147
|
+
super().__init__(**kwargs)
|
|
148
|
+
self._info: Optional[ConnectionInfo] = None
|
|
149
|
+
self._frame = 0
|
|
150
|
+
|
|
151
|
+
def set_connection(self, info: ConnectionInfo) -> None:
|
|
152
|
+
"""Set connection info."""
|
|
153
|
+
self._info = info
|
|
154
|
+
self.refresh()
|
|
155
|
+
|
|
156
|
+
def animate(self) -> None:
|
|
157
|
+
"""Advance animation frame."""
|
|
158
|
+
self._frame += 1
|
|
159
|
+
if self._info and self._info.state in (ConnectionState.CONNECTING, ConnectionState.BUSY):
|
|
160
|
+
self.refresh()
|
|
161
|
+
|
|
162
|
+
def render(self) -> Text:
|
|
163
|
+
text = Text()
|
|
164
|
+
|
|
165
|
+
if not self._info:
|
|
166
|
+
text.append("○ ", style="#52525b")
|
|
167
|
+
text.append("Not connected", style="#52525b")
|
|
168
|
+
return text
|
|
169
|
+
|
|
170
|
+
info = self._info
|
|
171
|
+
state_style = STATE_STYLES.get(info.state, STATE_STYLES[ConnectionState.DISCONNECTED])
|
|
172
|
+
|
|
173
|
+
# Animated state icon for connecting/busy
|
|
174
|
+
if info.state in (ConnectionState.CONNECTING, ConnectionState.BUSY):
|
|
175
|
+
icons = ["◐", "◓", "◑", "◒"]
|
|
176
|
+
icon = icons[self._frame % len(icons)]
|
|
177
|
+
else:
|
|
178
|
+
icon = state_style["icon"]
|
|
179
|
+
|
|
180
|
+
text.append(f"{icon} ", style=f"bold {state_style['color']}")
|
|
181
|
+
|
|
182
|
+
# Provider icon and name
|
|
183
|
+
provider_style = PROVIDER_STYLES.get(
|
|
184
|
+
info.provider.lower(), {"icon": "🤖", "color": "#a1a1aa", "name": info.provider}
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
if info.agent_name:
|
|
188
|
+
text.append(f"{provider_style['icon']} ", style=provider_style["color"])
|
|
189
|
+
text.append(info.agent_name, style=provider_style["color"])
|
|
190
|
+
elif info.model_name:
|
|
191
|
+
text.append(f"{provider_style['icon']} ", style=provider_style["color"])
|
|
192
|
+
text.append(info.model_name, style=provider_style["color"])
|
|
193
|
+
else:
|
|
194
|
+
text.append(info.state.value.title(), style="#a1a1aa")
|
|
195
|
+
|
|
196
|
+
# Connection type badge
|
|
197
|
+
type_style = TYPE_STYLES.get(info.connection_type, TYPE_STYLES[ConnectionType.BYOK])
|
|
198
|
+
text.append(f" [{type_style['label']}]", style="#6b7280")
|
|
199
|
+
|
|
200
|
+
return text
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class ConnectionPanel(Container):
|
|
204
|
+
"""
|
|
205
|
+
Full connection status panel.
|
|
206
|
+
|
|
207
|
+
Shows detailed connection information including:
|
|
208
|
+
- Agent/model info
|
|
209
|
+
- Session stats
|
|
210
|
+
- Token usage and cost
|
|
211
|
+
- Connection health
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
DEFAULT_CSS = """
|
|
215
|
+
ConnectionPanel {
|
|
216
|
+
height: auto;
|
|
217
|
+
border: solid #27272a;
|
|
218
|
+
background: #0a0a0a;
|
|
219
|
+
padding: 1;
|
|
220
|
+
margin: 0 0 1 0;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
ConnectionPanel.connected {
|
|
224
|
+
border: solid #22c55e;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
ConnectionPanel.error {
|
|
228
|
+
border: solid #ef4444;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
ConnectionPanel .panel-header {
|
|
232
|
+
height: 2;
|
|
233
|
+
margin-bottom: 1;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
ConnectionPanel .panel-content {
|
|
237
|
+
height: auto;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
ConnectionPanel .panel-stats {
|
|
241
|
+
height: auto;
|
|
242
|
+
margin-top: 1;
|
|
243
|
+
}
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
def __init__(self, **kwargs):
|
|
247
|
+
super().__init__(**kwargs)
|
|
248
|
+
self._info: Optional[ConnectionInfo] = None
|
|
249
|
+
|
|
250
|
+
def set_connection(self, info: ConnectionInfo) -> None:
|
|
251
|
+
"""Set connection info."""
|
|
252
|
+
self._info = info
|
|
253
|
+
|
|
254
|
+
# Update CSS class
|
|
255
|
+
self.remove_class("connected", "error")
|
|
256
|
+
if info.state == ConnectionState.CONNECTED:
|
|
257
|
+
self.add_class("connected")
|
|
258
|
+
elif info.state == ConnectionState.ERROR:
|
|
259
|
+
self.add_class("error")
|
|
260
|
+
|
|
261
|
+
self._update_display()
|
|
262
|
+
|
|
263
|
+
def _update_display(self) -> None:
|
|
264
|
+
"""Update the display."""
|
|
265
|
+
try:
|
|
266
|
+
header = self.query_one(".panel-header", Static)
|
|
267
|
+
content = self.query_one(".panel-content", Static)
|
|
268
|
+
stats = self.query_one(".panel-stats", Static)
|
|
269
|
+
except Exception:
|
|
270
|
+
return
|
|
271
|
+
|
|
272
|
+
if not self._info:
|
|
273
|
+
header.update(Text("No connection", style="#52525b"))
|
|
274
|
+
content.update("")
|
|
275
|
+
stats.update("")
|
|
276
|
+
return
|
|
277
|
+
|
|
278
|
+
info = self._info
|
|
279
|
+
|
|
280
|
+
# Header
|
|
281
|
+
header_text = Text()
|
|
282
|
+
state_style = STATE_STYLES.get(info.state, STATE_STYLES[ConnectionState.DISCONNECTED])
|
|
283
|
+
type_style = TYPE_STYLES.get(info.connection_type, TYPE_STYLES[ConnectionType.BYOK])
|
|
284
|
+
|
|
285
|
+
header_text.append(f"{type_style['icon']} ", style="#6b7280")
|
|
286
|
+
header_text.append(f"{type_style['label']} Connection", style="bold #e4e4e7")
|
|
287
|
+
header_text.append(f" {state_style['icon']} ", style=state_style["color"])
|
|
288
|
+
header_text.append(info.state.value.title(), style=state_style["color"])
|
|
289
|
+
|
|
290
|
+
header.update(header_text)
|
|
291
|
+
|
|
292
|
+
# Content - connection details
|
|
293
|
+
content_text = Text()
|
|
294
|
+
|
|
295
|
+
if info.agent_name:
|
|
296
|
+
provider_style = PROVIDER_STYLES.get(
|
|
297
|
+
info.provider.lower(), {"icon": "🤖", "color": "#a1a1aa"}
|
|
298
|
+
)
|
|
299
|
+
content_text.append(f" {provider_style['icon']} Agent: ", style="#6b7280")
|
|
300
|
+
content_text.append(f"{info.agent_name}", style=provider_style["color"])
|
|
301
|
+
if info.agent_version:
|
|
302
|
+
content_text.append(f" v{info.agent_version}", style="#52525b")
|
|
303
|
+
content_text.append("\n")
|
|
304
|
+
|
|
305
|
+
if info.model_name:
|
|
306
|
+
content_text.append(" 🧠 Model: ", style="#6b7280")
|
|
307
|
+
content_text.append(f"{info.model_name}", style="#e4e4e7")
|
|
308
|
+
content_text.append("\n")
|
|
309
|
+
|
|
310
|
+
if info.provider:
|
|
311
|
+
content_text.append(" 🏢 Provider: ", style="#6b7280")
|
|
312
|
+
content_text.append(f"{info.provider.title()}", style="#a1a1aa")
|
|
313
|
+
content_text.append("\n")
|
|
314
|
+
|
|
315
|
+
if info.session_id:
|
|
316
|
+
content_text.append(" 📋 Session: ", style="#6b7280")
|
|
317
|
+
content_text.append(f"{info.session_id[:12]}...", style="#52525b")
|
|
318
|
+
content_text.append("\n")
|
|
319
|
+
|
|
320
|
+
if info.connected_at:
|
|
321
|
+
duration = (datetime.now() - info.connected_at).total_seconds()
|
|
322
|
+
if duration < 60:
|
|
323
|
+
dur_str = f"{duration:.0f}s"
|
|
324
|
+
elif duration < 3600:
|
|
325
|
+
dur_str = f"{duration / 60:.0f}m"
|
|
326
|
+
else:
|
|
327
|
+
dur_str = f"{duration / 3600:.1f}h"
|
|
328
|
+
content_text.append(" ⏱️ Connected: ", style="#6b7280")
|
|
329
|
+
content_text.append(dur_str, style="#a1a1aa")
|
|
330
|
+
content_text.append("\n")
|
|
331
|
+
|
|
332
|
+
if info.last_error:
|
|
333
|
+
content_text.append(" ❌ Error: ", style="#ef4444")
|
|
334
|
+
content_text.append(info.last_error[:50], style="#ef4444")
|
|
335
|
+
content_text.append("\n")
|
|
336
|
+
|
|
337
|
+
content.update(content_text)
|
|
338
|
+
|
|
339
|
+
# Stats
|
|
340
|
+
stats_text = Text()
|
|
341
|
+
stats_text.append(" ─────────────────────────────\n", style="#27272a")
|
|
342
|
+
|
|
343
|
+
# Message counts
|
|
344
|
+
stats_text.append(" 💬 ", style="#6b7280")
|
|
345
|
+
stats_text.append(f"{info.messages_sent}↑ ", style="#3b82f6")
|
|
346
|
+
stats_text.append(f"{info.messages_received}↓ ", style="#22c55e")
|
|
347
|
+
|
|
348
|
+
# Tool calls
|
|
349
|
+
stats_text.append(" │ 🔧 ", style="#27272a")
|
|
350
|
+
stats_text.append(f"{info.tool_calls}", style="#f59e0b")
|
|
351
|
+
|
|
352
|
+
# Token usage
|
|
353
|
+
if info.token_usage.total_tokens > 0:
|
|
354
|
+
stats_text.append("\n 📊 Tokens: ", style="#6b7280")
|
|
355
|
+
stats_text.append(f"{info.token_usage.prompt_tokens:,}", style="#3b82f6")
|
|
356
|
+
stats_text.append(" → ", style="#52525b")
|
|
357
|
+
stats_text.append(f"{info.token_usage.completion_tokens:,}", style="#22c55e")
|
|
358
|
+
stats_text.append(f" ({info.token_usage.total_tokens:,} total)", style="#52525b")
|
|
359
|
+
|
|
360
|
+
# Cost
|
|
361
|
+
if info.token_usage.total_cost > 0:
|
|
362
|
+
stats_text.append("\n 💰 Cost: ", style="#6b7280")
|
|
363
|
+
stats_text.append(f"${info.token_usage.total_cost:.4f}", style="#fbbf24")
|
|
364
|
+
|
|
365
|
+
# Latency
|
|
366
|
+
if info.latency_ms is not None:
|
|
367
|
+
stats_text.append("\n 📶 Latency: ", style="#6b7280")
|
|
368
|
+
latency_color = (
|
|
369
|
+
"#22c55e"
|
|
370
|
+
if info.latency_ms < 200
|
|
371
|
+
else "#f59e0b"
|
|
372
|
+
if info.latency_ms < 500
|
|
373
|
+
else "#ef4444"
|
|
374
|
+
)
|
|
375
|
+
stats_text.append(f"{info.latency_ms:.0f}ms", style=latency_color)
|
|
376
|
+
|
|
377
|
+
stats.update(stats_text)
|
|
378
|
+
|
|
379
|
+
def compose(self):
|
|
380
|
+
"""Compose the panel."""
|
|
381
|
+
yield Static("", classes="panel-header")
|
|
382
|
+
yield Static("", classes="panel-content")
|
|
383
|
+
yield Static("", classes="panel-stats")
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
class ModelChanged(Message):
|
|
387
|
+
"""
|
|
388
|
+
Message posted when user changes the model.
|
|
389
|
+
|
|
390
|
+
When a user selects a new model, this message is posted to inform
|
|
391
|
+
the parent app that it should reset the session with the new model.
|
|
392
|
+
"""
|
|
393
|
+
|
|
394
|
+
def __init__(self, model_info: Dict[str, str]) -> None:
|
|
395
|
+
"""
|
|
396
|
+
Initialize ModelChanged message.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
model_info: Dictionary with model details:
|
|
400
|
+
- name: Display name of the model
|
|
401
|
+
- provider: Provider name (anthropic, openai, etc.)
|
|
402
|
+
- id: Model identifier for API calls
|
|
403
|
+
"""
|
|
404
|
+
super().__init__()
|
|
405
|
+
self.model_info = model_info
|
|
406
|
+
self.model_name = model_info.get("name", "")
|
|
407
|
+
self.model_id = model_info.get("id", "")
|
|
408
|
+
self.provider = model_info.get("provider", "")
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
class ModelSelector(Container):
|
|
412
|
+
"""
|
|
413
|
+
Model selection widget.
|
|
414
|
+
|
|
415
|
+
Allows switching between available models/providers.
|
|
416
|
+
Posts ModelChanged message when user selects a new model.
|
|
417
|
+
"""
|
|
418
|
+
|
|
419
|
+
DEFAULT_CSS = """
|
|
420
|
+
ModelSelector {
|
|
421
|
+
height: auto;
|
|
422
|
+
border: solid #27272a;
|
|
423
|
+
background: #0a0a0a;
|
|
424
|
+
padding: 1;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
ModelSelector .selector-header {
|
|
428
|
+
height: 1;
|
|
429
|
+
margin-bottom: 1;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
ModelSelector .model-list {
|
|
433
|
+
height: auto;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
ModelSelector .model-item {
|
|
437
|
+
height: 1;
|
|
438
|
+
padding: 0 1;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
ModelSelector .model-item:hover {
|
|
442
|
+
background: #1a1a1a;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
ModelSelector .model-item.selected {
|
|
446
|
+
background: #1a1a2a;
|
|
447
|
+
border-left: tall #3b82f6;
|
|
448
|
+
}
|
|
449
|
+
"""
|
|
450
|
+
|
|
451
|
+
selected_index: reactive[int] = reactive(0)
|
|
452
|
+
|
|
453
|
+
def __init__(
|
|
454
|
+
self,
|
|
455
|
+
models: List[Dict[str, str]],
|
|
456
|
+
on_select: Optional[Callable[[Dict[str, str]], None]] = None,
|
|
457
|
+
**kwargs,
|
|
458
|
+
):
|
|
459
|
+
super().__init__(**kwargs)
|
|
460
|
+
self.models = models # [{"name": "...", "provider": "...", "id": "..."}]
|
|
461
|
+
self._on_select = on_select
|
|
462
|
+
self._current_model_id: Optional[str] = None
|
|
463
|
+
|
|
464
|
+
def on_key(self, event: events.Key) -> None:
|
|
465
|
+
"""Handle key events."""
|
|
466
|
+
if event.key == "up":
|
|
467
|
+
self.selected_index = max(0, self.selected_index - 1)
|
|
468
|
+
event.prevent_default()
|
|
469
|
+
elif event.key == "down":
|
|
470
|
+
self.selected_index = min(len(self.models) - 1, self.selected_index + 1)
|
|
471
|
+
event.prevent_default()
|
|
472
|
+
elif event.key == "enter":
|
|
473
|
+
if self.models:
|
|
474
|
+
selected_model = self.models[self.selected_index]
|
|
475
|
+
new_model_id = selected_model.get("id", "")
|
|
476
|
+
|
|
477
|
+
# Only trigger if model actually changed
|
|
478
|
+
if new_model_id != self._current_model_id:
|
|
479
|
+
self._current_model_id = new_model_id
|
|
480
|
+
|
|
481
|
+
# Post ModelChanged message for parent app to handle
|
|
482
|
+
self.post_message(ModelChanged(selected_model))
|
|
483
|
+
|
|
484
|
+
# Also call callback if provided
|
|
485
|
+
if self._on_select:
|
|
486
|
+
self._on_select(selected_model)
|
|
487
|
+
|
|
488
|
+
event.prevent_default()
|
|
489
|
+
|
|
490
|
+
def set_current_model(self, model_id: str) -> None:
|
|
491
|
+
"""Set the current model ID (to detect actual changes)."""
|
|
492
|
+
self._current_model_id = model_id
|
|
493
|
+
|
|
494
|
+
# Update selected index to match
|
|
495
|
+
for i, model in enumerate(self.models):
|
|
496
|
+
if model.get("id") == model_id:
|
|
497
|
+
self.selected_index = i
|
|
498
|
+
break
|
|
499
|
+
|
|
500
|
+
def watch_selected_index(self, index: int) -> None:
|
|
501
|
+
"""React to selection changes."""
|
|
502
|
+
self._update_display()
|
|
503
|
+
|
|
504
|
+
def _update_display(self) -> None:
|
|
505
|
+
"""Update the display."""
|
|
506
|
+
try:
|
|
507
|
+
model_list = self.query_one(".model-list", Container)
|
|
508
|
+
except Exception:
|
|
509
|
+
return
|
|
510
|
+
|
|
511
|
+
model_list.remove_children()
|
|
512
|
+
|
|
513
|
+
for i, model in enumerate(self.models):
|
|
514
|
+
provider_style = PROVIDER_STYLES.get(
|
|
515
|
+
model.get("provider", "").lower(), {"icon": "🤖", "color": "#a1a1aa"}
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
text = Text()
|
|
519
|
+
text.append(f" {provider_style['icon']} ", style=provider_style["color"])
|
|
520
|
+
text.append(model.get("name", "Unknown"), style="#e4e4e7")
|
|
521
|
+
text.append(f" ({model.get('provider', '')})", style="#6b7280")
|
|
522
|
+
|
|
523
|
+
item = Static(text, classes="model-item")
|
|
524
|
+
if i == self.selected_index:
|
|
525
|
+
item.add_class("selected")
|
|
526
|
+
|
|
527
|
+
model_list.mount(item)
|
|
528
|
+
|
|
529
|
+
def compose(self):
|
|
530
|
+
"""Compose the selector."""
|
|
531
|
+
yield Static(Text("🧠 Select Model", style="bold #e4e4e7"), classes="selector-header")
|
|
532
|
+
with Container(classes="model-list"):
|
|
533
|
+
pass
|
|
534
|
+
|
|
535
|
+
def on_mount(self) -> None:
|
|
536
|
+
"""Initialize display."""
|
|
537
|
+
self._update_display()
|