code-puppy 0.0.97__py3-none-any.whl → 0.0.119__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.
- code_puppy/__init__.py +2 -5
- code_puppy/__main__.py +10 -0
- code_puppy/agent.py +125 -40
- code_puppy/agent_prompts.py +30 -24
- code_puppy/callbacks.py +152 -0
- code_puppy/command_line/command_handler.py +359 -0
- code_puppy/command_line/load_context_completion.py +59 -0
- code_puppy/command_line/model_picker_completion.py +14 -21
- code_puppy/command_line/motd.py +44 -28
- code_puppy/command_line/prompt_toolkit_completion.py +42 -23
- code_puppy/config.py +266 -26
- code_puppy/http_utils.py +122 -0
- code_puppy/main.py +570 -383
- code_puppy/message_history_processor.py +195 -104
- code_puppy/messaging/__init__.py +46 -0
- code_puppy/messaging/message_queue.py +288 -0
- code_puppy/messaging/queue_console.py +293 -0
- code_puppy/messaging/renderers.py +305 -0
- code_puppy/messaging/spinner/__init__.py +55 -0
- code_puppy/messaging/spinner/console_spinner.py +200 -0
- code_puppy/messaging/spinner/spinner_base.py +66 -0
- code_puppy/messaging/spinner/textual_spinner.py +97 -0
- code_puppy/model_factory.py +73 -105
- code_puppy/plugins/__init__.py +32 -0
- code_puppy/reopenable_async_client.py +225 -0
- code_puppy/state_management.py +60 -21
- code_puppy/summarization_agent.py +56 -35
- code_puppy/token_utils.py +7 -9
- code_puppy/tools/__init__.py +1 -4
- code_puppy/tools/command_runner.py +187 -32
- code_puppy/tools/common.py +44 -35
- code_puppy/tools/file_modifications.py +335 -118
- code_puppy/tools/file_operations.py +368 -95
- code_puppy/tools/token_check.py +27 -11
- code_puppy/tools/tools_content.py +53 -0
- code_puppy/tui/__init__.py +10 -0
- code_puppy/tui/app.py +1050 -0
- code_puppy/tui/components/__init__.py +21 -0
- code_puppy/tui/components/chat_view.py +512 -0
- code_puppy/tui/components/command_history_modal.py +218 -0
- code_puppy/tui/components/copy_button.py +139 -0
- code_puppy/tui/components/custom_widgets.py +58 -0
- code_puppy/tui/components/input_area.py +167 -0
- code_puppy/tui/components/sidebar.py +309 -0
- code_puppy/tui/components/status_bar.py +182 -0
- code_puppy/tui/messages.py +27 -0
- code_puppy/tui/models/__init__.py +8 -0
- code_puppy/tui/models/chat_message.py +25 -0
- code_puppy/tui/models/command_history.py +89 -0
- code_puppy/tui/models/enums.py +24 -0
- code_puppy/tui/screens/__init__.py +13 -0
- code_puppy/tui/screens/help.py +130 -0
- code_puppy/tui/screens/settings.py +255 -0
- code_puppy/tui/screens/tools.py +74 -0
- code_puppy/tui/tests/__init__.py +1 -0
- code_puppy/tui/tests/test_chat_message.py +28 -0
- code_puppy/tui/tests/test_chat_view.py +88 -0
- code_puppy/tui/tests/test_command_history.py +89 -0
- code_puppy/tui/tests/test_copy_button.py +191 -0
- code_puppy/tui/tests/test_custom_widgets.py +27 -0
- code_puppy/tui/tests/test_disclaimer.py +27 -0
- code_puppy/tui/tests/test_enums.py +15 -0
- code_puppy/tui/tests/test_file_browser.py +60 -0
- code_puppy/tui/tests/test_help.py +38 -0
- code_puppy/tui/tests/test_history_file_reader.py +107 -0
- code_puppy/tui/tests/test_input_area.py +33 -0
- code_puppy/tui/tests/test_settings.py +44 -0
- code_puppy/tui/tests/test_sidebar.py +33 -0
- code_puppy/tui/tests/test_sidebar_history.py +153 -0
- code_puppy/tui/tests/test_sidebar_history_navigation.py +132 -0
- code_puppy/tui/tests/test_status_bar.py +54 -0
- code_puppy/tui/tests/test_timestamped_history.py +52 -0
- code_puppy/tui/tests/test_tools.py +82 -0
- code_puppy/version_checker.py +26 -3
- {code_puppy-0.0.97.dist-info → code_puppy-0.0.119.dist-info}/METADATA +9 -2
- code_puppy-0.0.119.dist-info/RECORD +86 -0
- code_puppy-0.0.97.dist-info/RECORD +0 -32
- {code_puppy-0.0.97.data → code_puppy-0.0.119.data}/data/code_puppy/models.json +0 -0
- {code_puppy-0.0.97.dist-info → code_puppy-0.0.119.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.97.dist-info → code_puppy-0.0.119.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.97.dist-info → code_puppy-0.0.119.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
]
|
|
@@ -0,0 +1,512 @@
|
|
|
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: #1e3a8a;
|
|
34
|
+
color: #ffffff;
|
|
35
|
+
margin: 0 0 1 0;
|
|
36
|
+
margin-top: 0;
|
|
37
|
+
padding: 0;
|
|
38
|
+
padding-top: 0;
|
|
39
|
+
text-wrap: wrap;
|
|
40
|
+
border: round $primary;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.agent-message {
|
|
44
|
+
background: #374151;
|
|
45
|
+
color: #f3f4f6;
|
|
46
|
+
margin: 0 0 1 0;
|
|
47
|
+
margin-top: 0;
|
|
48
|
+
padding: 0;
|
|
49
|
+
padding-top: 0;
|
|
50
|
+
text-wrap: wrap;
|
|
51
|
+
border: round $primary;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.system-message {
|
|
55
|
+
background: #1f2937;
|
|
56
|
+
color: #d1d5db;
|
|
57
|
+
margin: 0 0 1 0;
|
|
58
|
+
margin-top: 0;
|
|
59
|
+
padding: 0;
|
|
60
|
+
padding-top: 0;
|
|
61
|
+
text-style: italic;
|
|
62
|
+
text-wrap: wrap;
|
|
63
|
+
border: round $primary;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.error-message {
|
|
67
|
+
background: #7f1d1d;
|
|
68
|
+
color: #fef2f2;
|
|
69
|
+
margin: 0 0 1 0;
|
|
70
|
+
margin-top: 0;
|
|
71
|
+
padding: 0;
|
|
72
|
+
padding-top: 0;
|
|
73
|
+
text-wrap: wrap;
|
|
74
|
+
border: round $primary;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.agent_reasoning-message {
|
|
78
|
+
background: #1f2937;
|
|
79
|
+
color: #f3e8ff;
|
|
80
|
+
margin: 0 0 1 0;
|
|
81
|
+
margin-top: 0;
|
|
82
|
+
padding: 0;
|
|
83
|
+
padding-top: 0;
|
|
84
|
+
text-wrap: wrap;
|
|
85
|
+
text-style: italic;
|
|
86
|
+
border: round $primary;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.planned_next_steps-message {
|
|
90
|
+
background: #1f2937;
|
|
91
|
+
color: #f3e8ff;
|
|
92
|
+
margin: 0 0 1 0;
|
|
93
|
+
margin-top: 0;
|
|
94
|
+
padding: 0;
|
|
95
|
+
padding-top: 0;
|
|
96
|
+
text-wrap: wrap;
|
|
97
|
+
text-style: italic;
|
|
98
|
+
border: round $primary;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.agent_response-message {
|
|
102
|
+
background: #1f2937;
|
|
103
|
+
color: #f3e8ff;
|
|
104
|
+
margin: 0 0 1 0;
|
|
105
|
+
margin-top: 0;
|
|
106
|
+
padding: 0;
|
|
107
|
+
padding-top: 0;
|
|
108
|
+
text-wrap: wrap;
|
|
109
|
+
border: round $primary;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.info-message {
|
|
113
|
+
background: #065f46;
|
|
114
|
+
color: #d1fae5;
|
|
115
|
+
margin: 0 0 1 0;
|
|
116
|
+
margin-top: 0;
|
|
117
|
+
padding: 0;
|
|
118
|
+
padding-top: 0;
|
|
119
|
+
text-wrap: wrap;
|
|
120
|
+
border: round $primary;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.success-message {
|
|
124
|
+
background: #064e3b;
|
|
125
|
+
color: #d1fae5;
|
|
126
|
+
margin: 0 0 1 0;
|
|
127
|
+
margin-top: 0;
|
|
128
|
+
padding: 0;
|
|
129
|
+
padding-top: 0;
|
|
130
|
+
text-wrap: wrap;
|
|
131
|
+
border: round $primary;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.warning-message {
|
|
135
|
+
background: #92400e;
|
|
136
|
+
color: #fef3c7;
|
|
137
|
+
margin: 0 0 1 0;
|
|
138
|
+
margin-top: 0;
|
|
139
|
+
padding: 0;
|
|
140
|
+
padding-top: 0;
|
|
141
|
+
text-wrap: wrap;
|
|
142
|
+
border: round $primary;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.tool_output-message {
|
|
146
|
+
background: #1e40af;
|
|
147
|
+
color: #dbeafe;
|
|
148
|
+
margin: 0 0 1 0;
|
|
149
|
+
margin-top: 0;
|
|
150
|
+
padding: 0;
|
|
151
|
+
padding-top: 0;
|
|
152
|
+
text-wrap: wrap;
|
|
153
|
+
border: round $primary;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.command_output-message {
|
|
157
|
+
background: #7c2d12;
|
|
158
|
+
color: #fed7aa;
|
|
159
|
+
margin: 0 0 1 0;
|
|
160
|
+
margin-top: 0;
|
|
161
|
+
padding: 0;
|
|
162
|
+
padding-top: 0;
|
|
163
|
+
text-wrap: wrap;
|
|
164
|
+
border: round $primary;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.message-container {
|
|
168
|
+
margin: 0 0 1 0;
|
|
169
|
+
padding: 0;
|
|
170
|
+
width: 1fr;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.copy-button-container {
|
|
174
|
+
margin: 0 0 1 0;
|
|
175
|
+
padding: 0 1;
|
|
176
|
+
width: 1fr;
|
|
177
|
+
height: auto;
|
|
178
|
+
align: left top;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* Ensure first message has no top spacing */
|
|
182
|
+
ChatView > *:first-child {
|
|
183
|
+
margin-top: 0;
|
|
184
|
+
padding-top: 0;
|
|
185
|
+
}
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
def __init__(self, **kwargs):
|
|
189
|
+
super().__init__(**kwargs)
|
|
190
|
+
self.messages: List[ChatMessage] = []
|
|
191
|
+
self.message_groups: dict = {} # Track groups for visual grouping
|
|
192
|
+
self.group_widgets: dict = {} # Track widgets by group_id for enhanced grouping
|
|
193
|
+
self._scroll_pending = False # Track if scroll is already scheduled
|
|
194
|
+
|
|
195
|
+
def _render_agent_message_with_syntax(self, prefix: str, content: str):
|
|
196
|
+
"""Render agent message with proper syntax highlighting for code blocks."""
|
|
197
|
+
# Split content by code blocks
|
|
198
|
+
parts = re.split(r"(```[\s\S]*?```)", content)
|
|
199
|
+
rendered_parts = []
|
|
200
|
+
|
|
201
|
+
# Add prefix as the first part
|
|
202
|
+
rendered_parts.append(Text(prefix, style="bold"))
|
|
203
|
+
|
|
204
|
+
for i, part in enumerate(parts):
|
|
205
|
+
if part.startswith("```") and part.endswith("```"):
|
|
206
|
+
# This is a code block
|
|
207
|
+
lines = part.strip("`").split("\n")
|
|
208
|
+
if lines:
|
|
209
|
+
# First line might contain language identifier
|
|
210
|
+
language = lines[0].strip() if lines[0].strip() else "text"
|
|
211
|
+
code_content = "\n".join(lines[1:]) if len(lines) > 1 else ""
|
|
212
|
+
|
|
213
|
+
if code_content.strip():
|
|
214
|
+
# Create syntax highlighted code
|
|
215
|
+
try:
|
|
216
|
+
syntax = Syntax(
|
|
217
|
+
code_content,
|
|
218
|
+
language,
|
|
219
|
+
theme="github-dark",
|
|
220
|
+
background_color="default",
|
|
221
|
+
line_numbers=True,
|
|
222
|
+
word_wrap=True,
|
|
223
|
+
)
|
|
224
|
+
rendered_parts.append(syntax)
|
|
225
|
+
except Exception:
|
|
226
|
+
# Fallback to plain text if syntax highlighting fails
|
|
227
|
+
rendered_parts.append(Text(part))
|
|
228
|
+
else:
|
|
229
|
+
rendered_parts.append(Text(part))
|
|
230
|
+
else:
|
|
231
|
+
rendered_parts.append(Text(part))
|
|
232
|
+
else:
|
|
233
|
+
# Regular text
|
|
234
|
+
if part.strip():
|
|
235
|
+
rendered_parts.append(Text(part))
|
|
236
|
+
|
|
237
|
+
return Group(*rendered_parts)
|
|
238
|
+
|
|
239
|
+
def _append_to_existing_group(self, message: ChatMessage) -> None:
|
|
240
|
+
"""Append a message to an existing group by group_id."""
|
|
241
|
+
if message.group_id not in self.group_widgets:
|
|
242
|
+
# If group doesn't exist, fall back to normal message creation
|
|
243
|
+
return
|
|
244
|
+
|
|
245
|
+
# Find the most recent message in this group to append to
|
|
246
|
+
group_widgets = self.group_widgets[message.group_id]
|
|
247
|
+
if not group_widgets:
|
|
248
|
+
return
|
|
249
|
+
|
|
250
|
+
# Get the last widget entry for this group
|
|
251
|
+
last_entry = group_widgets[-1]
|
|
252
|
+
last_message = last_entry["message"]
|
|
253
|
+
last_widget = last_entry["widget"]
|
|
254
|
+
copy_button = last_entry.get("copy_button")
|
|
255
|
+
|
|
256
|
+
# Create a separator for different message types in the same group
|
|
257
|
+
if message.type != last_message.type:
|
|
258
|
+
separator = "\n" + "─" * 40 + "\n"
|
|
259
|
+
else:
|
|
260
|
+
separator = "\n"
|
|
261
|
+
|
|
262
|
+
# Update the message content
|
|
263
|
+
last_message.content += separator + message.content
|
|
264
|
+
|
|
265
|
+
# Update the widget based on message type
|
|
266
|
+
if last_message.type == MessageType.AGENT_RESPONSE:
|
|
267
|
+
# Re-render agent response with updated content
|
|
268
|
+
prefix = "AGENT RESPONSE:\n"
|
|
269
|
+
try:
|
|
270
|
+
md = Markdown(last_message.content)
|
|
271
|
+
header = Text(prefix, style="bold")
|
|
272
|
+
group_content = Group(header, md)
|
|
273
|
+
last_widget.update(group_content)
|
|
274
|
+
except Exception:
|
|
275
|
+
full_content = f"{prefix}{last_message.content}"
|
|
276
|
+
last_widget.update(Text(full_content))
|
|
277
|
+
|
|
278
|
+
# Update the copy button if it exists
|
|
279
|
+
if copy_button:
|
|
280
|
+
copy_button.update_text_to_copy(last_message.content)
|
|
281
|
+
else:
|
|
282
|
+
# Handle other message types
|
|
283
|
+
content = last_message.content
|
|
284
|
+
|
|
285
|
+
# Apply the same rendering logic as in add_message
|
|
286
|
+
if (
|
|
287
|
+
"[" in content
|
|
288
|
+
and "]" in content
|
|
289
|
+
and (
|
|
290
|
+
content.strip().startswith("$ ")
|
|
291
|
+
or content.strip().startswith("git ")
|
|
292
|
+
)
|
|
293
|
+
):
|
|
294
|
+
# Treat as literal text
|
|
295
|
+
last_widget.update(Text(content))
|
|
296
|
+
else:
|
|
297
|
+
# Try to render markup
|
|
298
|
+
try:
|
|
299
|
+
last_widget.update(Text.from_markup(content))
|
|
300
|
+
except Exception:
|
|
301
|
+
last_widget.update(Text(content))
|
|
302
|
+
|
|
303
|
+
# Add the new message to our tracking lists
|
|
304
|
+
self.messages.append(message)
|
|
305
|
+
if message.group_id in self.message_groups:
|
|
306
|
+
self.message_groups[message.group_id].append(message)
|
|
307
|
+
|
|
308
|
+
# Auto-scroll to bottom with refresh to fix scroll bar issues (debounced)
|
|
309
|
+
self._schedule_scroll()
|
|
310
|
+
|
|
311
|
+
def add_message(self, message: ChatMessage) -> None:
|
|
312
|
+
"""Add a new message to the chat view."""
|
|
313
|
+
# Enhanced grouping: check if we can append to ANY existing group
|
|
314
|
+
if message.group_id is not None and message.group_id in self.group_widgets:
|
|
315
|
+
self._append_to_existing_group(message)
|
|
316
|
+
return
|
|
317
|
+
|
|
318
|
+
# Old logic for consecutive grouping (keeping as fallback)
|
|
319
|
+
if (
|
|
320
|
+
message.group_id is not None
|
|
321
|
+
and self.messages
|
|
322
|
+
and self.messages[-1].group_id == message.group_id
|
|
323
|
+
):
|
|
324
|
+
# This case should now be handled by _append_to_existing_group above
|
|
325
|
+
# but keeping for safety
|
|
326
|
+
self._append_to_existing_group(message)
|
|
327
|
+
return
|
|
328
|
+
|
|
329
|
+
# Add to messages list
|
|
330
|
+
self.messages.append(message)
|
|
331
|
+
|
|
332
|
+
# Track groups for potential future use
|
|
333
|
+
if message.group_id:
|
|
334
|
+
if message.group_id not in self.message_groups:
|
|
335
|
+
self.message_groups[message.group_id] = []
|
|
336
|
+
self.message_groups[message.group_id].append(message)
|
|
337
|
+
|
|
338
|
+
# Create the message widget
|
|
339
|
+
css_class = f"{message.type.value}-message"
|
|
340
|
+
|
|
341
|
+
if message.type == MessageType.USER:
|
|
342
|
+
content = f"{message.content}"
|
|
343
|
+
message_widget = Static(Text(content), classes=css_class)
|
|
344
|
+
elif message.type == MessageType.AGENT:
|
|
345
|
+
prefix = "AGENT: "
|
|
346
|
+
content = f"{message.content}"
|
|
347
|
+
message_widget = Static(
|
|
348
|
+
Text.from_markup(message.content), classes=css_class
|
|
349
|
+
)
|
|
350
|
+
# Try to render markup
|
|
351
|
+
try:
|
|
352
|
+
message_widget = Static(Text.from_markup(content), classes=css_class)
|
|
353
|
+
except Exception:
|
|
354
|
+
message_widget = Static(Text(content), classes=css_class)
|
|
355
|
+
|
|
356
|
+
elif message.type == MessageType.SYSTEM:
|
|
357
|
+
# Check if content is a Rich object (like Markdown)
|
|
358
|
+
if hasattr(message.content, "__rich_console__"):
|
|
359
|
+
# Render Rich objects directly (like Markdown)
|
|
360
|
+
message_widget = Static(message.content, classes=css_class)
|
|
361
|
+
else:
|
|
362
|
+
content = f"{message.content}"
|
|
363
|
+
# Try to render markup
|
|
364
|
+
try:
|
|
365
|
+
message_widget = Static(
|
|
366
|
+
Text.from_markup(content), classes=css_class
|
|
367
|
+
)
|
|
368
|
+
except Exception:
|
|
369
|
+
message_widget = Static(Text(content), classes=css_class)
|
|
370
|
+
|
|
371
|
+
elif message.type == MessageType.AGENT_REASONING:
|
|
372
|
+
prefix = "AGENT REASONING:\n"
|
|
373
|
+
content = f"{prefix}{message.content}"
|
|
374
|
+
message_widget = Static(Text(content), classes=css_class)
|
|
375
|
+
elif message.type == MessageType.PLANNED_NEXT_STEPS:
|
|
376
|
+
prefix = "PLANNED NEXT STEPS:\n"
|
|
377
|
+
content = f"{prefix}{message.content}"
|
|
378
|
+
message_widget = Static(Text(content), classes=css_class)
|
|
379
|
+
elif message.type == MessageType.AGENT_RESPONSE:
|
|
380
|
+
prefix = "AGENT RESPONSE:\n"
|
|
381
|
+
content = message.content
|
|
382
|
+
|
|
383
|
+
try:
|
|
384
|
+
# First try to render as markdown with proper syntax highlighting
|
|
385
|
+
md = Markdown(content)
|
|
386
|
+
# Create a group with the header and markdown content
|
|
387
|
+
header = Text(prefix, style="bold")
|
|
388
|
+
group_content = Group(header, md)
|
|
389
|
+
message_widget = Static(group_content, classes=css_class)
|
|
390
|
+
except Exception:
|
|
391
|
+
# If markdown parsing fails, fall back to simple text display
|
|
392
|
+
full_content = f"{prefix}{content}"
|
|
393
|
+
message_widget = Static(Text(full_content), classes=css_class)
|
|
394
|
+
|
|
395
|
+
# Try to create copy button - use simpler approach
|
|
396
|
+
try:
|
|
397
|
+
# Create copy button for agent responses
|
|
398
|
+
copy_button = CopyButton(content) # Copy the raw content without prefix
|
|
399
|
+
|
|
400
|
+
# Mount the message first
|
|
401
|
+
self.mount(message_widget)
|
|
402
|
+
|
|
403
|
+
# Then mount the copy button directly
|
|
404
|
+
self.mount(copy_button)
|
|
405
|
+
|
|
406
|
+
# Track both the widget and copy button for group-based updates
|
|
407
|
+
if message.group_id:
|
|
408
|
+
if message.group_id not in self.group_widgets:
|
|
409
|
+
self.group_widgets[message.group_id] = []
|
|
410
|
+
self.group_widgets[message.group_id].append(
|
|
411
|
+
{
|
|
412
|
+
"message": message,
|
|
413
|
+
"widget": message_widget,
|
|
414
|
+
"copy_button": copy_button,
|
|
415
|
+
}
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
# Auto-scroll to bottom with refresh to fix scroll bar issues (debounced)
|
|
419
|
+
self._schedule_scroll()
|
|
420
|
+
return # Early return only if copy button creation succeeded
|
|
421
|
+
|
|
422
|
+
except Exception as e:
|
|
423
|
+
# If copy button creation fails, fall back to normal message display
|
|
424
|
+
# Log the error but don't let it prevent the message from showing
|
|
425
|
+
import sys
|
|
426
|
+
|
|
427
|
+
print(f"Warning: Copy button creation failed: {e}", file=sys.stderr)
|
|
428
|
+
# Continue to normal message mounting below
|
|
429
|
+
elif message.type == MessageType.INFO:
|
|
430
|
+
prefix = "INFO: "
|
|
431
|
+
content = f"{prefix}{message.content}"
|
|
432
|
+
message_widget = Static(Text(content), classes=css_class)
|
|
433
|
+
elif message.type == MessageType.SUCCESS:
|
|
434
|
+
prefix = "SUCCESS: "
|
|
435
|
+
content = f"{prefix}{message.content}"
|
|
436
|
+
message_widget = Static(Text(content), classes=css_class)
|
|
437
|
+
elif message.type == MessageType.WARNING:
|
|
438
|
+
prefix = "WARNING: "
|
|
439
|
+
content = f"{prefix}{message.content}"
|
|
440
|
+
message_widget = Static(Text(content), classes=css_class)
|
|
441
|
+
elif message.type == MessageType.TOOL_OUTPUT:
|
|
442
|
+
prefix = "TOOL OUTPUT: "
|
|
443
|
+
content = f"{prefix}{message.content}"
|
|
444
|
+
message_widget = Static(Text(content), classes=css_class)
|
|
445
|
+
elif message.type == MessageType.COMMAND_OUTPUT:
|
|
446
|
+
prefix = "COMMAND: "
|
|
447
|
+
content = f"{prefix}{message.content}"
|
|
448
|
+
message_widget = Static(Text(content), classes=css_class)
|
|
449
|
+
else: # ERROR and fallback
|
|
450
|
+
prefix = "Error: " if message.type == MessageType.ERROR else "Unknown: "
|
|
451
|
+
content = f"{prefix}{message.content}"
|
|
452
|
+
message_widget = Static(Text(content), classes=css_class)
|
|
453
|
+
|
|
454
|
+
self.mount(message_widget)
|
|
455
|
+
|
|
456
|
+
# Track the widget for group-based updates
|
|
457
|
+
if message.group_id:
|
|
458
|
+
if message.group_id not in self.group_widgets:
|
|
459
|
+
self.group_widgets[message.group_id] = []
|
|
460
|
+
self.group_widgets[message.group_id].append(
|
|
461
|
+
{
|
|
462
|
+
"message": message,
|
|
463
|
+
"widget": message_widget,
|
|
464
|
+
"copy_button": None, # Will be set if created
|
|
465
|
+
}
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
# Auto-scroll to bottom with refresh to fix scroll bar issues (debounced)
|
|
469
|
+
self._schedule_scroll()
|
|
470
|
+
|
|
471
|
+
def clear_messages(self) -> None:
|
|
472
|
+
"""Clear all messages from the chat view."""
|
|
473
|
+
self.messages.clear()
|
|
474
|
+
self.message_groups.clear() # Clear groups too
|
|
475
|
+
self.group_widgets.clear() # Clear widget tracking too
|
|
476
|
+
# Remove all message widgets (Static widgets, CopyButtons, and any Vertical containers)
|
|
477
|
+
for widget in self.query(Static):
|
|
478
|
+
widget.remove()
|
|
479
|
+
for widget in self.query(CopyButton):
|
|
480
|
+
widget.remove()
|
|
481
|
+
for widget in self.query(Vertical):
|
|
482
|
+
widget.remove()
|
|
483
|
+
|
|
484
|
+
@on(CopyButton.CopyCompleted)
|
|
485
|
+
def on_copy_completed(self, event: CopyButton.CopyCompleted) -> None:
|
|
486
|
+
"""Handle copy button completion events."""
|
|
487
|
+
if event.success:
|
|
488
|
+
# Could add a temporary success message or visual feedback
|
|
489
|
+
# For now, the button itself provides visual feedback
|
|
490
|
+
pass
|
|
491
|
+
else:
|
|
492
|
+
# Show error message in chat if copy failed
|
|
493
|
+
from datetime import datetime, timezone
|
|
494
|
+
|
|
495
|
+
error_message = ChatMessage(
|
|
496
|
+
id=f"copy_error_{datetime.now(timezone.utc).timestamp()}",
|
|
497
|
+
type=MessageType.ERROR,
|
|
498
|
+
content=f"Failed to copy to clipboard: {event.error}",
|
|
499
|
+
timestamp=datetime.now(timezone.utc),
|
|
500
|
+
)
|
|
501
|
+
self.add_message(error_message)
|
|
502
|
+
|
|
503
|
+
def _schedule_scroll(self) -> None:
|
|
504
|
+
"""Schedule a scroll operation, avoiding duplicate calls."""
|
|
505
|
+
if not self._scroll_pending:
|
|
506
|
+
self._scroll_pending = True
|
|
507
|
+
self.call_after_refresh(self._do_scroll)
|
|
508
|
+
|
|
509
|
+
def _do_scroll(self) -> None:
|
|
510
|
+
"""Perform the actual scroll operation."""
|
|
511
|
+
self._scroll_pending = False
|
|
512
|
+
self.scroll_end(animate=False)
|