code-puppy 0.0.124__py3-none-any.whl → 0.0.126__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/agent.py +20 -4
- code_puppy/agents/__init__.py +25 -0
- code_puppy/{agent_prompts.py → agents/agent_code_puppy.py} +46 -13
- code_puppy/agents/agent_creator_agent.py +446 -0
- code_puppy/agents/agent_manager.py +211 -0
- code_puppy/agents/base_agent.py +60 -0
- code_puppy/agents/json_agent.py +129 -0
- code_puppy/callbacks.py +6 -0
- code_puppy/command_line/command_handler.py +91 -1
- code_puppy/config.py +13 -1
- code_puppy/main.py +6 -1
- code_puppy/tools/__init__.py +60 -7
- code_puppy/tools/command_runner.py +97 -0
- code_puppy/tools/file_modifications.py +176 -11
- code_puppy/tools/file_operations.py +171 -1
- code_puppy/tui/app.py +14 -158
- code_puppy/tui/tests/test_agent_command.py +72 -0
- code_puppy-0.0.126.dist-info/METADATA +634 -0
- {code_puppy-0.0.124.dist-info → code_puppy-0.0.126.dist-info}/RECORD +23 -17
- code_puppy-0.0.124.dist-info/METADATA +0 -192
- {code_puppy-0.0.124.data → code_puppy-0.0.126.data}/data/code_puppy/models.json +0 -0
- {code_puppy-0.0.124.dist-info → code_puppy-0.0.126.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.124.dist-info → code_puppy-0.0.126.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.124.dist-info → code_puppy-0.0.126.dist-info}/licenses/LICENSE +0 -0
code_puppy/tui/app.py
CHANGED
|
@@ -10,7 +10,7 @@ from textual.binding import Binding
|
|
|
10
10
|
from textual.containers import Container
|
|
11
11
|
from textual.events import Resize
|
|
12
12
|
from textual.reactive import reactive
|
|
13
|
-
from textual.widgets import Footer,
|
|
13
|
+
from textual.widgets import Footer, ListView
|
|
14
14
|
|
|
15
15
|
from code_puppy.agent import get_code_generation_agent, get_custom_usage_limits
|
|
16
16
|
from code_puppy.command_line.command_handler import handle_command
|
|
@@ -408,6 +408,15 @@ class CodePuppyTUI(App):
|
|
|
408
408
|
self.action_clear_chat()
|
|
409
409
|
return
|
|
410
410
|
|
|
411
|
+
# Let the command handler process all /agent commands
|
|
412
|
+
# result will be handled by the command handler directly through messaging system
|
|
413
|
+
if message.strip().startswith("/agent"):
|
|
414
|
+
# The command handler will emit messages directly to our messaging system
|
|
415
|
+
handle_command(message.strip())
|
|
416
|
+
# Refresh our agent instance after potential change
|
|
417
|
+
self.agent = get_code_generation_agent()
|
|
418
|
+
return
|
|
419
|
+
|
|
411
420
|
# Handle exit commands
|
|
412
421
|
if message.strip().lower() in ("/exit", "/quit"):
|
|
413
422
|
self.add_system_message("Goodbye!")
|
|
@@ -416,36 +425,11 @@ class CodePuppyTUI(App):
|
|
|
416
425
|
return
|
|
417
426
|
|
|
418
427
|
# Use the existing command handler
|
|
428
|
+
# The command handler directly uses the messaging system, so we don't need to capture stdout
|
|
419
429
|
try:
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
from code_puppy.tools.common import console as rich_console
|
|
424
|
-
|
|
425
|
-
# Capture the output from the command handler
|
|
426
|
-
old_stdout = sys.stdout
|
|
427
|
-
captured_output = StringIO()
|
|
428
|
-
sys.stdout = captured_output
|
|
429
|
-
|
|
430
|
-
# Also capture Rich console output
|
|
431
|
-
rich_console.file = captured_output
|
|
432
|
-
|
|
433
|
-
try:
|
|
434
|
-
# Call the existing command handler
|
|
435
|
-
result = handle_command(message.strip())
|
|
436
|
-
if result: # Command was handled
|
|
437
|
-
output = captured_output.getvalue()
|
|
438
|
-
if output.strip():
|
|
439
|
-
self.add_system_message(output.strip())
|
|
440
|
-
else:
|
|
441
|
-
self.add_system_message(f"Command '{message}' executed")
|
|
442
|
-
else:
|
|
443
|
-
self.add_system_message(f"Unknown command: {message}")
|
|
444
|
-
finally:
|
|
445
|
-
# Restore stdout and console
|
|
446
|
-
sys.stdout = old_stdout
|
|
447
|
-
rich_console.file = sys.__stdout__
|
|
448
|
-
|
|
430
|
+
result = handle_command(message.strip())
|
|
431
|
+
if not result:
|
|
432
|
+
self.add_system_message(f"Unknown command: {message}")
|
|
449
433
|
except Exception as e:
|
|
450
434
|
self.add_error_message(f"Error executing command: {str(e)}")
|
|
451
435
|
return
|
|
@@ -668,134 +652,6 @@ class CodePuppyTUI(App):
|
|
|
668
652
|
# Automatically submit the message
|
|
669
653
|
self.action_send_message()
|
|
670
654
|
|
|
671
|
-
# History management methods
|
|
672
|
-
def load_history_list(self) -> None:
|
|
673
|
-
"""Load session history into the history tab."""
|
|
674
|
-
try:
|
|
675
|
-
from datetime import datetime, timezone
|
|
676
|
-
|
|
677
|
-
history_list = self.query_one("#history-list", ListView)
|
|
678
|
-
|
|
679
|
-
# Get history from session memory
|
|
680
|
-
if self.session_memory:
|
|
681
|
-
# Get recent history (last 24 hours by default)
|
|
682
|
-
recent_history = self.session_memory.get_history(within_minutes=24 * 60)
|
|
683
|
-
|
|
684
|
-
if not recent_history:
|
|
685
|
-
# No history available
|
|
686
|
-
history_list.append(
|
|
687
|
-
ListItem(Label("No recent history", classes="history-empty"))
|
|
688
|
-
)
|
|
689
|
-
return
|
|
690
|
-
|
|
691
|
-
# Filter out model loading entries and group history by type, display most recent first
|
|
692
|
-
filtered_history = [
|
|
693
|
-
entry
|
|
694
|
-
for entry in recent_history
|
|
695
|
-
if not entry.get("description", "").startswith("Agent loaded")
|
|
696
|
-
]
|
|
697
|
-
|
|
698
|
-
# Get sidebar width for responsive text truncation
|
|
699
|
-
try:
|
|
700
|
-
sidebar_width = (
|
|
701
|
-
self.query_one("Sidebar").size.width
|
|
702
|
-
if hasattr(self.query_one("Sidebar"), "size")
|
|
703
|
-
else 30
|
|
704
|
-
)
|
|
705
|
-
except Exception:
|
|
706
|
-
sidebar_width = 30
|
|
707
|
-
|
|
708
|
-
# Adjust text length based on sidebar width
|
|
709
|
-
if sidebar_width >= 35:
|
|
710
|
-
max_text_length = 45
|
|
711
|
-
time_format = "%H:%M:%S"
|
|
712
|
-
elif sidebar_width >= 25:
|
|
713
|
-
max_text_length = 30
|
|
714
|
-
time_format = "%H:%M"
|
|
715
|
-
else:
|
|
716
|
-
max_text_length = 20
|
|
717
|
-
time_format = "%H:%M"
|
|
718
|
-
|
|
719
|
-
for entry in reversed(filtered_history[-20:]): # Show last 20 entries
|
|
720
|
-
timestamp_str = entry.get("timestamp", "")
|
|
721
|
-
description = entry.get("description", "Unknown task")
|
|
722
|
-
|
|
723
|
-
# Parse timestamp for display with safe parsing
|
|
724
|
-
def parse_timestamp_safely_for_display(timestamp_str: str) -> str:
|
|
725
|
-
"""Parse timestamp string safely for display purposes."""
|
|
726
|
-
try:
|
|
727
|
-
# Handle 'Z' suffix (common UTC format)
|
|
728
|
-
cleaned_timestamp = timestamp_str.replace("Z", "+00:00")
|
|
729
|
-
parsed_dt = datetime.fromisoformat(cleaned_timestamp)
|
|
730
|
-
|
|
731
|
-
# If the datetime is naive (no timezone), assume UTC
|
|
732
|
-
if parsed_dt.tzinfo is None:
|
|
733
|
-
parsed_dt = parsed_dt.replace(tzinfo=timezone.utc)
|
|
734
|
-
|
|
735
|
-
return parsed_dt.strftime(time_format)
|
|
736
|
-
except (ValueError, AttributeError, TypeError):
|
|
737
|
-
# Handle invalid timestamp formats gracefully
|
|
738
|
-
fallback = (
|
|
739
|
-
timestamp_str[:5]
|
|
740
|
-
if sidebar_width < 25
|
|
741
|
-
else timestamp_str[:8]
|
|
742
|
-
)
|
|
743
|
-
return "??:??" if len(fallback) < 5 else fallback
|
|
744
|
-
|
|
745
|
-
time_display = parse_timestamp_safely_for_display(timestamp_str)
|
|
746
|
-
|
|
747
|
-
# Format description for display with responsive truncation
|
|
748
|
-
if description.startswith("Interactive task:"):
|
|
749
|
-
task_text = description[
|
|
750
|
-
17:
|
|
751
|
-
].strip() # Remove "Interactive task: "
|
|
752
|
-
truncated = task_text[:max_text_length] + (
|
|
753
|
-
"..." if len(task_text) > max_text_length else ""
|
|
754
|
-
)
|
|
755
|
-
display_text = f"[{time_display}] 💬 {truncated}"
|
|
756
|
-
css_class = "history-interactive"
|
|
757
|
-
elif description.startswith("TUI interaction:"):
|
|
758
|
-
task_text = description[
|
|
759
|
-
16:
|
|
760
|
-
].strip() # Remove "TUI interaction: "
|
|
761
|
-
truncated = task_text[:max_text_length] + (
|
|
762
|
-
"..." if len(task_text) > max_text_length else ""
|
|
763
|
-
)
|
|
764
|
-
display_text = f"[{time_display}] 🖥️ {truncated}"
|
|
765
|
-
css_class = "history-tui"
|
|
766
|
-
elif description.startswith("Command executed"):
|
|
767
|
-
cmd_text = description[
|
|
768
|
-
18:
|
|
769
|
-
].strip() # Remove "Command executed: "
|
|
770
|
-
truncated = cmd_text[: max_text_length - 5] + (
|
|
771
|
-
"..." if len(cmd_text) > max_text_length - 5 else ""
|
|
772
|
-
)
|
|
773
|
-
display_text = f"[{time_display}] ⚡ {truncated}"
|
|
774
|
-
css_class = "history-command"
|
|
775
|
-
else:
|
|
776
|
-
# Generic entry
|
|
777
|
-
truncated = description[:max_text_length] + (
|
|
778
|
-
"..." if len(description) > max_text_length else ""
|
|
779
|
-
)
|
|
780
|
-
display_text = f"[{time_display}] 📝 {truncated}"
|
|
781
|
-
css_class = "history-generic"
|
|
782
|
-
|
|
783
|
-
label = Label(display_text, classes=css_class)
|
|
784
|
-
history_item = ListItem(label)
|
|
785
|
-
history_item.history_entry = (
|
|
786
|
-
entry # Store full entry for detail view
|
|
787
|
-
)
|
|
788
|
-
history_list.append(history_item)
|
|
789
|
-
else:
|
|
790
|
-
history_list.append(
|
|
791
|
-
ListItem(
|
|
792
|
-
Label("Session memory not available", classes="history-error")
|
|
793
|
-
)
|
|
794
|
-
)
|
|
795
|
-
|
|
796
|
-
except Exception as e:
|
|
797
|
-
self.add_error_message(f"Failed to load history: {e}")
|
|
798
|
-
|
|
799
655
|
def show_history_details(self, history_entry: dict) -> None:
|
|
800
656
|
"""Show detailed information about a selected history entry."""
|
|
801
657
|
try:
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Tests for the /agent command handling in TUI mode."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import patch, MagicMock
|
|
4
|
+
|
|
5
|
+
from code_puppy.tui.app import CodePuppyTUI
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestTUIAgentCommand:
|
|
9
|
+
"""Test the TUI's handling of /agent commands."""
|
|
10
|
+
|
|
11
|
+
@patch("code_puppy.tui.app.get_code_generation_agent")
|
|
12
|
+
@patch("code_puppy.tui.app.handle_command")
|
|
13
|
+
def test_tui_handles_agent_command(self, mock_handle_command, mock_get_agent):
|
|
14
|
+
"""Test that TUI properly delegates /agent commands to command handler."""
|
|
15
|
+
# Create a TUI app instance
|
|
16
|
+
app = CodePuppyTUI()
|
|
17
|
+
|
|
18
|
+
# Mock the agent
|
|
19
|
+
mock_agent_instance = MagicMock()
|
|
20
|
+
mock_get_agent.return_value = mock_agent_instance
|
|
21
|
+
|
|
22
|
+
# Mock handle_command to simulate successful processing
|
|
23
|
+
mock_handle_command.return_value = True
|
|
24
|
+
|
|
25
|
+
# Simulate processing an /agent command
|
|
26
|
+
message = "/agent code-puppy"
|
|
27
|
+
app.agent = mock_agent_instance
|
|
28
|
+
|
|
29
|
+
# Call the method that processes messages
|
|
30
|
+
# We'll need to mock some UI elements to avoid complex setup
|
|
31
|
+
with (
|
|
32
|
+
patch.object(app, "add_user_message"),
|
|
33
|
+
patch.object(app, "_update_submit_cancel_button"),
|
|
34
|
+
patch.object(app, "start_agent_progress"),
|
|
35
|
+
patch.object(app, "stop_agent_progress"),
|
|
36
|
+
patch.object(app, "refresh_history_display"),
|
|
37
|
+
):
|
|
38
|
+
import asyncio
|
|
39
|
+
|
|
40
|
+
# Create an event loop for the async test
|
|
41
|
+
loop = asyncio.get_event_loop()
|
|
42
|
+
loop.run_until_complete(app.process_message(message))
|
|
43
|
+
|
|
44
|
+
# Verify that handle_command was called with the correct argument
|
|
45
|
+
mock_handle_command.assert_called_once_with(message)
|
|
46
|
+
|
|
47
|
+
# Verify that get_code_generation_agent was called to refresh the agent instance
|
|
48
|
+
mock_get_agent.assert_called()
|
|
49
|
+
|
|
50
|
+
@patch("code_puppy.tui.app.get_code_generation_agent")
|
|
51
|
+
def test_tui_refreshes_agent_after_command(self, mock_get_agent):
|
|
52
|
+
"""Test that TUI refreshes its agent instance after processing /agent command."""
|
|
53
|
+
# Create a TUI app instance
|
|
54
|
+
app = CodePuppyTUI()
|
|
55
|
+
|
|
56
|
+
# Set initial agent
|
|
57
|
+
initial_agent = MagicMock()
|
|
58
|
+
app.agent = initial_agent
|
|
59
|
+
|
|
60
|
+
# Mock get_code_generation_agent to return a new agent instance
|
|
61
|
+
new_agent = MagicMock()
|
|
62
|
+
mock_get_agent.return_value = new_agent
|
|
63
|
+
|
|
64
|
+
# Simulate that an /agent command was processed
|
|
65
|
+
with patch("code_puppy.tui.app.handle_command"):
|
|
66
|
+
import asyncio
|
|
67
|
+
|
|
68
|
+
loop = asyncio.get_event_loop()
|
|
69
|
+
loop.run_until_complete(app.process_message("/agent code-puppy"))
|
|
70
|
+
|
|
71
|
+
# Verify that the agent was refreshed
|
|
72
|
+
mock_get_agent.assert_called()
|