code-puppy 0.0.97__py3-none-any.whl → 0.0.118__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 (81) hide show
  1. code_puppy/__init__.py +2 -5
  2. code_puppy/__main__.py +10 -0
  3. code_puppy/agent.py +125 -40
  4. code_puppy/agent_prompts.py +30 -24
  5. code_puppy/callbacks.py +152 -0
  6. code_puppy/command_line/command_handler.py +359 -0
  7. code_puppy/command_line/load_context_completion.py +59 -0
  8. code_puppy/command_line/model_picker_completion.py +14 -21
  9. code_puppy/command_line/motd.py +44 -28
  10. code_puppy/command_line/prompt_toolkit_completion.py +42 -23
  11. code_puppy/config.py +266 -26
  12. code_puppy/http_utils.py +122 -0
  13. code_puppy/main.py +570 -383
  14. code_puppy/message_history_processor.py +195 -104
  15. code_puppy/messaging/__init__.py +46 -0
  16. code_puppy/messaging/message_queue.py +288 -0
  17. code_puppy/messaging/queue_console.py +293 -0
  18. code_puppy/messaging/renderers.py +305 -0
  19. code_puppy/messaging/spinner/__init__.py +55 -0
  20. code_puppy/messaging/spinner/console_spinner.py +200 -0
  21. code_puppy/messaging/spinner/spinner_base.py +66 -0
  22. code_puppy/messaging/spinner/textual_spinner.py +97 -0
  23. code_puppy/model_factory.py +73 -105
  24. code_puppy/plugins/__init__.py +32 -0
  25. code_puppy/reopenable_async_client.py +225 -0
  26. code_puppy/state_management.py +60 -21
  27. code_puppy/summarization_agent.py +56 -35
  28. code_puppy/token_utils.py +7 -9
  29. code_puppy/tools/__init__.py +1 -4
  30. code_puppy/tools/command_runner.py +187 -32
  31. code_puppy/tools/common.py +44 -35
  32. code_puppy/tools/file_modifications.py +335 -118
  33. code_puppy/tools/file_operations.py +368 -95
  34. code_puppy/tools/token_check.py +27 -11
  35. code_puppy/tools/tools_content.py +53 -0
  36. code_puppy/tui/__init__.py +10 -0
  37. code_puppy/tui/app.py +1050 -0
  38. code_puppy/tui/components/__init__.py +21 -0
  39. code_puppy/tui/components/chat_view.py +512 -0
  40. code_puppy/tui/components/command_history_modal.py +218 -0
  41. code_puppy/tui/components/copy_button.py +139 -0
  42. code_puppy/tui/components/custom_widgets.py +58 -0
  43. code_puppy/tui/components/input_area.py +167 -0
  44. code_puppy/tui/components/sidebar.py +309 -0
  45. code_puppy/tui/components/status_bar.py +182 -0
  46. code_puppy/tui/messages.py +27 -0
  47. code_puppy/tui/models/__init__.py +8 -0
  48. code_puppy/tui/models/chat_message.py +25 -0
  49. code_puppy/tui/models/command_history.py +89 -0
  50. code_puppy/tui/models/enums.py +24 -0
  51. code_puppy/tui/screens/__init__.py +13 -0
  52. code_puppy/tui/screens/help.py +130 -0
  53. code_puppy/tui/screens/settings.py +256 -0
  54. code_puppy/tui/screens/tools.py +74 -0
  55. code_puppy/tui/tests/__init__.py +1 -0
  56. code_puppy/tui/tests/test_chat_message.py +28 -0
  57. code_puppy/tui/tests/test_chat_view.py +88 -0
  58. code_puppy/tui/tests/test_command_history.py +89 -0
  59. code_puppy/tui/tests/test_copy_button.py +191 -0
  60. code_puppy/tui/tests/test_custom_widgets.py +27 -0
  61. code_puppy/tui/tests/test_disclaimer.py +27 -0
  62. code_puppy/tui/tests/test_enums.py +15 -0
  63. code_puppy/tui/tests/test_file_browser.py +60 -0
  64. code_puppy/tui/tests/test_help.py +38 -0
  65. code_puppy/tui/tests/test_history_file_reader.py +107 -0
  66. code_puppy/tui/tests/test_input_area.py +33 -0
  67. code_puppy/tui/tests/test_settings.py +44 -0
  68. code_puppy/tui/tests/test_sidebar.py +33 -0
  69. code_puppy/tui/tests/test_sidebar_history.py +153 -0
  70. code_puppy/tui/tests/test_sidebar_history_navigation.py +132 -0
  71. code_puppy/tui/tests/test_status_bar.py +54 -0
  72. code_puppy/tui/tests/test_timestamped_history.py +52 -0
  73. code_puppy/tui/tests/test_tools.py +82 -0
  74. code_puppy/version_checker.py +26 -3
  75. {code_puppy-0.0.97.dist-info → code_puppy-0.0.118.dist-info}/METADATA +9 -2
  76. code_puppy-0.0.118.dist-info/RECORD +86 -0
  77. code_puppy-0.0.97.dist-info/RECORD +0 -32
  78. {code_puppy-0.0.97.data → code_puppy-0.0.118.data}/data/code_puppy/models.json +0 -0
  79. {code_puppy-0.0.97.dist-info → code_puppy-0.0.118.dist-info}/WHEEL +0 -0
  80. {code_puppy-0.0.97.dist-info → code_puppy-0.0.118.dist-info}/entry_points.txt +0 -0
  81. {code_puppy-0.0.97.dist-info → code_puppy-0.0.118.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)