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