code-puppy 0.0.169__py3-none-any.whl → 0.0.366__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 (243) hide show
  1. code_puppy/__init__.py +7 -1
  2. code_puppy/agents/__init__.py +8 -8
  3. code_puppy/agents/agent_c_reviewer.py +155 -0
  4. code_puppy/agents/agent_code_puppy.py +9 -2
  5. code_puppy/agents/agent_code_reviewer.py +90 -0
  6. code_puppy/agents/agent_cpp_reviewer.py +132 -0
  7. code_puppy/agents/agent_creator_agent.py +48 -9
  8. code_puppy/agents/agent_golang_reviewer.py +151 -0
  9. code_puppy/agents/agent_javascript_reviewer.py +160 -0
  10. code_puppy/agents/agent_manager.py +146 -199
  11. code_puppy/agents/agent_pack_leader.py +383 -0
  12. code_puppy/agents/agent_planning.py +163 -0
  13. code_puppy/agents/agent_python_programmer.py +165 -0
  14. code_puppy/agents/agent_python_reviewer.py +90 -0
  15. code_puppy/agents/agent_qa_expert.py +163 -0
  16. code_puppy/agents/agent_qa_kitten.py +208 -0
  17. code_puppy/agents/agent_security_auditor.py +181 -0
  18. code_puppy/agents/agent_terminal_qa.py +323 -0
  19. code_puppy/agents/agent_typescript_reviewer.py +166 -0
  20. code_puppy/agents/base_agent.py +1713 -1
  21. code_puppy/agents/event_stream_handler.py +350 -0
  22. code_puppy/agents/json_agent.py +12 -1
  23. code_puppy/agents/pack/__init__.py +34 -0
  24. code_puppy/agents/pack/bloodhound.py +304 -0
  25. code_puppy/agents/pack/husky.py +321 -0
  26. code_puppy/agents/pack/retriever.py +393 -0
  27. code_puppy/agents/pack/shepherd.py +348 -0
  28. code_puppy/agents/pack/terrier.py +287 -0
  29. code_puppy/agents/pack/watchdog.py +367 -0
  30. code_puppy/agents/prompt_reviewer.py +145 -0
  31. code_puppy/agents/subagent_stream_handler.py +276 -0
  32. code_puppy/api/__init__.py +13 -0
  33. code_puppy/api/app.py +169 -0
  34. code_puppy/api/main.py +21 -0
  35. code_puppy/api/pty_manager.py +446 -0
  36. code_puppy/api/routers/__init__.py +12 -0
  37. code_puppy/api/routers/agents.py +36 -0
  38. code_puppy/api/routers/commands.py +217 -0
  39. code_puppy/api/routers/config.py +74 -0
  40. code_puppy/api/routers/sessions.py +232 -0
  41. code_puppy/api/templates/terminal.html +361 -0
  42. code_puppy/api/websocket.py +154 -0
  43. code_puppy/callbacks.py +174 -4
  44. code_puppy/chatgpt_codex_client.py +283 -0
  45. code_puppy/claude_cache_client.py +586 -0
  46. code_puppy/cli_runner.py +916 -0
  47. code_puppy/command_line/add_model_menu.py +1079 -0
  48. code_puppy/command_line/agent_menu.py +395 -0
  49. code_puppy/command_line/attachments.py +395 -0
  50. code_puppy/command_line/autosave_menu.py +605 -0
  51. code_puppy/command_line/clipboard.py +527 -0
  52. code_puppy/command_line/colors_menu.py +520 -0
  53. code_puppy/command_line/command_handler.py +233 -627
  54. code_puppy/command_line/command_registry.py +150 -0
  55. code_puppy/command_line/config_commands.py +715 -0
  56. code_puppy/command_line/core_commands.py +792 -0
  57. code_puppy/command_line/diff_menu.py +863 -0
  58. code_puppy/command_line/load_context_completion.py +15 -22
  59. code_puppy/command_line/mcp/base.py +1 -4
  60. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  61. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  62. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  63. code_puppy/command_line/mcp/edit_command.py +148 -0
  64. code_puppy/command_line/mcp/handler.py +9 -4
  65. code_puppy/command_line/mcp/help_command.py +6 -5
  66. code_puppy/command_line/mcp/install_command.py +16 -27
  67. code_puppy/command_line/mcp/install_menu.py +685 -0
  68. code_puppy/command_line/mcp/list_command.py +3 -3
  69. code_puppy/command_line/mcp/logs_command.py +174 -65
  70. code_puppy/command_line/mcp/remove_command.py +2 -2
  71. code_puppy/command_line/mcp/restart_command.py +12 -4
  72. code_puppy/command_line/mcp/search_command.py +17 -11
  73. code_puppy/command_line/mcp/start_all_command.py +22 -13
  74. code_puppy/command_line/mcp/start_command.py +50 -31
  75. code_puppy/command_line/mcp/status_command.py +6 -7
  76. code_puppy/command_line/mcp/stop_all_command.py +11 -8
  77. code_puppy/command_line/mcp/stop_command.py +11 -10
  78. code_puppy/command_line/mcp/test_command.py +2 -2
  79. code_puppy/command_line/mcp/utils.py +1 -1
  80. code_puppy/command_line/mcp/wizard_utils.py +22 -18
  81. code_puppy/command_line/mcp_completion.py +174 -0
  82. code_puppy/command_line/model_picker_completion.py +89 -30
  83. code_puppy/command_line/model_settings_menu.py +884 -0
  84. code_puppy/command_line/motd.py +14 -8
  85. code_puppy/command_line/onboarding_slides.py +179 -0
  86. code_puppy/command_line/onboarding_wizard.py +340 -0
  87. code_puppy/command_line/pin_command_completion.py +329 -0
  88. code_puppy/command_line/prompt_toolkit_completion.py +626 -75
  89. code_puppy/command_line/session_commands.py +296 -0
  90. code_puppy/command_line/utils.py +54 -0
  91. code_puppy/config.py +1181 -51
  92. code_puppy/error_logging.py +118 -0
  93. code_puppy/gemini_code_assist.py +385 -0
  94. code_puppy/gemini_model.py +602 -0
  95. code_puppy/http_utils.py +220 -104
  96. code_puppy/keymap.py +128 -0
  97. code_puppy/main.py +5 -594
  98. code_puppy/{mcp → mcp_}/__init__.py +17 -0
  99. code_puppy/{mcp → mcp_}/async_lifecycle.py +35 -4
  100. code_puppy/{mcp → mcp_}/blocking_startup.py +70 -43
  101. code_puppy/{mcp → mcp_}/captured_stdio_server.py +2 -2
  102. code_puppy/{mcp → mcp_}/config_wizard.py +5 -5
  103. code_puppy/{mcp → mcp_}/dashboard.py +15 -6
  104. code_puppy/{mcp → mcp_}/examples/retry_example.py +4 -1
  105. code_puppy/{mcp → mcp_}/managed_server.py +66 -39
  106. code_puppy/{mcp → mcp_}/manager.py +146 -52
  107. code_puppy/mcp_/mcp_logs.py +224 -0
  108. code_puppy/{mcp → mcp_}/registry.py +6 -6
  109. code_puppy/{mcp → mcp_}/server_registry_catalog.py +25 -8
  110. code_puppy/messaging/__init__.py +199 -2
  111. code_puppy/messaging/bus.py +610 -0
  112. code_puppy/messaging/commands.py +167 -0
  113. code_puppy/messaging/markdown_patches.py +57 -0
  114. code_puppy/messaging/message_queue.py +17 -48
  115. code_puppy/messaging/messages.py +500 -0
  116. code_puppy/messaging/queue_console.py +1 -24
  117. code_puppy/messaging/renderers.py +43 -146
  118. code_puppy/messaging/rich_renderer.py +1027 -0
  119. code_puppy/messaging/spinner/__init__.py +33 -5
  120. code_puppy/messaging/spinner/console_spinner.py +92 -52
  121. code_puppy/messaging/spinner/spinner_base.py +29 -0
  122. code_puppy/messaging/subagent_console.py +461 -0
  123. code_puppy/model_factory.py +686 -80
  124. code_puppy/model_utils.py +167 -0
  125. code_puppy/models.json +86 -104
  126. code_puppy/models_dev_api.json +1 -0
  127. code_puppy/models_dev_parser.py +592 -0
  128. code_puppy/plugins/__init__.py +164 -10
  129. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  130. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  131. code_puppy/plugins/antigravity_oauth/antigravity_model.py +704 -0
  132. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  133. code_puppy/plugins/antigravity_oauth/constants.py +136 -0
  134. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  135. code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
  136. code_puppy/plugins/antigravity_oauth/storage.py +271 -0
  137. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  138. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  139. code_puppy/plugins/antigravity_oauth/transport.py +767 -0
  140. code_puppy/plugins/antigravity_oauth/utils.py +169 -0
  141. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  142. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  143. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
  144. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
  145. code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
  146. code_puppy/plugins/chatgpt_oauth/utils.py +489 -0
  147. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  148. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  149. code_puppy/plugins/claude_code_oauth/__init__.py +6 -0
  150. code_puppy/plugins/claude_code_oauth/config.py +50 -0
  151. code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
  152. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  153. code_puppy/plugins/claude_code_oauth/utils.py +518 -0
  154. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  155. code_puppy/plugins/customizable_commands/register_callbacks.py +169 -0
  156. code_puppy/plugins/example_custom_command/README.md +280 -0
  157. code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
  158. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  159. code_puppy/plugins/file_permission_handler/register_callbacks.py +523 -0
  160. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  161. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  162. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  163. code_puppy/plugins/oauth_puppy_html.py +228 -0
  164. code_puppy/plugins/shell_safety/__init__.py +6 -0
  165. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  166. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  167. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  168. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  169. code_puppy/prompts/codex_system_prompt.md +310 -0
  170. code_puppy/pydantic_patches.py +131 -0
  171. code_puppy/reopenable_async_client.py +8 -8
  172. code_puppy/round_robin_model.py +10 -15
  173. code_puppy/session_storage.py +294 -0
  174. code_puppy/status_display.py +21 -4
  175. code_puppy/summarization_agent.py +52 -14
  176. code_puppy/terminal_utils.py +418 -0
  177. code_puppy/tools/__init__.py +139 -6
  178. code_puppy/tools/agent_tools.py +548 -49
  179. code_puppy/tools/browser/__init__.py +37 -0
  180. code_puppy/tools/browser/browser_control.py +289 -0
  181. code_puppy/tools/browser/browser_interactions.py +545 -0
  182. code_puppy/tools/browser/browser_locators.py +640 -0
  183. code_puppy/tools/browser/browser_manager.py +316 -0
  184. code_puppy/tools/browser/browser_navigation.py +251 -0
  185. code_puppy/tools/browser/browser_screenshot.py +179 -0
  186. code_puppy/tools/browser/browser_scripts.py +462 -0
  187. code_puppy/tools/browser/browser_workflows.py +221 -0
  188. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  189. code_puppy/tools/browser/terminal_command_tools.py +521 -0
  190. code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
  191. code_puppy/tools/browser/terminal_tools.py +525 -0
  192. code_puppy/tools/command_runner.py +941 -153
  193. code_puppy/tools/common.py +1146 -6
  194. code_puppy/tools/display.py +84 -0
  195. code_puppy/tools/file_modifications.py +288 -89
  196. code_puppy/tools/file_operations.py +352 -266
  197. code_puppy/tools/subagent_context.py +158 -0
  198. code_puppy/uvx_detection.py +242 -0
  199. code_puppy/version_checker.py +30 -11
  200. code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
  201. code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
  202. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/METADATA +184 -67
  203. code_puppy-0.0.366.dist-info/RECORD +217 -0
  204. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
  205. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +1 -0
  206. code_puppy/agent.py +0 -231
  207. code_puppy/agents/agent_orchestrator.json +0 -26
  208. code_puppy/agents/runtime_manager.py +0 -272
  209. code_puppy/command_line/mcp/add_command.py +0 -183
  210. code_puppy/command_line/meta_command_handler.py +0 -153
  211. code_puppy/message_history_processor.py +0 -490
  212. code_puppy/messaging/spinner/textual_spinner.py +0 -101
  213. code_puppy/state_management.py +0 -200
  214. code_puppy/tui/__init__.py +0 -10
  215. code_puppy/tui/app.py +0 -986
  216. code_puppy/tui/components/__init__.py +0 -21
  217. code_puppy/tui/components/chat_view.py +0 -550
  218. code_puppy/tui/components/command_history_modal.py +0 -218
  219. code_puppy/tui/components/copy_button.py +0 -139
  220. code_puppy/tui/components/custom_widgets.py +0 -63
  221. code_puppy/tui/components/human_input_modal.py +0 -175
  222. code_puppy/tui/components/input_area.py +0 -167
  223. code_puppy/tui/components/sidebar.py +0 -309
  224. code_puppy/tui/components/status_bar.py +0 -182
  225. code_puppy/tui/messages.py +0 -27
  226. code_puppy/tui/models/__init__.py +0 -8
  227. code_puppy/tui/models/chat_message.py +0 -25
  228. code_puppy/tui/models/command_history.py +0 -89
  229. code_puppy/tui/models/enums.py +0 -24
  230. code_puppy/tui/screens/__init__.py +0 -15
  231. code_puppy/tui/screens/help.py +0 -130
  232. code_puppy/tui/screens/mcp_install_wizard.py +0 -803
  233. code_puppy/tui/screens/settings.py +0 -290
  234. code_puppy/tui/screens/tools.py +0 -74
  235. code_puppy-0.0.169.data/data/code_puppy/models.json +0 -128
  236. code_puppy-0.0.169.dist-info/RECORD +0 -112
  237. /code_puppy/{mcp → mcp_}/circuit_breaker.py +0 -0
  238. /code_puppy/{mcp → mcp_}/error_isolation.py +0 -0
  239. /code_puppy/{mcp → mcp_}/health_monitor.py +0 -0
  240. /code_puppy/{mcp → mcp_}/retry_manager.py +0 -0
  241. /code_puppy/{mcp → mcp_}/status_tracker.py +0 -0
  242. /code_puppy/{mcp → mcp_}/system_tools.py +0 -0
  243. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/licenses/LICENSE +0 -0
@@ -1,21 +0,0 @@
1
- """
2
- TUI components package.
3
- """
4
-
5
- from .chat_view import ChatView
6
- from .copy_button import CopyButton
7
- from .custom_widgets import CustomTextArea
8
- from .input_area import InputArea, SimpleSpinnerWidget, SubmitCancelButton
9
- from .sidebar import Sidebar
10
- from .status_bar import StatusBar
11
-
12
- __all__ = [
13
- "CustomTextArea",
14
- "StatusBar",
15
- "ChatView",
16
- "CopyButton",
17
- "InputArea",
18
- "SimpleSpinnerWidget",
19
- "SubmitCancelButton",
20
- "Sidebar",
21
- ]
@@ -1,550 +0,0 @@
1
- """
2
- Chat view component for displaying conversation history.
3
- """
4
-
5
- import re
6
- from typing import List
7
-
8
- from rich.console import Group
9
- from rich.markdown import Markdown
10
- from rich.syntax import Syntax
11
- from rich.text import Text
12
- from textual import on
13
- from textual.containers import Vertical, VerticalScroll
14
- from textual.widgets import Static
15
-
16
- from ..models import ChatMessage, MessageType
17
- from .copy_button import CopyButton
18
-
19
-
20
- class ChatView(VerticalScroll):
21
- """Main chat interface displaying conversation history."""
22
-
23
- DEFAULT_CSS = """
24
- ChatView {
25
- background: $background;
26
- scrollbar-background: $primary;
27
- scrollbar-color: $accent;
28
- margin: 0 0 1 0;
29
- padding: 0;
30
- }
31
-
32
- .user-message {
33
- background: $primary-darken-3;
34
- color: #ffffff;
35
- margin: 0 0 1 0;
36
- margin-top: 0;
37
- padding: 1;
38
- padding-top: 1;
39
- text-wrap: wrap;
40
- border: none;
41
- border-left: thick $accent;
42
- text-style: bold;
43
- }
44
-
45
- .agent-message {
46
- background: transparent;
47
- color: #f3f4f6;
48
- margin: 0 0 1 0;
49
- margin-top: 0;
50
- padding: 0;
51
- padding-top: 0;
52
- text-wrap: wrap;
53
- border: none;
54
- }
55
-
56
- .system-message {
57
- background: transparent;
58
- color: #d1d5db;
59
- margin: 0 0 1 0;
60
- margin-top: 0;
61
- padding: 0;
62
- padding-top: 0;
63
- text-style: italic;
64
- text-wrap: wrap;
65
- border: none;
66
- }
67
-
68
- .error-message {
69
- background: transparent;
70
- color: #fef2f2;
71
- margin: 0 0 1 0;
72
- margin-top: 0;
73
- padding: 0;
74
- padding-top: 0;
75
- text-wrap: wrap;
76
- border: none;
77
- }
78
-
79
- .agent_reasoning-message {
80
- background: transparent;
81
- color: #f3e8ff;
82
- margin: 0 0 1 0;
83
- margin-top: 0;
84
- padding: 0;
85
- padding-top: 0;
86
- text-wrap: wrap;
87
- text-style: italic;
88
- border: none;
89
- }
90
-
91
- .planned_next_steps-message {
92
- background: transparent;
93
- color: #f3e8ff;
94
- margin: 0 0 1 0;
95
- margin-top: 0;
96
- padding: 0;
97
- padding-top: 0;
98
- text-wrap: wrap;
99
- text-style: italic;
100
- border: none;
101
- }
102
-
103
- .agent_response-message {
104
- background: transparent;
105
- color: #f3e8ff;
106
- margin: 0 0 1 0;
107
- margin-top: 0;
108
- padding: 0;
109
- padding-top: 0;
110
- text-wrap: wrap;
111
- border: none;
112
- }
113
-
114
- .info-message {
115
- background: transparent;
116
- color: #d1fae5;
117
- margin: 0 0 1 0;
118
- margin-top: 0;
119
- padding: 0;
120
- padding-top: 0;
121
- text-wrap: wrap;
122
- border: none;
123
- }
124
-
125
- .success-message {
126
- background: #0d9488;
127
- color: #d1fae5;
128
- margin: 0 0 1 0;
129
- margin-top: 0;
130
- padding: 0;
131
- padding-top: 0;
132
- text-wrap: wrap;
133
- border: none;
134
- }
135
-
136
- .warning-message {
137
- background: #d97706;
138
- color: #fef3c7;
139
- margin: 0 0 1 0;
140
- margin-top: 0;
141
- padding: 0;
142
- padding-top: 0;
143
- text-wrap: wrap;
144
- border: none;
145
- }
146
-
147
- .tool_output-message {
148
- background: #5b21b6;
149
- color: #dbeafe;
150
- margin: 0 0 1 0;
151
- margin-top: 0;
152
- padding: 0;
153
- padding-top: 0;
154
- text-wrap: wrap;
155
- border: none;
156
- }
157
-
158
- .command_output-message {
159
- background: #9a3412;
160
- color: #fed7aa;
161
- margin: 0 0 1 0;
162
- margin-top: 0;
163
- padding: 0;
164
- padding-top: 0;
165
- text-wrap: wrap;
166
- border: none;
167
- }
168
-
169
- .message-container {
170
- margin: 0 0 1 0;
171
- padding: 0;
172
- width: 1fr;
173
- }
174
-
175
- .copy-button-container {
176
- margin: 0 0 1 0;
177
- padding: 0 1;
178
- width: 1fr;
179
- height: auto;
180
- align: left top;
181
- }
182
-
183
- /* Ensure first message has no top spacing */
184
- ChatView > *:first-child {
185
- margin-top: 0;
186
- padding-top: 0;
187
- }
188
- """
189
-
190
- def __init__(self, **kwargs):
191
- super().__init__(**kwargs)
192
- self.messages: List[ChatMessage] = []
193
- self.message_groups: dict = {} # Track groups for visual grouping
194
- self.group_widgets: dict = {} # Track widgets by group_id for enhanced grouping
195
- self._scroll_pending = False # Track if scroll is already scheduled
196
-
197
- def _render_agent_message_with_syntax(self, prefix: str, content: str):
198
- """Render agent message with proper syntax highlighting for code blocks."""
199
- # Split content by code blocks
200
- parts = re.split(r"(```[\s\S]*?```)", content)
201
- rendered_parts = []
202
-
203
- # Add prefix as the first part
204
- rendered_parts.append(Text(prefix, style="bold"))
205
-
206
- for i, part in enumerate(parts):
207
- if part.startswith("```") and part.endswith("```"):
208
- # This is a code block
209
- lines = part.strip("`").split("\n")
210
- if lines:
211
- # First line might contain language identifier
212
- language = lines[0].strip() if lines[0].strip() else "text"
213
- code_content = "\n".join(lines[1:]) if len(lines) > 1 else ""
214
-
215
- if code_content.strip():
216
- # Create syntax highlighted code
217
- try:
218
- syntax = Syntax(
219
- code_content,
220
- language,
221
- theme="github-dark",
222
- background_color="default",
223
- line_numbers=True,
224
- word_wrap=True,
225
- )
226
- rendered_parts.append(syntax)
227
- except Exception:
228
- # Fallback to plain text if syntax highlighting fails
229
- rendered_parts.append(Text(part))
230
- else:
231
- rendered_parts.append(Text(part))
232
- else:
233
- rendered_parts.append(Text(part))
234
- else:
235
- # Regular text
236
- if part.strip():
237
- rendered_parts.append(Text(part))
238
-
239
- return Group(*rendered_parts)
240
-
241
- def _append_to_existing_group(self, message: ChatMessage) -> None:
242
- """Append a message to an existing group by group_id."""
243
- if message.group_id not in self.group_widgets:
244
- # If group doesn't exist, fall back to normal message creation
245
- return
246
-
247
- # Find the most recent message in this group to append to
248
- group_widgets = self.group_widgets[message.group_id]
249
- if not group_widgets:
250
- return
251
-
252
- # Get the last widget entry for this group
253
- last_entry = group_widgets[-1]
254
- last_message = last_entry["message"]
255
- last_widget = last_entry["widget"]
256
- copy_button = last_entry.get("copy_button")
257
-
258
- # Create a separator for different message types in the same group
259
- if message.type != last_message.type:
260
- separator = "\n" + "─" * 40 + "\n"
261
- else:
262
- separator = "\n"
263
-
264
- # Handle content concatenation carefully to preserve Rich objects
265
- if hasattr(last_message.content, "__rich_console__") or hasattr(
266
- message.content, "__rich_console__"
267
- ):
268
- # If either content is a Rich object, convert both to text and concatenate
269
- from io import StringIO
270
- from rich.console import Console
271
-
272
- # Convert existing content to string
273
- if hasattr(last_message.content, "__rich_console__"):
274
- string_io = StringIO()
275
- temp_console = Console(
276
- file=string_io, width=80, legacy_windows=False, markup=False
277
- )
278
- temp_console.print(last_message.content)
279
- existing_content = string_io.getvalue().rstrip("\n")
280
- else:
281
- existing_content = str(last_message.content)
282
-
283
- # Convert new content to string
284
- if hasattr(message.content, "__rich_console__"):
285
- string_io = StringIO()
286
- temp_console = Console(
287
- file=string_io, width=80, legacy_windows=False, markup=False
288
- )
289
- temp_console.print(message.content)
290
- new_content = string_io.getvalue().rstrip("\n")
291
- else:
292
- new_content = str(message.content)
293
-
294
- # Combine as plain text
295
- last_message.content = existing_content + separator + new_content
296
- else:
297
- # Both are strings, safe to concatenate
298
- last_message.content += separator + message.content
299
-
300
- # Update the widget based on message type
301
- if last_message.type == MessageType.AGENT_RESPONSE:
302
- # Re-render agent response with updated content
303
- prefix = "AGENT RESPONSE:\n"
304
- try:
305
- md = Markdown(last_message.content)
306
- header = Text(prefix, style="bold")
307
- group_content = Group(header, md)
308
- last_widget.update(group_content)
309
- except Exception:
310
- full_content = f"{prefix}{last_message.content}"
311
- last_widget.update(Text(full_content))
312
-
313
- # Update the copy button if it exists
314
- if copy_button:
315
- copy_button.update_text_to_copy(last_message.content)
316
- else:
317
- # Handle other message types
318
- # After the content concatenation above, content is always a string
319
- # Try to parse markup when safe to do so
320
- try:
321
- # Try to parse as markup first - this handles rich styling correctly
322
- last_widget.update(Text.from_markup(last_message.content))
323
- except Exception:
324
- # If markup parsing fails, fall back to plain text
325
- # This handles cases where content contains literal square brackets
326
- last_widget.update(Text(last_message.content))
327
-
328
- # Add the new message to our tracking lists
329
- self.messages.append(message)
330
- if message.group_id in self.message_groups:
331
- self.message_groups[message.group_id].append(message)
332
-
333
- # Auto-scroll to bottom with refresh to fix scroll bar issues (debounced)
334
- self._schedule_scroll()
335
-
336
- def add_message(self, message: ChatMessage) -> None:
337
- """Add a new message to the chat view."""
338
- # Enhanced grouping: check if we can append to ANY existing group
339
- if message.group_id is not None and message.group_id in self.group_widgets:
340
- self._append_to_existing_group(message)
341
- return
342
-
343
- # Old logic for consecutive grouping (keeping as fallback)
344
- if (
345
- message.group_id is not None
346
- and self.messages
347
- and self.messages[-1].group_id == message.group_id
348
- ):
349
- # This case should now be handled by _append_to_existing_group above
350
- # but keeping for safety
351
- self._append_to_existing_group(message)
352
- return
353
-
354
- # Add to messages list
355
- self.messages.append(message)
356
-
357
- # Track groups for potential future use
358
- if message.group_id:
359
- if message.group_id not in self.message_groups:
360
- self.message_groups[message.group_id] = []
361
- self.message_groups[message.group_id].append(message)
362
-
363
- # Create the message widget
364
- css_class = f"{message.type.value}-message"
365
-
366
- if message.type == MessageType.USER:
367
- # Add user indicator and make it stand out
368
- content_lines = message.content.split("\n")
369
- if len(content_lines) > 1:
370
- # Multi-line user message
371
- formatted_content = f"╔══ USER ══╗\n{message.content}\n╚══════════╝"
372
- else:
373
- # Single line user message
374
- formatted_content = f"▶ USER: {message.content}"
375
-
376
- message_widget = Static(Text(formatted_content), classes=css_class)
377
- # User messages are not collapsible - mount directly
378
- self.mount(message_widget)
379
- # Auto-scroll to bottom
380
- self._schedule_scroll()
381
- return
382
- elif message.type == MessageType.AGENT:
383
- prefix = "AGENT: "
384
- content = f"{message.content}"
385
- message_widget = Static(
386
- Text.from_markup(message.content), classes=css_class
387
- )
388
- # Try to render markup
389
- try:
390
- message_widget = Static(Text.from_markup(content), classes=css_class)
391
- except Exception:
392
- message_widget = Static(Text(content), classes=css_class)
393
-
394
- elif message.type == MessageType.SYSTEM:
395
- # Check if content is a Rich object (like Markdown)
396
- if hasattr(message.content, "__rich_console__"):
397
- # Render Rich objects directly (like Markdown)
398
- message_widget = Static(message.content, classes=css_class)
399
- else:
400
- content = f"{message.content}"
401
- # Try to render markup
402
- try:
403
- message_widget = Static(
404
- Text.from_markup(content), classes=css_class
405
- )
406
- except Exception:
407
- message_widget = Static(Text(content), classes=css_class)
408
-
409
- elif message.type == MessageType.AGENT_REASONING:
410
- prefix = "AGENT REASONING:\n"
411
- content = f"{prefix}{message.content}"
412
- message_widget = Static(Text(content), classes=css_class)
413
- elif message.type == MessageType.PLANNED_NEXT_STEPS:
414
- prefix = "PLANNED NEXT STEPS:\n"
415
- content = f"{prefix}{message.content}"
416
- message_widget = Static(Text(content), classes=css_class)
417
- elif message.type == MessageType.AGENT_RESPONSE:
418
- prefix = "AGENT RESPONSE:\n"
419
- content = message.content
420
-
421
- try:
422
- # First try to render as markdown with proper syntax highlighting
423
- md = Markdown(content)
424
- # Create a group with the header and markdown content
425
- header = Text(prefix, style="bold")
426
- group_content = Group(header, md)
427
- message_widget = Static(group_content, classes=css_class)
428
- except Exception:
429
- # If markdown parsing fails, fall back to simple text display
430
- full_content = f"{prefix}{content}"
431
- message_widget = Static(Text(full_content), classes=css_class)
432
-
433
- # Try to create copy button - use simpler approach
434
- try:
435
- # Create copy button for agent responses
436
- copy_button = CopyButton(content) # Copy the raw content without prefix
437
-
438
- # Mount the message first
439
- self.mount(message_widget)
440
-
441
- # Then mount the copy button directly
442
- self.mount(copy_button)
443
-
444
- # Track both the widget and copy button for group-based updates
445
- if message.group_id:
446
- if message.group_id not in self.group_widgets:
447
- self.group_widgets[message.group_id] = []
448
- self.group_widgets[message.group_id].append(
449
- {
450
- "message": message,
451
- "widget": message_widget,
452
- "copy_button": copy_button,
453
- }
454
- )
455
-
456
- # Auto-scroll to bottom with refresh to fix scroll bar issues (debounced)
457
- self._schedule_scroll()
458
- return # Early return only if copy button creation succeeded
459
-
460
- except Exception as e:
461
- # If copy button creation fails, fall back to normal message display
462
- # Log the error but don't let it prevent the message from showing
463
- import sys
464
-
465
- print(f"Warning: Copy button creation failed: {e}", file=sys.stderr)
466
- # Continue to normal message mounting below
467
- elif message.type == MessageType.INFO:
468
- prefix = "INFO: "
469
- content = f"{prefix}{message.content}"
470
- message_widget = Static(Text(content), classes=css_class)
471
- elif message.type == MessageType.SUCCESS:
472
- prefix = "SUCCESS: "
473
- content = f"{prefix}{message.content}"
474
- message_widget = Static(Text(content), classes=css_class)
475
- elif message.type == MessageType.WARNING:
476
- prefix = "WARNING: "
477
- content = f"{prefix}{message.content}"
478
- message_widget = Static(Text(content), classes=css_class)
479
- elif message.type == MessageType.TOOL_OUTPUT:
480
- prefix = "TOOL OUTPUT: "
481
- content = f"{prefix}{message.content}"
482
- message_widget = Static(Text(content), classes=css_class)
483
- elif message.type == MessageType.COMMAND_OUTPUT:
484
- prefix = "COMMAND: "
485
- content = f"{prefix}{message.content}"
486
- message_widget = Static(Text(content), classes=css_class)
487
- else: # ERROR and fallback
488
- prefix = "Error: " if message.type == MessageType.ERROR else "Unknown: "
489
- content = f"{prefix}{message.content}"
490
- message_widget = Static(Text(content), classes=css_class)
491
-
492
- self.mount(message_widget)
493
-
494
- # Track the widget for group-based updates
495
- if message.group_id:
496
- if message.group_id not in self.group_widgets:
497
- self.group_widgets[message.group_id] = []
498
- self.group_widgets[message.group_id].append(
499
- {
500
- "message": message,
501
- "widget": message_widget,
502
- "copy_button": None, # Will be set if created
503
- }
504
- )
505
-
506
- # Auto-scroll to bottom with refresh to fix scroll bar issues (debounced)
507
- self._schedule_scroll()
508
-
509
- def clear_messages(self) -> None:
510
- """Clear all messages from the chat view."""
511
- self.messages.clear()
512
- self.message_groups.clear() # Clear groups too
513
- self.group_widgets.clear() # Clear widget tracking too
514
- # Remove all message widgets (Static widgets, CopyButtons, and any Vertical containers)
515
- for widget in self.query(Static):
516
- widget.remove()
517
- for widget in self.query(CopyButton):
518
- widget.remove()
519
- for widget in self.query(Vertical):
520
- widget.remove()
521
-
522
- @on(CopyButton.CopyCompleted)
523
- def on_copy_completed(self, event: CopyButton.CopyCompleted) -> None:
524
- """Handle copy button completion events."""
525
- if event.success:
526
- # Could add a temporary success message or visual feedback
527
- # For now, the button itself provides visual feedback
528
- pass
529
- else:
530
- # Show error message in chat if copy failed
531
- from datetime import datetime, timezone
532
-
533
- error_message = ChatMessage(
534
- id=f"copy_error_{datetime.now(timezone.utc).timestamp()}",
535
- type=MessageType.ERROR,
536
- content=f"Failed to copy to clipboard: {event.error}",
537
- timestamp=datetime.now(timezone.utc),
538
- )
539
- self.add_message(error_message)
540
-
541
- def _schedule_scroll(self) -> None:
542
- """Schedule a scroll operation, avoiding duplicate calls."""
543
- if not self._scroll_pending:
544
- self._scroll_pending = True
545
- self.call_after_refresh(self._do_scroll)
546
-
547
- def _do_scroll(self) -> None:
548
- """Perform the actual scroll operation."""
549
- self._scroll_pending = False
550
- self.scroll_end(animate=False)