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,613 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rich Tool Display Widget - Beautiful Tool Call Visualization.
|
|
3
|
+
|
|
4
|
+
Displays tool calls with:
|
|
5
|
+
- Collapsible sections
|
|
6
|
+
- File diff previews with syntax highlighting
|
|
7
|
+
- Progress indicators and animations
|
|
8
|
+
- Grouped by type (file, shell, search, etc.)
|
|
9
|
+
- Status badges and duration tracking
|
|
10
|
+
|
|
11
|
+
Uses SuperQode's signature style and 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
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
22
|
+
|
|
23
|
+
from rich.console import RenderableType, Group
|
|
24
|
+
from rich.panel import Panel
|
|
25
|
+
from rich.syntax import Syntax
|
|
26
|
+
from rich.table import Table
|
|
27
|
+
from rich.text import Text
|
|
28
|
+
from rich.box import ROUNDED, SIMPLE
|
|
29
|
+
from textual.reactive import reactive
|
|
30
|
+
from textual.widgets import Static, Collapsible
|
|
31
|
+
from textual.containers import Container, Vertical, Horizontal
|
|
32
|
+
from textual.timer import Timer
|
|
33
|
+
from textual import events
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ToolKind(Enum):
|
|
37
|
+
"""Type of tool operation."""
|
|
38
|
+
|
|
39
|
+
FILE_READ = "read"
|
|
40
|
+
FILE_WRITE = "write"
|
|
41
|
+
FILE_EDIT = "edit"
|
|
42
|
+
FILE_DELETE = "delete"
|
|
43
|
+
SHELL = "shell"
|
|
44
|
+
SEARCH = "search"
|
|
45
|
+
GLOB = "glob"
|
|
46
|
+
LSP = "lsp"
|
|
47
|
+
BROWSER = "browser"
|
|
48
|
+
MCP = "mcp"
|
|
49
|
+
OTHER = "other"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ToolState(Enum):
|
|
53
|
+
"""State of a tool call."""
|
|
54
|
+
|
|
55
|
+
PENDING = "pending"
|
|
56
|
+
RUNNING = "running"
|
|
57
|
+
SUCCESS = "success"
|
|
58
|
+
ERROR = "error"
|
|
59
|
+
CANCELLED = "cancelled"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# Tool styling configuration
|
|
63
|
+
TOOL_STYLES = {
|
|
64
|
+
ToolKind.FILE_READ: {"icon": "📖", "color": "#3b82f6", "label": "Read"},
|
|
65
|
+
ToolKind.FILE_WRITE: {"icon": "✏️", "color": "#22c55e", "label": "Write"},
|
|
66
|
+
ToolKind.FILE_EDIT: {"icon": "🔧", "color": "#f59e0b", "label": "Edit"},
|
|
67
|
+
ToolKind.FILE_DELETE: {"icon": "🗑️", "color": "#ef4444", "label": "Delete"},
|
|
68
|
+
ToolKind.SHELL: {"icon": "💻", "color": "#8b5cf6", "label": "Shell"},
|
|
69
|
+
ToolKind.SEARCH: {"icon": "🔍", "color": "#06b6d4", "label": "Search"},
|
|
70
|
+
ToolKind.GLOB: {"icon": "📁", "color": "#14b8a6", "label": "Glob"},
|
|
71
|
+
ToolKind.LSP: {"icon": "🔬", "color": "#ec4899", "label": "LSP"},
|
|
72
|
+
ToolKind.BROWSER: {"icon": "🌐", "color": "#f97316", "label": "Browser"},
|
|
73
|
+
ToolKind.MCP: {"icon": "🔌", "color": "#a855f7", "label": "MCP"},
|
|
74
|
+
ToolKind.OTHER: {"icon": "⚡", "color": "#71717a", "label": "Tool"},
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
STATE_STYLES = {
|
|
78
|
+
ToolState.PENDING: {"icon": "○", "color": "#71717a", "animate": False},
|
|
79
|
+
ToolState.RUNNING: {"icon": "◐", "color": "#fbbf24", "animate": True},
|
|
80
|
+
ToolState.SUCCESS: {"icon": "✓", "color": "#22c55e", "animate": False},
|
|
81
|
+
ToolState.ERROR: {"icon": "✗", "color": "#ef4444", "animate": False},
|
|
82
|
+
ToolState.CANCELLED: {"icon": "⊘", "color": "#71717a", "animate": False},
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class DiffContent:
|
|
88
|
+
"""Diff content for file operations."""
|
|
89
|
+
|
|
90
|
+
path: str
|
|
91
|
+
old_text: str = ""
|
|
92
|
+
new_text: str = ""
|
|
93
|
+
language: str = "text"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass
|
|
97
|
+
class ToolCallData:
|
|
98
|
+
"""Data for a tool call."""
|
|
99
|
+
|
|
100
|
+
id: str
|
|
101
|
+
name: str
|
|
102
|
+
kind: ToolKind
|
|
103
|
+
state: ToolState = ToolState.PENDING
|
|
104
|
+
|
|
105
|
+
# Timing
|
|
106
|
+
start_time: Optional[datetime] = None
|
|
107
|
+
end_time: Optional[datetime] = None
|
|
108
|
+
|
|
109
|
+
# Arguments
|
|
110
|
+
arguments: Dict[str, Any] = field(default_factory=dict)
|
|
111
|
+
|
|
112
|
+
# Results
|
|
113
|
+
result: str = ""
|
|
114
|
+
error: str = ""
|
|
115
|
+
|
|
116
|
+
# File operations
|
|
117
|
+
file_path: Optional[str] = None
|
|
118
|
+
diff: Optional[DiffContent] = None
|
|
119
|
+
|
|
120
|
+
# Shell operations
|
|
121
|
+
command: Optional[str] = None
|
|
122
|
+
exit_code: Optional[int] = None
|
|
123
|
+
output: str = ""
|
|
124
|
+
|
|
125
|
+
# Search operations
|
|
126
|
+
matches: List[Dict] = field(default_factory=list)
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def duration_ms(self) -> Optional[float]:
|
|
130
|
+
if self.start_time and self.end_time:
|
|
131
|
+
return (self.end_time - self.start_time).total_seconds() * 1000
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def duration_str(self) -> str:
|
|
136
|
+
ms = self.duration_ms
|
|
137
|
+
if ms is None:
|
|
138
|
+
return "..."
|
|
139
|
+
if ms < 1000:
|
|
140
|
+
return f"{ms:.0f}ms"
|
|
141
|
+
if ms < 60000:
|
|
142
|
+
return f"{ms / 1000:.1f}s"
|
|
143
|
+
return f"{ms / 60000:.1f}m"
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def display_title(self) -> str:
|
|
147
|
+
"""Get display title for the tool call."""
|
|
148
|
+
if self.file_path:
|
|
149
|
+
return Path(self.file_path).name
|
|
150
|
+
if self.command:
|
|
151
|
+
cmd_short = self.command[:40] + "..." if len(self.command) > 40 else self.command
|
|
152
|
+
return cmd_short
|
|
153
|
+
return self.name
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def detect_language(path: str) -> str:
|
|
157
|
+
"""Detect language from file extension."""
|
|
158
|
+
ext_map = {
|
|
159
|
+
".py": "python",
|
|
160
|
+
".pyi": "python",
|
|
161
|
+
".js": "javascript",
|
|
162
|
+
".jsx": "javascript",
|
|
163
|
+
".ts": "typescript",
|
|
164
|
+
".tsx": "typescript",
|
|
165
|
+
".go": "go",
|
|
166
|
+
".rs": "rust",
|
|
167
|
+
".java": "java",
|
|
168
|
+
".kt": "kotlin",
|
|
169
|
+
".rb": "ruby",
|
|
170
|
+
".php": "php",
|
|
171
|
+
".c": "c",
|
|
172
|
+
".h": "c",
|
|
173
|
+
".cpp": "cpp",
|
|
174
|
+
".hpp": "cpp",
|
|
175
|
+
".cs": "csharp",
|
|
176
|
+
".html": "html",
|
|
177
|
+
".css": "css",
|
|
178
|
+
".json": "json",
|
|
179
|
+
".yaml": "yaml",
|
|
180
|
+
".yml": "yaml",
|
|
181
|
+
".toml": "toml",
|
|
182
|
+
".md": "markdown",
|
|
183
|
+
".sql": "sql",
|
|
184
|
+
".sh": "bash",
|
|
185
|
+
}
|
|
186
|
+
ext = Path(path).suffix.lower()
|
|
187
|
+
return ext_map.get(ext, "text")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class SingleToolDisplay(Static):
|
|
191
|
+
"""Display widget for a single tool call."""
|
|
192
|
+
|
|
193
|
+
DEFAULT_CSS = """
|
|
194
|
+
SingleToolDisplay {
|
|
195
|
+
height: auto;
|
|
196
|
+
margin: 0 0 1 0;
|
|
197
|
+
padding: 0;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
SingleToolDisplay.expanded {
|
|
201
|
+
height: auto;
|
|
202
|
+
}
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
expanded: reactive[bool] = reactive(False)
|
|
206
|
+
|
|
207
|
+
def __init__(self, tool: ToolCallData, **kwargs):
|
|
208
|
+
super().__init__(**kwargs)
|
|
209
|
+
self.tool = tool
|
|
210
|
+
self._spinner_frame = 0
|
|
211
|
+
|
|
212
|
+
def update_tool(self, tool: ToolCallData) -> None:
|
|
213
|
+
"""Update tool data."""
|
|
214
|
+
self.tool = tool
|
|
215
|
+
self.refresh()
|
|
216
|
+
|
|
217
|
+
def on_click(self, event: events.Click) -> None:
|
|
218
|
+
"""Toggle expansion on click."""
|
|
219
|
+
self.expanded = not self.expanded
|
|
220
|
+
self.refresh()
|
|
221
|
+
|
|
222
|
+
def _get_spinner(self) -> str:
|
|
223
|
+
"""Get animated spinner character."""
|
|
224
|
+
spinners = ["◐", "◓", "◑", "◒"]
|
|
225
|
+
return spinners[self._spinner_frame % len(spinners)]
|
|
226
|
+
|
|
227
|
+
def _render_diff(self) -> Text:
|
|
228
|
+
"""Render file diff."""
|
|
229
|
+
if not self.tool.diff:
|
|
230
|
+
return Text()
|
|
231
|
+
|
|
232
|
+
result = Text()
|
|
233
|
+
diff = self.tool.diff
|
|
234
|
+
|
|
235
|
+
old_lines = diff.old_text.splitlines() if diff.old_text else []
|
|
236
|
+
new_lines = diff.new_text.splitlines() if diff.new_text else []
|
|
237
|
+
|
|
238
|
+
# Show stats
|
|
239
|
+
result.append(" ")
|
|
240
|
+
if old_lines:
|
|
241
|
+
result.append(f"-{len(old_lines)} ", style="bold #ef4444")
|
|
242
|
+
if new_lines:
|
|
243
|
+
result.append(f"+{len(new_lines)}", style="bold #22c55e")
|
|
244
|
+
result.append(" lines\n\n")
|
|
245
|
+
|
|
246
|
+
# Show diff preview (limited)
|
|
247
|
+
max_lines = 8
|
|
248
|
+
shown = 0
|
|
249
|
+
|
|
250
|
+
for line in old_lines[: max_lines // 2]:
|
|
251
|
+
line_preview = line[:70] + "..." if len(line) > 70 else line
|
|
252
|
+
result.append(f" -{line_preview}\n", style="on #2d1f1f #ef4444")
|
|
253
|
+
shown += 1
|
|
254
|
+
|
|
255
|
+
if len(old_lines) > max_lines // 2:
|
|
256
|
+
result.append(
|
|
257
|
+
f" ... {len(old_lines) - max_lines // 2} more removed\n", style="#71717a"
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
for line in new_lines[: max_lines // 2]:
|
|
261
|
+
line_preview = line[:70] + "..." if len(line) > 70 else line
|
|
262
|
+
result.append(f" +{line_preview}\n", style="on #1f2d1f #22c55e")
|
|
263
|
+
shown += 1
|
|
264
|
+
|
|
265
|
+
if len(new_lines) > max_lines // 2:
|
|
266
|
+
result.append(
|
|
267
|
+
f" ... {len(new_lines) - max_lines // 2} more added\n", style="#71717a"
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
return result
|
|
271
|
+
|
|
272
|
+
def _render_shell_output(self) -> Text:
|
|
273
|
+
"""Render shell command output."""
|
|
274
|
+
result = Text()
|
|
275
|
+
|
|
276
|
+
if self.tool.command:
|
|
277
|
+
result.append(f" $ {self.tool.command}\n", style="bold #a1a1aa")
|
|
278
|
+
|
|
279
|
+
if self.tool.output:
|
|
280
|
+
lines = self.tool.output.splitlines()[:10]
|
|
281
|
+
for line in lines:
|
|
282
|
+
line_preview = line[:70] + "..." if len(line) > 70 else line
|
|
283
|
+
result.append(f" {line_preview}\n", style="#6b7280")
|
|
284
|
+
|
|
285
|
+
if len(self.tool.output.splitlines()) > 10:
|
|
286
|
+
result.append(
|
|
287
|
+
f" ... {len(self.tool.output.splitlines()) - 10} more lines\n",
|
|
288
|
+
style="#52525b",
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
if self.tool.exit_code is not None:
|
|
292
|
+
style = "#22c55e" if self.tool.exit_code == 0 else "#ef4444"
|
|
293
|
+
result.append(f" Exit: {self.tool.exit_code}\n", style=style)
|
|
294
|
+
|
|
295
|
+
return result
|
|
296
|
+
|
|
297
|
+
def _render_search_results(self) -> Text:
|
|
298
|
+
"""Render search results."""
|
|
299
|
+
result = Text()
|
|
300
|
+
|
|
301
|
+
matches = self.tool.matches[:5]
|
|
302
|
+
for match in matches:
|
|
303
|
+
path = match.get("path", "")
|
|
304
|
+
line = match.get("line", "")
|
|
305
|
+
preview = match.get("preview", "")[:50]
|
|
306
|
+
|
|
307
|
+
result.append(f" {path}", style="#3b82f6")
|
|
308
|
+
if line:
|
|
309
|
+
result.append(f":{line}", style="#6b7280")
|
|
310
|
+
result.append("\n")
|
|
311
|
+
if preview:
|
|
312
|
+
result.append(f" {preview}\n", style="#a1a1aa")
|
|
313
|
+
|
|
314
|
+
if len(self.tool.matches) > 5:
|
|
315
|
+
result.append(f" ... {len(self.tool.matches) - 5} more matches\n", style="#52525b")
|
|
316
|
+
|
|
317
|
+
return result
|
|
318
|
+
|
|
319
|
+
def render(self) -> RenderableType:
|
|
320
|
+
"""Render the tool call."""
|
|
321
|
+
content = Text()
|
|
322
|
+
|
|
323
|
+
tool_style = TOOL_STYLES.get(self.tool.kind, TOOL_STYLES[ToolKind.OTHER])
|
|
324
|
+
state_style = STATE_STYLES.get(self.tool.state, STATE_STYLES[ToolState.PENDING])
|
|
325
|
+
|
|
326
|
+
# Status icon
|
|
327
|
+
state_icon = self._get_spinner() if state_style["animate"] else state_style["icon"]
|
|
328
|
+
content.append(f"{state_icon} ", style=f"bold {state_style['color']}")
|
|
329
|
+
|
|
330
|
+
# Tool icon and type
|
|
331
|
+
content.append(f"{tool_style['icon']} ", style=tool_style["color"])
|
|
332
|
+
content.append(f"{tool_style['label']}: ", style=f"bold {tool_style['color']}")
|
|
333
|
+
|
|
334
|
+
# Title/path
|
|
335
|
+
content.append(self.tool.display_title, style="#e4e4e7")
|
|
336
|
+
|
|
337
|
+
# Duration
|
|
338
|
+
if self.tool.duration_ms is not None:
|
|
339
|
+
content.append(f" ({self.tool.duration_str})", style="#6b7280")
|
|
340
|
+
|
|
341
|
+
# Expand indicator
|
|
342
|
+
expand_icon = "▼" if self.expanded else "▶"
|
|
343
|
+
content.append(f" {expand_icon}", style="#52525b")
|
|
344
|
+
|
|
345
|
+
content.append("\n")
|
|
346
|
+
|
|
347
|
+
# Expanded content
|
|
348
|
+
if self.expanded:
|
|
349
|
+
if self.tool.error:
|
|
350
|
+
content.append(f" ❌ {self.tool.error}\n", style="#ef4444")
|
|
351
|
+
|
|
352
|
+
if self.tool.diff:
|
|
353
|
+
content.append(self._render_diff())
|
|
354
|
+
|
|
355
|
+
if self.tool.kind == ToolKind.SHELL:
|
|
356
|
+
content.append(self._render_shell_output())
|
|
357
|
+
|
|
358
|
+
if self.tool.matches:
|
|
359
|
+
content.append(self._render_search_results())
|
|
360
|
+
|
|
361
|
+
if self.tool.result and not self.tool.diff and self.tool.kind != ToolKind.SHELL:
|
|
362
|
+
result_preview = (
|
|
363
|
+
self.tool.result[:200] + "..."
|
|
364
|
+
if len(self.tool.result) > 200
|
|
365
|
+
else self.tool.result
|
|
366
|
+
)
|
|
367
|
+
content.append(f" {result_preview}\n", style="#a1a1aa")
|
|
368
|
+
|
|
369
|
+
return content
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
class ToolCallPanel(Container):
|
|
373
|
+
"""
|
|
374
|
+
Panel displaying all tool calls with grouping and filtering.
|
|
375
|
+
|
|
376
|
+
Features:
|
|
377
|
+
- Groups tool calls by type
|
|
378
|
+
- Collapsible sections
|
|
379
|
+
- Progress indicators
|
|
380
|
+
- Summary statistics
|
|
381
|
+
"""
|
|
382
|
+
|
|
383
|
+
DEFAULT_CSS = """
|
|
384
|
+
ToolCallPanel {
|
|
385
|
+
height: auto;
|
|
386
|
+
max-height: 50%;
|
|
387
|
+
border: solid #27272a;
|
|
388
|
+
background: #0a0a0a;
|
|
389
|
+
padding: 1;
|
|
390
|
+
margin: 0 0 1 0;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
ToolCallPanel .tools-header {
|
|
394
|
+
height: 1;
|
|
395
|
+
margin-bottom: 1;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
ToolCallPanel .tools-content {
|
|
399
|
+
height: auto;
|
|
400
|
+
overflow-y: auto;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
ToolCallPanel .tools-stats {
|
|
404
|
+
height: 1;
|
|
405
|
+
margin-top: 1;
|
|
406
|
+
}
|
|
407
|
+
"""
|
|
408
|
+
|
|
409
|
+
collapsed: reactive[bool] = reactive(False)
|
|
410
|
+
|
|
411
|
+
def __init__(self, **kwargs):
|
|
412
|
+
super().__init__(**kwargs)
|
|
413
|
+
self._tools: Dict[str, ToolCallData] = {}
|
|
414
|
+
self._widgets: Dict[str, SingleToolDisplay] = {}
|
|
415
|
+
self._timer: Optional[Timer] = None
|
|
416
|
+
|
|
417
|
+
def on_mount(self) -> None:
|
|
418
|
+
"""Start animation timer."""
|
|
419
|
+
self._timer = self.set_interval(0.25, self._tick)
|
|
420
|
+
|
|
421
|
+
def _tick(self) -> None:
|
|
422
|
+
"""Animation tick for running tools."""
|
|
423
|
+
for widget in self._widgets.values():
|
|
424
|
+
if widget.tool.state == ToolState.RUNNING:
|
|
425
|
+
widget._spinner_frame += 1
|
|
426
|
+
widget.refresh()
|
|
427
|
+
|
|
428
|
+
def add_tool(self, tool: ToolCallData) -> None:
|
|
429
|
+
"""Add or update a tool call."""
|
|
430
|
+
self._tools[tool.id] = tool
|
|
431
|
+
|
|
432
|
+
if tool.id not in self._widgets:
|
|
433
|
+
widget = SingleToolDisplay(tool, id=f"tool-{tool.id}")
|
|
434
|
+
self._widgets[tool.id] = widget
|
|
435
|
+
|
|
436
|
+
content = self.query_one(".tools-content", Container)
|
|
437
|
+
content.mount(widget)
|
|
438
|
+
else:
|
|
439
|
+
self._widgets[tool.id].update_tool(tool)
|
|
440
|
+
|
|
441
|
+
self._update_header()
|
|
442
|
+
|
|
443
|
+
def update_tool(self, tool_id: str, **updates) -> None:
|
|
444
|
+
"""Update a tool call."""
|
|
445
|
+
if tool_id in self._tools:
|
|
446
|
+
tool = self._tools[tool_id]
|
|
447
|
+
for key, value in updates.items():
|
|
448
|
+
if hasattr(tool, key):
|
|
449
|
+
setattr(tool, key, value)
|
|
450
|
+
|
|
451
|
+
if tool_id in self._widgets:
|
|
452
|
+
self._widgets[tool_id].update_tool(tool)
|
|
453
|
+
|
|
454
|
+
self._update_header()
|
|
455
|
+
|
|
456
|
+
def complete_tool(self, tool_id: str, result: str = "", error: str = "") -> None:
|
|
457
|
+
"""Mark a tool as complete."""
|
|
458
|
+
if tool_id in self._tools:
|
|
459
|
+
tool = self._tools[tool_id]
|
|
460
|
+
tool.end_time = datetime.now()
|
|
461
|
+
tool.state = ToolState.ERROR if error else ToolState.SUCCESS
|
|
462
|
+
tool.result = result
|
|
463
|
+
tool.error = error
|
|
464
|
+
|
|
465
|
+
if tool_id in self._widgets:
|
|
466
|
+
self._widgets[tool_id].update_tool(tool)
|
|
467
|
+
|
|
468
|
+
self._update_header()
|
|
469
|
+
|
|
470
|
+
def _update_header(self) -> None:
|
|
471
|
+
"""Update the header with current stats."""
|
|
472
|
+
header = self.query_one(".tools-header", Static)
|
|
473
|
+
|
|
474
|
+
total = len(self._tools)
|
|
475
|
+
running = sum(1 for t in self._tools.values() if t.state == ToolState.RUNNING)
|
|
476
|
+
success = sum(1 for t in self._tools.values() if t.state == ToolState.SUCCESS)
|
|
477
|
+
errors = sum(1 for t in self._tools.values() if t.state == ToolState.ERROR)
|
|
478
|
+
|
|
479
|
+
text = Text()
|
|
480
|
+
text.append("🔧 ", style="bold #f59e0b")
|
|
481
|
+
text.append("Tool Calls", style="bold #e4e4e7")
|
|
482
|
+
text.append(f" ({total})", style="#6b7280")
|
|
483
|
+
|
|
484
|
+
if running > 0:
|
|
485
|
+
text.append(f" ◐ {running}", style="#fbbf24")
|
|
486
|
+
if success > 0:
|
|
487
|
+
text.append(f" ✓ {success}", style="#22c55e")
|
|
488
|
+
if errors > 0:
|
|
489
|
+
text.append(f" ✗ {errors}", style="#ef4444")
|
|
490
|
+
|
|
491
|
+
header.update(text)
|
|
492
|
+
|
|
493
|
+
def clear(self) -> None:
|
|
494
|
+
"""Clear all tool calls."""
|
|
495
|
+
self._tools.clear()
|
|
496
|
+
|
|
497
|
+
content = self.query_one(".tools-content", Container)
|
|
498
|
+
for widget in self._widgets.values():
|
|
499
|
+
widget.remove()
|
|
500
|
+
self._widgets.clear()
|
|
501
|
+
|
|
502
|
+
self._update_header()
|
|
503
|
+
|
|
504
|
+
def compose(self):
|
|
505
|
+
"""Compose the panel layout."""
|
|
506
|
+
yield Static("", classes="tools-header")
|
|
507
|
+
with Container(classes="tools-content"):
|
|
508
|
+
pass
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
class CompactToolIndicator(Static):
|
|
512
|
+
"""Compact tool call indicator for status bar."""
|
|
513
|
+
|
|
514
|
+
DEFAULT_CSS = """
|
|
515
|
+
CompactToolIndicator {
|
|
516
|
+
width: auto;
|
|
517
|
+
height: 1;
|
|
518
|
+
padding: 0 1;
|
|
519
|
+
}
|
|
520
|
+
"""
|
|
521
|
+
|
|
522
|
+
def __init__(self, **kwargs):
|
|
523
|
+
super().__init__(**kwargs)
|
|
524
|
+
self._running = 0
|
|
525
|
+
self._completed = 0
|
|
526
|
+
self._errors = 0
|
|
527
|
+
|
|
528
|
+
def update_counts(self, running: int, completed: int, errors: int) -> None:
|
|
529
|
+
"""Update the counts."""
|
|
530
|
+
self._running = running
|
|
531
|
+
self._completed = completed
|
|
532
|
+
self._errors = errors
|
|
533
|
+
self.refresh()
|
|
534
|
+
|
|
535
|
+
def render(self) -> Text:
|
|
536
|
+
text = Text()
|
|
537
|
+
|
|
538
|
+
text.append("🔧 ", style="#f59e0b")
|
|
539
|
+
|
|
540
|
+
total = self._running + self._completed + self._errors
|
|
541
|
+
if total == 0:
|
|
542
|
+
text.append("-", style="#52525b")
|
|
543
|
+
else:
|
|
544
|
+
if self._running > 0:
|
|
545
|
+
text.append(f"◐{self._running} ", style="bold #fbbf24")
|
|
546
|
+
if self._completed > 0:
|
|
547
|
+
text.append(f"✓{self._completed} ", style="#22c55e")
|
|
548
|
+
if self._errors > 0:
|
|
549
|
+
text.append(f"✗{self._errors}", style="#ef4444")
|
|
550
|
+
|
|
551
|
+
return text
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
# Helper functions for creating tool data
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def create_file_read_tool(tool_id: str, path: str) -> ToolCallData:
|
|
558
|
+
"""Create a file read tool call."""
|
|
559
|
+
return ToolCallData(
|
|
560
|
+
id=tool_id,
|
|
561
|
+
name="read_file",
|
|
562
|
+
kind=ToolKind.FILE_READ,
|
|
563
|
+
state=ToolState.RUNNING,
|
|
564
|
+
start_time=datetime.now(),
|
|
565
|
+
file_path=path,
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def create_file_write_tool(
|
|
570
|
+
tool_id: str,
|
|
571
|
+
path: str,
|
|
572
|
+
old_content: str = "",
|
|
573
|
+
new_content: str = "",
|
|
574
|
+
) -> ToolCallData:
|
|
575
|
+
"""Create a file write tool call."""
|
|
576
|
+
return ToolCallData(
|
|
577
|
+
id=tool_id,
|
|
578
|
+
name="write_file",
|
|
579
|
+
kind=ToolKind.FILE_WRITE,
|
|
580
|
+
state=ToolState.RUNNING,
|
|
581
|
+
start_time=datetime.now(),
|
|
582
|
+
file_path=path,
|
|
583
|
+
diff=DiffContent(
|
|
584
|
+
path=path,
|
|
585
|
+
old_text=old_content,
|
|
586
|
+
new_text=new_content,
|
|
587
|
+
language=detect_language(path),
|
|
588
|
+
),
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def create_shell_tool(tool_id: str, command: str) -> ToolCallData:
|
|
593
|
+
"""Create a shell command tool call."""
|
|
594
|
+
return ToolCallData(
|
|
595
|
+
id=tool_id,
|
|
596
|
+
name="bash",
|
|
597
|
+
kind=ToolKind.SHELL,
|
|
598
|
+
state=ToolState.RUNNING,
|
|
599
|
+
start_time=datetime.now(),
|
|
600
|
+
command=command,
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def create_search_tool(tool_id: str, pattern: str) -> ToolCallData:
|
|
605
|
+
"""Create a search tool call."""
|
|
606
|
+
return ToolCallData(
|
|
607
|
+
id=tool_id,
|
|
608
|
+
name="grep",
|
|
609
|
+
kind=ToolKind.SEARCH,
|
|
610
|
+
state=ToolState.RUNNING,
|
|
611
|
+
start_time=datetime.now(),
|
|
612
|
+
arguments={"pattern": pattern},
|
|
613
|
+
)
|