code-puppy 0.0.196__tar.gz → 0.0.198__tar.gz

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 (129) hide show
  1. {code_puppy-0.0.196 → code_puppy-0.0.198}/PKG-INFO +13 -2
  2. {code_puppy-0.0.196 → code_puppy-0.0.198}/README.md +11 -0
  3. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/agents/base_agent.py +3 -5
  4. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/agents/json_agent.py +8 -0
  5. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/command_line/command_handler.py +108 -113
  6. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/command_line/prompt_toolkit_completion.py +34 -10
  7. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/config.py +6 -0
  8. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/main.py +6 -6
  9. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/tui/app.py +153 -7
  10. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/tui/components/input_area.py +1 -1
  11. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/tui/components/status_bar.py +4 -1
  12. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/tui/screens/__init__.py +2 -0
  13. code_puppy-0.0.198/code_puppy/tui/screens/autosave_picker.py +166 -0
  14. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/tui/screens/settings.py +4 -2
  15. {code_puppy-0.0.196 → code_puppy-0.0.198}/pyproject.toml +2 -2
  16. {code_puppy-0.0.196 → code_puppy-0.0.198}/.gitignore +0 -0
  17. {code_puppy-0.0.196 → code_puppy-0.0.198}/LICENSE +0 -0
  18. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/__init__.py +0 -0
  19. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/__main__.py +0 -0
  20. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/agents/__init__.py +0 -0
  21. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/agents/agent_c_reviewer.py +0 -0
  22. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/agents/agent_code_puppy.py +0 -0
  23. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/agents/agent_code_reviewer.py +0 -0
  24. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/agents/agent_cpp_reviewer.py +0 -0
  25. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/agents/agent_creator_agent.py +0 -0
  26. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/agents/agent_golang_reviewer.py +0 -0
  27. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/agents/agent_javascript_reviewer.py +0 -0
  28. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/agents/agent_manager.py +0 -0
  29. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/agents/agent_python_reviewer.py +0 -0
  30. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/agents/agent_qa_expert.py +0 -0
  31. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/agents/agent_qa_kitten.py +0 -0
  32. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/agents/agent_security_auditor.py +0 -0
  33. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/agents/agent_typescript_reviewer.py +0 -0
  34. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/callbacks.py +0 -0
  35. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/command_line/__init__.py +0 -0
  36. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/command_line/file_path_completion.py +0 -0
  37. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/command_line/load_context_completion.py +0 -0
  38. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/command_line/mcp/__init__.py +0 -0
  39. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/command_line/mcp/add_command.py +0 -0
  40. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/command_line/mcp/base.py +0 -0
  41. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/command_line/mcp/handler.py +0 -0
  42. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/command_line/mcp/help_command.py +0 -0
  43. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/command_line/mcp/install_command.py +0 -0
  44. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/command_line/mcp/list_command.py +0 -0
  45. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/command_line/mcp/logs_command.py +0 -0
  46. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/command_line/mcp/remove_command.py +0 -0
  47. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/command_line/mcp/restart_command.py +0 -0
  48. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/command_line/mcp/search_command.py +0 -0
  49. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/command_line/mcp/start_all_command.py +0 -0
  50. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/command_line/mcp/start_command.py +0 -0
  51. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/command_line/mcp/status_command.py +0 -0
  52. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/command_line/mcp/stop_all_command.py +0 -0
  53. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/command_line/mcp/stop_command.py +0 -0
  54. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/command_line/mcp/test_command.py +0 -0
  55. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/command_line/mcp/utils.py +0 -0
  56. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/command_line/mcp/wizard_utils.py +0 -0
  57. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/command_line/model_picker_completion.py +0 -0
  58. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/command_line/motd.py +0 -0
  59. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/command_line/utils.py +0 -0
  60. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/http_utils.py +0 -0
  61. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/mcp_/__init__.py +0 -0
  62. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/mcp_/async_lifecycle.py +0 -0
  63. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/mcp_/blocking_startup.py +0 -0
  64. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/mcp_/captured_stdio_server.py +0 -0
  65. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/mcp_/circuit_breaker.py +0 -0
  66. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/mcp_/config_wizard.py +0 -0
  67. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/mcp_/dashboard.py +0 -0
  68. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/mcp_/error_isolation.py +0 -0
  69. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/mcp_/examples/retry_example.py +0 -0
  70. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/mcp_/health_monitor.py +0 -0
  71. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/mcp_/managed_server.py +0 -0
  72. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/mcp_/manager.py +0 -0
  73. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/mcp_/registry.py +0 -0
  74. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/mcp_/retry_manager.py +0 -0
  75. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/mcp_/server_registry_catalog.py +0 -0
  76. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/mcp_/status_tracker.py +0 -0
  77. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/mcp_/system_tools.py +0 -0
  78. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/messaging/__init__.py +0 -0
  79. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/messaging/message_queue.py +0 -0
  80. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/messaging/queue_console.py +0 -0
  81. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/messaging/renderers.py +0 -0
  82. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/messaging/spinner/__init__.py +0 -0
  83. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/messaging/spinner/console_spinner.py +0 -0
  84. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/messaging/spinner/spinner_base.py +0 -0
  85. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/messaging/spinner/textual_spinner.py +0 -0
  86. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/model_factory.py +0 -0
  87. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/models.json +0 -0
  88. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/plugins/__init__.py +0 -0
  89. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/plugins/example_custom_command/register_callbacks.py +0 -0
  90. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/reopenable_async_client.py +0 -0
  91. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/round_robin_model.py +0 -0
  92. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/session_storage.py +0 -0
  93. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/status_display.py +0 -0
  94. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/summarization_agent.py +0 -0
  95. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/tools/__init__.py +0 -0
  96. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/tools/agent_tools.py +0 -0
  97. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/tools/browser/__init__.py +0 -0
  98. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/tools/browser/browser_control.py +0 -0
  99. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/tools/browser/browser_interactions.py +0 -0
  100. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/tools/browser/browser_locators.py +0 -0
  101. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/tools/browser/browser_navigation.py +0 -0
  102. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/tools/browser/browser_screenshot.py +0 -0
  103. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/tools/browser/browser_scripts.py +0 -0
  104. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/tools/browser/browser_workflows.py +0 -0
  105. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/tools/browser/camoufox_manager.py +0 -0
  106. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/tools/browser/vqa_agent.py +0 -0
  107. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/tools/command_runner.py +0 -0
  108. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/tools/common.py +0 -0
  109. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/tools/file_modifications.py +0 -0
  110. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/tools/file_operations.py +0 -0
  111. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/tools/tools_content.py +0 -0
  112. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/tui/__init__.py +0 -0
  113. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/tui/components/__init__.py +0 -0
  114. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/tui/components/chat_view.py +0 -0
  115. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/tui/components/command_history_modal.py +0 -0
  116. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/tui/components/copy_button.py +0 -0
  117. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/tui/components/custom_widgets.py +0 -0
  118. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/tui/components/human_input_modal.py +0 -0
  119. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/tui/components/sidebar.py +0 -0
  120. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/tui/messages.py +0 -0
  121. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/tui/models/__init__.py +0 -0
  122. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/tui/models/chat_message.py +0 -0
  123. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/tui/models/command_history.py +0 -0
  124. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/tui/models/enums.py +0 -0
  125. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/tui/screens/help.py +0 -0
  126. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/tui/screens/mcp_install_wizard.py +0 -0
  127. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/tui/screens/tools.py +0 -0
  128. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/tui_state.py +0 -0
  129. {code_puppy-0.0.196 → code_puppy-0.0.198}/code_puppy/version_checker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-puppy
3
- Version: 0.0.196
3
+ Version: 0.0.198
4
4
  Summary: Code generation agent
5
5
  Project-URL: repository, https://github.com/mpfaffenberger/code_puppy
6
6
  Project-URL: HomePage, https://github.com/mpfaffenberger/code_puppy
@@ -25,7 +25,7 @@ Requires-Dist: logfire>=0.7.1
25
25
  Requires-Dist: openai>=1.99.1
26
26
  Requires-Dist: pathspec>=0.11.0
27
27
  Requires-Dist: playwright>=1.40.0
28
- Requires-Dist: prompt-toolkit>=3.0.38
28
+ Requires-Dist: prompt-toolkit>=3.0.52
29
29
  Requires-Dist: pydantic-ai==1.0.6
30
30
  Requires-Dist: pydantic>=2.4.0
31
31
  Requires-Dist: pyjwt>=2.8.0
@@ -69,6 +69,17 @@ Code Puppy is an AI-powered code generation agent, designed to understand progra
69
69
 
70
70
  ## Features
71
71
 
72
+ ### Session Autosave & Contexts
73
+ - Autosaves live in `~/.code_puppy/autosaves` and include a `.pkl` and `_meta.json` per session.
74
+ - On startup, you’ll be prompted to optionally load a recent autosave (with message counts and timestamps).
75
+ - Autosaves use a stable session ID per interactive run so subsequent prompts overwrite the same session (not N new files). Rotate via `/session new` when you want a fresh session.
76
+ - Loading an autosave makes it the active autosave target (future autosaves overwrite that loaded session).
77
+ - Loading a manual context with `/load_context <name>` automatically rotates the autosave ID to avoid overwriting anything.
78
+ - Helpers:
79
+ - `/session id` shows the current autosave ID and file prefix
80
+ - `/session new` rotates the autosave ID
81
+
82
+
72
83
  - **Multi-language support**: Capable of generating code in various programming languages.
73
84
  - **Interactive CLI**: A command-line interface for interactive use.
74
85
  - **Detailed explanations**: Provides insights into generated code to understand its logic and structure.
@@ -25,6 +25,17 @@ Code Puppy is an AI-powered code generation agent, designed to understand progra
25
25
 
26
26
  ## Features
27
27
 
28
+ ### Session Autosave & Contexts
29
+ - Autosaves live in `~/.code_puppy/autosaves` and include a `.pkl` and `_meta.json` per session.
30
+ - On startup, you’ll be prompted to optionally load a recent autosave (with message counts and timestamps).
31
+ - Autosaves use a stable session ID per interactive run so subsequent prompts overwrite the same session (not N new files). Rotate via `/session new` when you want a fresh session.
32
+ - Loading an autosave makes it the active autosave target (future autosaves overwrite that loaded session).
33
+ - Loading a manual context with `/load_context <name>` automatically rotates the autosave ID to avoid overwriting anything.
34
+ - Helpers:
35
+ - `/session id` shows the current autosave ID and file prefix
36
+ - `/session new` rotates the autosave ID
37
+
38
+
28
39
  - **Multi-language support**: Capable of generating code in various programming languages.
29
40
  - **Interactive CLI**: A command-line interface for interactive use.
30
41
  - **Detailed explanations**: Provides insights into generated code to understand its logic and structure.
@@ -720,10 +720,7 @@ class BaseAgent(ABC):
720
720
  emit_system_message(
721
721
  f"[green]Successfully loaded {len(servers)} MCP server(s)[/green]"
722
722
  )
723
- else:
724
- emit_system_message(
725
- "[yellow]No MCP servers available (check if servers are enabled)[/yellow]"
726
- )
723
+ # Stay silent when there are no servers configured/available
727
724
  return servers
728
725
 
729
726
  def reload_mcp_servers(self):
@@ -891,7 +888,8 @@ class BaseAgent(ABC):
891
888
  asyncio.CancelledError: When execution is cancelled by user
892
889
  """
893
890
  group_id = str(uuid.uuid4())
894
- pydantic_agent = self.reload_code_generation_agent()
891
+ # Avoid double-loading: reuse existing agent if already built
892
+ pydantic_agent = self._code_generation_agent or self.reload_code_generation_agent()
895
893
 
896
894
  async def run_agent_task():
897
895
  try:
@@ -101,6 +101,14 @@ class JSONAgent(BaseAgent):
101
101
  """Get tool configuration from JSON config."""
102
102
  return self._config.get("tools_config")
103
103
 
104
+ def refresh_config(self) -> None:
105
+ """Reload the agent configuration from disk.
106
+
107
+ This keeps long-lived agent instances in sync after external edits.
108
+ """
109
+ self._config = self._load_config()
110
+ self._validate_config()
111
+
104
112
  def get_model_name(self) -> Optional[str]:
105
113
  """Get pinned model name from JSON config, if specified.
106
114
 
@@ -5,131 +5,103 @@ from pathlib import Path
5
5
  from code_puppy.command_line.model_picker_completion import update_model_in_input
6
6
  from code_puppy.command_line.motd import print_motd
7
7
  from code_puppy.command_line.utils import make_directory_table
8
- from code_puppy.config import CONTEXTS_DIR, get_config_keys
8
+ from code_puppy.config import (
9
+ CONTEXTS_DIR,
10
+ finalize_autosave_session,
11
+ get_config_keys,
12
+ )
9
13
  from code_puppy.session_storage import list_sessions, load_session, save_session
10
14
  from code_puppy.tools.tools_content import tools_content
11
15
 
12
16
 
13
17
  def get_commands_help():
14
- """Generate commands help using Rich Text objects to avoid markup conflicts."""
18
+ """Generate aligned commands help using Rich Text for safe markup."""
15
19
  from rich.text import Text
16
20
 
17
21
  # Ensure plugins are loaded so custom help can register
18
22
  _ensure_plugins_loaded()
19
23
 
20
- # Build help text programmatically
21
- help_lines = []
22
-
23
- # Title
24
- help_lines.append(Text("Commands Help", style="bold magenta"))
25
-
26
- # Commands - build each line programmatically
27
- help_lines.append(
28
- Text("/help, /h", style="cyan") + Text(" Show this help message")
29
- )
30
- help_lines.append(
31
- Text("/cd", style="cyan")
32
- + Text(" <dir> Change directory or show directories")
33
- )
34
- help_lines.append(
35
- Text("/agent", style="cyan")
36
- + Text(" <name> Switch to a different agent or show available agents")
37
- )
38
- help_lines.append(
39
- Text("/exit, /quit", style="cyan") + Text(" Exit interactive mode")
40
- )
41
- help_lines.append(
42
- Text("/generate-pr-description", style="cyan")
43
- + Text(" [@dir] Generate comprehensive PR description")
44
- )
45
- help_lines.append(
46
- Text("/model, /m", style="cyan") + Text(" <model> Set active model")
47
- )
48
- help_lines.append(
49
- Text("/reasoning", style="cyan")
50
- + Text(" <low|medium|high> Set OpenAI reasoning effort for GPT-5 models")
51
- )
52
- help_lines.append(
53
- Text("/pin_model", style="cyan")
54
- + Text(" <agent> <model> Pin a specific model to an agent")
55
- )
56
- help_lines.append(
57
- Text("/mcp", style="cyan")
58
- + Text(" Manage MCP servers (list, start, stop, status, etc.)")
59
- )
60
- help_lines.append(
61
- Text("/motd", style="cyan")
62
- + Text(" Show the latest message of the day (MOTD)")
63
- )
64
- help_lines.append(
65
- Text("/show", style="cyan")
66
- + Text(" Show puppy config key-values")
67
- )
68
- help_lines.append(
69
- Text("/compact", style="cyan")
70
- + Text(
71
- " Summarize and compact current chat history (uses compaction_strategy config)"
72
- )
73
- )
74
- help_lines.append(
75
- Text("/dump_context", style="cyan")
76
- + Text(" <name> Save current message history to file")
77
- )
78
- help_lines.append(
79
- Text("/load_context", style="cyan")
80
- + Text(" <name> Load message history from file")
81
- )
82
- help_lines.append(
83
- Text("/set", style="cyan")
84
- + Text(
85
- " Set puppy config key-values (e.g., /set yolo_mode true, /set auto_save_session true)"
86
- )
87
- )
88
- help_lines.append(
89
- Text("/tools", style="cyan")
90
- + Text(" Show available tools and capabilities")
91
- )
92
- help_lines.append(
93
- Text("/truncate", style="cyan")
94
- + Text(
95
- " <N> Truncate message history to N most recent messages (keeping system message)"
96
- )
97
- )
98
- help_lines.append(
99
- Text("/<unknown>", style="cyan")
100
- + Text(" Show unknown command warning")
101
- )
24
+ # Collect core commands with their syntax parts and descriptions
25
+ # (cmd_syntax, description)
26
+ core_cmds = [
27
+ ("/help, /h", "Show this help message"),
28
+ ("/cd <dir>", "Change directory or show directories"),
29
+ (
30
+ "/agent <name>",
31
+ "Switch to a different agent or show available agents",
32
+ ),
33
+ ("/exit, /quit", "Exit interactive mode"),
34
+ ("/generate-pr-description [@dir]", "Generate comprehensive PR description"),
35
+ ("/model, /m <model>", "Set active model"),
36
+ ("/reasoning <low|medium|high>", "Set OpenAI reasoning effort for GPT-5 models"),
37
+ ("/pin_model <agent> <model>", "Pin a specific model to an agent"),
38
+ ("/mcp", "Manage MCP servers (list, start, stop, status, etc.)"),
39
+ ("/motd", "Show the latest message of the day (MOTD)"),
40
+ ("/show", "Show puppy config key-values"),
41
+ (
42
+ "/compact",
43
+ "Summarize and compact current chat history (uses compaction_strategy config)",
44
+ ),
45
+ ("/dump_context <name>", "Save current message history to file"),
46
+ ("/load_context <name>", "Load message history from file"),
47
+ (
48
+ "/set",
49
+ "Set puppy config (e.g., /set yolo_mode true, /set auto_save_session true)",
50
+ ),
51
+ ("/tools", "Show available tools and capabilities"),
52
+ (
53
+ "/truncate <N>",
54
+ "Truncate history to N most recent messages (keeping system message)",
55
+ ),
56
+ ("/<unknown>", "Show unknown command warning"),
57
+ ]
58
+
59
+ # Determine padding width for the left column
60
+ left_width = max(len(cmd) for cmd, _ in core_cmds) + 2 # add spacing
61
+
62
+ lines: list[Text] = []
63
+ lines.append(Text("Commands Help", style="bold magenta"))
64
+
65
+ for cmd, desc in core_cmds:
66
+ left = Text(cmd.ljust(left_width), style="cyan")
67
+ right = Text(desc)
68
+ line = Text()
69
+ line.append_text(left)
70
+ line.append_text(right)
71
+ lines.append(line)
102
72
 
103
73
  # Add custom commands from plugins (if any)
104
74
  try:
105
75
  from code_puppy import callbacks
106
76
 
107
77
  custom_help_results = callbacks.on_custom_command_help()
108
- # Flatten various returns into a list of (name, description)
109
- custom_entries = []
78
+ custom_entries: list[tuple[str, str]] = []
110
79
  for res in custom_help_results:
111
80
  if not res:
112
81
  continue
113
82
  if isinstance(res, tuple) and len(res) == 2:
114
- custom_entries.append(res)
83
+ custom_entries.append((str(res[0]), str(res[1])))
115
84
  elif isinstance(res, list):
116
85
  for item in res:
117
86
  if isinstance(item, tuple) and len(item) == 2:
118
- custom_entries.append(item)
87
+ custom_entries.append((str(item[0]), str(item[1])))
119
88
  if custom_entries:
120
- help_lines.append(Text("\n", style="dim"))
121
- help_lines.append(Text("Custom Commands", style="bold magenta"))
89
+ lines.append(Text("", style="dim"))
90
+ lines.append(Text("Custom Commands", style="bold magenta"))
91
+ # Compute padding for custom commands as well
92
+ custom_left_width = max(len(name) for name, _ in custom_entries) + 3
122
93
  for name, desc in custom_entries:
123
- help_lines.append(
124
- Text(f"/{name}", style="cyan") + Text(f" {desc}")
125
- )
94
+ left = Text(f"/{name}".ljust(custom_left_width), style="cyan")
95
+ right = Text(desc)
96
+ line = Text()
97
+ line.append_text(left)
98
+ line.append_text(right)
99
+ lines.append(line)
126
100
  except Exception:
127
- # If callbacks fail, skip custom help silently
128
101
  pass
129
102
 
130
- # Combine all lines
131
103
  final_text = Text()
132
- for i, line in enumerate(help_lines):
104
+ for i, line in enumerate(lines):
133
105
  if i > 0:
134
106
  final_text.append("\n")
135
107
  final_text.append_text(line)
@@ -461,31 +433,44 @@ def handle_command(command: str):
461
433
  import uuid
462
434
 
463
435
  group_id = str(uuid.uuid4())
436
+ available_agents = get_available_agents()
464
437
 
465
- if set_current_agent(agent_name):
466
- # Reload the agent with new configuration
467
- agent = get_current_agent()
468
- agent.reload_code_generation_agent()
469
- new_agent = get_current_agent()
470
- emit_success(
471
- f"Switched to agent: {new_agent.display_name}",
438
+ if agent_name not in available_agents:
439
+ emit_error(f"Agent '{agent_name}' not found", message_group=group_id)
440
+ emit_warning(
441
+ f"Available agents: {', '.join(available_agents.keys())}",
472
442
  message_group=group_id,
473
443
  )
474
- emit_info(f"[dim]{new_agent.description}[/dim]", message_group=group_id)
475
444
  return True
476
- else:
477
- # Generate a group ID for all messages in this command
478
- import uuid
479
445
 
480
- group_id = str(uuid.uuid4())
446
+ current_agent = get_current_agent()
447
+ if current_agent.name == agent_name:
448
+ emit_info(
449
+ f"Already using agent: {current_agent.display_name}",
450
+ message_group=group_id,
451
+ )
452
+ return True
481
453
 
482
- available_agents = get_available_agents()
483
- emit_error(f"Agent '{agent_name}' not found", message_group=group_id)
454
+ new_session_id = finalize_autosave_session()
455
+ if not set_current_agent(agent_name):
484
456
  emit_warning(
485
- f"Available agents: {', '.join(available_agents.keys())}",
457
+ "Agent switch failed after autosave rotation. Your context was preserved.",
486
458
  message_group=group_id,
487
459
  )
488
460
  return True
461
+
462
+ new_agent = get_current_agent()
463
+ new_agent.reload_code_generation_agent()
464
+ emit_success(
465
+ f"Switched to agent: {new_agent.display_name}",
466
+ message_group=group_id,
467
+ )
468
+ emit_info(f"[dim]{new_agent.description}[/dim]", message_group=group_id)
469
+ emit_info(
470
+ f"[dim]Auto-save session rotated to: {new_session_id}[/dim]",
471
+ message_group=group_id,
472
+ )
473
+ return True
489
474
  else:
490
475
  emit_warning("Usage: /agent [agent-name]")
491
476
  return True
@@ -625,12 +610,22 @@ def handle_command(command: str):
625
610
 
626
611
  emit_success(f"Model '{model_name}' pinned to agent '{agent_name}'")
627
612
 
628
- # If this is the current agent, reload it to use the new model
613
+ # If this is the current agent, refresh it so the prompt updates immediately
629
614
  from code_puppy.agents import get_current_agent
630
615
 
631
616
  current_agent = get_current_agent()
632
617
  if current_agent.name == agent_name:
633
- emit_info(f"Active agent reloaded with pinned model '{model_name}'")
618
+ try:
619
+ if is_json_agent and hasattr(current_agent, "refresh_config"):
620
+ current_agent.refresh_config()
621
+ current_agent.reload_code_generation_agent()
622
+ emit_info(
623
+ f"Active agent reloaded with pinned model '{model_name}'"
624
+ )
625
+ except Exception as reload_error:
626
+ emit_warning(
627
+ f"Pinned model applied but reload failed: {reload_error}"
628
+ )
634
629
 
635
630
  return True
636
631
 
@@ -194,24 +194,48 @@ async def get_input_with_combined_completion(
194
194
  LoadContextCompleter(trigger="/load_context"),
195
195
  ]
196
196
  )
197
- # Add custom key bindings for multiline input
197
+ # Add custom key bindings and multiline toggle
198
198
  bindings = KeyBindings()
199
199
 
200
- @bindings.add(Keys.Escape, "m") # Alt+M (legacy support)
200
+ # Multiline mode state
201
+ multiline = {"enabled": False}
202
+
203
+ # Toggle multiline with Alt+M
204
+ @bindings.add(Keys.Escape, "m")
201
205
  def _(event):
202
- event.app.current_buffer.insert_text("\n")
206
+ multiline["enabled"] = not multiline["enabled"]
207
+ status = "ON" if multiline["enabled"] else "OFF"
208
+ # Print status for user feedback (version-agnostic)
209
+ print(f"[multiline] {status}", flush=True)
210
+
211
+ # Also toggle multiline with F2 (more reliable across platforms)
212
+ @bindings.add("f2")
213
+ def _(event):
214
+ multiline["enabled"] = not multiline["enabled"]
215
+ status = "ON" if multiline["enabled"] else "OFF"
216
+ print(f"[multiline] {status}", flush=True)
203
217
 
204
- # Create a special binding for shift+enter
205
- @bindings.add("escape", "enter")
218
+ # Newline insert bindings robust and explicit
219
+ # Ctrl+J (line feed) works in virtually all terminals; mark eager so it wins
220
+ @bindings.add("c-j", eager=True)
206
221
  def _(event):
207
- """Pressing alt+enter (meta+enter) inserts a newline."""
208
222
  event.app.current_buffer.insert_text("\n")
209
223
 
210
- # Override the default enter behavior to check for shift
211
- @bindings.add("enter", filter=~is_searching)
224
+ # Also allow Ctrl+Enter for newline (terminal-dependent)
225
+ try:
226
+ @bindings.add("c-enter", eager=True)
227
+ def _(event):
228
+ event.app.current_buffer.insert_text("\n")
229
+ except Exception:
230
+ pass
231
+
232
+ # Enter behavior depends on multiline mode
233
+ @bindings.add("enter", filter=~is_searching, eager=True)
212
234
  def _(event):
213
- """Accept input only when we're not in an interactive search buffer."""
214
- event.current_buffer.validate_and_handle()
235
+ if multiline["enabled"]:
236
+ event.app.current_buffer.insert_text("\n")
237
+ else:
238
+ event.current_buffer.validate_and_handle()
215
239
 
216
240
  @bindings.add(Keys.Escape)
217
241
  def _(event):
@@ -784,3 +784,9 @@ def auto_save_session_if_enabled() -> bool:
784
784
 
785
785
  Console().print(f"[dim]❌ Failed to auto-save session: {exc}[/dim]")
786
786
  return False
787
+
788
+
789
+ def finalize_autosave_session() -> str:
790
+ """Persist the current autosave snapshot and rotate to a fresh session."""
791
+ auto_save_session_if_enabled()
792
+ return rotate_autosave_id()
@@ -24,6 +24,7 @@ from code_puppy.config import (
24
24
  AUTOSAVE_DIR,
25
25
  COMMAND_HISTORY_FILE,
26
26
  ensure_config_exists,
27
+ finalize_autosave_session,
27
28
  initialize_command_history_file,
28
29
  save_command_to_history,
29
30
  )
@@ -272,16 +273,13 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
272
273
  emit_info("[bold green]Code Puppy[/bold green] - Interactive Mode")
273
274
  emit_system_message("Type '/exit' or '/quit' to exit the interactive mode.")
274
275
  emit_system_message("Type 'clear' to reset the conversation history.")
276
+ emit_system_message("[dim]Type /help to view all commands[/dim]")
275
277
  emit_system_message(
276
- "Type [bold blue]@[/bold blue] for path completion, or [bold blue]/m[/bold blue] to pick a model. Use [bold blue]Esc+Enter[/bold blue] for multi-line input."
278
+ "Type [bold blue]@[/bold blue] for path completion, or [bold blue]/m[/bold blue] to pick a model. Toggle multiline with [bold blue]Alt+M[/bold blue] or [bold blue]F2[/bold blue]; newline: [bold blue]Ctrl+J[/bold blue]."
277
279
  )
278
280
  emit_system_message(
279
281
  "Press [bold red]Ctrl+C[/bold red] during processing to cancel the current task or inference."
280
282
  )
281
- from code_puppy.command_line.command_handler import get_commands_help
282
-
283
- help_text = get_commands_help()
284
- emit_system_message(help_text)
285
283
  try:
286
284
  from code_puppy.command_line.motd import print_motd
287
285
 
@@ -417,12 +415,14 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
417
415
 
418
416
  # Check for clear command (supports both `clear` and `/clear`)
419
417
  if task.strip().lower() in ("clear", "/clear"):
420
- from code_puppy.messaging import emit_system_message, emit_warning
418
+ from code_puppy.messaging import emit_info, emit_system_message, emit_warning
421
419
 
422
420
  agent = get_current_agent()
421
+ new_session_id = finalize_autosave_session()
423
422
  agent.clear_message_history()
424
423
  emit_warning("Conversation history cleared!")
425
424
  emit_system_message("The agent will not remember previous interactions.\n")
425
+ emit_info(f"[dim]Auto-save session rotated to: {new_session_id}[/dim]")
426
426
  continue
427
427
 
428
428
  # Handle / commands before anything else