code-puppy 0.0.124__py3-none-any.whl → 0.0.125__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/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, Label, ListItem, ListView
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
- import sys
421
- from io import StringIO
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()