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,1180 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SuperQode Sidebar Panels - Advanced Panel Widgets.
|
|
3
|
+
|
|
4
|
+
Provides advanced sidebar panels:
|
|
5
|
+
- AgentPanel: Connection info, model, tokens, cost
|
|
6
|
+
- ContextPanel: Files in context with token counts
|
|
7
|
+
- TerminalPanel: Embedded PTY terminal
|
|
8
|
+
- DiffPanel: Pending file changes
|
|
9
|
+
- HistoryPanel: Conversation history
|
|
10
|
+
|
|
11
|
+
All panels use SuperQode's unique design system.
|
|
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, auto
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Callable, Dict, List, Optional, TYPE_CHECKING
|
|
22
|
+
|
|
23
|
+
from rich.text import Text
|
|
24
|
+
from rich.syntax import Syntax
|
|
25
|
+
from rich.progress_bar import ProgressBar
|
|
26
|
+
|
|
27
|
+
from textual.widgets import Static, Button, Input
|
|
28
|
+
from textual.containers import Container, Vertical, Horizontal, ScrollableContainer
|
|
29
|
+
from textual.reactive import reactive
|
|
30
|
+
from textual.message import Message
|
|
31
|
+
from textual import on
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from textual.app import App
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ============================================================================
|
|
38
|
+
# DESIGN SYSTEM
|
|
39
|
+
# ============================================================================
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
from superqode.design_system import COLORS as SQ_COLORS, GRADIENT_PURPLE, SUPERQODE_ICONS
|
|
43
|
+
except ImportError:
|
|
44
|
+
|
|
45
|
+
class SQ_COLORS:
|
|
46
|
+
primary = "#7c3aed"
|
|
47
|
+
primary_light = "#a855f7"
|
|
48
|
+
secondary = "#ec4899"
|
|
49
|
+
success = "#10b981"
|
|
50
|
+
error = "#f43f5e"
|
|
51
|
+
warning = "#f59e0b"
|
|
52
|
+
info = "#06b6d4"
|
|
53
|
+
text_primary = "#fafafa"
|
|
54
|
+
text_secondary = "#e4e4e7"
|
|
55
|
+
text_muted = "#a1a1aa"
|
|
56
|
+
text_dim = "#71717a"
|
|
57
|
+
text_ghost = "#52525b"
|
|
58
|
+
bg_surface = "#050505"
|
|
59
|
+
border_subtle = "#1a1a1a"
|
|
60
|
+
|
|
61
|
+
SUPERQODE_ICONS = {
|
|
62
|
+
"connected": "●",
|
|
63
|
+
"disconnected": "○",
|
|
64
|
+
"success": "✦",
|
|
65
|
+
"error": "✕",
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ============================================================================
|
|
70
|
+
# COMMON PANEL STYLES
|
|
71
|
+
# ============================================================================
|
|
72
|
+
|
|
73
|
+
PANEL_CSS = """
|
|
74
|
+
.panel-header {
|
|
75
|
+
height: 2;
|
|
76
|
+
background: #0a0a0a;
|
|
77
|
+
border-bottom: solid #1a1a1a;
|
|
78
|
+
padding: 0 1;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.panel-content {
|
|
82
|
+
height: 1fr;
|
|
83
|
+
background: #000000;
|
|
84
|
+
padding: 1;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.panel-footer {
|
|
88
|
+
height: 2;
|
|
89
|
+
background: #0a0a0a;
|
|
90
|
+
border-top: solid #1a1a1a;
|
|
91
|
+
padding: 0 1;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.panel-item {
|
|
95
|
+
height: auto;
|
|
96
|
+
padding: 0 1;
|
|
97
|
+
margin-bottom: 1;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.panel-item:hover {
|
|
101
|
+
background: #0a0a0a;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.panel-item.selected {
|
|
105
|
+
background: #7c3aed20;
|
|
106
|
+
border-left: solid #7c3aed;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.panel-empty {
|
|
110
|
+
text-align: center;
|
|
111
|
+
color: #52525b;
|
|
112
|
+
padding: 2;
|
|
113
|
+
}
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# ============================================================================
|
|
118
|
+
# AGENT PANEL
|
|
119
|
+
# ============================================================================
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass
|
|
123
|
+
class AgentInfo:
|
|
124
|
+
"""Agent connection information."""
|
|
125
|
+
|
|
126
|
+
name: str = ""
|
|
127
|
+
model: str = ""
|
|
128
|
+
provider: str = ""
|
|
129
|
+
connection_type: str = "" # "acp", "byok", "local"
|
|
130
|
+
connected: bool = False
|
|
131
|
+
connected_at: Optional[datetime] = None
|
|
132
|
+
|
|
133
|
+
# Session stats
|
|
134
|
+
message_count: int = 0
|
|
135
|
+
tool_count: int = 0
|
|
136
|
+
prompt_tokens: int = 0
|
|
137
|
+
completion_tokens: int = 0
|
|
138
|
+
total_cost: float = 0.0
|
|
139
|
+
|
|
140
|
+
# Session duration
|
|
141
|
+
@property
|
|
142
|
+
def duration_str(self) -> str:
|
|
143
|
+
if not self.connected_at:
|
|
144
|
+
return "—"
|
|
145
|
+
delta = datetime.now() - self.connected_at
|
|
146
|
+
mins = int(delta.total_seconds() // 60)
|
|
147
|
+
secs = int(delta.total_seconds() % 60)
|
|
148
|
+
return f"{mins}m {secs}s"
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class AgentPanel(Container):
|
|
152
|
+
"""
|
|
153
|
+
Panel showing connected agent information.
|
|
154
|
+
|
|
155
|
+
Features:
|
|
156
|
+
- Connection status indicator
|
|
157
|
+
- Agent name and model
|
|
158
|
+
- Token usage (prompt/completion)
|
|
159
|
+
- Cost tracking
|
|
160
|
+
- Session stats
|
|
161
|
+
- Disconnect button
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
DEFAULT_CSS = (
|
|
165
|
+
PANEL_CSS
|
|
166
|
+
+ """
|
|
167
|
+
AgentPanel {
|
|
168
|
+
height: 100%;
|
|
169
|
+
background: #000000;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
AgentPanel #agent-status {
|
|
173
|
+
height: auto;
|
|
174
|
+
padding: 1;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
AgentPanel #agent-stats {
|
|
178
|
+
height: auto;
|
|
179
|
+
padding: 1;
|
|
180
|
+
border-top: solid #1a1a1a;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
AgentPanel #agent-tokens {
|
|
184
|
+
height: auto;
|
|
185
|
+
padding: 1;
|
|
186
|
+
border-top: solid #1a1a1a;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
AgentPanel .stat-row {
|
|
190
|
+
height: 1;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
AgentPanel .stat-label {
|
|
194
|
+
width: 12;
|
|
195
|
+
color: #71717a;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
AgentPanel .stat-value {
|
|
199
|
+
width: 1fr;
|
|
200
|
+
color: #e4e4e7;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
AgentPanel #disconnect-btn {
|
|
204
|
+
margin-top: 1;
|
|
205
|
+
width: 100%;
|
|
206
|
+
}
|
|
207
|
+
"""
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
class DisconnectRequested(Message):
|
|
211
|
+
"""Posted when disconnect button is clicked."""
|
|
212
|
+
|
|
213
|
+
pass
|
|
214
|
+
|
|
215
|
+
def __init__(self, **kwargs):
|
|
216
|
+
super().__init__(**kwargs)
|
|
217
|
+
self._agent_info = AgentInfo()
|
|
218
|
+
|
|
219
|
+
def compose(self):
|
|
220
|
+
"""Compose the agent panel."""
|
|
221
|
+
yield Static(self._render_header(), id="panel-header", classes="panel-header")
|
|
222
|
+
|
|
223
|
+
with ScrollableContainer(id="panel-content", classes="panel-content"):
|
|
224
|
+
yield Static(self._render_status(), id="agent-status")
|
|
225
|
+
yield Static(self._render_stats(), id="agent-stats")
|
|
226
|
+
yield Static(self._render_tokens(), id="agent-tokens")
|
|
227
|
+
yield Button("Disconnect", id="disconnect-btn", variant="error")
|
|
228
|
+
|
|
229
|
+
def _render_header(self) -> Text:
|
|
230
|
+
"""Render panel header."""
|
|
231
|
+
text = Text()
|
|
232
|
+
text.append("◈ ", style=f"bold {SQ_COLORS.primary}")
|
|
233
|
+
text.append("Agent", style=f"bold {SQ_COLORS.text_secondary}")
|
|
234
|
+
return text
|
|
235
|
+
|
|
236
|
+
def _render_status(self) -> Text:
|
|
237
|
+
"""Render connection status."""
|
|
238
|
+
info = self._agent_info
|
|
239
|
+
text = Text()
|
|
240
|
+
|
|
241
|
+
# Connection indicator
|
|
242
|
+
if info.connected:
|
|
243
|
+
text.append("● ", style=f"bold {SQ_COLORS.success}")
|
|
244
|
+
text.append("Connected\n", style=SQ_COLORS.success)
|
|
245
|
+
else:
|
|
246
|
+
text.append("○ ", style=SQ_COLORS.text_dim)
|
|
247
|
+
text.append("Not connected\n", style=SQ_COLORS.text_dim)
|
|
248
|
+
return text
|
|
249
|
+
|
|
250
|
+
# Agent name
|
|
251
|
+
text.append("\n")
|
|
252
|
+
text.append("Agent: ", style=SQ_COLORS.text_dim)
|
|
253
|
+
text.append(f"{info.name}\n", style=f"bold {SQ_COLORS.text_primary}")
|
|
254
|
+
|
|
255
|
+
# Model
|
|
256
|
+
text.append("Model: ", style=SQ_COLORS.text_dim)
|
|
257
|
+
text.append(f"{info.model}\n", style=SQ_COLORS.info)
|
|
258
|
+
|
|
259
|
+
# Connection type
|
|
260
|
+
conn_colors = {"acp": SQ_COLORS.success, "byok": SQ_COLORS.info, "local": SQ_COLORS.warning}
|
|
261
|
+
text.append("Type: ", style=SQ_COLORS.text_dim)
|
|
262
|
+
text.append(
|
|
263
|
+
f"{info.connection_type.upper()}\n",
|
|
264
|
+
style=conn_colors.get(info.connection_type, SQ_COLORS.text_muted),
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
# Duration
|
|
268
|
+
text.append("Time: ", style=SQ_COLORS.text_dim)
|
|
269
|
+
text.append(f"{info.duration_str}\n", style=SQ_COLORS.text_muted)
|
|
270
|
+
|
|
271
|
+
return text
|
|
272
|
+
|
|
273
|
+
def _render_stats(self) -> Text:
|
|
274
|
+
"""Render session stats."""
|
|
275
|
+
info = self._agent_info
|
|
276
|
+
text = Text()
|
|
277
|
+
|
|
278
|
+
text.append("Session Stats\n", style=f"bold {SQ_COLORS.text_muted}")
|
|
279
|
+
text.append("\n")
|
|
280
|
+
|
|
281
|
+
text.append("Messages: ", style=SQ_COLORS.text_dim)
|
|
282
|
+
text.append(f"{info.message_count}\n", style=SQ_COLORS.text_secondary)
|
|
283
|
+
|
|
284
|
+
text.append("Tools: ", style=SQ_COLORS.text_dim)
|
|
285
|
+
text.append(f"{info.tool_count}\n", style=SQ_COLORS.text_secondary)
|
|
286
|
+
|
|
287
|
+
return text
|
|
288
|
+
|
|
289
|
+
def _render_tokens(self) -> Text:
|
|
290
|
+
"""Render token usage."""
|
|
291
|
+
info = self._agent_info
|
|
292
|
+
text = Text()
|
|
293
|
+
|
|
294
|
+
text.append("Token Usage\n", style=f"bold {SQ_COLORS.text_muted}")
|
|
295
|
+
text.append("\n")
|
|
296
|
+
|
|
297
|
+
text.append("Prompt: ", style=SQ_COLORS.text_dim)
|
|
298
|
+
text.append(f"{info.prompt_tokens:,}\n", style=SQ_COLORS.text_secondary)
|
|
299
|
+
|
|
300
|
+
text.append("Completion: ", style=SQ_COLORS.text_dim)
|
|
301
|
+
text.append(f"{info.completion_tokens:,}\n", style=SQ_COLORS.text_secondary)
|
|
302
|
+
|
|
303
|
+
total = info.prompt_tokens + info.completion_tokens
|
|
304
|
+
text.append("Total: ", style=SQ_COLORS.text_dim)
|
|
305
|
+
text.append(f"{total:,}\n", style=f"bold {SQ_COLORS.text_primary}")
|
|
306
|
+
|
|
307
|
+
if info.total_cost > 0:
|
|
308
|
+
text.append("\nCost: ", style=SQ_COLORS.text_dim)
|
|
309
|
+
text.append(f"${info.total_cost:.4f}", style=f"bold {SQ_COLORS.warning}")
|
|
310
|
+
|
|
311
|
+
return text
|
|
312
|
+
|
|
313
|
+
def update_agent(self, **kwargs) -> None:
|
|
314
|
+
"""Update agent information."""
|
|
315
|
+
for key, value in kwargs.items():
|
|
316
|
+
if hasattr(self._agent_info, key):
|
|
317
|
+
setattr(self._agent_info, key, value)
|
|
318
|
+
self._refresh()
|
|
319
|
+
|
|
320
|
+
def set_agent(self, info: AgentInfo) -> None:
|
|
321
|
+
"""Set agent info directly."""
|
|
322
|
+
self._agent_info = info
|
|
323
|
+
self._refresh()
|
|
324
|
+
|
|
325
|
+
def clear(self) -> None:
|
|
326
|
+
"""Clear agent info (disconnect)."""
|
|
327
|
+
self._agent_info = AgentInfo()
|
|
328
|
+
self._refresh()
|
|
329
|
+
|
|
330
|
+
def _refresh(self) -> None:
|
|
331
|
+
"""Refresh all displays."""
|
|
332
|
+
try:
|
|
333
|
+
self.query_one("#agent-status", Static).update(self._render_status())
|
|
334
|
+
self.query_one("#agent-stats", Static).update(self._render_stats())
|
|
335
|
+
self.query_one("#agent-tokens", Static).update(self._render_tokens())
|
|
336
|
+
except Exception:
|
|
337
|
+
pass
|
|
338
|
+
|
|
339
|
+
@on(Button.Pressed, "#disconnect-btn")
|
|
340
|
+
def _on_disconnect(self) -> None:
|
|
341
|
+
"""Handle disconnect button."""
|
|
342
|
+
self.post_message(self.DisconnectRequested())
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
# ============================================================================
|
|
346
|
+
# CONTEXT PANEL
|
|
347
|
+
# ============================================================================
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
@dataclass
|
|
351
|
+
class ContextFile:
|
|
352
|
+
"""A file in the agent's context."""
|
|
353
|
+
|
|
354
|
+
path: str
|
|
355
|
+
name: str
|
|
356
|
+
token_count: int = 0
|
|
357
|
+
added_at: Optional[datetime] = None
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
class ContextPanel(Container):
|
|
361
|
+
"""
|
|
362
|
+
Panel showing files in agent context.
|
|
363
|
+
|
|
364
|
+
Features:
|
|
365
|
+
- List of files with token counts
|
|
366
|
+
- Progress bar showing context usage
|
|
367
|
+
- Add/remove file buttons
|
|
368
|
+
- Clear all button
|
|
369
|
+
"""
|
|
370
|
+
|
|
371
|
+
DEFAULT_CSS = (
|
|
372
|
+
PANEL_CSS
|
|
373
|
+
+ """
|
|
374
|
+
ContextPanel {
|
|
375
|
+
height: 100%;
|
|
376
|
+
background: #000000;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
ContextPanel #context-usage {
|
|
380
|
+
height: auto;
|
|
381
|
+
padding: 1;
|
|
382
|
+
border-bottom: solid #1a1a1a;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
ContextPanel #context-files {
|
|
386
|
+
height: 1fr;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
ContextPanel .context-file {
|
|
390
|
+
height: 2;
|
|
391
|
+
padding: 0 1;
|
|
392
|
+
border-bottom: solid #0a0a0a;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
ContextPanel .context-file:hover {
|
|
396
|
+
background: #0a0a0a;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
ContextPanel #context-actions {
|
|
400
|
+
height: 3;
|
|
401
|
+
padding: 1;
|
|
402
|
+
border-top: solid #1a1a1a;
|
|
403
|
+
}
|
|
404
|
+
"""
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
class FileRemoved(Message):
|
|
408
|
+
"""Posted when a file is removed from context."""
|
|
409
|
+
|
|
410
|
+
def __init__(self, path: str) -> None:
|
|
411
|
+
self.path = path
|
|
412
|
+
super().__init__()
|
|
413
|
+
|
|
414
|
+
class ContextCleared(Message):
|
|
415
|
+
"""Posted when context is cleared."""
|
|
416
|
+
|
|
417
|
+
pass
|
|
418
|
+
|
|
419
|
+
def __init__(self, context_window: int = 128000, **kwargs):
|
|
420
|
+
super().__init__(**kwargs)
|
|
421
|
+
self._files: List[ContextFile] = []
|
|
422
|
+
self._context_window = context_window
|
|
423
|
+
|
|
424
|
+
def compose(self):
|
|
425
|
+
"""Compose the context panel."""
|
|
426
|
+
yield Static(self._render_header(), id="panel-header", classes="panel-header")
|
|
427
|
+
yield Static(self._render_usage(), id="context-usage")
|
|
428
|
+
|
|
429
|
+
with ScrollableContainer(id="context-files", classes="panel-content"):
|
|
430
|
+
yield Static(self._render_files(), id="files-list")
|
|
431
|
+
|
|
432
|
+
with Horizontal(id="context-actions"):
|
|
433
|
+
yield Button("Clear All", id="clear-btn", variant="warning")
|
|
434
|
+
|
|
435
|
+
def _render_header(self) -> Text:
|
|
436
|
+
"""Render panel header."""
|
|
437
|
+
text = Text()
|
|
438
|
+
text.append("◈ ", style=f"bold {SQ_COLORS.primary}")
|
|
439
|
+
text.append("Context", style=f"bold {SQ_COLORS.text_secondary}")
|
|
440
|
+
text.append(f" ({len(self._files)} files)", style=SQ_COLORS.text_dim)
|
|
441
|
+
return text
|
|
442
|
+
|
|
443
|
+
def _render_usage(self) -> Text:
|
|
444
|
+
"""Render context usage bar."""
|
|
445
|
+
total_tokens = sum(f.token_count for f in self._files)
|
|
446
|
+
usage_pct = (total_tokens / self._context_window) * 100 if self._context_window > 0 else 0
|
|
447
|
+
|
|
448
|
+
text = Text()
|
|
449
|
+
text.append("Context Usage\n", style=f"bold {SQ_COLORS.text_muted}")
|
|
450
|
+
|
|
451
|
+
# Progress bar
|
|
452
|
+
bar_width = 20
|
|
453
|
+
filled = int((usage_pct / 100) * bar_width)
|
|
454
|
+
empty = bar_width - filled
|
|
455
|
+
|
|
456
|
+
# Color based on usage
|
|
457
|
+
if usage_pct < 50:
|
|
458
|
+
bar_color = SQ_COLORS.success
|
|
459
|
+
elif usage_pct < 80:
|
|
460
|
+
bar_color = SQ_COLORS.warning
|
|
461
|
+
else:
|
|
462
|
+
bar_color = SQ_COLORS.error
|
|
463
|
+
|
|
464
|
+
text.append("[", style=SQ_COLORS.text_dim)
|
|
465
|
+
text.append("█" * filled, style=bar_color)
|
|
466
|
+
text.append("░" * empty, style=SQ_COLORS.text_ghost)
|
|
467
|
+
text.append("]", style=SQ_COLORS.text_dim)
|
|
468
|
+
text.append(f" {usage_pct:.1f}%\n", style=SQ_COLORS.text_muted)
|
|
469
|
+
|
|
470
|
+
text.append(f"{total_tokens:,} / {self._context_window:,} tokens", style=SQ_COLORS.text_dim)
|
|
471
|
+
|
|
472
|
+
return text
|
|
473
|
+
|
|
474
|
+
def _render_files(self) -> Text:
|
|
475
|
+
"""Render file list."""
|
|
476
|
+
if not self._files:
|
|
477
|
+
text = Text()
|
|
478
|
+
text.append("\n No files in context\n", style=SQ_COLORS.text_ghost)
|
|
479
|
+
text.append(" Files are added automatically\n", style=SQ_COLORS.text_ghost)
|
|
480
|
+
return text
|
|
481
|
+
|
|
482
|
+
text = Text()
|
|
483
|
+
for f in self._files:
|
|
484
|
+
text.append(" ↳ ", style=SQ_COLORS.info)
|
|
485
|
+
|
|
486
|
+
# File name
|
|
487
|
+
name = f.name if len(f.name) <= 20 else f.name[:17] + "..."
|
|
488
|
+
text.append(name, style=SQ_COLORS.text_secondary)
|
|
489
|
+
|
|
490
|
+
# Token count
|
|
491
|
+
text.append(f" {f.token_count:,}t\n", style=SQ_COLORS.text_dim)
|
|
492
|
+
|
|
493
|
+
return text
|
|
494
|
+
|
|
495
|
+
def add_file(self, path: str, token_count: int = 0) -> None:
|
|
496
|
+
"""Add a file to context."""
|
|
497
|
+
# Check if already exists
|
|
498
|
+
for f in self._files:
|
|
499
|
+
if f.path == path:
|
|
500
|
+
f.token_count = token_count
|
|
501
|
+
self._refresh()
|
|
502
|
+
return
|
|
503
|
+
|
|
504
|
+
self._files.append(
|
|
505
|
+
ContextFile(
|
|
506
|
+
path=path,
|
|
507
|
+
name=Path(path).name,
|
|
508
|
+
token_count=token_count,
|
|
509
|
+
added_at=datetime.now(),
|
|
510
|
+
)
|
|
511
|
+
)
|
|
512
|
+
self._refresh()
|
|
513
|
+
|
|
514
|
+
def remove_file(self, path: str) -> None:
|
|
515
|
+
"""Remove a file from context."""
|
|
516
|
+
self._files = [f for f in self._files if f.path != path]
|
|
517
|
+
self._refresh()
|
|
518
|
+
self.post_message(self.FileRemoved(path))
|
|
519
|
+
|
|
520
|
+
def clear(self) -> None:
|
|
521
|
+
"""Clear all files from context."""
|
|
522
|
+
self._files.clear()
|
|
523
|
+
self._refresh()
|
|
524
|
+
self.post_message(self.ContextCleared())
|
|
525
|
+
|
|
526
|
+
def _refresh(self) -> None:
|
|
527
|
+
"""Refresh displays."""
|
|
528
|
+
try:
|
|
529
|
+
self.query_one("#panel-header", Static).update(self._render_header())
|
|
530
|
+
self.query_one("#context-usage", Static).update(self._render_usage())
|
|
531
|
+
self.query_one("#files-list", Static).update(self._render_files())
|
|
532
|
+
except Exception:
|
|
533
|
+
pass
|
|
534
|
+
|
|
535
|
+
@on(Button.Pressed, "#clear-btn")
|
|
536
|
+
def _on_clear(self) -> None:
|
|
537
|
+
"""Handle clear button."""
|
|
538
|
+
self.clear()
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
# ============================================================================
|
|
542
|
+
# TERMINAL PANEL
|
|
543
|
+
# ============================================================================
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
class TerminalPanel(Container):
|
|
547
|
+
"""
|
|
548
|
+
Panel with embedded terminal.
|
|
549
|
+
|
|
550
|
+
Features:
|
|
551
|
+
- PTY terminal emulation
|
|
552
|
+
- Quick command buttons
|
|
553
|
+
- Output history
|
|
554
|
+
"""
|
|
555
|
+
|
|
556
|
+
DEFAULT_CSS = (
|
|
557
|
+
PANEL_CSS
|
|
558
|
+
+ """
|
|
559
|
+
TerminalPanel {
|
|
560
|
+
height: 100%;
|
|
561
|
+
background: #000000;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
TerminalPanel #terminal-output {
|
|
565
|
+
height: 1fr;
|
|
566
|
+
background: #0c0c0c;
|
|
567
|
+
padding: 1;
|
|
568
|
+
overflow-y: auto;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
TerminalPanel #terminal-input {
|
|
572
|
+
height: 3;
|
|
573
|
+
border-top: solid #1a1a1a;
|
|
574
|
+
padding: 1;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
TerminalPanel #terminal-input Input {
|
|
578
|
+
width: 100%;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
TerminalPanel #quick-commands {
|
|
582
|
+
height: 2;
|
|
583
|
+
border-top: solid #1a1a1a;
|
|
584
|
+
padding: 0 1;
|
|
585
|
+
}
|
|
586
|
+
"""
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
class CommandSubmitted(Message):
|
|
590
|
+
"""Posted when a command is submitted."""
|
|
591
|
+
|
|
592
|
+
def __init__(self, command: str) -> None:
|
|
593
|
+
self.command = command
|
|
594
|
+
super().__init__()
|
|
595
|
+
|
|
596
|
+
def __init__(self, **kwargs):
|
|
597
|
+
super().__init__(**kwargs)
|
|
598
|
+
self._output_lines: List[str] = []
|
|
599
|
+
self._max_lines = 500
|
|
600
|
+
|
|
601
|
+
def compose(self):
|
|
602
|
+
"""Compose the terminal panel."""
|
|
603
|
+
yield Static(self._render_header(), id="panel-header", classes="panel-header")
|
|
604
|
+
|
|
605
|
+
with ScrollableContainer(id="terminal-output"):
|
|
606
|
+
yield Static(self._render_output(), id="output-content")
|
|
607
|
+
|
|
608
|
+
with Container(id="terminal-input"):
|
|
609
|
+
yield Input(placeholder="$ Enter command...", id="cmd-input")
|
|
610
|
+
|
|
611
|
+
yield Static(self._render_quick_commands(), id="quick-commands")
|
|
612
|
+
|
|
613
|
+
def _render_header(self) -> Text:
|
|
614
|
+
"""Render panel header."""
|
|
615
|
+
text = Text()
|
|
616
|
+
text.append("▸ ", style=f"bold {SQ_COLORS.warning}")
|
|
617
|
+
text.append("Terminal", style=f"bold {SQ_COLORS.text_secondary}")
|
|
618
|
+
return text
|
|
619
|
+
|
|
620
|
+
def _render_output(self) -> Text:
|
|
621
|
+
"""Render terminal output."""
|
|
622
|
+
if not self._output_lines:
|
|
623
|
+
text = Text()
|
|
624
|
+
text.append("Terminal ready.\n", style=SQ_COLORS.text_dim)
|
|
625
|
+
text.append("Type a command or use quick buttons.\n", style=SQ_COLORS.text_ghost)
|
|
626
|
+
return text
|
|
627
|
+
|
|
628
|
+
text = Text()
|
|
629
|
+
for line in self._output_lines[-100:]: # Show last 100 lines
|
|
630
|
+
text.append(f"{line}\n", style=SQ_COLORS.text_secondary)
|
|
631
|
+
|
|
632
|
+
return text
|
|
633
|
+
|
|
634
|
+
def _render_quick_commands(self) -> Text:
|
|
635
|
+
"""Render quick command buttons."""
|
|
636
|
+
text = Text()
|
|
637
|
+
|
|
638
|
+
commands = ["git status", "npm test", "ls -la"]
|
|
639
|
+
for i, cmd in enumerate(commands):
|
|
640
|
+
if i > 0:
|
|
641
|
+
text.append(" │ ", style=SQ_COLORS.text_ghost)
|
|
642
|
+
text.append(cmd, style=SQ_COLORS.info)
|
|
643
|
+
|
|
644
|
+
return text
|
|
645
|
+
|
|
646
|
+
def add_output(self, text: str) -> None:
|
|
647
|
+
"""Add output to terminal."""
|
|
648
|
+
lines = text.split("\n")
|
|
649
|
+
self._output_lines.extend(lines)
|
|
650
|
+
|
|
651
|
+
# Trim if too long
|
|
652
|
+
if len(self._output_lines) > self._max_lines:
|
|
653
|
+
self._output_lines = self._output_lines[-self._max_lines :]
|
|
654
|
+
|
|
655
|
+
self._refresh()
|
|
656
|
+
|
|
657
|
+
def add_command(self, cmd: str, output: str = "", success: bool = True) -> None:
|
|
658
|
+
"""Add a command and its output."""
|
|
659
|
+
self._output_lines.append(f"$ {cmd}")
|
|
660
|
+
if output:
|
|
661
|
+
self._output_lines.extend(output.split("\n"))
|
|
662
|
+
self._refresh()
|
|
663
|
+
|
|
664
|
+
def clear(self) -> None:
|
|
665
|
+
"""Clear terminal output."""
|
|
666
|
+
self._output_lines.clear()
|
|
667
|
+
self._refresh()
|
|
668
|
+
|
|
669
|
+
def _refresh(self) -> None:
|
|
670
|
+
"""Refresh output display."""
|
|
671
|
+
try:
|
|
672
|
+
self.query_one("#output-content", Static).update(self._render_output())
|
|
673
|
+
except Exception:
|
|
674
|
+
pass
|
|
675
|
+
|
|
676
|
+
@on(Input.Submitted, "#cmd-input")
|
|
677
|
+
def _on_command(self, event: Input.Submitted) -> None:
|
|
678
|
+
"""Handle command submission."""
|
|
679
|
+
cmd = event.value.strip()
|
|
680
|
+
if cmd:
|
|
681
|
+
event.input.value = ""
|
|
682
|
+
self.post_message(self.CommandSubmitted(cmd))
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
# ============================================================================
|
|
686
|
+
# DIFF PANEL
|
|
687
|
+
# ============================================================================
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
@dataclass
|
|
691
|
+
class FileDiff:
|
|
692
|
+
"""A file with pending changes."""
|
|
693
|
+
|
|
694
|
+
path: str
|
|
695
|
+
name: str
|
|
696
|
+
status: str = "modified" # "modified", "added", "deleted"
|
|
697
|
+
additions: int = 0
|
|
698
|
+
deletions: int = 0
|
|
699
|
+
diff_text: str = ""
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
class DiffPanel(Container):
|
|
703
|
+
"""
|
|
704
|
+
Panel showing pending file changes.
|
|
705
|
+
|
|
706
|
+
Features:
|
|
707
|
+
- List of modified files
|
|
708
|
+
- Click to see diff
|
|
709
|
+
- Accept/reject buttons
|
|
710
|
+
- Stage for commit
|
|
711
|
+
"""
|
|
712
|
+
|
|
713
|
+
DEFAULT_CSS = (
|
|
714
|
+
PANEL_CSS
|
|
715
|
+
+ """
|
|
716
|
+
DiffPanel {
|
|
717
|
+
height: 100%;
|
|
718
|
+
background: #000000;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
DiffPanel #diff-files {
|
|
722
|
+
height: 50%;
|
|
723
|
+
border-bottom: solid #1a1a1a;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
DiffPanel #diff-preview {
|
|
727
|
+
height: 50%;
|
|
728
|
+
background: #0c0c0c;
|
|
729
|
+
padding: 1;
|
|
730
|
+
overflow: auto;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
DiffPanel #diff-actions {
|
|
734
|
+
height: 3;
|
|
735
|
+
padding: 1;
|
|
736
|
+
border-top: solid #1a1a1a;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
DiffPanel .diff-file {
|
|
740
|
+
height: 2;
|
|
741
|
+
padding: 0 1;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
DiffPanel .diff-file:hover {
|
|
745
|
+
background: #0a0a0a;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
DiffPanel .diff-file.selected {
|
|
749
|
+
background: #7c3aed20;
|
|
750
|
+
}
|
|
751
|
+
"""
|
|
752
|
+
)
|
|
753
|
+
|
|
754
|
+
class FileAccepted(Message):
|
|
755
|
+
"""Posted when a file change is accepted."""
|
|
756
|
+
|
|
757
|
+
def __init__(self, path: str) -> None:
|
|
758
|
+
self.path = path
|
|
759
|
+
super().__init__()
|
|
760
|
+
|
|
761
|
+
class FileRejected(Message):
|
|
762
|
+
"""Posted when a file change is rejected."""
|
|
763
|
+
|
|
764
|
+
def __init__(self, path: str) -> None:
|
|
765
|
+
self.path = path
|
|
766
|
+
super().__init__()
|
|
767
|
+
|
|
768
|
+
class AllAccepted(Message):
|
|
769
|
+
"""Posted when all changes are accepted."""
|
|
770
|
+
|
|
771
|
+
pass
|
|
772
|
+
|
|
773
|
+
class AllRejected(Message):
|
|
774
|
+
"""Posted when all changes are rejected."""
|
|
775
|
+
|
|
776
|
+
pass
|
|
777
|
+
|
|
778
|
+
def __init__(self, **kwargs):
|
|
779
|
+
super().__init__(**kwargs)
|
|
780
|
+
self._files: List[FileDiff] = []
|
|
781
|
+
self._selected_index: int = -1
|
|
782
|
+
|
|
783
|
+
def compose(self):
|
|
784
|
+
"""Compose the diff panel."""
|
|
785
|
+
yield Static(self._render_header(), id="panel-header", classes="panel-header")
|
|
786
|
+
|
|
787
|
+
with ScrollableContainer(id="diff-files"):
|
|
788
|
+
yield Static(self._render_files(), id="files-list")
|
|
789
|
+
|
|
790
|
+
with ScrollableContainer(id="diff-preview"):
|
|
791
|
+
yield Static(self._render_preview(), id="preview-content")
|
|
792
|
+
|
|
793
|
+
with Horizontal(id="diff-actions"):
|
|
794
|
+
yield Button("Accept All", id="accept-all-btn", variant="success")
|
|
795
|
+
yield Button("Reject All", id="reject-all-btn", variant="error")
|
|
796
|
+
|
|
797
|
+
def _render_header(self) -> Text:
|
|
798
|
+
"""Render panel header."""
|
|
799
|
+
text = Text()
|
|
800
|
+
text.append("⟳ ", style=f"bold {SQ_COLORS.warning}")
|
|
801
|
+
text.append("Changes", style=f"bold {SQ_COLORS.text_secondary}")
|
|
802
|
+
|
|
803
|
+
if self._files:
|
|
804
|
+
adds = sum(f.additions for f in self._files)
|
|
805
|
+
dels = sum(f.deletions for f in self._files)
|
|
806
|
+
text.append(f" +{adds}", style=SQ_COLORS.success)
|
|
807
|
+
text.append(f" -{dels}", style=SQ_COLORS.error)
|
|
808
|
+
|
|
809
|
+
return text
|
|
810
|
+
|
|
811
|
+
def _render_files(self) -> Text:
|
|
812
|
+
"""Render file list."""
|
|
813
|
+
if not self._files:
|
|
814
|
+
text = Text()
|
|
815
|
+
text.append("\n No pending changes\n", style=SQ_COLORS.text_ghost)
|
|
816
|
+
return text
|
|
817
|
+
|
|
818
|
+
text = Text()
|
|
819
|
+
for i, f in enumerate(self._files):
|
|
820
|
+
is_selected = i == self._selected_index
|
|
821
|
+
|
|
822
|
+
# Status icon
|
|
823
|
+
status_icons = {"modified": "⟳", "added": "+", "deleted": "−"}
|
|
824
|
+
status_colors = {
|
|
825
|
+
"modified": SQ_COLORS.warning,
|
|
826
|
+
"added": SQ_COLORS.success,
|
|
827
|
+
"deleted": SQ_COLORS.error,
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
icon = status_icons.get(f.status, "•")
|
|
831
|
+
color = status_colors.get(f.status, SQ_COLORS.text_muted)
|
|
832
|
+
|
|
833
|
+
if is_selected:
|
|
834
|
+
text.append("▸ ", style=f"bold {SQ_COLORS.primary}")
|
|
835
|
+
else:
|
|
836
|
+
text.append(" ", style="")
|
|
837
|
+
|
|
838
|
+
text.append(f"{icon} ", style=f"bold {color}")
|
|
839
|
+
text.append(
|
|
840
|
+
f"{f.name}", style=SQ_COLORS.text_secondary if is_selected else SQ_COLORS.text_muted
|
|
841
|
+
)
|
|
842
|
+
text.append(f" +{f.additions}", style=SQ_COLORS.success)
|
|
843
|
+
text.append(f" -{f.deletions}\n", style=SQ_COLORS.error)
|
|
844
|
+
|
|
845
|
+
return text
|
|
846
|
+
|
|
847
|
+
def _render_preview(self) -> Text:
|
|
848
|
+
"""Render diff preview."""
|
|
849
|
+
if self._selected_index < 0 or self._selected_index >= len(self._files):
|
|
850
|
+
text = Text()
|
|
851
|
+
text.append("Select a file to preview diff", style=SQ_COLORS.text_ghost)
|
|
852
|
+
return text
|
|
853
|
+
|
|
854
|
+
f = self._files[self._selected_index]
|
|
855
|
+
|
|
856
|
+
if not f.diff_text:
|
|
857
|
+
text = Text()
|
|
858
|
+
text.append(f"No diff available for {f.name}", style=SQ_COLORS.text_ghost)
|
|
859
|
+
return text
|
|
860
|
+
|
|
861
|
+
# Render diff with colors
|
|
862
|
+
text = Text()
|
|
863
|
+
for line in f.diff_text.split("\n"):
|
|
864
|
+
if line.startswith("+") and not line.startswith("+++"):
|
|
865
|
+
text.append(f"{line}\n", style=SQ_COLORS.success)
|
|
866
|
+
elif line.startswith("-") and not line.startswith("---"):
|
|
867
|
+
text.append(f"{line}\n", style=SQ_COLORS.error)
|
|
868
|
+
elif line.startswith("@@"):
|
|
869
|
+
text.append(f"{line}\n", style=SQ_COLORS.info)
|
|
870
|
+
else:
|
|
871
|
+
text.append(f"{line}\n", style=SQ_COLORS.text_dim)
|
|
872
|
+
|
|
873
|
+
return text
|
|
874
|
+
|
|
875
|
+
def add_file(
|
|
876
|
+
self,
|
|
877
|
+
path: str,
|
|
878
|
+
status: str = "modified",
|
|
879
|
+
additions: int = 0,
|
|
880
|
+
deletions: int = 0,
|
|
881
|
+
diff_text: str = "",
|
|
882
|
+
) -> None:
|
|
883
|
+
"""Add a file to the diff list."""
|
|
884
|
+
# Check if exists
|
|
885
|
+
for f in self._files:
|
|
886
|
+
if f.path == path:
|
|
887
|
+
f.status = status
|
|
888
|
+
f.additions = additions
|
|
889
|
+
f.deletions = deletions
|
|
890
|
+
f.diff_text = diff_text
|
|
891
|
+
self._refresh()
|
|
892
|
+
return
|
|
893
|
+
|
|
894
|
+
self._files.append(
|
|
895
|
+
FileDiff(
|
|
896
|
+
path=path,
|
|
897
|
+
name=Path(path).name,
|
|
898
|
+
status=status,
|
|
899
|
+
additions=additions,
|
|
900
|
+
deletions=deletions,
|
|
901
|
+
diff_text=diff_text,
|
|
902
|
+
)
|
|
903
|
+
)
|
|
904
|
+
self._refresh()
|
|
905
|
+
|
|
906
|
+
def remove_file(self, path: str) -> None:
|
|
907
|
+
"""Remove a file from the list."""
|
|
908
|
+
self._files = [f for f in self._files if f.path != path]
|
|
909
|
+
if self._selected_index >= len(self._files):
|
|
910
|
+
self._selected_index = len(self._files) - 1
|
|
911
|
+
self._refresh()
|
|
912
|
+
|
|
913
|
+
def select_file(self, index: int) -> None:
|
|
914
|
+
"""Select a file by index."""
|
|
915
|
+
if 0 <= index < len(self._files):
|
|
916
|
+
self._selected_index = index
|
|
917
|
+
self._refresh()
|
|
918
|
+
|
|
919
|
+
def clear(self) -> None:
|
|
920
|
+
"""Clear all files."""
|
|
921
|
+
self._files.clear()
|
|
922
|
+
self._selected_index = -1
|
|
923
|
+
self._refresh()
|
|
924
|
+
|
|
925
|
+
def _refresh(self) -> None:
|
|
926
|
+
"""Refresh displays."""
|
|
927
|
+
try:
|
|
928
|
+
self.query_one("#panel-header", Static).update(self._render_header())
|
|
929
|
+
self.query_one("#files-list", Static).update(self._render_files())
|
|
930
|
+
self.query_one("#preview-content", Static).update(self._render_preview())
|
|
931
|
+
except Exception:
|
|
932
|
+
pass
|
|
933
|
+
|
|
934
|
+
@on(Button.Pressed, "#accept-all-btn")
|
|
935
|
+
def _on_accept_all(self) -> None:
|
|
936
|
+
"""Handle accept all."""
|
|
937
|
+
self.post_message(self.AllAccepted())
|
|
938
|
+
|
|
939
|
+
@on(Button.Pressed, "#reject-all-btn")
|
|
940
|
+
def _on_reject_all(self) -> None:
|
|
941
|
+
"""Handle reject all."""
|
|
942
|
+
self.post_message(self.AllRejected())
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
# ============================================================================
|
|
946
|
+
# HISTORY PANEL
|
|
947
|
+
# ============================================================================
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
@dataclass
|
|
951
|
+
class HistoryMessage:
|
|
952
|
+
"""A message in history."""
|
|
953
|
+
|
|
954
|
+
id: str
|
|
955
|
+
role: str # "user", "assistant", "system"
|
|
956
|
+
content: str
|
|
957
|
+
timestamp: datetime
|
|
958
|
+
agent_name: str = ""
|
|
959
|
+
token_count: int = 0
|
|
960
|
+
|
|
961
|
+
|
|
962
|
+
class HistoryPanel(Container):
|
|
963
|
+
"""
|
|
964
|
+
Panel showing conversation history.
|
|
965
|
+
|
|
966
|
+
Features:
|
|
967
|
+
- Message timeline
|
|
968
|
+
- Filter by user/agent
|
|
969
|
+
- Search box
|
|
970
|
+
- Click to scroll to message
|
|
971
|
+
- Export to markdown
|
|
972
|
+
"""
|
|
973
|
+
|
|
974
|
+
DEFAULT_CSS = (
|
|
975
|
+
PANEL_CSS
|
|
976
|
+
+ """
|
|
977
|
+
HistoryPanel {
|
|
978
|
+
height: 100%;
|
|
979
|
+
background: #000000;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
HistoryPanel #history-search {
|
|
983
|
+
height: 3;
|
|
984
|
+
padding: 1;
|
|
985
|
+
border-bottom: solid #1a1a1a;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
HistoryPanel #history-search Input {
|
|
989
|
+
width: 100%;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
HistoryPanel #history-messages {
|
|
993
|
+
height: 1fr;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
HistoryPanel #history-actions {
|
|
997
|
+
height: 2;
|
|
998
|
+
padding: 0 1;
|
|
999
|
+
border-top: solid #1a1a1a;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
HistoryPanel .history-message {
|
|
1003
|
+
height: auto;
|
|
1004
|
+
padding: 1;
|
|
1005
|
+
border-bottom: solid #0a0a0a;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
HistoryPanel .history-message:hover {
|
|
1009
|
+
background: #0a0a0a;
|
|
1010
|
+
}
|
|
1011
|
+
"""
|
|
1012
|
+
)
|
|
1013
|
+
|
|
1014
|
+
class MessageSelected(Message):
|
|
1015
|
+
"""Posted when a message is selected."""
|
|
1016
|
+
|
|
1017
|
+
def __init__(self, message_id: str) -> None:
|
|
1018
|
+
self.message_id = message_id
|
|
1019
|
+
super().__init__()
|
|
1020
|
+
|
|
1021
|
+
class ExportRequested(Message):
|
|
1022
|
+
"""Posted when export is requested."""
|
|
1023
|
+
|
|
1024
|
+
pass
|
|
1025
|
+
|
|
1026
|
+
def __init__(self, **kwargs):
|
|
1027
|
+
super().__init__(**kwargs)
|
|
1028
|
+
self._messages: List[HistoryMessage] = []
|
|
1029
|
+
self._filter: str = "" # "", "user", "assistant"
|
|
1030
|
+
self._search: str = ""
|
|
1031
|
+
|
|
1032
|
+
def compose(self):
|
|
1033
|
+
"""Compose the history panel."""
|
|
1034
|
+
yield Static(self._render_header(), id="panel-header", classes="panel-header")
|
|
1035
|
+
|
|
1036
|
+
with Container(id="history-search"):
|
|
1037
|
+
yield Input(placeholder="Search messages...", id="search-input")
|
|
1038
|
+
|
|
1039
|
+
with ScrollableContainer(id="history-messages", classes="panel-content"):
|
|
1040
|
+
yield Static(self._render_messages(), id="messages-list")
|
|
1041
|
+
|
|
1042
|
+
yield Static(self._render_actions(), id="history-actions")
|
|
1043
|
+
|
|
1044
|
+
def _render_header(self) -> Text:
|
|
1045
|
+
"""Render panel header."""
|
|
1046
|
+
text = Text()
|
|
1047
|
+
text.append("◇ ", style=f"bold {SQ_COLORS.secondary}")
|
|
1048
|
+
text.append("History", style=f"bold {SQ_COLORS.text_secondary}")
|
|
1049
|
+
text.append(f" ({len(self._messages)} msgs)", style=SQ_COLORS.text_dim)
|
|
1050
|
+
return text
|
|
1051
|
+
|
|
1052
|
+
def _render_messages(self) -> Text:
|
|
1053
|
+
"""Render message list."""
|
|
1054
|
+
messages = self._get_filtered_messages()
|
|
1055
|
+
|
|
1056
|
+
if not messages:
|
|
1057
|
+
text = Text()
|
|
1058
|
+
if self._search:
|
|
1059
|
+
text.append("\n No messages match search\n", style=SQ_COLORS.text_ghost)
|
|
1060
|
+
else:
|
|
1061
|
+
text.append("\n No messages yet\n", style=SQ_COLORS.text_ghost)
|
|
1062
|
+
return text
|
|
1063
|
+
|
|
1064
|
+
text = Text()
|
|
1065
|
+
for msg in messages[-20:]: # Show last 20
|
|
1066
|
+
# Time
|
|
1067
|
+
time_str = msg.timestamp.strftime("%H:%M")
|
|
1068
|
+
text.append(f"{time_str} ", style=SQ_COLORS.text_ghost)
|
|
1069
|
+
|
|
1070
|
+
# Role indicator
|
|
1071
|
+
if msg.role == "user":
|
|
1072
|
+
text.append("▸ ", style=f"bold {SQ_COLORS.primary}")
|
|
1073
|
+
text.append("You: ", style=SQ_COLORS.primary)
|
|
1074
|
+
elif msg.role == "assistant":
|
|
1075
|
+
text.append("◇ ", style=f"bold {SQ_COLORS.secondary}")
|
|
1076
|
+
if msg.agent_name:
|
|
1077
|
+
text.append(f"{msg.agent_name}: ", style=SQ_COLORS.secondary)
|
|
1078
|
+
else:
|
|
1079
|
+
text.append("Agent: ", style=SQ_COLORS.secondary)
|
|
1080
|
+
else:
|
|
1081
|
+
text.append("• ", style=SQ_COLORS.text_dim)
|
|
1082
|
+
text.append("System: ", style=SQ_COLORS.text_dim)
|
|
1083
|
+
|
|
1084
|
+
# Content preview
|
|
1085
|
+
preview = msg.content[:40] + "..." if len(msg.content) > 40 else msg.content
|
|
1086
|
+
preview = preview.replace("\n", " ")
|
|
1087
|
+
text.append(f"{preview}\n", style=SQ_COLORS.text_muted)
|
|
1088
|
+
|
|
1089
|
+
return text
|
|
1090
|
+
|
|
1091
|
+
def _render_actions(self) -> Text:
|
|
1092
|
+
"""Render action buttons."""
|
|
1093
|
+
text = Text()
|
|
1094
|
+
text.append("Filter: ", style=SQ_COLORS.text_ghost)
|
|
1095
|
+
text.append(
|
|
1096
|
+
"[All]", style=f"bold {SQ_COLORS.info}" if not self._filter else SQ_COLORS.text_dim
|
|
1097
|
+
)
|
|
1098
|
+
text.append(" ", style="")
|
|
1099
|
+
text.append(
|
|
1100
|
+
"[User]",
|
|
1101
|
+
style=f"bold {SQ_COLORS.info}" if self._filter == "user" else SQ_COLORS.text_dim,
|
|
1102
|
+
)
|
|
1103
|
+
text.append(" ", style="")
|
|
1104
|
+
text.append(
|
|
1105
|
+
"[Agent]",
|
|
1106
|
+
style=f"bold {SQ_COLORS.info}" if self._filter == "assistant" else SQ_COLORS.text_dim,
|
|
1107
|
+
)
|
|
1108
|
+
return text
|
|
1109
|
+
|
|
1110
|
+
def _get_filtered_messages(self) -> List[HistoryMessage]:
|
|
1111
|
+
"""Get messages with current filter and search."""
|
|
1112
|
+
messages = self._messages
|
|
1113
|
+
|
|
1114
|
+
if self._filter:
|
|
1115
|
+
messages = [m for m in messages if m.role == self._filter]
|
|
1116
|
+
|
|
1117
|
+
if self._search:
|
|
1118
|
+
search_lower = self._search.lower()
|
|
1119
|
+
messages = [m for m in messages if search_lower in m.content.lower()]
|
|
1120
|
+
|
|
1121
|
+
return messages
|
|
1122
|
+
|
|
1123
|
+
def add_message(
|
|
1124
|
+
self, role: str, content: str, agent_name: str = "", token_count: int = 0
|
|
1125
|
+
) -> None:
|
|
1126
|
+
"""Add a message to history."""
|
|
1127
|
+
msg = HistoryMessage(
|
|
1128
|
+
id=f"msg-{len(self._messages)}",
|
|
1129
|
+
role=role,
|
|
1130
|
+
content=content,
|
|
1131
|
+
timestamp=datetime.now(),
|
|
1132
|
+
agent_name=agent_name,
|
|
1133
|
+
token_count=token_count,
|
|
1134
|
+
)
|
|
1135
|
+
self._messages.append(msg)
|
|
1136
|
+
self._refresh()
|
|
1137
|
+
|
|
1138
|
+
def clear(self) -> None:
|
|
1139
|
+
"""Clear all messages."""
|
|
1140
|
+
self._messages.clear()
|
|
1141
|
+
self._refresh()
|
|
1142
|
+
|
|
1143
|
+
def set_filter(self, filter_type: str) -> None:
|
|
1144
|
+
"""Set message filter."""
|
|
1145
|
+
self._filter = filter_type if filter_type in ("user", "assistant") else ""
|
|
1146
|
+
self._refresh()
|
|
1147
|
+
|
|
1148
|
+
def _refresh(self) -> None:
|
|
1149
|
+
"""Refresh displays."""
|
|
1150
|
+
try:
|
|
1151
|
+
self.query_one("#panel-header", Static).update(self._render_header())
|
|
1152
|
+
self.query_one("#messages-list", Static).update(self._render_messages())
|
|
1153
|
+
self.query_one("#history-actions", Static).update(self._render_actions())
|
|
1154
|
+
except Exception:
|
|
1155
|
+
pass
|
|
1156
|
+
|
|
1157
|
+
@on(Input.Changed, "#search-input")
|
|
1158
|
+
def _on_search(self, event: Input.Changed) -> None:
|
|
1159
|
+
"""Handle search input."""
|
|
1160
|
+
self._search = event.value
|
|
1161
|
+
self._refresh()
|
|
1162
|
+
|
|
1163
|
+
|
|
1164
|
+
# ============================================================================
|
|
1165
|
+
# EXPORTS
|
|
1166
|
+
# ============================================================================
|
|
1167
|
+
|
|
1168
|
+
__all__ = [
|
|
1169
|
+
# Data classes
|
|
1170
|
+
"AgentInfo",
|
|
1171
|
+
"ContextFile",
|
|
1172
|
+
"FileDiff",
|
|
1173
|
+
"HistoryMessage",
|
|
1174
|
+
# Panels
|
|
1175
|
+
"AgentPanel",
|
|
1176
|
+
"ContextPanel",
|
|
1177
|
+
"TerminalPanel",
|
|
1178
|
+
"DiffPanel",
|
|
1179
|
+
"HistoryPanel",
|
|
1180
|
+
]
|