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,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
+ ]