code-puppy 0.0.195__tar.gz → 0.0.197__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.195 → code_puppy-0.0.197}/PKG-INFO +13 -2
  2. {code_puppy-0.0.195 → code_puppy-0.0.197}/README.md +11 -0
  3. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/agents/base_agent.py +3 -5
  4. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/command_line/command_handler.py +96 -97
  5. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/command_line/prompt_toolkit_completion.py +34 -10
  6. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/config.py +37 -16
  7. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/main.py +2 -5
  8. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/session_storage.py +15 -6
  9. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/tui/app.py +153 -7
  10. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/tui/components/input_area.py +1 -1
  11. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/tui/components/status_bar.py +4 -1
  12. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/tui/screens/__init__.py +2 -0
  13. code_puppy-0.0.197/code_puppy/tui/screens/autosave_picker.py +166 -0
  14. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/tui/screens/settings.py +4 -2
  15. {code_puppy-0.0.195 → code_puppy-0.0.197}/pyproject.toml +2 -2
  16. {code_puppy-0.0.195 → code_puppy-0.0.197}/.gitignore +0 -0
  17. {code_puppy-0.0.195 → code_puppy-0.0.197}/LICENSE +0 -0
  18. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/__init__.py +0 -0
  19. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/__main__.py +0 -0
  20. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/agents/__init__.py +0 -0
  21. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/agents/agent_c_reviewer.py +0 -0
  22. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/agents/agent_code_puppy.py +0 -0
  23. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/agents/agent_code_reviewer.py +0 -0
  24. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/agents/agent_cpp_reviewer.py +0 -0
  25. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/agents/agent_creator_agent.py +0 -0
  26. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/agents/agent_golang_reviewer.py +0 -0
  27. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/agents/agent_javascript_reviewer.py +0 -0
  28. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/agents/agent_manager.py +0 -0
  29. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/agents/agent_python_reviewer.py +0 -0
  30. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/agents/agent_qa_expert.py +0 -0
  31. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/agents/agent_qa_kitten.py +0 -0
  32. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/agents/agent_security_auditor.py +0 -0
  33. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/agents/agent_typescript_reviewer.py +0 -0
  34. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/agents/json_agent.py +0 -0
  35. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/callbacks.py +0 -0
  36. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/command_line/__init__.py +0 -0
  37. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/command_line/file_path_completion.py +0 -0
  38. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/command_line/load_context_completion.py +0 -0
  39. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/command_line/mcp/__init__.py +0 -0
  40. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/command_line/mcp/add_command.py +0 -0
  41. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/command_line/mcp/base.py +0 -0
  42. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/command_line/mcp/handler.py +0 -0
  43. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/command_line/mcp/help_command.py +0 -0
  44. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/command_line/mcp/install_command.py +0 -0
  45. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/command_line/mcp/list_command.py +0 -0
  46. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/command_line/mcp/logs_command.py +0 -0
  47. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/command_line/mcp/remove_command.py +0 -0
  48. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/command_line/mcp/restart_command.py +0 -0
  49. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/command_line/mcp/search_command.py +0 -0
  50. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/command_line/mcp/start_all_command.py +0 -0
  51. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/command_line/mcp/start_command.py +0 -0
  52. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/command_line/mcp/status_command.py +0 -0
  53. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/command_line/mcp/stop_all_command.py +0 -0
  54. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/command_line/mcp/stop_command.py +0 -0
  55. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/command_line/mcp/test_command.py +0 -0
  56. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/command_line/mcp/utils.py +0 -0
  57. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/command_line/mcp/wizard_utils.py +0 -0
  58. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/command_line/model_picker_completion.py +0 -0
  59. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/command_line/motd.py +0 -0
  60. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/command_line/utils.py +0 -0
  61. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/http_utils.py +0 -0
  62. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/mcp_/__init__.py +0 -0
  63. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/mcp_/async_lifecycle.py +0 -0
  64. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/mcp_/blocking_startup.py +0 -0
  65. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/mcp_/captured_stdio_server.py +0 -0
  66. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/mcp_/circuit_breaker.py +0 -0
  67. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/mcp_/config_wizard.py +0 -0
  68. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/mcp_/dashboard.py +0 -0
  69. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/mcp_/error_isolation.py +0 -0
  70. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/mcp_/examples/retry_example.py +0 -0
  71. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/mcp_/health_monitor.py +0 -0
  72. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/mcp_/managed_server.py +0 -0
  73. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/mcp_/manager.py +0 -0
  74. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/mcp_/registry.py +0 -0
  75. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/mcp_/retry_manager.py +0 -0
  76. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/mcp_/server_registry_catalog.py +0 -0
  77. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/mcp_/status_tracker.py +0 -0
  78. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/mcp_/system_tools.py +0 -0
  79. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/messaging/__init__.py +0 -0
  80. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/messaging/message_queue.py +0 -0
  81. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/messaging/queue_console.py +0 -0
  82. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/messaging/renderers.py +0 -0
  83. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/messaging/spinner/__init__.py +0 -0
  84. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/messaging/spinner/console_spinner.py +0 -0
  85. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/messaging/spinner/spinner_base.py +0 -0
  86. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/messaging/spinner/textual_spinner.py +0 -0
  87. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/model_factory.py +0 -0
  88. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/models.json +0 -0
  89. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/plugins/__init__.py +0 -0
  90. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/plugins/example_custom_command/register_callbacks.py +0 -0
  91. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/reopenable_async_client.py +0 -0
  92. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/round_robin_model.py +0 -0
  93. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/status_display.py +0 -0
  94. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/summarization_agent.py +0 -0
  95. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/tools/__init__.py +0 -0
  96. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/tools/agent_tools.py +0 -0
  97. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/tools/browser/__init__.py +0 -0
  98. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/tools/browser/browser_control.py +0 -0
  99. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/tools/browser/browser_interactions.py +0 -0
  100. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/tools/browser/browser_locators.py +0 -0
  101. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/tools/browser/browser_navigation.py +0 -0
  102. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/tools/browser/browser_screenshot.py +0 -0
  103. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/tools/browser/browser_scripts.py +0 -0
  104. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/tools/browser/browser_workflows.py +0 -0
  105. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/tools/browser/camoufox_manager.py +0 -0
  106. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/tools/browser/vqa_agent.py +0 -0
  107. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/tools/command_runner.py +0 -0
  108. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/tools/common.py +0 -0
  109. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/tools/file_modifications.py +0 -0
  110. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/tools/file_operations.py +0 -0
  111. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/tools/tools_content.py +0 -0
  112. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/tui/__init__.py +0 -0
  113. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/tui/components/__init__.py +0 -0
  114. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/tui/components/chat_view.py +0 -0
  115. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/tui/components/command_history_modal.py +0 -0
  116. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/tui/components/copy_button.py +0 -0
  117. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/tui/components/custom_widgets.py +0 -0
  118. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/tui/components/human_input_modal.py +0 -0
  119. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/tui/components/sidebar.py +0 -0
  120. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/tui/messages.py +0 -0
  121. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/tui/models/__init__.py +0 -0
  122. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/tui/models/chat_message.py +0 -0
  123. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/tui/models/command_history.py +0 -0
  124. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/tui/models/enums.py +0 -0
  125. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/tui/screens/help.py +0 -0
  126. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/tui/screens/mcp_install_wizard.py +0 -0
  127. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/tui/screens/tools.py +0 -0
  128. {code_puppy-0.0.195 → code_puppy-0.0.197}/code_puppy/tui_state.py +0 -0
  129. {code_puppy-0.0.195 → code_puppy-0.0.197}/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.195
3
+ Version: 0.0.197
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:
@@ -11,125 +11,93 @@ from code_puppy.tools.tools_content import tools_content
11
11
 
12
12
 
13
13
  def get_commands_help():
14
- """Generate commands help using Rich Text objects to avoid markup conflicts."""
14
+ """Generate aligned commands help using Rich Text for safe markup."""
15
15
  from rich.text import Text
16
16
 
17
17
  # Ensure plugins are loaded so custom help can register
18
18
  _ensure_plugins_loaded()
19
19
 
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, /set max_saved_sessions 20)"
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
- )
20
+ # Collect core commands with their syntax parts and descriptions
21
+ # (cmd_syntax, description)
22
+ core_cmds = [
23
+ ("/help, /h", "Show this help message"),
24
+ ("/cd <dir>", "Change directory or show directories"),
25
+ (
26
+ "/agent <name>",
27
+ "Switch to a different agent or show available agents",
28
+ ),
29
+ ("/exit, /quit", "Exit interactive mode"),
30
+ ("/generate-pr-description [@dir]", "Generate comprehensive PR description"),
31
+ ("/model, /m <model>", "Set active model"),
32
+ ("/reasoning <low|medium|high>", "Set OpenAI reasoning effort for GPT-5 models"),
33
+ ("/pin_model <agent> <model>", "Pin a specific model to an agent"),
34
+ ("/mcp", "Manage MCP servers (list, start, stop, status, etc.)"),
35
+ ("/motd", "Show the latest message of the day (MOTD)"),
36
+ ("/show", "Show puppy config key-values"),
37
+ (
38
+ "/compact",
39
+ "Summarize and compact current chat history (uses compaction_strategy config)",
40
+ ),
41
+ ("/dump_context <name>", "Save current message history to file"),
42
+ ("/load_context <name>", "Load message history from file"),
43
+ (
44
+ "/set",
45
+ "Set puppy config (e.g., /set yolo_mode true, /set auto_save_session true)",
46
+ ),
47
+ ("/tools", "Show available tools and capabilities"),
48
+ (
49
+ "/truncate <N>",
50
+ "Truncate history to N most recent messages (keeping system message)",
51
+ ),
52
+ ("/<unknown>", "Show unknown command warning"),
53
+ ]
54
+
55
+ # Determine padding width for the left column
56
+ left_width = max(len(cmd) for cmd, _ in core_cmds) + 2 # add spacing
57
+
58
+ lines: list[Text] = []
59
+ lines.append(Text("Commands Help", style="bold magenta"))
60
+
61
+ for cmd, desc in core_cmds:
62
+ left = Text(cmd.ljust(left_width), style="cyan")
63
+ right = Text(desc)
64
+ line = Text()
65
+ line.append_text(left)
66
+ line.append_text(right)
67
+ lines.append(line)
102
68
 
103
69
  # Add custom commands from plugins (if any)
104
70
  try:
105
71
  from code_puppy import callbacks
106
72
 
107
73
  custom_help_results = callbacks.on_custom_command_help()
108
- # Flatten various returns into a list of (name, description)
109
- custom_entries = []
74
+ custom_entries: list[tuple[str, str]] = []
110
75
  for res in custom_help_results:
111
76
  if not res:
112
77
  continue
113
78
  if isinstance(res, tuple) and len(res) == 2:
114
- custom_entries.append(res)
79
+ custom_entries.append((str(res[0]), str(res[1])))
115
80
  elif isinstance(res, list):
116
81
  for item in res:
117
82
  if isinstance(item, tuple) and len(item) == 2:
118
- custom_entries.append(item)
83
+ custom_entries.append((str(item[0]), str(item[1])))
119
84
  if custom_entries:
120
- help_lines.append(Text("\n", style="dim"))
121
- help_lines.append(Text("Custom Commands", style="bold magenta"))
85
+ lines.append(Text("", style="dim"))
86
+ lines.append(Text("Custom Commands", style="bold magenta"))
87
+ # Compute padding for custom commands as well
88
+ custom_left_width = max(len(name) for name, _ in custom_entries) + 3
122
89
  for name, desc in custom_entries:
123
- help_lines.append(
124
- Text(f"/{name}", style="cyan") + Text(f" {desc}")
125
- )
90
+ left = Text(f"/{name}".ljust(custom_left_width), style="cyan")
91
+ right = Text(desc)
92
+ line = Text()
93
+ line.append_text(left)
94
+ line.append_text(right)
95
+ lines.append(line)
126
96
  except Exception:
127
- # If callbacks fail, skip custom help silently
128
97
  pass
129
98
 
130
- # Combine all lines
131
99
  final_text = Text()
132
- for i, line in enumerate(help_lines):
100
+ for i, line in enumerate(lines):
133
101
  if i > 0:
134
102
  final_text.append("\n")
135
103
  final_text.append_text(line)
@@ -336,6 +304,30 @@ def handle_command(command: str):
336
304
  )
337
305
  return True
338
306
 
307
+ if command.startswith("/session"):
308
+ # /session id -> show current autosave id
309
+ # /session new -> rotate autosave id
310
+ tokens = command.split()
311
+ from code_puppy.config import (
312
+ AUTOSAVE_DIR,
313
+ get_current_autosave_id,
314
+ get_current_autosave_session_name,
315
+ rotate_autosave_id,
316
+ )
317
+ if len(tokens) == 1 or tokens[1] == "id":
318
+ sid = get_current_autosave_id()
319
+ emit_info(
320
+ f"[bold magenta]Autosave Session[/bold magenta]: {sid}\n"
321
+ f"Files prefix: {Path(AUTOSAVE_DIR) / get_current_autosave_session_name()}"
322
+ )
323
+ return True
324
+ if tokens[1] == "new":
325
+ new_sid = rotate_autosave_id()
326
+ emit_success(f"New autosave session id: {new_sid}")
327
+ return True
328
+ emit_warning("Usage: /session [id|new]")
329
+ return True
330
+
339
331
  if command.startswith("/set"):
340
332
  # Syntax: /set KEY=VALUE or /set KEY VALUE
341
333
  from code_puppy.config import set_config_value
@@ -361,7 +353,6 @@ def handle_command(command: str):
361
353
  session_help = (
362
354
  "\n[yellow]Session Management[/yellow]"
363
355
  "\n [cyan]auto_save_session[/cyan] Auto-save chat after every response (true/false)"
364
- "\n [cyan]max_saved_sessions[/cyan] Cap how many auto-saves to keep (0 = unlimited)"
365
356
  )
366
357
  emit_warning(
367
358
  f"Usage: /set KEY=VALUE or /set KEY VALUE\nConfig keys: {', '.join(config_keys)}\n[dim]Note: compaction_strategy can be 'summarization' or 'truncation'[/dim]{session_help}"
@@ -712,9 +703,17 @@ def handle_command(command: str):
712
703
  agent.set_message_history(history)
713
704
  total_tokens = sum(agent.estimate_tokens_for_message(m) for m in history)
714
705
 
706
+ # Rotate autosave id to avoid overwriting any existing autosave
707
+ try:
708
+ from code_puppy.config import rotate_autosave_id
709
+ new_id = rotate_autosave_id()
710
+ autosave_info = f"\n[dim]Autosave session rotated to: {new_id}[/dim]"
711
+ except Exception:
712
+ autosave_info = ""
713
+
715
714
  emit_success(
716
715
  f"✅ Context loaded: {len(history)} messages ({total_tokens} tokens)\n"
717
- f"📁 From: {session_path}"
716
+ f"📁 From: {session_path}{autosave_info}"
718
717
  )
719
718
  return True
720
719
 
@@ -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):
@@ -3,8 +3,9 @@ import datetime
3
3
  import json
4
4
  import os
5
5
  import pathlib
6
+ from typing import Optional
6
7
 
7
- from code_puppy.session_storage import cleanup_sessions, save_session
8
+ from code_puppy.session_storage import save_session
8
9
 
9
10
  CONFIG_DIR = os.path.join(os.path.expanduser("~"), ".code_puppy")
10
11
  CONFIG_FILE = os.path.join(CONFIG_DIR, "puppy.cfg")
@@ -19,6 +20,9 @@ AUTOSAVE_DIR = os.path.join(CONFIG_DIR, "autosaves")
19
20
  DEFAULT_SECTION = "puppy"
20
21
  REQUIRED_KEYS = ["puppy_name", "owner_name"]
21
22
 
23
+ # Runtime-only autosave session ID (per-process)
24
+ _CURRENT_AUTOSAVE_ID: Optional[str] = None
25
+
22
26
  # Cache containers for model validation and defaults
23
27
  _model_validation_cache = {}
24
28
  _default_model_cache = None
@@ -702,22 +706,40 @@ def set_max_saved_sessions(max_sessions: int):
702
706
  set_config_value("max_saved_sessions", str(max_sessions))
703
707
 
704
708
 
705
- def _cleanup_old_sessions():
706
- """Remove oldest auto-saved sessions if we exceed the max_saved_sessions limit."""
707
- max_sessions = get_max_saved_sessions()
708
- if max_sessions <= 0:
709
- return
709
+ def get_current_autosave_id() -> str:
710
+ """Get or create the current autosave session ID for this process."""
711
+ global _CURRENT_AUTOSAVE_ID
712
+ if not _CURRENT_AUTOSAVE_ID:
713
+ # Use a full timestamp so tests and UX can predict the name if needed
714
+ _CURRENT_AUTOSAVE_ID = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
715
+ return _CURRENT_AUTOSAVE_ID
710
716
 
711
- autosave_dir = pathlib.Path(AUTOSAVE_DIR)
712
- removed_sessions = cleanup_sessions(autosave_dir, max_sessions)
713
- if not removed_sessions:
714
- return
715
717
 
716
- from rich.console import Console
718
+ def rotate_autosave_id() -> str:
719
+ """Force a new autosave session ID and return it."""
720
+ global _CURRENT_AUTOSAVE_ID
721
+ _CURRENT_AUTOSAVE_ID = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
722
+ return _CURRENT_AUTOSAVE_ID
723
+
717
724
 
718
- console = Console()
719
- for session_name in removed_sessions:
720
- console.print(f"[dim]🗑️ Removed old session: {session_name}.pkl[/dim]")
725
+ def get_current_autosave_session_name() -> str:
726
+ """Return the full session name used for autosaves (no file extension)."""
727
+ return f"auto_session_{get_current_autosave_id()}"
728
+
729
+
730
+ def set_current_autosave_from_session_name(session_name: str) -> str:
731
+ """Set the current autosave ID based on a full session name.
732
+
733
+ Accepts names like 'auto_session_YYYYMMDD_HHMMSS' and extracts the ID part.
734
+ Returns the ID that was set.
735
+ """
736
+ global _CURRENT_AUTOSAVE_ID
737
+ prefix = "auto_session_"
738
+ if session_name.startswith(prefix):
739
+ _CURRENT_AUTOSAVE_ID = session_name[len(prefix):]
740
+ else:
741
+ _CURRENT_AUTOSAVE_ID = session_name
742
+ return _CURRENT_AUTOSAVE_ID
721
743
 
722
744
 
723
745
  def auto_save_session_if_enabled() -> bool:
@@ -739,7 +761,7 @@ def auto_save_session_if_enabled() -> bool:
739
761
  return False
740
762
 
741
763
  now = datetime.datetime.now()
742
- session_name = f"auto_session_{now.strftime('%Y%m%d_%H%M%S')}"
764
+ session_name = get_current_autosave_session_name()
743
765
  autosave_dir = pathlib.Path(AUTOSAVE_DIR)
744
766
 
745
767
  metadata = save_session(
@@ -755,7 +777,6 @@ def auto_save_session_if_enabled() -> bool:
755
777
  f"🐾 [dim]Auto-saved session: {metadata.message_count} messages ({metadata.total_tokens} tokens)[/dim]"
756
778
  )
757
779
 
758
- _cleanup_old_sessions()
759
780
  return True
760
781
 
761
782
  except Exception as exc: # pragma: no cover - defensive logging
@@ -272,16 +272,13 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
272
272
  emit_info("[bold green]Code Puppy[/bold green] - Interactive Mode")
273
273
  emit_system_message("Type '/exit' or '/quit' to exit the interactive mode.")
274
274
  emit_system_message("Type 'clear' to reset the conversation history.")
275
+ emit_system_message("[dim]Type /help to view all commands[/dim]")
275
276
  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."
277
+ "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
278
  )
278
279
  emit_system_message(
279
280
  "Press [bold red]Ctrl+C[/bold red] during processing to cancel the current task or inference."
280
281
  )
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
282
  try:
286
283
  from code_puppy.command_line.motd import print_motd
287
284
 
@@ -96,6 +96,12 @@ def load_session(session_name: str, base_dir: Path) -> SessionHistory:
96
96
  return pickle.load(pickle_file)
97
97
 
98
98
 
99
+ def list_sessions(base_dir: Path) -> List[str]:
100
+ if not base_dir.exists():
101
+ return []
102
+ return sorted(path.stem for path in base_dir.glob("*.pkl"))
103
+
104
+
99
105
  def cleanup_sessions(base_dir: Path, max_sessions: int) -> List[str]:
100
106
  if max_sessions <= 0:
101
107
  return []
@@ -126,12 +132,6 @@ def cleanup_sessions(base_dir: Path, max_sessions: int) -> List[str]:
126
132
  return removed_sessions
127
133
 
128
134
 
129
- def list_sessions(base_dir: Path) -> List[str]:
130
- if not base_dir.exists():
131
- return []
132
- return sorted(path.stem for path in base_dir.glob("*.pkl"))
133
-
134
-
135
135
  async def restore_autosave_interactively(base_dir: Path) -> None:
136
136
  """Prompt the user to load an autosave session from base_dir, if any exist.
137
137
 
@@ -232,6 +232,15 @@ async def restore_autosave_interactively(base_dir: Path) -> None:
232
232
 
233
233
  agent = get_current_agent()
234
234
  agent.set_message_history(history)
235
+
236
+ # Set current autosave session id so subsequent autosaves overwrite this session
237
+ try:
238
+ from code_puppy.config import set_current_autosave_from_session_name
239
+
240
+ set_current_autosave_from_session_name(chosen_name)
241
+ except Exception:
242
+ pass
243
+
235
244
  total_tokens = sum(agent.estimate_tokens_for_message(msg) for msg in history)
236
245
 
237
246
  session_path = base_dir / f"{chosen_name}.pkl"