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,1591 @@
1
+ """
2
+ SuperQode App Widgets - All UI widget classes.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import math
8
+ import random
9
+ from time import monotonic
10
+ from typing import Any
11
+
12
+ from textual.widgets import Static, RichLog
13
+ from textual.reactive import reactive
14
+ from rich.text import Text
15
+ from rich.panel import Panel
16
+ from rich.markdown import Markdown
17
+ from rich.console import Group
18
+ from rich.box import ROUNDED, HEAVY
19
+
20
+ from .constants import (
21
+ ASCII_LOGO,
22
+ TAGLINE_PART1,
23
+ GRADIENT,
24
+ RAINBOW,
25
+ THEME,
26
+ ICONS,
27
+ AGENT_COLORS,
28
+ AGENT_ICONS,
29
+ )
30
+
31
+
32
+ class GradientLogo(Static):
33
+ """ASCII logo with purple→pink→orange gradient - BIG display."""
34
+
35
+ def render(self) -> Text:
36
+ # Split and filter empty lines, but preserve leading whitespace
37
+ lines = [line for line in ASCII_LOGO.split("\n") if line.strip()]
38
+ result = Text()
39
+
40
+ for i, line in enumerate(lines):
41
+ color = GRADIENT[i % len(GRADIENT)]
42
+ result.append(line, style=f"bold {color}")
43
+ if i < len(lines) - 1:
44
+ result.append("\n")
45
+
46
+ return result
47
+
48
+
49
+ class ColorfulStatusBar(Static):
50
+ """Colorful SuperQode status bar - always visible at top with BYOK status."""
51
+
52
+ # BYOK status properties
53
+ byok_provider: reactive[str] = reactive("")
54
+ byok_model: reactive[str] = reactive("")
55
+ byok_tokens: reactive[int] = reactive(0)
56
+ byok_cost: reactive[float] = reactive(0.0)
57
+
58
+ def render(self) -> Text:
59
+ result = Text()
60
+
61
+ # Logo part - with gradient colors
62
+ super_colors = ["#a855f7", "#b366f9", "#c177fb", "#cf88fd", "#dd99ff"]
63
+ for i, char in enumerate("Super"):
64
+ color = super_colors[i % len(super_colors)]
65
+ result.append(char, style=f"bold {color}")
66
+ qode_colors = ["#ec4899", "#f472b6", "#f97316", "#fb923c"]
67
+ for i, char in enumerate("Qode"):
68
+ color = qode_colors[i % len(qode_colors)]
69
+ result.append(char, style=f"bold {color}")
70
+ result.append(" ✨", style="bold #fbbf24")
71
+ result.append(" ", style="")
72
+ # "Multi-Agentic" in normal text color (no gradient)
73
+ result.append("Multi-Agentic", style="")
74
+ result.append(" Orchestration of ", style=THEME["muted"])
75
+ # "Coding Agents" in normal text color (no gradient)
76
+ result.append("Coding Agents", style="")
77
+
78
+ # BYOK status (if connected)
79
+ if self.byok_provider:
80
+ result.append(" │ ", style="#3f3f46")
81
+ result.append(f"{self.byok_provider}", style="bold #10b981")
82
+ if self.byok_model:
83
+ # Show shortened model name
84
+ model_short = (
85
+ self.byok_model.split("-")[0]
86
+ if "-" in self.byok_model
87
+ else self.byok_model[:12]
88
+ )
89
+ result.append(f"/{model_short}", style="#a1a1aa")
90
+
91
+ # Show usage
92
+ if self.byok_tokens > 0:
93
+ result.append(" ", style="")
94
+ if self.byok_tokens >= 1000:
95
+ result.append(f"{self.byok_tokens // 1000}K", style="#06b6d4")
96
+ else:
97
+ result.append(f"{self.byok_tokens}", style="#06b6d4")
98
+ result.append(" tok", style="#52525b")
99
+
100
+ # Show cost
101
+ if self.byok_cost > 0:
102
+ result.append(" ", style="")
103
+ if self.byok_cost >= 0.01:
104
+ result.append(f"${self.byok_cost:.2f}", style="#fbbf24")
105
+ else:
106
+ result.append(f"${self.byok_cost:.3f}", style="#fbbf24")
107
+
108
+ return result
109
+
110
+ def update_byok_status(
111
+ self, provider: str = "", model: str = "", tokens: int = 0, cost: float = 0.0
112
+ ):
113
+ """Update BYOK status display."""
114
+ self.byok_provider = provider
115
+ self.byok_model = model
116
+ self.byok_tokens = tokens
117
+ self.byok_cost = cost
118
+
119
+
120
+ class GradientTagline(Static):
121
+ """Tagline with gradient colors for visual impact."""
122
+
123
+ PART1_GRADIENT = ["#06b6d4", "#0ea5e9", "#3b82f6", "#6366f1", "#8b5cf6", "#a855f7"]
124
+ PART2_GRADIENT = ["#fbbf24", "#f59e0b", "#f97316", "#ef4444", "#ec4899"]
125
+
126
+ def render(self) -> Text:
127
+ result = Text()
128
+ result.append("🚀 ", style="bold #06b6d4")
129
+
130
+ part1 = TAGLINE_PART1
131
+ for i, char in enumerate(part1):
132
+ color_idx = int(i / len(part1) * len(self.PART1_GRADIENT))
133
+ color_idx = min(color_idx, len(self.PART1_GRADIENT) - 1)
134
+ color = self.PART1_GRADIENT[color_idx]
135
+ result.append(char, style=f"bold {color}")
136
+
137
+ result.append(" â€ĸ ", style="bold #71717a")
138
+
139
+ part2 = "Automate Your SDLC"
140
+ for i, char in enumerate(part2):
141
+ color_idx = int(i / len(part2) * len(self.PART2_GRADIENT))
142
+ color_idx = min(color_idx, len(self.PART2_GRADIENT) - 1)
143
+ color = self.PART2_GRADIENT[color_idx]
144
+ result.append(char, style=f"bold {color}")
145
+
146
+ result.append(" ✨", style="bold #fbbf24")
147
+
148
+ return result
149
+
150
+
151
+ class PulseWaveBar(Static):
152
+ """Animated pulse wave bar - unique SuperQode style."""
153
+
154
+ frame = reactive(0)
155
+ WAVE_CHARS = "▁▂▃▄▅▆▇█▇▆▅▄▃▂▁"
156
+ PULSE_COLORS = [
157
+ "#7c3aed",
158
+ "#8b5cf6",
159
+ "#a855f7",
160
+ "#c026d3",
161
+ "#d946ef",
162
+ "#ec4899",
163
+ "#f472b6",
164
+ "#fbbf24",
165
+ ]
166
+
167
+ def on_mount(self):
168
+ self.auto_refresh = 1 / 20
169
+
170
+ def render(self) -> Text:
171
+ width = self.size.width or 80
172
+ t = monotonic()
173
+ result = Text()
174
+ wave_len = len(self.WAVE_CHARS)
175
+
176
+ for i in range(width):
177
+ wave_primary = math.sin(t * 2 + i * 0.15) * 0.5
178
+ wave_secondary = math.sin(t * 4 + i * 0.25 + math.pi / 3) * 0.3
179
+ wave_tertiary = math.sin(t * 1.5 + i * 0.08 + math.pi / 2) * 0.2
180
+ combined = (wave_primary + wave_secondary + wave_tertiary + 1.2) / 2.4
181
+ combined = max(0, min(1, combined))
182
+ char_idx = int(combined * (wave_len - 1))
183
+ char = self.WAVE_CHARS[char_idx]
184
+ color_pos = (i / width + t * 0.15) % 1.0
185
+ color_idx = int(color_pos * len(self.PULSE_COLORS)) % len(self.PULSE_COLORS)
186
+ color = self.PULSE_COLORS[color_idx]
187
+
188
+ if char in "▅▆▇█":
189
+ result.append(char, style=f"bold {color}")
190
+ elif char in "▃▄":
191
+ result.append(char, style=color)
192
+ else:
193
+ result.append(char, style=f"dim {color}")
194
+
195
+ return result
196
+
197
+
198
+ # Alias for backward compatibility
199
+ RainbowProgressBar = PulseWaveBar
200
+
201
+
202
+ class ScanningLine(Static):
203
+ """Scanning line that sweeps left-to-right like a radar."""
204
+
205
+ is_active = reactive(False)
206
+ needs_approval = reactive(False)
207
+
208
+ SCAN_COLORS = ["#a855f7", "#c026d3", "#ec4899", "#f472b6"]
209
+ APPROVAL_COLORS = ["#f59e0b", "#fbbf24", "#f97316", "#ef4444"]
210
+
211
+ def on_mount(self):
212
+ self.auto_refresh = 1 / 30
213
+
214
+ def render(self) -> Text:
215
+ if not self.is_active:
216
+ return Text("")
217
+
218
+ width = self.size.width or 80
219
+ t = monotonic()
220
+ result = Text()
221
+
222
+ speed = 0.6 if self.needs_approval else 0.4
223
+ scan_pos = (t * speed) % 1.0
224
+ scan_x = int(scan_pos * width)
225
+ trail_len = 12
226
+
227
+ for i in range(width):
228
+ dist = scan_x - i
229
+ if dist < 0:
230
+ dist += width
231
+
232
+ if dist == 0:
233
+ result.append("█", style="bold #ffffff")
234
+ elif dist > 0 and dist <= trail_len:
235
+ fade = 1.0 - (dist / trail_len)
236
+ if self.needs_approval:
237
+ if fade > 0.7:
238
+ result.append("▓", style="bold #fbbf24")
239
+ elif fade > 0.4:
240
+ result.append("▒", style="#f59e0b")
241
+ elif fade > 0.2:
242
+ result.append("░", style="#f97316")
243
+ else:
244
+ result.append("░", style="#7c2d12")
245
+ else:
246
+ if fade > 0.7:
247
+ result.append("▓", style="bold #ec4899")
248
+ elif fade > 0.4:
249
+ result.append("▒", style="#c026d3")
250
+ elif fade > 0.2:
251
+ result.append("░", style="#a855f7")
252
+ else:
253
+ result.append("░", style="#4a1a6b")
254
+ else:
255
+ if self.needs_approval:
256
+ bg_color = "#2a1a00" if int(t * 4) % 2 == 0 else "#1a1a1a"
257
+ result.append("─", style=bg_color)
258
+ else:
259
+ result.append("─", style="#1a1a1a")
260
+
261
+ return result
262
+
263
+
264
+ class TopScanningLine(Static):
265
+ """Top scanning line - flowing wave animation."""
266
+
267
+ is_active = reactive(False)
268
+ needs_approval = reactive(False)
269
+ WAVE_COLORS = ["#3b82f6", "#6366f1", "#8b5cf6", "#a855f7", "#c026d3", "#ec4899"]
270
+
271
+ def on_mount(self):
272
+ self.auto_refresh = 1 / 25
273
+
274
+ def render(self) -> Text:
275
+ if not self.is_active:
276
+ return Text("")
277
+
278
+ width = self.size.width or 80
279
+ t = monotonic()
280
+ result = Text()
281
+
282
+ for i in range(width):
283
+ wave1 = math.sin(t * 2 + i * 0.2) * 0.4
284
+ wave2 = math.sin(t * 3.5 + i * 0.15 + 1.5) * 0.3
285
+ combined = (wave1 + wave2 + 1) / 2
286
+ combined = max(0, min(1, combined))
287
+
288
+ if combined > 0.8:
289
+ char = "█"
290
+ elif combined > 0.6:
291
+ char = "▓"
292
+ elif combined > 0.4:
293
+ char = "▒"
294
+ elif combined > 0.2:
295
+ char = "░"
296
+ else:
297
+ char = "─"
298
+
299
+ color_pos = (i / width + t * 0.1) % 1.0
300
+ color_idx = int(color_pos * len(self.WAVE_COLORS)) % len(self.WAVE_COLORS)
301
+ color = self.WAVE_COLORS[color_idx]
302
+
303
+ if char in "█▓":
304
+ result.append(char, style=f"bold {color}")
305
+ elif char == "▒":
306
+ result.append(char, style=color)
307
+ else:
308
+ result.append(char, style=f"dim {color}")
309
+
310
+ return result
311
+
312
+
313
+ class BottomScanningLine(Static):
314
+ """Bottom scanning line - radar sweep animation."""
315
+
316
+ is_active = reactive(False)
317
+ needs_approval = reactive(False)
318
+
319
+ def on_mount(self):
320
+ self.auto_refresh = 1 / 30
321
+
322
+ def render(self) -> Text:
323
+ if not self.is_active:
324
+ return Text("")
325
+
326
+ width = self.size.width or 80
327
+ t = monotonic()
328
+ result = Text()
329
+
330
+ sweep_pos = (t * 0.5) % 1.0
331
+ sweep_x = int(sweep_pos * width)
332
+
333
+ for i in range(width):
334
+ dist = abs(i - sweep_x)
335
+
336
+ if dist == 0:
337
+ result.append("█", style="bold #ffffff")
338
+ elif dist <= 3:
339
+ fade = 1.0 - (dist / 3.0)
340
+ if fade > 0.7:
341
+ result.append("▓", style="bold #ec4899")
342
+ elif fade > 0.4:
343
+ result.append("▒", style="#c026d3")
344
+ elif fade > 0.2:
345
+ result.append("░", style="#a855f7")
346
+ else:
347
+ result.append("░", style="#4a1a6b")
348
+ elif dist <= 8:
349
+ fade = 1.0 - ((dist - 3) / 5.0)
350
+ if fade > 0.7:
351
+ result.append("▓", style="bold #ec4899")
352
+ elif fade > 0.4:
353
+ result.append("▒", style="#c026d3")
354
+ elif fade > 0.2:
355
+ result.append("░", style="#a855f7")
356
+ else:
357
+ result.append("░", style="#4a1a6b")
358
+ else:
359
+ result.append("─", style="#1a1a1a")
360
+
361
+ return result
362
+
363
+
364
+ # Aliases for compatibility
365
+ ProgressChase = ScanningLine
366
+ SparkleTrail = ScanningLine
367
+ ThinkingWave = ScanningLine
368
+
369
+
370
+ class StreamingThinkingIndicator(Static):
371
+ """Animated thinking indicator for streaming."""
372
+
373
+ is_active = reactive(False)
374
+ SPINNER_FRAMES = ["⠋", "⠙", "â š", "â ¸", "â ŧ", "â ´", "â Ļ", "â §", "⠇", "⠏"]
375
+
376
+ THINKING_PHRASES = [
377
+ "🧠 Thinking deeply",
378
+ "💭 Processing your request",
379
+ "⚡ Analyzing the problem",
380
+ "🔍 Understanding context",
381
+ "✨ Generating response",
382
+ "đŸŽ¯ Computing solution",
383
+ "🚀 Working on it",
384
+ "💡 Light bulb moment",
385
+ "đŸŽĒ Juggling possibilities",
386
+ "🎨 Painting a masterpiece",
387
+ "🧩 Solving the puzzle",
388
+ "đŸ‘¨â€đŸŗ Cooking up magic",
389
+ "🚀 Launching into orbit",
390
+ "đŸĒ„ Casting a spell",
391
+ "đŸ’ģ Compiling thoughts",
392
+ "🔧 Tightening the bolts",
393
+ "🐝 Busy bee mode",
394
+ "đŸ—ī¸ Under construction",
395
+ "đŸ§™â€â™‚ī¸ Wizarding up a solution",
396
+ "đŸĻ„ Summoning unicorn power",
397
+ "🐉 Awakening the code dragon",
398
+ "🌟 Aligning the stars",
399
+ "🔭 Scanning the codeverse",
400
+ "âš›ī¸ Splitting atoms of logic",
401
+ "🌌 Exploring the galaxy",
402
+ "🛸 Beaming down answers",
403
+ "🔮 Consulting the crystal ball",
404
+ "đŸŽŦ Directing the scene",
405
+ "🎸 Jamming on your code",
406
+ "🎲 Rolling for initiative",
407
+ "đŸŗ Frying some fresh code",
408
+ "☕ Brewing the perfect response",
409
+ "🍕 Serving hot code",
410
+ "đŸĻŠ Being clever like a fox",
411
+ "🐙 Multitasking like an octopus",
412
+ "đŸĻ… Eagle-eye analyzing",
413
+ "đŸ”Ĩ Firing up the engines",
414
+ "💎 Polishing the gem",
415
+ "🎭 Getting into character",
416
+ "🎡 Spinning up ideas",
417
+ "đŸŽ¯ Locking onto target",
418
+ "âš™ī¸ Processing information",
419
+ "đŸ§Ē Experimenting with solutions",
420
+ "đŸ”Ŧ Running analysis",
421
+ "📊 Crunching numbers",
422
+ "🎨 Creating art",
423
+ "đŸŽĒ Performing magic",
424
+ "🎭 Acting out the solution",
425
+ ]
426
+
427
+ def on_mount(self):
428
+ self.auto_refresh = 1 / 15
429
+
430
+ def render(self) -> Text:
431
+ if not self.is_active:
432
+ return Text("")
433
+
434
+ t = monotonic()
435
+ result = Text()
436
+
437
+ spinner_idx = int(t * 10) % len(self.SPINNER_FRAMES)
438
+ phrase_idx = int(t / 1.5) % len(self.THINKING_PHRASES)
439
+
440
+ colors = [
441
+ "#a855f7",
442
+ "#c026d3",
443
+ "#d946ef",
444
+ "#ec4899",
445
+ "#f97316",
446
+ "#fbbf24",
447
+ "#22c55e",
448
+ "#06b6d4",
449
+ ]
450
+ color = colors[int(t * 4) % len(colors)]
451
+
452
+ spinner = self.SPINNER_FRAMES[spinner_idx]
453
+ phrase = self.THINKING_PHRASES[phrase_idx]
454
+
455
+ dot_count = int(t * 3) % 4
456
+ dots = "." * dot_count
457
+
458
+ sparkles = ["✨", "⭐", "đŸ’Ģ", "🌟"]
459
+ sparkle = sparkles[int(t * 2) % len(sparkles)]
460
+
461
+ result.append(f" {spinner} ", style=f"bold {color}")
462
+ result.append(phrase, style=f"bold {color}")
463
+ result.append(dots, style=color)
464
+ result.append(f" {sparkle}", style=color)
465
+ result.append(" ", style="")
466
+
467
+ return result
468
+
469
+
470
+ class ModeBadge(Static):
471
+ """Shows current mode with rich styling and connection info."""
472
+
473
+ mode = reactive("home")
474
+ role = reactive("")
475
+ agent = reactive("")
476
+ model = reactive("")
477
+ provider = reactive("")
478
+ execution_mode = reactive("")
479
+ approval_mode = reactive("auto")
480
+
481
+ def render(self) -> Text:
482
+ t = Text()
483
+
484
+ if self.execution_mode == "pure":
485
+ t.append(" đŸ§Ē ", style=f"bold {THEME['pink']}")
486
+ t.append("PURE", style=f"bold {THEME['pink']} reverse")
487
+ t.append(" â€ĸ ", style=THEME["muted"])
488
+
489
+ if self.provider:
490
+ t.append(f"{self.provider.upper()}", style=f"bold {THEME['cyan']}")
491
+
492
+ if self.model:
493
+ t.append(" ", style="")
494
+ t.append(f"📊 {self.model}", style=THEME["muted"])
495
+
496
+ return t
497
+
498
+ if self.agent:
499
+ color = AGENT_COLORS.get(self.agent, THEME["purple"])
500
+ icon = AGENT_ICONS.get(self.agent, "🤖")
501
+
502
+ if self.execution_mode == "acp":
503
+ t.append(" 🔗 ", style=f"bold {THEME['cyan']}")
504
+ t.append("ACP", style=f"bold {THEME['cyan']} reverse")
505
+ t.append(" â€ĸ ", style=THEME["muted"])
506
+ elif self.execution_mode == "byok":
507
+ t.append(" ⚡ ", style=f"bold {THEME['success']}")
508
+ t.append("BYOK", style=f"bold {THEME['success']} reverse")
509
+ t.append(" â€ĸ ", style=THEME["muted"])
510
+
511
+ t.append(f"{icon} ", style=f"bold {color}")
512
+ t.append(self.agent.upper(), style=f"bold {color}")
513
+
514
+ if self.model:
515
+ t.append(" ", style="")
516
+ t.append(f"📊 {self.model}", style=THEME["muted"])
517
+ if self.provider:
518
+ t.append(" ", style="")
519
+ t.append(f"â˜ī¸ {self.provider}", style=THEME["dim"])
520
+
521
+ mode_icons = {"auto": "đŸŸĸ", "ask": "🟡", "deny": "🔴"}
522
+ mode_colors = {
523
+ "auto": THEME["success"],
524
+ "ask": THEME["warning"],
525
+ "deny": THEME["error"],
526
+ }
527
+ approval_icon = mode_icons.get(self.approval_mode, "🟡")
528
+ approval_color = mode_colors.get(self.approval_mode, THEME["warning"])
529
+ t.append(" ", style="")
530
+ t.append(f"{approval_icon}", style=approval_color)
531
+
532
+ elif self.role:
533
+ mode_styles = {
534
+ "dev": (ICONS["dev"], THEME["success"], "đŸ’ģ"),
535
+ "qa": (ICONS["qa"], THEME["orange"], "đŸ§Ē"),
536
+ "devops": (ICONS["devops"], THEME["cyan"], "âš™ī¸"),
537
+ }
538
+ icon, color, emoji = mode_styles.get(self.mode, (ICONS["home"], THEME["purple"], "🏠"))
539
+
540
+ if self.execution_mode == "acp":
541
+ t.append(" 🔗 ", style=f"bold {THEME['cyan']}")
542
+ t.append("ACP", style=f"bold {THEME['cyan']} reverse")
543
+ t.append(" â€ĸ ", style=THEME["muted"])
544
+ elif self.execution_mode == "byok":
545
+ t.append(" ⚡ ", style=f"bold {THEME['success']}")
546
+ t.append("BYOK", style=f"bold {THEME['success']} reverse")
547
+ t.append(" â€ĸ ", style=THEME["muted"])
548
+
549
+ t.append(f"{emoji} ", style=f"bold {color}")
550
+ t.append(f"{self.mode.upper()}", style=f"bold {color}")
551
+ t.append(" â€ē ", style=THEME["muted"])
552
+ t.append(self.role, style=f"bold {color}")
553
+
554
+ if self.model:
555
+ t.append(" ", style="")
556
+ t.append(f"📊 {self.model}", style=THEME["dim"])
557
+
558
+ mode_icons = {"auto": "đŸŸĸ", "ask": "🟡", "deny": "🔴"}
559
+ mode_colors = {
560
+ "auto": THEME["success"],
561
+ "ask": THEME["warning"],
562
+ "deny": THEME["error"],
563
+ }
564
+ approval_icon = mode_icons.get(self.approval_mode, "🟡")
565
+ approval_color = mode_colors.get(self.approval_mode, THEME["warning"])
566
+ t.append(" ", style="")
567
+ t.append(f"{approval_icon}", style=approval_color)
568
+ else:
569
+ t.append(f" 🏠 ", style=f"bold {THEME['purple']}")
570
+ t.append("HOME", style=f"bold {THEME['purple']} reverse")
571
+ t.append(" ", style="")
572
+ t.append("ready to code", style=f"dim {THEME['muted']}")
573
+
574
+ return t
575
+
576
+
577
+ class HintsBar(Static):
578
+ """Context hints with gradient colors and emojis."""
579
+
580
+ approval_mode = reactive("auto")
581
+
582
+ def render(self) -> Text:
583
+ t = Text()
584
+
585
+ # t.append("\n", style="")
586
+
587
+ hints = [
588
+ ("🏠 :home", THEME["cyan"]),
589
+ ("❓ :h [:help]", THEME["purple"]),
590
+ ("🚀 :i [:init]", THEME["success"]),
591
+ ("📚 :s [:sidebar]", THEME["cyan"]),
592
+ ("🔌 :c [:connect]", THEME["pink"]),
593
+ ("👋 :q [:quit]", THEME["orange"]),
594
+ ]
595
+ for i, (hint, color) in enumerate(hints):
596
+ if i > 0:
597
+ t.append(" â€ĸ ", style=THEME["dim"])
598
+ t.append(hint, style=color)
599
+
600
+ return t
601
+
602
+
603
+ class SelectableTextArea(Static):
604
+ """A text area that allows mouse selection and copying.
605
+
606
+ Used as a popup overlay when user wants to select/copy text.
607
+ """
608
+
609
+ DEFAULT_CSS = """
610
+ SelectableTextArea {
611
+ background: #0a0a0a;
612
+ border: round #7c3aed;
613
+ padding: 1 2;
614
+ width: 80%;
615
+ height: 80%;
616
+ layer: overlay;
617
+ }
618
+
619
+ SelectableTextArea .title {
620
+ text-align: center;
621
+ color: #a855f7;
622
+ text-style: bold;
623
+ margin-bottom: 1;
624
+ }
625
+
626
+ SelectableTextArea .hint {
627
+ text-align: center;
628
+ color: #71717a;
629
+ margin-top: 1;
630
+ }
631
+ """
632
+
633
+ def __init__(self, content: str, title: str = "Response", **kwargs):
634
+ super().__init__(**kwargs)
635
+ self._content = content
636
+ self._title = title
637
+
638
+ def render(self) -> Text:
639
+ t = Text()
640
+ t.append(f"📋 {self._title}\n", style=f"bold {THEME['purple']}")
641
+ t.append("─" * 40 + "\n\n", style=THEME["border"])
642
+ t.append(self._content, style=THEME["text"])
643
+ t.append("\n\n" + "─" * 40 + "\n", style=THEME["border"])
644
+ t.append(
645
+ "Hold Shift + drag to select â€ĸ Ctrl+C to copy â€ĸ Escape to close", style=THEME["muted"]
646
+ )
647
+ return t
648
+
649
+
650
+ class ConversationLog(RichLog):
651
+ """Chat log with styled messages and rich formatting.
652
+
653
+ Text Selection:
654
+ - Hold Shift while dragging to select text (terminal native selection)
655
+ - Ctrl+Shift+C to copy last response
656
+ - :copy command to copy to clipboard
657
+ - :select to open selectable view
658
+ """
659
+
660
+ DEFAULT_CSS = """
661
+ ConversationLog {
662
+ scrollbar-gutter: stable;
663
+ background: #000000;
664
+ width: 100%;
665
+ padding: 0;
666
+ margin: 0;
667
+ }
668
+ """
669
+
670
+ def __init__(self, *args, **kwargs):
671
+ # Remove any width-related kwargs that might limit display
672
+ kwargs.pop("max_width", None)
673
+ kwargs.pop("width", None)
674
+ super().__init__(*args, **kwargs)
675
+ # Track messages for copy functionality
676
+ self._messages: list[tuple[str, str, str]] = [] # (role, text, agent_name)
677
+ self._last_response: str = ""
678
+ self._last_error: str = "" # Track last error for easy copy
679
+ # Track thinking and tool calls for agent sessions
680
+ self._thinking_lines: list[str] = []
681
+ self._tool_calls: list[dict] = []
682
+ self._streaming_response: str = ""
683
+ # Force console width to None (unlimited) immediately after init
684
+ self._force_unlimited_width = True
685
+
686
+ def on_mount(self) -> None:
687
+ """Configure console width when widget is mounted."""
688
+ super().on_mount()
689
+ # Override Rich's internal console width to use full available width
690
+ # RichLog uses an internal Console that might have a default width limit
691
+ # Try multiple possible attribute names for the internal console
692
+ self._update_console_width()
693
+ # Also try to set it after a small delay to ensure it's applied
694
+ self.set_timer(0.1, self._update_console_width)
695
+
696
+ def _update_console_width(self) -> None:
697
+ """Update the internal Rich console width - FORCE UNLIMITED (NO CHARACTER LIMITS)."""
698
+ # Get actual terminal width - use a very large value to prevent truncation
699
+ import shutil
700
+
701
+ try:
702
+ terminal_width = shutil.get_terminal_size().columns
703
+ # Use terminal width * 2 to ensure no truncation, minimum 200
704
+ target_width = max(terminal_width * 2, 200) if terminal_width > 0 else 500
705
+ except Exception:
706
+ target_width = 500 # Large fallback
707
+
708
+ # Set console width on all possible console attributes
709
+ console_attrs = [
710
+ "_console",
711
+ "console",
712
+ "_rich_console",
713
+ "rich_console",
714
+ "_log_console",
715
+ "log_console",
716
+ ]
717
+
718
+ for attr in console_attrs:
719
+ try:
720
+ if hasattr(self, attr):
721
+ console = getattr(self, attr)
722
+ if console and hasattr(console, "width"):
723
+ console.width = target_width
724
+ if hasattr(console, "legacy_width"):
725
+ console.legacy_width = target_width
726
+ if hasattr(console, "max_width"):
727
+ console.max_width = target_width
728
+ # Also set soft_wrap to True for natural wrapping
729
+ if hasattr(console, "soft_wrap"):
730
+ console.soft_wrap = True
731
+ except Exception:
732
+ continue
733
+
734
+ # Also try to access console through various internal attributes
735
+ internal_attrs = ["_renderable", "_log", "_buffer", "_output"]
736
+ for attr in internal_attrs:
737
+ try:
738
+ if hasattr(self, attr):
739
+ obj = getattr(self, attr)
740
+ if hasattr(obj, "_console"):
741
+ obj._console.width = target_width
742
+ if hasattr(obj, "console"):
743
+ obj.console.width = target_width
744
+ except Exception:
745
+ continue
746
+
747
+ # Try to access through __dict__ to find any console-like objects
748
+ try:
749
+ for key, value in self.__dict__.items():
750
+ if "console" in key.lower() and hasattr(value, "width"):
751
+ value.width = target_width
752
+ except Exception:
753
+ pass
754
+
755
+ def on_resize(self, event) -> None:
756
+ """Update console width when widget is resized."""
757
+ super().on_resize(event)
758
+ # Update console width when widget size changes
759
+ self._update_console_width()
760
+
761
+ def add_user(self, text: str):
762
+ self._messages.append(("user", text, ""))
763
+ # Use None for width to allow full width usage
764
+ panel = Panel(
765
+ Text(text, style=THEME["text"], overflow="fold"),
766
+ title=f"[bold {THEME['cyan']}]👩‍đŸ’ģ👨‍đŸ’ģ >[/]",
767
+ border_style=THEME["border"],
768
+ box=ROUNDED,
769
+ padding=(0, 1),
770
+ width=None, # Use full available width
771
+ )
772
+ self.write(panel)
773
+
774
+ def add_agent(self, text: str, agent: str = "Agent"):
775
+ self._messages.append(("agent", text, agent))
776
+ self._last_response = text # Track for easy copy
777
+ color = AGENT_COLORS.get(agent.lower(), THEME["purple"])
778
+ icon = AGENT_ICONS.get(agent.lower(), "🤖")
779
+ # Use overflow="fold" to wrap instead of truncate
780
+ content = (
781
+ Markdown(text) if "```" in text else Text(text, style=THEME["text"], overflow="fold")
782
+ )
783
+ panel = Panel(
784
+ content,
785
+ title=f"[bold {color}]{icon} {agent} Agent[/]",
786
+ border_style=color,
787
+ box=ROUNDED,
788
+ padding=(0, 1),
789
+ width=None, # Use full available width
790
+ )
791
+ self.write(panel)
792
+
793
+ def add_assistant(self, text: str, agent: str = "Assistant"):
794
+ """Alias for add_agent - used by TUI for assistant responses."""
795
+ self.add_agent(text, agent)
796
+
797
+ def write(self, *args, **kwargs):
798
+ """Override write to ensure console width is always correct - NO LIMITS."""
799
+ # Ensure console width is updated before writing
800
+ self._update_console_width()
801
+
802
+ # Process args to ensure Text objects have proper overflow handling
803
+ processed_args = []
804
+ for arg in args:
805
+ if isinstance(arg, Text):
806
+ # Ensure Text uses fold overflow for natural wrapping
807
+ if not hasattr(arg, "overflow") or arg.overflow != "fold":
808
+ arg.overflow = "fold"
809
+ processed_args.append(arg)
810
+
811
+ return super().write(*processed_args, **kwargs)
812
+
813
+ def add_system(self, text: str):
814
+ self._messages.append(("system", text, ""))
815
+ self.write(Text(f" ✨ {text}", style=f"italic {THEME['muted']}"))
816
+
817
+ def add_error(self, text: str):
818
+ self._messages.append(("error", text, ""))
819
+ self._last_error = text # Track for easy copy
820
+
821
+ # Try to display with rich markup support
822
+ try:
823
+ self.write(Text(f" ❌ {text}", markup=True))
824
+ except Exception:
825
+ # Fallback to plain text
826
+ self.write(Text(f" ❌ {text}", style=THEME["error"]))
827
+
828
+ def add_success(self, text: str):
829
+ self._messages.append(("success", text, ""))
830
+ self.write(Text(f" ✅ {text}", style=THEME["success"]))
831
+
832
+ def add_info(self, text: str):
833
+ self._messages.append(("info", text, ""))
834
+ self.write(Text(f" â„šī¸ {text}", style=THEME["cyan"]))
835
+
836
+ def add_shell(self, cmd: str, output: str, ok: bool = True):
837
+ """Add shell command output to the log."""
838
+ self._messages.append(("shell", f"{cmd}\n{output}", ""))
839
+ status_icon = "⚡" if ok else "đŸ’Ĩ"
840
+ status_style = THEME["success"] if ok else THEME["error"]
841
+
842
+ # Format command and output with distinct styling
843
+ content = Text.assemble(
844
+ (f" {status_icon} ", status_style),
845
+ (f"Terminal", f"bold {THEME['cyan']}"),
846
+ (": ", THEME["muted"]),
847
+ (f"{cmd}\n", f"bold {THEME['text']}"),
848
+ (output, THEME["text"] if ok else THEME["error"]),
849
+ )
850
+ self.write(content)
851
+
852
+ def get_last_response(self) -> str:
853
+ """Get the last agent response text for copying."""
854
+ return self._last_response
855
+
856
+ def get_last_error(self) -> str:
857
+ """Get the last error text for copying."""
858
+ return self._last_error
859
+
860
+ def get_last_message(self, role: str = None) -> str:
861
+ """Get the last message, optionally filtered by role."""
862
+ if not self._messages:
863
+ return ""
864
+ if role:
865
+ for msg_role, text, _ in reversed(self._messages):
866
+ if msg_role == role:
867
+ return text
868
+ return ""
869
+ return self._messages[-1][1]
870
+
871
+ def get_all_text(self) -> str:
872
+ """Get all messages as plain text for export."""
873
+ lines = []
874
+ for role, text, agent in self._messages:
875
+ if role == "user":
876
+ lines.append(f"You: {text}")
877
+ elif role == "agent":
878
+ lines.append(f"{agent or 'Agent'}: {text}")
879
+ elif role == "system":
880
+ lines.append(f"System: {text}")
881
+ elif role == "error":
882
+ lines.append(f"Error: {text}")
883
+ elif role == "success":
884
+ lines.append(f"Success: {text}")
885
+ elif role == "info":
886
+ lines.append(f"Info: {text}")
887
+ return "\n\n".join(lines)
888
+
889
+ def copy_to_clipboard(self, text: str = None) -> bool:
890
+ """Copy text to clipboard. Returns True if successful."""
891
+ try:
892
+ import pyperclip
893
+
894
+ content = text if text is not None else self._last_response
895
+ if content:
896
+ pyperclip.copy(content)
897
+ return True
898
+ except ImportError:
899
+ # pyperclip not available, try platform-specific
900
+ try:
901
+ import subprocess
902
+ import sys
903
+
904
+ content = text if text is not None else self._last_response
905
+ if not content:
906
+ return False
907
+ if sys.platform == "darwin":
908
+ subprocess.run(["pbcopy"], input=content.encode(), check=True)
909
+ return True
910
+ elif sys.platform.startswith("linux"):
911
+ subprocess.run(
912
+ ["xclip", "-selection", "clipboard"], input=content.encode(), check=True
913
+ )
914
+ return True
915
+ elif sys.platform == "win32":
916
+ subprocess.run(["clip"], input=content.encode(), check=True)
917
+ return True
918
+ except Exception:
919
+ pass
920
+ except Exception:
921
+ pass
922
+ return False
923
+
924
+ def add_tool_approval_needed(self, tool_name: str, description: str = ""):
925
+ """Display a prominent inline notification that tool approval is needed."""
926
+ self.write(
927
+ Panel(
928
+ Text.assemble(
929
+ ("âš ī¸ ACTION REQUIRED\n\n", f"bold {THEME['warning']}"),
930
+ ("Tool: ", THEME["muted"]),
931
+ (f"{tool_name}\n", f"bold {THEME['cyan']}"),
932
+ (f"{description}\n\n" if description else "\n", THEME["text"]),
933
+ ("↑ ", f"bold {THEME['warning']}"),
934
+ ("Type in prompt box above: ", THEME["text"]),
935
+ ("y", f"bold {THEME['success']}"),
936
+ (" to approve, ", THEME["muted"]),
937
+ ("n", f"bold {THEME['error']}"),
938
+ (" to reject", THEME["muted"]),
939
+ ),
940
+ title=f"[bold {THEME['warning']}]🔔 Tool Approval Needed[/]",
941
+ border_style=THEME["warning"],
942
+ box=HEAVY,
943
+ padding=(1, 2),
944
+ )
945
+ )
946
+
947
+ # =========================================================================
948
+ # ENHANCED STREAMING OUTPUT METHODS
949
+ # These methods provide better display for agent thinking and responses
950
+ # Compatible with BYOK, ACP, and Local modes
951
+ # =========================================================================
952
+
953
+ def start_agent_session(
954
+ self,
955
+ agent_name: str,
956
+ model_name: str = "",
957
+ mode: str = "acp",
958
+ approval_mode: str = "ask",
959
+ ):
960
+ """
961
+ Start a new agent output session with header.
962
+
963
+ Args:
964
+ agent_name: Name of the agent (e.g., "OpenCode", "Claude")
965
+ model_name: Model being used (e.g., "gpt-4o", "claude-sonnet")
966
+ mode: Connection mode ("acp", "byok", "local")
967
+ approval_mode: Approval mode ("auto", "ask", "deny")
968
+ """
969
+ # Reset streaming state
970
+ self._streaming_response = ""
971
+ self._streaming_thinking = ""
972
+ self._thinking_lines = []
973
+ self._tool_calls = []
974
+ self._session_start_time = None
975
+
976
+ try:
977
+ from time import monotonic
978
+
979
+ self._session_start_time = monotonic()
980
+ except Exception:
981
+ pass
982
+
983
+ # Mode badges
984
+ mode_badges = {
985
+ "acp": ("🔌", "ACP", THEME["success"]),
986
+ "byok": ("🔑", "BYOK", THEME["cyan"]),
987
+ "local": ("đŸ’ģ", "Local", THEME["warning"]),
988
+ }
989
+ mode_icon, mode_label, mode_color = mode_badges.get(
990
+ mode.lower(), ("●", mode.upper(), THEME["muted"])
991
+ )
992
+
993
+ # Approval mode
994
+ approval_badges = {
995
+ "auto": ("đŸŸĸ", "AUTO", THEME["success"]),
996
+ "ask": ("🟡", "ASK", THEME["warning"]),
997
+ "deny": ("🔴", "DENY", THEME["error"]),
998
+ }
999
+ app_icon, app_label, app_color = approval_badges.get(
1000
+ approval_mode, ("🟡", "ASK", THEME["warning"])
1001
+ )
1002
+
1003
+ # Build header
1004
+ header = Text()
1005
+ header.append("\n")
1006
+
1007
+ # Gradient line
1008
+ gradient = ["#6d28d9", "#7c3aed", "#8b5cf6", "#a855f7", "#c084fc"]
1009
+ line = "─" * 60
1010
+ for i, char in enumerate(line):
1011
+ header.append(char, style=gradient[i % len(gradient)])
1012
+ header.append("\n")
1013
+
1014
+ # Agent name
1015
+ agent_color = AGENT_COLORS.get(agent_name.lower(), THEME["purple"])
1016
+ agent_icon = AGENT_ICONS.get(agent_name.lower(), "🤖")
1017
+ header.append(f" {agent_icon} ", style=f"bold {agent_color}")
1018
+ header.append(agent_name.upper(), style=f"bold {THEME['text']}")
1019
+ header.append(" is working\n", style=THEME["muted"])
1020
+
1021
+ # Model and mode info
1022
+ header.append(" Model: ", style=THEME["dim"])
1023
+ header.append(model_name or "auto", style=f"bold {THEME['cyan']}")
1024
+ header.append(f" │ {mode_icon} ", style=mode_color)
1025
+ header.append(mode_label, style=f"bold {mode_color}")
1026
+ header.append(f" │ {app_icon} ", style=app_color)
1027
+ header.append(app_label, style=f"bold {app_color}")
1028
+ header.append("\n")
1029
+
1030
+ self.write(header)
1031
+
1032
+ def add_thinking(self, text: str, category: str = "general"):
1033
+ """
1034
+ Add a thinking/reasoning line (always visible).
1035
+
1036
+ This method ALWAYS shows thinking regardless of show_thinking_logs setting
1037
+ because it's explicitly called for important agent reasoning.
1038
+
1039
+ Args:
1040
+ text: The thinking text to display
1041
+ category: Category for icon selection (planning, analyzing, etc.)
1042
+ """
1043
+ if not text or not text.strip():
1044
+ return
1045
+
1046
+ # Ensure auto-scroll is ON
1047
+ self.auto_scroll = True
1048
+
1049
+ # Store for later copy
1050
+ self._thinking_lines.append(text)
1051
+
1052
+ # Category icons and colors
1053
+ category_styles = {
1054
+ "planning": ("📋", "#f472b6"),
1055
+ "analyzing": ("đŸ”Ŧ", "#c084fc"),
1056
+ "deciding": ("🤔", "#fbbf24"),
1057
+ "searching": ("🔍", "#60a5fa"),
1058
+ "reading": ("📖", "#34d399"),
1059
+ "writing": ("âœī¸", "#818cf8"),
1060
+ "debugging": ("🐛", "#ef4444"),
1061
+ "executing": ("⚡", "#fb923c"),
1062
+ "verifying": ("✅", "#22c55e"),
1063
+ "testing": ("đŸ§Ē", "#a78bfa"),
1064
+ "refactoring": ("🔧", "#9ca3af"),
1065
+ "discovery": ("🔭", "#06b6d4"),
1066
+ "thinking": ("🧠", "#e879f9"),
1067
+ "notifying": ("🔔", "#facc15"),
1068
+ "general": ("💭", "#94a3b8"),
1069
+ }
1070
+
1071
+ icon, color = category_styles.get(category.lower(), category_styles["general"])
1072
+
1073
+ # Auto-detect category from text if not specified (or is general)
1074
+ if category == "general":
1075
+ text_lower = text.lower()
1076
+ if any(w in text_lower for w in ["test", "pytest", "expect", "assertion"]):
1077
+ icon, color = category_styles["testing"]
1078
+ elif any(w in text_lower for w in ["run", "execute", "command", "bash", "shell"]):
1079
+ icon, color = category_styles["executing"]
1080
+ elif any(w in text_lower for w in ["verify", "confirm", "check", "validation"]):
1081
+ icon, color = category_styles["verifying"]
1082
+ elif any(w in text_lower for w in ["debug", "error", "fix", "bug", "traceback"]):
1083
+ icon, color = category_styles["debugging"]
1084
+ elif any(w in text_lower for w in ["plan", "step", "approach", "todo"]):
1085
+ icon, color = category_styles["planning"]
1086
+ elif any(w in text_lower for w in ["search", "find", "look", "grep", "glob"]):
1087
+ icon, color = category_styles["searching"]
1088
+ elif any(w in text_lower for w in ["read", "file", "content", "cat"]):
1089
+ icon, color = category_styles["reading"]
1090
+ elif any(w in text_lower for w in ["write", "create", "add", "edit", "save"]):
1091
+ icon, color = category_styles["writing"]
1092
+ elif any(w in text_lower for w in ["discover", "list", "explore", "scan"]):
1093
+ icon, color = category_styles["discovery"]
1094
+ elif any(w in text_lower for w in ["think", "reason", "ponder", "analyze"]):
1095
+ icon, color = category_styles["thinking"]
1096
+ elif any(w in text_lower for w in ["info", "note", "alert", "notice"]):
1097
+ icon, color = category_styles["notifying"]
1098
+ else:
1099
+ # Randomize generic icon to avoid repetition
1100
+ generic_icons = ["💭", "💡", "âš™ī¸", "🧩", "🔮", "✨", "📡"]
1101
+ import random
1102
+
1103
+ icon = random.choice(generic_icons)
1104
+ # Keep neutral color for generic thoughts
1105
+
1106
+ # Display thinking line
1107
+ line = Text()
1108
+ line.append(f" {icon} ", style=f"bold {color}")
1109
+ line.append(text, style=f"italic {THEME['muted']}")
1110
+ line.append("\n")
1111
+ self.write(line)
1112
+
1113
+ def add_response_chunk(self, text: str):
1114
+ """
1115
+ Add a chunk of response text (for streaming).
1116
+
1117
+ Accumulates chunks and displays them intelligently to avoid word-per-line display.
1118
+ Highlights code blocks in real-time.
1119
+
1120
+ Args:
1121
+ text: The response chunk to add
1122
+ """
1123
+ if not text:
1124
+ return
1125
+
1126
+ self._streaming_response += text
1127
+ self.auto_scroll = True
1128
+
1129
+ # Check for code block state
1130
+ if not hasattr(self, "_in_code_block"):
1131
+ self._in_code_block = False
1132
+
1133
+ # Toggle code block state
1134
+ if "```" in text:
1135
+ # Count occurrences to toggle state correctly
1136
+ count = text.count("```")
1137
+ if count % 2 != 0:
1138
+ self._in_code_block = not self._in_code_block
1139
+
1140
+ # Buffer chunks and only write on natural boundaries to avoid word-per-line
1141
+ if not hasattr(self, "_chunk_buffer"):
1142
+ self._chunk_buffer = ""
1143
+ self._chunk_buffer += text
1144
+
1145
+ # Write when we have:
1146
+ # 1. A complete sentence (ends with . ! ? : ; followed by space or newline)
1147
+ # 2. A newline character in the text
1148
+ # 3. Accumulated enough text (50+ chars with a space near the end)
1149
+ buffer = self._chunk_buffer
1150
+ should_write = (
1151
+ (
1152
+ buffer.rstrip().endswith((".", "!", "?", ":", ";"))
1153
+ and (text.endswith(" ") or text.endswith("\n") or len(buffer) > 30)
1154
+ )
1155
+ or "\n" in text
1156
+ or (len(buffer) > 50 and " " in buffer[-15:])
1157
+ )
1158
+
1159
+ if should_write:
1160
+ chunk_text = Text()
1161
+ style = f"bold {THEME['cyan']}" if self._in_code_block else THEME["text"]
1162
+ chunk_text.append(buffer, style=style)
1163
+ self.write(chunk_text)
1164
+ self._chunk_buffer = ""
1165
+
1166
+ def flush_response_buffer(self):
1167
+ """Flush any remaining buffered response chunks."""
1168
+ if hasattr(self, "_chunk_buffer") and self._chunk_buffer:
1169
+ chunk_text = Text()
1170
+ style = (
1171
+ f"bold {THEME['cyan']}" if getattr(self, "_in_code_block", False) else THEME["text"]
1172
+ )
1173
+ chunk_text.append(self._chunk_buffer, style=style)
1174
+ self.write(chunk_text)
1175
+ self._chunk_buffer = ""
1176
+
1177
+ def add_tool_call(
1178
+ self,
1179
+ tool_name: str,
1180
+ status: str = "running",
1181
+ file_path: str = "",
1182
+ command: str = "",
1183
+ output: str = "",
1184
+ ):
1185
+ """
1186
+ Add a tool call display.
1187
+
1188
+ Args:
1189
+ tool_name: Name of the tool being called
1190
+ status: Status ("pending", "running", "success", "error")
1191
+ file_path: File path if applicable
1192
+ command: Command if it's a shell tool
1193
+ output: Tool output/result
1194
+ """
1195
+ self._tool_calls.append(
1196
+ {
1197
+ "name": tool_name,
1198
+ "status": status,
1199
+ "path": file_path,
1200
+ "command": command,
1201
+ }
1202
+ )
1203
+
1204
+ # Track file modifications
1205
+ if status in ("running", "success") and file_path:
1206
+ # Initialize _files_modified if not exists
1207
+ if not hasattr(self, "_files_modified"):
1208
+ self._files_modified = set()
1209
+
1210
+ # Add to set if it's a write/edit operation
1211
+ tool_lower = tool_name.lower()
1212
+ if any(op in tool_lower for op in ("write", "edit", "create", "append", "patch")):
1213
+ self._files_modified.add(file_path)
1214
+
1215
+ # Status icons and colors
1216
+ status_map = {
1217
+ "pending": ("○", THEME["muted"]),
1218
+ "running": ("◐", THEME["purple"]),
1219
+ "success": ("âœĻ", THEME["success"]),
1220
+ "error": ("✕", THEME["error"]),
1221
+ }
1222
+ status_icon, status_color = status_map.get(status, ("●", THEME["muted"]))
1223
+
1224
+ # Tool type icons
1225
+ tool_icons = {
1226
+ "read": "â†ŗ",
1227
+ "write": "↲",
1228
+ "edit": "âŸŗ",
1229
+ "shell": "▸",
1230
+ "bash": "▸",
1231
+ "search": "⌕",
1232
+ "glob": "⋮",
1233
+ "grep": "⌕",
1234
+ }
1235
+ tool_icon = "â€ĸ"
1236
+ for key, icon in tool_icons.items():
1237
+ if key in tool_name.lower():
1238
+ tool_icon = icon
1239
+ break
1240
+
1241
+ # Build display
1242
+ line = Text()
1243
+ line.append(f" {status_icon} ", style=f"bold {status_color}")
1244
+ line.append(f"{tool_icon} ", style=THEME["dim"])
1245
+ line.append(tool_name, style=THEME["text"])
1246
+
1247
+ if file_path:
1248
+ # Show full file path - let widget handle wrapping
1249
+ line.append(f" {file_path}", style=THEME["dim"])
1250
+ elif command:
1251
+ # Show more of the command - 100 chars instead of 40
1252
+ cmd_short = command[:100] + "..." if len(command) > 100 else command
1253
+ line.append(f" $ {cmd_short}", style=THEME["dim"])
1254
+
1255
+ if output and status in ("success", "error"):
1256
+ # Show full output - no truncation, let the widget handle wrapping
1257
+ line.append(f"\n → {output}", style=THEME["muted"])
1258
+
1259
+ line.append("\n")
1260
+ self.write(line)
1261
+
1262
+ def end_agent_session(
1263
+ self,
1264
+ success: bool = True,
1265
+ response_text: str = "",
1266
+ prompt_tokens: int = 0,
1267
+ completion_tokens: int = 0,
1268
+ thinking_tokens: int = 0,
1269
+ cost: float = 0.0,
1270
+ ):
1271
+ """
1272
+ End the agent output session with a rich Mission Report summary.
1273
+
1274
+ Args:
1275
+ success: Whether the session completed successfully
1276
+ response_text: Final response text (if not already streamed)
1277
+ prompt_tokens: Number of prompt tokens used
1278
+ completion_tokens: Number of completion tokens used
1279
+ thinking_tokens: Number of thinking tokens used
1280
+ cost: Cost in dollars
1281
+ """
1282
+ # Flush any remaining buffered response chunks
1283
+ self.flush_response_buffer()
1284
+
1285
+ # Calculate duration
1286
+ duration = 0.0
1287
+ if hasattr(self, "_session_start_time") and self._session_start_time:
1288
+ try:
1289
+ from time import monotonic
1290
+
1291
+ duration = monotonic() - self._session_start_time
1292
+ except Exception:
1293
+ pass
1294
+
1295
+ # Store final response for copy
1296
+ if response_text:
1297
+ self._last_response = response_text
1298
+ self._streaming_response = response_text
1299
+ elif self._streaming_response:
1300
+ self._last_response = self._streaming_response
1301
+
1302
+ # Build rich summary panel
1303
+ summary_content = Text()
1304
+
1305
+ # 1. Header
1306
+ if success:
1307
+ summary_content.append("✅ Mission Accomplished", style=f"bold {THEME['success']}")
1308
+ else:
1309
+ summary_content.append("❌ Mission Failed", style=f"bold {THEME['error']}")
1310
+ summary_content.append("\n\n")
1311
+
1312
+ # 2. Tool Usage Stats
1313
+ tool_counts = {}
1314
+ for tool in getattr(self, "_tool_calls", []):
1315
+ name = tool.get("name", "Unknown")
1316
+ tool_counts[name] = tool_counts.get(name, 0) + 1
1317
+
1318
+ if tool_counts:
1319
+ summary_content.append("đŸ› ī¸ Tool Usage:\n", style="bold")
1320
+ for name, count in tool_counts.items():
1321
+ summary_content.append(f" â€ĸ {name}: ", style=THEME["text"])
1322
+ summary_content.append(f"{count}\n", style=f"bold {THEME['cyan']}")
1323
+ summary_content.append("\n")
1324
+
1325
+ # 3. Modified Files
1326
+ files_mod = getattr(self, "_files_modified", set())
1327
+ if files_mod:
1328
+ summary_content.append("📁 Files Impacted:\n", style="bold")
1329
+ for f in sorted(files_mod):
1330
+ summary_content.append(f" â€ĸ {f}\n", style=THEME["warning"])
1331
+ summary_content.append("\n")
1332
+
1333
+ # 4. Performance Stats Grid
1334
+ summary_content.append("📊 Stats:\n", style="bold")
1335
+ total_tokens = prompt_tokens + completion_tokens
1336
+
1337
+ stats_line = []
1338
+ if duration > 0:
1339
+ stats_line.append(f"⏱ {duration:.1f}s")
1340
+ if total_tokens > 0:
1341
+ stats_line.append(f"🔤 {total_tokens:,} toks")
1342
+ if cost > 0:
1343
+ stats_line.append(f"💰 ${cost:.4f}")
1344
+
1345
+ summary_content.append(" " + " â€ĸ ".join(stats_line), style=THEME["dim"])
1346
+ summary_content.append("\n")
1347
+
1348
+ # Create panel
1349
+ panel = Panel(
1350
+ summary_content,
1351
+ title="[bold]Session Report[/bold]",
1352
+ border_style=THEME["success"] if success else THEME["error"],
1353
+ box=ROUNDED,
1354
+ padding=(1, 2),
1355
+ )
1356
+
1357
+ self.write(panel)
1358
+
1359
+ # Copy hint
1360
+ footer = Text()
1361
+ footer.append("\n")
1362
+ footer.append(
1363
+ " [Shift+Drag to select text] â€ĸ [Ctrl+Shift+C to copy full response]",
1364
+ style=THEME["dim"],
1365
+ )
1366
+ footer.append("\n")
1367
+
1368
+ self.write(footer)
1369
+
1370
+ def get_thinking_text(self) -> str:
1371
+ """Get all thinking text for copying."""
1372
+ return "\n".join(getattr(self, "_thinking_lines", []))
1373
+
1374
+ def get_streaming_response(self) -> str:
1375
+ """Get the accumulated streaming response."""
1376
+ return getattr(self, "_streaming_response", "")
1377
+
1378
+
1379
+ class ApprovalWidget(Static):
1380
+ """Widget for accepting/rejecting agent file changes."""
1381
+
1382
+ DEFAULT_CSS = """
1383
+ ApprovalWidget {
1384
+ height: auto;
1385
+ padding: 1;
1386
+ margin: 1 0;
1387
+ background: #1a1a1a;
1388
+ border: round #3a3a3a;
1389
+ }
1390
+ """
1391
+
1392
+ def __init__(self, title: str, description: str = "", file_path: str = ""):
1393
+ super().__init__()
1394
+ self.title = title
1395
+ self.description = description
1396
+ self.file_path = file_path
1397
+ self.approved = None
1398
+
1399
+ def render(self) -> Text:
1400
+ t = Text()
1401
+ t.append(f"\n âš ī¸ ", style=f"bold {THEME['warning']}")
1402
+ t.append("Approval Required\n", style=f"bold {THEME['warning']}")
1403
+ t.append(f" {self.title}\n", style=THEME["text"])
1404
+ if self.file_path:
1405
+ t.append(f" 📄 {self.file_path}\n", style=THEME["muted"])
1406
+ if self.description:
1407
+ t.append(f" {self.description}\n", style=THEME["dim"])
1408
+ t.append("\n ", style="")
1409
+ t.append("[A]", style=f"bold {THEME['success']}")
1410
+ t.append(" Accept ", style=THEME["success"])
1411
+ t.append("[R]", style=f"bold {THEME['error']}")
1412
+ t.append(" Reject ", style=THEME["error"])
1413
+ t.append("[E]", style=f"bold {THEME['cyan']}")
1414
+ t.append(" Edit ", style=THEME["cyan"])
1415
+ t.append("[V]", style=f"bold {THEME['purple']}")
1416
+ t.append(" View Diff\n", style=THEME["purple"])
1417
+ return t
1418
+
1419
+
1420
+ class DiffDisplay(Static):
1421
+ """Display code diff with syntax highlighting."""
1422
+
1423
+ def __init__(self, file_path: str, old_content: str, new_content: str):
1424
+ super().__init__()
1425
+ self.file_path = file_path
1426
+ self.old_content = old_content
1427
+ self.new_content = new_content
1428
+
1429
+ def render(self) -> Text:
1430
+ t = Text()
1431
+
1432
+ t.append(f"\n 📄 ", style=f"bold {THEME['cyan']}")
1433
+ t.append(f"{self.file_path}\n", style=f"bold {THEME['cyan']}")
1434
+ t.append(f" ─" * 30 + "\n", style=THEME["border"])
1435
+
1436
+ old_lines = self.old_content.split("\n") if self.old_content else []
1437
+ new_lines = self.new_content.split("\n") if self.new_content else []
1438
+
1439
+ additions = 0
1440
+ deletions = 0
1441
+
1442
+ for line in old_lines:
1443
+ if line not in new_lines:
1444
+ t.append(f" - {line}\n", style=f"on #3d1f1f {THEME['error']}")
1445
+ deletions += 1
1446
+
1447
+ for line in new_lines:
1448
+ if line not in old_lines:
1449
+ t.append(f" + {line}\n", style=f"on #1f3d1f {THEME['success']}")
1450
+ additions += 1
1451
+
1452
+ t.append(f"\n 📊 ", style=THEME["cyan"])
1453
+ t.append(f"+{additions}", style=f"bold {THEME['success']}")
1454
+ t.append(" / ", style=THEME["muted"])
1455
+ t.append(f"-{deletions}", style=f"bold {THEME['error']}")
1456
+ t.append(" lines changed\n", style=THEME["muted"])
1457
+
1458
+ return t
1459
+
1460
+
1461
+ class PlanDisplay(Static):
1462
+ """Display agent's plan with task status."""
1463
+
1464
+ def __init__(self, tasks: list[dict[str, Any]]):
1465
+ super().__init__()
1466
+ self.tasks = tasks
1467
+
1468
+ def render(self) -> Text:
1469
+ t = Text()
1470
+ t.append(f"\n 📋 ", style=f"bold {THEME['purple']}")
1471
+ t.append("Agent Plan\n", style=f"bold {THEME['purple']}")
1472
+ t.append(f" ─" * 25 + "\n", style=THEME["border"])
1473
+
1474
+ status_icons = {
1475
+ "pending": ("âŗ", THEME["muted"]),
1476
+ "in_progress": ("🔄", THEME["cyan"]),
1477
+ "completed": ("✅", THEME["success"]),
1478
+ "failed": ("❌", THEME["error"]),
1479
+ }
1480
+
1481
+ for i, task in enumerate(self.tasks, 1):
1482
+ status = task.get("status", "pending")
1483
+ icon, color = status_icons.get(status, ("○", THEME["muted"]))
1484
+ priority = task.get("priority", "medium")
1485
+
1486
+ priority_badges = {
1487
+ "high": ("🔴", THEME["error"]),
1488
+ "medium": ("🟡", THEME["warning"]),
1489
+ "low": ("đŸŸĸ", THEME["success"]),
1490
+ }
1491
+ p_icon, p_color = priority_badges.get(priority, ("○", THEME["muted"]))
1492
+
1493
+ t.append(f" {icon} ", style=color)
1494
+ t.append(f"{i}. ", style=THEME["muted"])
1495
+ t.append(
1496
+ task.get("content", "Task"), style=color if status == "completed" else THEME["text"]
1497
+ )
1498
+ t.append(f" {p_icon}\n", style=p_color)
1499
+
1500
+ return t
1501
+
1502
+
1503
+ class ToolCallDisplay(Static):
1504
+ """Display tool calls made by the agent."""
1505
+
1506
+ def __init__(self, tool_name: str, status: str = "pending", title: str = "", content: str = ""):
1507
+ super().__init__()
1508
+ self.tool_name = tool_name
1509
+ self.status = status
1510
+ self.title = title or tool_name
1511
+ self.content = content
1512
+
1513
+ def render(self) -> Text:
1514
+ t = Text()
1515
+
1516
+ status_styles = {
1517
+ "pending": ("âŗ", THEME["muted"]),
1518
+ "in_progress": ("🔄", THEME["cyan"]),
1519
+ "completed": ("✅", THEME["success"]),
1520
+ "failed": ("❌", THEME["error"]),
1521
+ }
1522
+ icon, color = status_styles.get(self.status, ("🔧", THEME["purple"]))
1523
+
1524
+ t.append(f" {icon} ", style=color)
1525
+ t.append("🔧 ", style=THEME["orange"])
1526
+ t.append(self.title, style=f"bold {color}")
1527
+
1528
+ if self.status == "completed":
1529
+ t.append(" ✔", style=THEME["success"])
1530
+ elif self.status == "failed":
1531
+ t.append(" ✗", style=THEME["error"])
1532
+
1533
+ t.append("\n", style="")
1534
+
1535
+ if self.content:
1536
+ content = self.content[:200] + "..." if len(self.content) > 200 else self.content
1537
+ t.append(f" {content}\n", style=THEME["dim"])
1538
+
1539
+ return t
1540
+
1541
+
1542
+ class FlashMessage(Static):
1543
+ """Quick flash notification message."""
1544
+
1545
+ def __init__(self, message: str, style: str = "default"):
1546
+ super().__init__()
1547
+ self.message = message
1548
+ self.flash_style = style
1549
+
1550
+ def render(self) -> Text:
1551
+ t = Text()
1552
+
1553
+ style_config = {
1554
+ "default": ("â„šī¸", THEME["cyan"], "#0a2a3a"),
1555
+ "success": ("✅", THEME["success"], "#0a3a1a"),
1556
+ "warning": ("âš ī¸", THEME["warning"], "#3a2a0a"),
1557
+ "error": ("❌", THEME["error"], "#3a0a0a"),
1558
+ }
1559
+
1560
+ icon, color, _ = style_config.get(self.flash_style, style_config["default"])
1561
+
1562
+ t.append(f" {icon} ", style=f"bold {color}")
1563
+ t.append(self.message, style=f"{color}")
1564
+
1565
+ return t
1566
+
1567
+
1568
+ class DangerWarning(Static):
1569
+ """Warning for dangerous operations."""
1570
+
1571
+ def __init__(self, level: str = "warning", message: str = ""):
1572
+ super().__init__()
1573
+ self.level = level
1574
+ self.message = message
1575
+
1576
+ def render(self) -> Text:
1577
+ t = Text()
1578
+
1579
+ if self.level == "destructive":
1580
+ t.append(f"\n 🚨 ", style=f"bold {THEME['error']}")
1581
+ t.append("DESTRUCTIVE OPERATION", style=f"bold {THEME['error']}")
1582
+ t.append("\n May alter files outside project directory!\n", style=THEME["error"])
1583
+ else:
1584
+ t.append(f"\n âš ī¸ ", style=f"bold {THEME['warning']}")
1585
+ t.append("Potentially Dangerous", style=f"bold {THEME['warning']}")
1586
+ t.append("\n Please review carefully before approving.\n", style=THEME["warning"])
1587
+
1588
+ if self.message:
1589
+ t.append(f" {self.message}\n", style=THEME["muted"])
1590
+
1591
+ return t