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,470 @@
1
+ """
2
+ Conversation History Widget - Navigate and Search Past Messages.
3
+
4
+ Provides conversation history management:
5
+ - Message timeline navigation
6
+ - Search through history
7
+ - Jump to specific messages
8
+ - Copy/export messages
9
+ - Session summaries
10
+
11
+ Makes it easy to reference and navigate long conversations.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import asyncio
17
+ from dataclasses import dataclass, field
18
+ from datetime import datetime
19
+ from enum import Enum
20
+ from typing import Any, Callable, Dict, List, Optional, Tuple
21
+
22
+ from rich.console import RenderableType
23
+ from rich.panel import Panel
24
+ from rich.text import Text
25
+ from rich.box import ROUNDED
26
+ from textual.reactive import reactive
27
+ from textual.widgets import Static, Input
28
+ from textual.containers import Container, Vertical, Horizontal, ScrollableContainer
29
+ from textual import events
30
+
31
+
32
+ class MessageType(Enum):
33
+ """Type of conversation message."""
34
+
35
+ USER = "user"
36
+ ASSISTANT = "assistant"
37
+ SYSTEM = "system"
38
+ TOOL = "tool"
39
+ ERROR = "error"
40
+
41
+
42
+ @dataclass
43
+ class HistoryMessage:
44
+ """A message in the conversation history."""
45
+
46
+ id: str
47
+ message_type: MessageType
48
+ content: str
49
+ timestamp: datetime = field(default_factory=datetime.now)
50
+
51
+ # Optional metadata
52
+ agent_name: str = ""
53
+ model_name: str = ""
54
+ tool_name: str = ""
55
+ token_count: int = 0
56
+ duration_ms: float = 0
57
+
58
+ # File references
59
+ files_mentioned: List[str] = field(default_factory=list)
60
+
61
+ @property
62
+ def preview(self) -> str:
63
+ """Get a short preview of the message."""
64
+ text = self.content.strip()
65
+ if len(text) > 80:
66
+ return text[:77] + "..."
67
+ return text
68
+
69
+ @property
70
+ def time_str(self) -> str:
71
+ """Get formatted timestamp."""
72
+ return self.timestamp.strftime("%H:%M")
73
+
74
+
75
+ MESSAGE_STYLES = {
76
+ MessageType.USER: {"icon": "👤", "color": "#3b82f6", "label": "You"},
77
+ MessageType.ASSISTANT: {"icon": "🤖", "color": "#a855f7", "label": "Agent"},
78
+ MessageType.SYSTEM: {"icon": "⚙️", "color": "#6b7280", "label": "System"},
79
+ MessageType.TOOL: {"icon": "🔧", "color": "#f59e0b", "label": "Tool"},
80
+ MessageType.ERROR: {"icon": "❌", "color": "#ef4444", "label": "Error"},
81
+ }
82
+
83
+
84
+ class MessagePreview(Static):
85
+ """Preview widget for a single message."""
86
+
87
+ DEFAULT_CSS = """
88
+ MessagePreview {
89
+ height: 2;
90
+ padding: 0 1;
91
+ margin: 0;
92
+ }
93
+
94
+ MessagePreview:hover {
95
+ background: #1a1a1a;
96
+ }
97
+
98
+ MessagePreview.selected {
99
+ background: #1a1a2a;
100
+ border-left: tall #3b82f6;
101
+ }
102
+ """
103
+
104
+ selected: reactive[bool] = reactive(False)
105
+
106
+ def __init__(
107
+ self,
108
+ message: HistoryMessage,
109
+ on_select: Optional[Callable[[], None]] = None,
110
+ **kwargs,
111
+ ):
112
+ super().__init__(**kwargs)
113
+ self.message = message
114
+ self._on_select = on_select
115
+
116
+ def on_click(self, event: events.Click) -> None:
117
+ """Handle click."""
118
+ if self._on_select:
119
+ self._on_select()
120
+
121
+ def watch_selected(self, selected: bool) -> None:
122
+ """React to selection changes."""
123
+ if selected:
124
+ self.add_class("selected")
125
+ else:
126
+ self.remove_class("selected")
127
+
128
+ def render(self) -> Text:
129
+ style = MESSAGE_STYLES.get(self.message.message_type, MESSAGE_STYLES[MessageType.ASSISTANT])
130
+
131
+ result = Text()
132
+
133
+ # Time and icon
134
+ result.append(f"{self.message.time_str} ", style="#52525b")
135
+ result.append(f"{style['icon']} ", style=style["color"])
136
+
137
+ # Sender
138
+ label = self.message.agent_name or style["label"]
139
+ result.append(f"{label}: ", style=f"bold {style['color']}")
140
+
141
+ # Preview
142
+ result.append(self.message.preview, style="#a1a1aa")
143
+
144
+ return result
145
+
146
+
147
+ class ConversationTimeline(Container):
148
+ """
149
+ Timeline view of conversation history.
150
+
151
+ Shows messages in chronological order with quick navigation.
152
+ """
153
+
154
+ DEFAULT_CSS = """
155
+ ConversationTimeline {
156
+ height: 100%;
157
+ border: solid #27272a;
158
+ background: #0a0a0a;
159
+ }
160
+
161
+ ConversationTimeline .timeline-header {
162
+ height: 3;
163
+ border-bottom: solid #27272a;
164
+ padding: 1;
165
+ }
166
+
167
+ ConversationTimeline .timeline-search {
168
+ height: 3;
169
+ border-bottom: solid #27272a;
170
+ padding: 0 1;
171
+ }
172
+
173
+ ConversationTimeline .timeline-content {
174
+ height: 1fr;
175
+ overflow-y: auto;
176
+ }
177
+
178
+ ConversationTimeline .timeline-footer {
179
+ height: 2;
180
+ border-top: solid #27272a;
181
+ padding: 0 1;
182
+ }
183
+ """
184
+
185
+ selected_index: reactive[int] = reactive(-1)
186
+ search_query: reactive[str] = reactive("")
187
+
188
+ def __init__(
189
+ self,
190
+ on_message_select: Optional[Callable[[HistoryMessage], None]] = None,
191
+ **kwargs,
192
+ ):
193
+ super().__init__(**kwargs)
194
+ self._messages: List[HistoryMessage] = []
195
+ self._filtered: List[HistoryMessage] = []
196
+ self._on_message_select = on_message_select
197
+
198
+ def add_message(self, message: HistoryMessage) -> None:
199
+ """Add a message to history."""
200
+ self._messages.append(message)
201
+ self._apply_filter()
202
+ self._update_display()
203
+
204
+ def set_messages(self, messages: List[HistoryMessage]) -> None:
205
+ """Set all messages."""
206
+ self._messages = list(messages)
207
+ self._apply_filter()
208
+ self._update_display()
209
+
210
+ def clear(self) -> None:
211
+ """Clear history."""
212
+ self._messages.clear()
213
+ self._filtered.clear()
214
+ self.selected_index = -1
215
+ self._update_display()
216
+
217
+ def _apply_filter(self) -> None:
218
+ """Apply search filter."""
219
+ if not self.search_query:
220
+ self._filtered = list(self._messages)
221
+ else:
222
+ query = self.search_query.lower()
223
+ self._filtered = [
224
+ m
225
+ for m in self._messages
226
+ if query in m.content.lower() or query in m.agent_name.lower()
227
+ ]
228
+
229
+ def watch_search_query(self, query: str) -> None:
230
+ """React to search query changes."""
231
+ self._apply_filter()
232
+ self._update_display()
233
+
234
+ def watch_selected_index(self, index: int) -> None:
235
+ """React to selection changes."""
236
+ self._update_selection()
237
+
238
+ if 0 <= index < len(self._filtered) and self._on_message_select:
239
+ self._on_message_select(self._filtered[index])
240
+
241
+ def _update_selection(self) -> None:
242
+ """Update selection state."""
243
+ try:
244
+ content = self.query_one(".timeline-content", ScrollableContainer)
245
+ for i, widget in enumerate(content.children):
246
+ if isinstance(widget, MessagePreview):
247
+ widget.selected = i == self.selected_index
248
+ except Exception:
249
+ pass
250
+
251
+ def _update_display(self) -> None:
252
+ """Update the display."""
253
+ try:
254
+ header = self.query_one(".timeline-header", Static)
255
+ content = self.query_one(".timeline-content", ScrollableContainer)
256
+ footer = self.query_one(".timeline-footer", Static)
257
+ except Exception:
258
+ return
259
+
260
+ # Header
261
+ header_text = Text()
262
+ header_text.append("📜 ", style="bold #3b82f6")
263
+ header_text.append("Conversation History", style="bold #e4e4e7")
264
+ header_text.append(f" ({len(self._messages)} messages)", style="#6b7280")
265
+ header.update(header_text)
266
+
267
+ # Content - message previews
268
+ content.remove_children()
269
+ for i, message in enumerate(self._filtered[-50:]): # Show last 50
270
+ preview = MessagePreview(
271
+ message,
272
+ on_select=lambda idx=i: self._select_message(idx),
273
+ )
274
+ if i == self.selected_index:
275
+ preview.selected = True
276
+ content.mount(preview)
277
+
278
+ # Footer
279
+ footer_text = Text()
280
+ footer_text.append("[↑/↓] Navigate ", style="#52525b")
281
+ footer_text.append("[Enter] View ", style="#52525b")
282
+ footer_text.append("[/] Search", style="#52525b")
283
+ footer.update(footer_text)
284
+
285
+ def _select_message(self, index: int) -> None:
286
+ """Select a message by index."""
287
+ self.selected_index = index
288
+
289
+ def on_key(self, event: events.Key) -> None:
290
+ """Handle keyboard navigation."""
291
+ if event.key == "up":
292
+ self.selected_index = max(0, self.selected_index - 1)
293
+ event.prevent_default()
294
+ elif event.key == "down":
295
+ self.selected_index = min(len(self._filtered) - 1, self.selected_index + 1)
296
+ event.prevent_default()
297
+ elif event.key == "home":
298
+ self.selected_index = 0
299
+ event.prevent_default()
300
+ elif event.key == "end":
301
+ self.selected_index = len(self._filtered) - 1
302
+ event.prevent_default()
303
+
304
+ def compose(self):
305
+ """Compose the timeline."""
306
+ yield Static("", classes="timeline-header")
307
+ yield Input(placeholder="Search messages...", classes="timeline-search")
308
+ yield ScrollableContainer(classes="timeline-content")
309
+ yield Static("", classes="timeline-footer")
310
+
311
+ def on_mount(self) -> None:
312
+ """Initialize."""
313
+ self._update_display()
314
+
315
+ def on_input_changed(self, event: Input.Changed) -> None:
316
+ """Handle search input."""
317
+ self.search_query = event.value
318
+
319
+
320
+ class MessageDetail(Container):
321
+ """
322
+ Detailed view of a single message.
323
+
324
+ Shows full message content with metadata.
325
+ """
326
+
327
+ DEFAULT_CSS = """
328
+ MessageDetail {
329
+ height: auto;
330
+ border: solid #27272a;
331
+ background: #0a0a0a;
332
+ padding: 1;
333
+ }
334
+
335
+ MessageDetail .detail-header {
336
+ height: 2;
337
+ margin-bottom: 1;
338
+ }
339
+
340
+ MessageDetail .detail-content {
341
+ height: auto;
342
+ padding: 0 1;
343
+ }
344
+
345
+ MessageDetail .detail-meta {
346
+ height: auto;
347
+ margin-top: 1;
348
+ border-top: solid #27272a;
349
+ padding-top: 1;
350
+ }
351
+ """
352
+
353
+ def __init__(self, **kwargs):
354
+ super().__init__(**kwargs)
355
+ self._message: Optional[HistoryMessage] = None
356
+
357
+ def set_message(self, message: Optional[HistoryMessage]) -> None:
358
+ """Set the message to display."""
359
+ self._message = message
360
+ self._update_display()
361
+
362
+ def _update_display(self) -> None:
363
+ """Update the display."""
364
+ try:
365
+ header = self.query_one(".detail-header", Static)
366
+ content = self.query_one(".detail-content", Static)
367
+ meta = self.query_one(".detail-meta", Static)
368
+ except Exception:
369
+ return
370
+
371
+ if not self._message:
372
+ header.update(Text("No message selected", style="#52525b"))
373
+ content.update("")
374
+ meta.update("")
375
+ return
376
+
377
+ msg = self._message
378
+ style = MESSAGE_STYLES.get(msg.message_type, MESSAGE_STYLES[MessageType.ASSISTANT])
379
+
380
+ # Header
381
+ header_text = Text()
382
+ header_text.append(f"{style['icon']} ", style=style["color"])
383
+
384
+ label = msg.agent_name or style["label"]
385
+ header_text.append(label, style=f"bold {style['color']}")
386
+
387
+ if msg.model_name:
388
+ header_text.append(f" ({msg.model_name})", style="#6b7280")
389
+
390
+ header_text.append(f"\n{msg.timestamp.strftime('%Y-%m-%d %H:%M:%S')}", style="#52525b")
391
+
392
+ header.update(header_text)
393
+
394
+ # Content
395
+ content.update(Text(msg.content, style="#e4e4e7"))
396
+
397
+ # Metadata
398
+ meta_text = Text()
399
+
400
+ if msg.token_count:
401
+ meta_text.append(f"📊 {msg.token_count} tokens", style="#6b7280")
402
+
403
+ if msg.duration_ms:
404
+ if meta_text:
405
+ meta_text.append(" │ ", style="#27272a")
406
+ meta_text.append(f"⏱️ {msg.duration_ms:.0f}ms", style="#6b7280")
407
+
408
+ if msg.files_mentioned:
409
+ if meta_text:
410
+ meta_text.append("\n")
411
+ meta_text.append(f"📁 Files: {', '.join(msg.files_mentioned[:3])}", style="#6b7280")
412
+ if len(msg.files_mentioned) > 3:
413
+ meta_text.append(f" +{len(msg.files_mentioned) - 3} more", style="#52525b")
414
+
415
+ meta.update(meta_text)
416
+
417
+ def compose(self):
418
+ """Compose the detail view."""
419
+ yield Static("", classes="detail-header")
420
+ yield Static("", classes="detail-content")
421
+ yield Static("", classes="detail-meta")
422
+
423
+
424
+ class ConversationNavigator(Static):
425
+ """
426
+ Compact conversation navigator for quick jumping.
427
+
428
+ Shows message count and allows jumping to messages.
429
+ """
430
+
431
+ DEFAULT_CSS = """
432
+ ConversationNavigator {
433
+ width: auto;
434
+ height: 1;
435
+ padding: 0 1;
436
+ }
437
+ """
438
+
439
+ def __init__(
440
+ self,
441
+ on_open_history: Optional[Callable[[], None]] = None,
442
+ **kwargs,
443
+ ):
444
+ super().__init__(**kwargs)
445
+ self._message_count = 0
446
+ self._current_index = 0
447
+ self._on_open_history = on_open_history
448
+
449
+ def set_counts(self, total: int, current: int = 0) -> None:
450
+ """Set message counts."""
451
+ self._message_count = total
452
+ self._current_index = current
453
+ self.refresh()
454
+
455
+ def on_click(self, event: events.Click) -> None:
456
+ """Handle click to open history."""
457
+ if self._on_open_history:
458
+ self._on_open_history()
459
+
460
+ def render(self) -> Text:
461
+ text = Text()
462
+
463
+ text.append("📜 ", style="#3b82f6")
464
+
465
+ if self._message_count == 0:
466
+ text.append("No messages", style="#52525b")
467
+ else:
468
+ text.append(f"{self._current_index + 1}/{self._message_count}", style="#a1a1aa")
469
+
470
+ return text
@@ -0,0 +1,155 @@
1
+ """
2
+ Diff Indicator Widget - Compact visual representation of file changes.
3
+
4
+ DiffChanges component, adapted for SuperQode's style.
5
+ Shows colored bars/indicators representing additions/deletions ratio.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Optional
11
+ from rich.text import Text
12
+
13
+
14
+ # SuperQode colors
15
+ COLORS = {
16
+ "addition": "#22c55e",
17
+ "deletion": "#ef4444",
18
+ "neutral": "#52525b",
19
+ "text_dim": "#71717a",
20
+ "text_muted": "#a1a1aa",
21
+ }
22
+
23
+
24
+ def render_diff_indicator(
25
+ additions: int,
26
+ deletions: int,
27
+ variant: str = "bars",
28
+ max_bars: int = 5,
29
+ ) -> Text:
30
+ """
31
+ Render a compact diff indicator showing additions/deletions.
32
+
33
+ Args:
34
+ additions: Number of lines added
35
+ deletions: Number of lines deleted
36
+ variant: "bars" for visual bars, "text" for +X -Y text
37
+ max_bars: Maximum number of bars to show (default 5)
38
+
39
+ Returns:
40
+ Rich Text object with the indicator
41
+ """
42
+ total = additions + deletions
43
+
44
+ if variant == "text":
45
+ # Simple text format: +X -Y
46
+ text = Text()
47
+ if additions > 0:
48
+ text.append(f"+{additions}", style=f"bold {COLORS['addition']}")
49
+ if deletions > 0:
50
+ if additions > 0:
51
+ text.append(" / ", style=COLORS["text_dim"])
52
+ text.append(f"-{deletions}", style=f"bold {COLORS['deletion']}")
53
+ if total == 0:
54
+ text.append("0", style=COLORS["text_muted"])
55
+ return text
56
+
57
+ # Bars variant - visual representation
58
+ if total == 0:
59
+ # Show all neutral bars
60
+ bars = Text()
61
+ for _ in range(max_bars):
62
+ bars.append("█", style=COLORS["neutral"])
63
+ return bars
64
+
65
+ # Calculate bar distribution
66
+ if total < max_bars:
67
+ # Small changes - show 1 bar each if present
68
+ added_bars = 1 if additions > 0 else 0
69
+ deleted_bars = 1 if deletions > 0 else 0
70
+ neutral_bars = max_bars - added_bars - deleted_bars
71
+ else:
72
+ # Larger changes - proportional distribution
73
+ percent_added = additions / total if total > 0 else 0
74
+ percent_deleted = deletions / total if total > 0 else 0
75
+
76
+ # Reserve at least 1 bar for each if present, but cap based on magnitude
77
+ BLOCKS_FOR_COLORS = max_bars - 1 if total < 20 else max_bars
78
+
79
+ added_raw = percent_added * BLOCKS_FOR_COLORS
80
+ deleted_raw = percent_deleted * BLOCKS_FOR_COLORS
81
+
82
+ added_bars = max(1, round(added_raw)) if additions > 0 else 0
83
+ deleted_bars = max(1, round(deleted_raw)) if deletions > 0 else 0
84
+
85
+ # Cap based on actual magnitude
86
+ if additions > 0 and additions <= 5:
87
+ added_bars = min(added_bars, 1)
88
+ elif additions > 5 and additions <= 10:
89
+ added_bars = min(added_bars, 2)
90
+
91
+ if deletions > 0 and deletions <= 5:
92
+ deleted_bars = min(deleted_bars, 1)
93
+ elif deletions > 5 and deletions <= 10:
94
+ deleted_bars = min(deleted_bars, 2)
95
+
96
+ # Ensure we don't exceed max_bars
97
+ total_allocated = added_bars + deleted_bars
98
+ if total_allocated > BLOCKS_FOR_COLORS:
99
+ if added_raw > deleted_raw:
100
+ added_bars = BLOCKS_FOR_COLORS - deleted_bars
101
+ else:
102
+ deleted_bars = BLOCKS_FOR_COLORS - added_bars
103
+
104
+ neutral_bars = max(0, max_bars - added_bars - deleted_bars)
105
+
106
+ # Render bars
107
+ bars = Text()
108
+ for _ in range(added_bars):
109
+ bars.append("█", style=COLORS["addition"])
110
+ for _ in range(deleted_bars):
111
+ bars.append("█", style=COLORS["deletion"])
112
+ for _ in range(neutral_bars):
113
+ bars.append("█", style=COLORS["neutral"])
114
+
115
+ return bars
116
+
117
+
118
+ def render_diff_indicator_with_text(
119
+ additions: int,
120
+ deletions: int,
121
+ show_bars: bool = True,
122
+ show_text: bool = True,
123
+ ) -> Text:
124
+ """
125
+ Render diff indicator with both bars and text.
126
+
127
+ Args:
128
+ additions: Number of lines added
129
+ deletions: Number of lines deleted
130
+ show_bars: Whether to show visual bars
131
+ show_text: Whether to show +X -Y text
132
+
133
+ Returns:
134
+ Rich Text object with both bars and text
135
+ """
136
+ result = Text()
137
+
138
+ if show_bars:
139
+ bars = render_diff_indicator(additions, deletions, variant="bars")
140
+ result.append(bars)
141
+ if show_text:
142
+ result.append(" ", style="")
143
+
144
+ if show_text:
145
+ text = render_diff_indicator(additions, deletions, variant="text")
146
+ result.append(text)
147
+
148
+ return result
149
+
150
+
151
+ __all__ = [
152
+ "render_diff_indicator",
153
+ "render_diff_indicator_with_text",
154
+ "COLORS",
155
+ ]