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.
Files changed (288) hide show
  1. superqode/__init__.py +33 -0
  2. superqode/acp/__init__.py +23 -0
  3. superqode/acp/client.py +913 -0
  4. superqode/acp/permission_screen.py +457 -0
  5. superqode/acp/types.py +480 -0
  6. superqode/acp_discovery.py +856 -0
  7. superqode/agent/__init__.py +22 -0
  8. superqode/agent/edit_strategies.py +334 -0
  9. superqode/agent/loop.py +892 -0
  10. superqode/agent/qe_report_templates.py +39 -0
  11. superqode/agent/system_prompts.py +353 -0
  12. superqode/agent_output.py +721 -0
  13. superqode/agent_stream.py +953 -0
  14. superqode/agents/__init__.py +59 -0
  15. superqode/agents/acp_registry.py +305 -0
  16. superqode/agents/client.py +249 -0
  17. superqode/agents/data/augmentcode.com.toml +51 -0
  18. superqode/agents/data/cagent.dev.toml +51 -0
  19. superqode/agents/data/claude.com.toml +60 -0
  20. superqode/agents/data/codeassistant.dev.toml +51 -0
  21. superqode/agents/data/codex.openai.com.toml +57 -0
  22. superqode/agents/data/fastagent.ai.toml +66 -0
  23. superqode/agents/data/geminicli.com.toml +77 -0
  24. superqode/agents/data/goose.block.xyz.toml +54 -0
  25. superqode/agents/data/junie.jetbrains.com.toml +56 -0
  26. superqode/agents/data/kimi.moonshot.cn.toml +57 -0
  27. superqode/agents/data/llmlingagent.dev.toml +51 -0
  28. superqode/agents/data/molt.bot.toml +49 -0
  29. superqode/agents/data/opencode.ai.toml +60 -0
  30. superqode/agents/data/stakpak.dev.toml +51 -0
  31. superqode/agents/data/vtcode.dev.toml +51 -0
  32. superqode/agents/discovery.py +266 -0
  33. superqode/agents/messaging.py +160 -0
  34. superqode/agents/persona.py +166 -0
  35. superqode/agents/registry.py +421 -0
  36. superqode/agents/schema.py +72 -0
  37. superqode/agents/unified.py +367 -0
  38. superqode/app/__init__.py +111 -0
  39. superqode/app/constants.py +314 -0
  40. superqode/app/css.py +366 -0
  41. superqode/app/models.py +118 -0
  42. superqode/app/suggester.py +125 -0
  43. superqode/app/widgets.py +1591 -0
  44. superqode/app_enhanced.py +399 -0
  45. superqode/app_main.py +17187 -0
  46. superqode/approval.py +312 -0
  47. superqode/atomic.py +296 -0
  48. superqode/commands/__init__.py +1 -0
  49. superqode/commands/acp.py +965 -0
  50. superqode/commands/agents.py +180 -0
  51. superqode/commands/auth.py +278 -0
  52. superqode/commands/config.py +374 -0
  53. superqode/commands/init.py +826 -0
  54. superqode/commands/providers.py +819 -0
  55. superqode/commands/qe.py +1145 -0
  56. superqode/commands/roles.py +380 -0
  57. superqode/commands/serve.py +172 -0
  58. superqode/commands/suggestions.py +127 -0
  59. superqode/commands/superqe.py +460 -0
  60. superqode/config/__init__.py +51 -0
  61. superqode/config/loader.py +812 -0
  62. superqode/config/schema.py +498 -0
  63. superqode/core/__init__.py +111 -0
  64. superqode/core/roles.py +281 -0
  65. superqode/danger.py +386 -0
  66. superqode/data/superqode-template.yaml +1522 -0
  67. superqode/design_system.py +1080 -0
  68. superqode/dialogs/__init__.py +6 -0
  69. superqode/dialogs/base.py +39 -0
  70. superqode/dialogs/model.py +130 -0
  71. superqode/dialogs/provider.py +870 -0
  72. superqode/diff_view.py +919 -0
  73. superqode/enterprise.py +21 -0
  74. superqode/evaluation/__init__.py +25 -0
  75. superqode/evaluation/adapters.py +93 -0
  76. superqode/evaluation/behaviors.py +89 -0
  77. superqode/evaluation/engine.py +209 -0
  78. superqode/evaluation/scenarios.py +96 -0
  79. superqode/execution/__init__.py +36 -0
  80. superqode/execution/linter.py +538 -0
  81. superqode/execution/modes.py +347 -0
  82. superqode/execution/resolver.py +283 -0
  83. superqode/execution/runner.py +642 -0
  84. superqode/file_explorer.py +811 -0
  85. superqode/file_viewer.py +471 -0
  86. superqode/flash.py +183 -0
  87. superqode/guidance/__init__.py +58 -0
  88. superqode/guidance/config.py +203 -0
  89. superqode/guidance/prompts.py +71 -0
  90. superqode/harness/__init__.py +54 -0
  91. superqode/harness/accelerator.py +291 -0
  92. superqode/harness/config.py +319 -0
  93. superqode/harness/validator.py +147 -0
  94. superqode/history.py +279 -0
  95. superqode/integrations/superopt_runner.py +124 -0
  96. superqode/logging/__init__.py +49 -0
  97. superqode/logging/adapters.py +219 -0
  98. superqode/logging/formatter.py +923 -0
  99. superqode/logging/integration.py +341 -0
  100. superqode/logging/sinks.py +170 -0
  101. superqode/logging/unified_log.py +417 -0
  102. superqode/lsp/__init__.py +26 -0
  103. superqode/lsp/client.py +544 -0
  104. superqode/main.py +1069 -0
  105. superqode/mcp/__init__.py +89 -0
  106. superqode/mcp/auth_storage.py +380 -0
  107. superqode/mcp/client.py +1236 -0
  108. superqode/mcp/config.py +319 -0
  109. superqode/mcp/integration.py +337 -0
  110. superqode/mcp/oauth.py +436 -0
  111. superqode/mcp/oauth_callback.py +385 -0
  112. superqode/mcp/types.py +290 -0
  113. superqode/memory/__init__.py +31 -0
  114. superqode/memory/feedback.py +342 -0
  115. superqode/memory/store.py +522 -0
  116. superqode/notifications.py +369 -0
  117. superqode/optimization/__init__.py +5 -0
  118. superqode/optimization/config.py +33 -0
  119. superqode/permissions/__init__.py +25 -0
  120. superqode/permissions/rules.py +488 -0
  121. superqode/plan.py +323 -0
  122. superqode/providers/__init__.py +33 -0
  123. superqode/providers/gateway/__init__.py +165 -0
  124. superqode/providers/gateway/base.py +228 -0
  125. superqode/providers/gateway/litellm_gateway.py +1170 -0
  126. superqode/providers/gateway/openresponses_gateway.py +436 -0
  127. superqode/providers/health.py +297 -0
  128. superqode/providers/huggingface/__init__.py +74 -0
  129. superqode/providers/huggingface/downloader.py +472 -0
  130. superqode/providers/huggingface/endpoints.py +442 -0
  131. superqode/providers/huggingface/hub.py +531 -0
  132. superqode/providers/huggingface/inference.py +394 -0
  133. superqode/providers/huggingface/transformers_runner.py +516 -0
  134. superqode/providers/local/__init__.py +100 -0
  135. superqode/providers/local/base.py +438 -0
  136. superqode/providers/local/discovery.py +418 -0
  137. superqode/providers/local/lmstudio.py +256 -0
  138. superqode/providers/local/mlx.py +457 -0
  139. superqode/providers/local/ollama.py +486 -0
  140. superqode/providers/local/sglang.py +268 -0
  141. superqode/providers/local/tgi.py +260 -0
  142. superqode/providers/local/tool_support.py +477 -0
  143. superqode/providers/local/vllm.py +258 -0
  144. superqode/providers/manager.py +1338 -0
  145. superqode/providers/models.py +1016 -0
  146. superqode/providers/models_dev.py +578 -0
  147. superqode/providers/openresponses/__init__.py +87 -0
  148. superqode/providers/openresponses/converters/__init__.py +17 -0
  149. superqode/providers/openresponses/converters/messages.py +343 -0
  150. superqode/providers/openresponses/converters/tools.py +268 -0
  151. superqode/providers/openresponses/schema/__init__.py +56 -0
  152. superqode/providers/openresponses/schema/models.py +585 -0
  153. superqode/providers/openresponses/streaming/__init__.py +5 -0
  154. superqode/providers/openresponses/streaming/parser.py +338 -0
  155. superqode/providers/openresponses/tools/__init__.py +21 -0
  156. superqode/providers/openresponses/tools/apply_patch.py +352 -0
  157. superqode/providers/openresponses/tools/code_interpreter.py +290 -0
  158. superqode/providers/openresponses/tools/file_search.py +333 -0
  159. superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
  160. superqode/providers/registry.py +716 -0
  161. superqode/providers/usage.py +332 -0
  162. superqode/pure_mode.py +384 -0
  163. superqode/qr/__init__.py +23 -0
  164. superqode/qr/dashboard.py +781 -0
  165. superqode/qr/generator.py +1018 -0
  166. superqode/qr/templates.py +135 -0
  167. superqode/safety/__init__.py +41 -0
  168. superqode/safety/sandbox.py +413 -0
  169. superqode/safety/warnings.py +256 -0
  170. superqode/server/__init__.py +33 -0
  171. superqode/server/lsp_server.py +775 -0
  172. superqode/server/web.py +250 -0
  173. superqode/session/__init__.py +25 -0
  174. superqode/session/persistence.py +580 -0
  175. superqode/session/sharing.py +477 -0
  176. superqode/session.py +475 -0
  177. superqode/sidebar.py +2991 -0
  178. superqode/stream_view.py +648 -0
  179. superqode/styles/__init__.py +3 -0
  180. superqode/superqe/__init__.py +184 -0
  181. superqode/superqe/acp_runner.py +1064 -0
  182. superqode/superqe/constitution/__init__.py +62 -0
  183. superqode/superqe/constitution/evaluator.py +308 -0
  184. superqode/superqe/constitution/loader.py +432 -0
  185. superqode/superqe/constitution/schema.py +250 -0
  186. superqode/superqe/events.py +591 -0
  187. superqode/superqe/frameworks/__init__.py +65 -0
  188. superqode/superqe/frameworks/base.py +234 -0
  189. superqode/superqe/frameworks/e2e.py +263 -0
  190. superqode/superqe/frameworks/executor.py +237 -0
  191. superqode/superqe/frameworks/javascript.py +409 -0
  192. superqode/superqe/frameworks/python.py +373 -0
  193. superqode/superqe/frameworks/registry.py +92 -0
  194. superqode/superqe/mcp_tools/__init__.py +47 -0
  195. superqode/superqe/mcp_tools/core_tools.py +418 -0
  196. superqode/superqe/mcp_tools/registry.py +230 -0
  197. superqode/superqe/mcp_tools/testing_tools.py +167 -0
  198. superqode/superqe/noise.py +89 -0
  199. superqode/superqe/orchestrator.py +778 -0
  200. superqode/superqe/roles.py +609 -0
  201. superqode/superqe/session.py +713 -0
  202. superqode/superqe/skills/__init__.py +57 -0
  203. superqode/superqe/skills/base.py +106 -0
  204. superqode/superqe/skills/core_skills.py +899 -0
  205. superqode/superqe/skills/registry.py +90 -0
  206. superqode/superqe/verifier.py +101 -0
  207. superqode/superqe_cli.py +76 -0
  208. superqode/tool_call.py +358 -0
  209. superqode/tools/__init__.py +93 -0
  210. superqode/tools/agent_tools.py +496 -0
  211. superqode/tools/base.py +324 -0
  212. superqode/tools/batch_tool.py +133 -0
  213. superqode/tools/diagnostics.py +311 -0
  214. superqode/tools/edit_tools.py +653 -0
  215. superqode/tools/enhanced_base.py +515 -0
  216. superqode/tools/file_tools.py +269 -0
  217. superqode/tools/file_tracking.py +45 -0
  218. superqode/tools/lsp_tools.py +610 -0
  219. superqode/tools/network_tools.py +350 -0
  220. superqode/tools/permissions.py +400 -0
  221. superqode/tools/question_tool.py +324 -0
  222. superqode/tools/search_tools.py +598 -0
  223. superqode/tools/shell_tools.py +259 -0
  224. superqode/tools/todo_tools.py +121 -0
  225. superqode/tools/validation.py +80 -0
  226. superqode/tools/web_tools.py +639 -0
  227. superqode/tui.py +1152 -0
  228. superqode/tui_integration.py +875 -0
  229. superqode/tui_widgets/__init__.py +27 -0
  230. superqode/tui_widgets/widgets/__init__.py +18 -0
  231. superqode/tui_widgets/widgets/progress.py +185 -0
  232. superqode/tui_widgets/widgets/tool_display.py +188 -0
  233. superqode/undo_manager.py +574 -0
  234. superqode/utils/__init__.py +5 -0
  235. superqode/utils/error_handling.py +323 -0
  236. superqode/utils/fuzzy.py +257 -0
  237. superqode/widgets/__init__.py +477 -0
  238. superqode/widgets/agent_collab.py +390 -0
  239. superqode/widgets/agent_store.py +936 -0
  240. superqode/widgets/agent_switcher.py +395 -0
  241. superqode/widgets/animation_manager.py +284 -0
  242. superqode/widgets/code_context.py +356 -0
  243. superqode/widgets/command_palette.py +412 -0
  244. superqode/widgets/connection_status.py +537 -0
  245. superqode/widgets/conversation_history.py +470 -0
  246. superqode/widgets/diff_indicator.py +155 -0
  247. superqode/widgets/enhanced_status_bar.py +385 -0
  248. superqode/widgets/enhanced_toast.py +476 -0
  249. superqode/widgets/file_browser.py +809 -0
  250. superqode/widgets/file_reference.py +585 -0
  251. superqode/widgets/issue_timeline.py +340 -0
  252. superqode/widgets/leader_key.py +264 -0
  253. superqode/widgets/mode_switcher.py +445 -0
  254. superqode/widgets/model_picker.py +234 -0
  255. superqode/widgets/permission_preview.py +1205 -0
  256. superqode/widgets/prompt.py +358 -0
  257. superqode/widgets/provider_connect.py +725 -0
  258. superqode/widgets/pty_shell.py +587 -0
  259. superqode/widgets/qe_dashboard.py +321 -0
  260. superqode/widgets/resizable_sidebar.py +377 -0
  261. superqode/widgets/response_changes.py +218 -0
  262. superqode/widgets/response_display.py +528 -0
  263. superqode/widgets/rich_tool_display.py +613 -0
  264. superqode/widgets/sidebar_panels.py +1180 -0
  265. superqode/widgets/slash_complete.py +356 -0
  266. superqode/widgets/split_view.py +612 -0
  267. superqode/widgets/status_bar.py +273 -0
  268. superqode/widgets/superqode_display.py +786 -0
  269. superqode/widgets/thinking_display.py +815 -0
  270. superqode/widgets/throbber.py +87 -0
  271. superqode/widgets/toast.py +206 -0
  272. superqode/widgets/unified_output.py +1073 -0
  273. superqode/workspace/__init__.py +75 -0
  274. superqode/workspace/artifacts.py +472 -0
  275. superqode/workspace/coordinator.py +353 -0
  276. superqode/workspace/diff_tracker.py +429 -0
  277. superqode/workspace/git_guard.py +373 -0
  278. superqode/workspace/git_snapshot.py +526 -0
  279. superqode/workspace/manager.py +750 -0
  280. superqode/workspace/snapshot.py +357 -0
  281. superqode/workspace/watcher.py +535 -0
  282. superqode/workspace/worktree.py +440 -0
  283. superqode-0.1.5.dist-info/METADATA +204 -0
  284. superqode-0.1.5.dist-info/RECORD +288 -0
  285. superqode-0.1.5.dist-info/WHEEL +5 -0
  286. superqode-0.1.5.dist-info/entry_points.txt +3 -0
  287. superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
  288. 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
+ )