code-puppy 0.0.196__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.196 → code_puppy-0.0.197}/PKG-INFO +13 -2
  2. {code_puppy-0.0.196 → code_puppy-0.0.197}/README.md +11 -0
  3. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/agents/base_agent.py +3 -5
  4. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/command_line/command_handler.py +63 -95
  5. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/command_line/prompt_toolkit_completion.py +34 -10
  6. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/main.py +2 -5
  7. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/tui/app.py +153 -7
  8. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/tui/components/input_area.py +1 -1
  9. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/tui/components/status_bar.py +4 -1
  10. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/tui/screens/__init__.py +2 -0
  11. code_puppy-0.0.197/code_puppy/tui/screens/autosave_picker.py +166 -0
  12. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/tui/screens/settings.py +4 -2
  13. {code_puppy-0.0.196 → code_puppy-0.0.197}/pyproject.toml +2 -2
  14. {code_puppy-0.0.196 → code_puppy-0.0.197}/.gitignore +0 -0
  15. {code_puppy-0.0.196 → code_puppy-0.0.197}/LICENSE +0 -0
  16. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/__init__.py +0 -0
  17. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/__main__.py +0 -0
  18. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/agents/__init__.py +0 -0
  19. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/agents/agent_c_reviewer.py +0 -0
  20. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/agents/agent_code_puppy.py +0 -0
  21. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/agents/agent_code_reviewer.py +0 -0
  22. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/agents/agent_cpp_reviewer.py +0 -0
  23. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/agents/agent_creator_agent.py +0 -0
  24. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/agents/agent_golang_reviewer.py +0 -0
  25. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/agents/agent_javascript_reviewer.py +0 -0
  26. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/agents/agent_manager.py +0 -0
  27. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/agents/agent_python_reviewer.py +0 -0
  28. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/agents/agent_qa_expert.py +0 -0
  29. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/agents/agent_qa_kitten.py +0 -0
  30. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/agents/agent_security_auditor.py +0 -0
  31. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/agents/agent_typescript_reviewer.py +0 -0
  32. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/agents/json_agent.py +0 -0
  33. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/callbacks.py +0 -0
  34. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/command_line/__init__.py +0 -0
  35. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/command_line/file_path_completion.py +0 -0
  36. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/command_line/load_context_completion.py +0 -0
  37. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/command_line/mcp/__init__.py +0 -0
  38. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/command_line/mcp/add_command.py +0 -0
  39. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/command_line/mcp/base.py +0 -0
  40. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/command_line/mcp/handler.py +0 -0
  41. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/command_line/mcp/help_command.py +0 -0
  42. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/command_line/mcp/install_command.py +0 -0
  43. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/command_line/mcp/list_command.py +0 -0
  44. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/command_line/mcp/logs_command.py +0 -0
  45. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/command_line/mcp/remove_command.py +0 -0
  46. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/command_line/mcp/restart_command.py +0 -0
  47. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/command_line/mcp/search_command.py +0 -0
  48. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/command_line/mcp/start_all_command.py +0 -0
  49. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/command_line/mcp/start_command.py +0 -0
  50. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/command_line/mcp/status_command.py +0 -0
  51. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/command_line/mcp/stop_all_command.py +0 -0
  52. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/command_line/mcp/stop_command.py +0 -0
  53. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/command_line/mcp/test_command.py +0 -0
  54. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/command_line/mcp/utils.py +0 -0
  55. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/command_line/mcp/wizard_utils.py +0 -0
  56. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/command_line/model_picker_completion.py +0 -0
  57. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/command_line/motd.py +0 -0
  58. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/command_line/utils.py +0 -0
  59. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/config.py +0 -0
  60. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/http_utils.py +0 -0
  61. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/mcp_/__init__.py +0 -0
  62. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/mcp_/async_lifecycle.py +0 -0
  63. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/mcp_/blocking_startup.py +0 -0
  64. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/mcp_/captured_stdio_server.py +0 -0
  65. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/mcp_/circuit_breaker.py +0 -0
  66. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/mcp_/config_wizard.py +0 -0
  67. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/mcp_/dashboard.py +0 -0
  68. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/mcp_/error_isolation.py +0 -0
  69. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/mcp_/examples/retry_example.py +0 -0
  70. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/mcp_/health_monitor.py +0 -0
  71. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/mcp_/managed_server.py +0 -0
  72. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/mcp_/manager.py +0 -0
  73. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/mcp_/registry.py +0 -0
  74. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/mcp_/retry_manager.py +0 -0
  75. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/mcp_/server_registry_catalog.py +0 -0
  76. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/mcp_/status_tracker.py +0 -0
  77. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/mcp_/system_tools.py +0 -0
  78. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/messaging/__init__.py +0 -0
  79. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/messaging/message_queue.py +0 -0
  80. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/messaging/queue_console.py +0 -0
  81. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/messaging/renderers.py +0 -0
  82. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/messaging/spinner/__init__.py +0 -0
  83. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/messaging/spinner/console_spinner.py +0 -0
  84. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/messaging/spinner/spinner_base.py +0 -0
  85. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/messaging/spinner/textual_spinner.py +0 -0
  86. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/model_factory.py +0 -0
  87. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/models.json +0 -0
  88. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/plugins/__init__.py +0 -0
  89. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/plugins/example_custom_command/register_callbacks.py +0 -0
  90. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/reopenable_async_client.py +0 -0
  91. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/round_robin_model.py +0 -0
  92. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/session_storage.py +0 -0
  93. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/status_display.py +0 -0
  94. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/summarization_agent.py +0 -0
  95. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/tools/__init__.py +0 -0
  96. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/tools/agent_tools.py +0 -0
  97. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/tools/browser/__init__.py +0 -0
  98. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/tools/browser/browser_control.py +0 -0
  99. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/tools/browser/browser_interactions.py +0 -0
  100. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/tools/browser/browser_locators.py +0 -0
  101. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/tools/browser/browser_navigation.py +0 -0
  102. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/tools/browser/browser_screenshot.py +0 -0
  103. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/tools/browser/browser_scripts.py +0 -0
  104. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/tools/browser/browser_workflows.py +0 -0
  105. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/tools/browser/camoufox_manager.py +0 -0
  106. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/tools/browser/vqa_agent.py +0 -0
  107. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/tools/command_runner.py +0 -0
  108. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/tools/common.py +0 -0
  109. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/tools/file_modifications.py +0 -0
  110. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/tools/file_operations.py +0 -0
  111. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/tools/tools_content.py +0 -0
  112. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/tui/__init__.py +0 -0
  113. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/tui/components/__init__.py +0 -0
  114. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/tui/components/chat_view.py +0 -0
  115. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/tui/components/command_history_modal.py +0 -0
  116. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/tui/components/copy_button.py +0 -0
  117. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/tui/components/custom_widgets.py +0 -0
  118. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/tui/components/human_input_modal.py +0 -0
  119. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/tui/components/sidebar.py +0 -0
  120. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/tui/messages.py +0 -0
  121. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/tui/models/__init__.py +0 -0
  122. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/tui/models/chat_message.py +0 -0
  123. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/tui/models/command_history.py +0 -0
  124. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/tui/models/enums.py +0 -0
  125. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/tui/screens/help.py +0 -0
  126. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/tui/screens/mcp_install_wizard.py +0 -0
  127. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/tui/screens/tools.py +0 -0
  128. {code_puppy-0.0.196 → code_puppy-0.0.197}/code_puppy/tui_state.py +0 -0
  129. {code_puppy-0.0.196 → 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.196
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)"
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)
@@ -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):
@@ -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
 
@@ -176,6 +176,13 @@ class CodePuppyTUI(App):
176
176
  # Start the message renderer EARLY to catch startup messages
177
177
  # Using call_after_refresh to start it as soon as possible after mount
178
178
  self.call_after_refresh(self.start_message_renderer_sync)
179
+
180
+ # Kick off a non-blocking preload of the agent/model so the
181
+ # status bar shows loading before first prompt
182
+ self.call_after_refresh(self.preload_agent_on_startup)
183
+
184
+ # After preload, offer to restore an autosave session (like interactive mode)
185
+ self.call_after_refresh(self.maybe_prompt_restore_autosave)
179
186
 
180
187
  # Apply responsive design adjustments
181
188
  self.apply_responsive_layout()
@@ -187,16 +194,40 @@ class CodePuppyTUI(App):
187
194
  if self.initial_command:
188
195
  self.call_after_refresh(self.process_initial_command)
189
196
 
197
+ def _tighten_text(self, text: str) -> str:
198
+ """Aggressively tighten whitespace: trim lines, collapse multiples, drop extra blanks."""
199
+ try:
200
+ import re
201
+
202
+ # Split into lines, strip each, drop empty runs
203
+ lines = [re.sub(r"\s+", " ", ln.strip()) for ln in text.splitlines()]
204
+ # Remove consecutive blank lines
205
+ tight_lines = []
206
+ last_blank = False
207
+ for ln in lines:
208
+ is_blank = (ln == "")
209
+ if is_blank and last_blank:
210
+ continue
211
+ tight_lines.append(ln)
212
+ last_blank = is_blank
213
+ return "\n".join(tight_lines).strip()
214
+ except Exception:
215
+ return text.strip()
216
+
190
217
  def add_system_message(
191
218
  self, content: str, message_group: str = None, group_id: str = None
192
219
  ) -> None:
193
220
  """Add a system message to the chat."""
194
221
  # Support both parameter names for backward compatibility
195
222
  final_group_id = message_group or group_id
223
+ # Tighten only plain strings
224
+ content_to_use = (
225
+ self._tighten_text(content) if isinstance(content, str) else content
226
+ )
196
227
  message = ChatMessage(
197
228
  id=f"sys_{datetime.now(timezone.utc).timestamp()}",
198
229
  type=MessageType.SYSTEM,
199
- content=content,
230
+ content=content_to_use,
200
231
  timestamp=datetime.now(timezone.utc),
201
232
  group_id=final_group_id,
202
233
  )
@@ -245,10 +276,13 @@ class CodePuppyTUI(App):
245
276
 
246
277
  def add_error_message(self, content: str, message_group: str = None) -> None:
247
278
  """Add an error message to the chat."""
279
+ content_to_use = (
280
+ self._tighten_text(content) if isinstance(content, str) else content
281
+ )
248
282
  message = ChatMessage(
249
283
  id=f"error_{datetime.now(timezone.utc).timestamp()}",
250
284
  type=MessageType.ERROR,
251
- content=content,
285
+ content=content_to_use,
252
286
  timestamp=datetime.now(timezone.utc),
253
287
  group_id=message_group,
254
288
  )
@@ -303,9 +337,9 @@ class CodePuppyTUI(App):
303
337
 
304
338
  # Only handle keys when input field is focused
305
339
  if input_field.has_focus:
306
- # Handle Ctrl+Enter for new lines (more reliable than Shift+Enter)
307
- if event.key == "ctrl+enter":
308
- input_field.insert("\\n")
340
+ # Handle Ctrl+Enter or Shift+Enter for a new line
341
+ if event.key in ("ctrl+enter", "shift+enter"):
342
+ input_field.insert("\n")
309
343
  event.prevent_default()
310
344
  return
311
345
 
@@ -484,6 +518,14 @@ class CodePuppyTUI(App):
484
518
  self.update_agent_progress("Processing", 75)
485
519
  agent_response = result.output
486
520
  self.add_agent_message(agent_response)
521
+
522
+ # Auto-save session if enabled (mirror --interactive)
523
+ try:
524
+ from code_puppy.config import auto_save_session_if_enabled
525
+ auto_save_session_if_enabled()
526
+ except Exception:
527
+ pass
528
+
487
529
  # Refresh history display to show new interaction
488
530
  self.refresh_history_display()
489
531
 
@@ -842,6 +884,36 @@ class CodePuppyTUI(App):
842
884
  """Synchronous wrapper to start message renderer via run_worker."""
843
885
  self.run_worker(self.start_message_renderer(), exclusive=False)
844
886
 
887
+ async def preload_agent_on_startup(self) -> None:
888
+ """Preload the agent/model at startup so loading status is visible."""
889
+ try:
890
+ # Show loading in status bar and spinner
891
+ self.start_agent_progress("Loading")
892
+
893
+ # Warm up agent/model without blocking UI
894
+ import asyncio
895
+
896
+ from code_puppy.agents.agent_manager import get_current_agent
897
+
898
+ agent = get_current_agent()
899
+
900
+ # Run the synchronous reload in a worker thread
901
+ await asyncio.to_thread(agent.reload_code_generation_agent)
902
+
903
+ # After load, refresh current model (in case of fallback or changes)
904
+ from code_puppy.config import get_global_model_name
905
+
906
+ self.current_model = get_global_model_name()
907
+
908
+ # Let the user know model/agent are ready
909
+ self.add_system_message("Model and agent preloaded. Ready to roll 🛼")
910
+ except Exception as e:
911
+ # Surface any preload issues but keep app usable
912
+ self.add_error_message(f"Startup preload failed: {e}")
913
+ finally:
914
+ # Always stop spinner and set ready state
915
+ self.stop_agent_progress()
916
+
845
917
  async def start_message_renderer(self):
846
918
  """Start the message renderer to consume messages from the queue."""
847
919
  if not self._renderer_started:
@@ -884,9 +956,9 @@ class CodePuppyTUI(App):
884
956
  f"Error processing startup message: {e}"
885
957
  )
886
958
 
887
- # Create a single grouped startup message
959
+ # Create a single grouped startup message (tightened)
888
960
  grouped_content = "\n".join(startup_content_lines)
889
- self.add_system_message(grouped_content)
961
+ self.add_system_message(self._tighten_text(grouped_content))
890
962
 
891
963
  # Clear the startup buffer after processing
892
964
  self.message_queue.clear_startup_buffer()
@@ -894,6 +966,80 @@ class CodePuppyTUI(App):
894
966
  # Now start the regular message renderer
895
967
  await self.message_renderer.start()
896
968
 
969
+ async def maybe_prompt_restore_autosave(self) -> None:
970
+ """Offer to restore an autosave session at startup (TUI version)."""
971
+ try:
972
+ import asyncio
973
+ from pathlib import Path
974
+
975
+ from code_puppy.config import AUTOSAVE_DIR, set_current_autosave_from_session_name
976
+ from code_puppy.session_storage import list_sessions, load_session
977
+
978
+ base_dir = Path(AUTOSAVE_DIR)
979
+ sessions = list_sessions(base_dir)
980
+ if not sessions:
981
+ return
982
+
983
+ # Show modal picker for selection
984
+ from .screens.autosave_picker import AutosavePicker
985
+
986
+ async def handle_result(result_name: str | None):
987
+ if not result_name:
988
+ return
989
+ try:
990
+ # Load history and set into agent
991
+ from code_puppy.agents.agent_manager import get_current_agent
992
+
993
+ history = load_session(result_name, base_dir)
994
+ agent = get_current_agent()
995
+ agent.set_message_history(history)
996
+
997
+ # Set current autosave session id so subsequent autosaves overwrite this session
998
+ try:
999
+ set_current_autosave_from_session_name(result_name)
1000
+ except Exception:
1001
+ pass
1002
+
1003
+ # Update token info/status bar
1004
+ total_tokens = sum(
1005
+ agent.estimate_tokens_for_message(msg) for msg in history
1006
+ )
1007
+ try:
1008
+ status_bar = self.query_one(StatusBar)
1009
+ status_bar.update_token_info(
1010
+ total_tokens,
1011
+ agent.get_model_context_length(),
1012
+ total_tokens / max(1, agent.get_model_context_length()),
1013
+ )
1014
+ except Exception:
1015
+ pass
1016
+
1017
+ # Notify
1018
+ session_path = base_dir / f"{result_name}.pkl"
1019
+ self.add_system_message(
1020
+ f"✅ Autosave loaded: {len(history)} messages ({total_tokens} tokens)\n"
1021
+ f"📁 From: {session_path}"
1022
+ )
1023
+
1024
+ # Refresh history sidebar
1025
+ self.refresh_history_display()
1026
+ except Exception as e:
1027
+ self.add_error_message(f"Failed to load autosave: {e}")
1028
+
1029
+ # Push modal and await result
1030
+ picker = AutosavePicker(base_dir)
1031
+
1032
+ # Use Textual's push_screen with a result callback
1033
+ def on_picker_result(result_name=None):
1034
+ # Schedule async handler to avoid blocking UI
1035
+ import asyncio
1036
+ self.run_worker(handle_result(result_name), exclusive=False)
1037
+
1038
+ self.push_screen(picker, on_picker_result)
1039
+ except Exception as e:
1040
+ # Fail silently but show debug in chat
1041
+ self.add_system_message(f"[dim]Autosave prompt error: {e}[/dim]")
1042
+
897
1043
  async def stop_message_renderer(self):
898
1044
  """Stop the message renderer."""
899
1045
  if self._renderer_started:
@@ -133,7 +133,7 @@ class InputArea(Container):
133
133
  yield CustomTextArea(id="input-field", show_line_numbers=False)
134
134
  yield SubmitCancelButton()
135
135
  yield Static(
136
- "Enter to send • Alt+Enter for new line • Ctrl+1 for help",
136
+ "Enter to send • Shift+Enter for new line • Ctrl+1 for help",
137
137
  id="input-help",
138
138
  )
139
139
 
@@ -83,7 +83,10 @@ class StatusBar(Static):
83
83
  elif self.agent_status == "Busy":
84
84
  status_indicator = "🔄"
85
85
  status_color = "orange"
86
- else: # Ready
86
+ elif self.agent_status == "Loading":
87
+ status_indicator = "⏳"
88
+ status_color = "cyan"
89
+ else: # Ready or anything else
87
90
  status_indicator = "✅"
88
91
  status_color = "green"
89
92
 
@@ -6,10 +6,12 @@ from .help import HelpScreen
6
6
  from .mcp_install_wizard import MCPInstallWizardScreen
7
7
  from .settings import SettingsScreen
8
8
  from .tools import ToolsScreen
9
+ from .autosave_picker import AutosavePicker
9
10
 
10
11
  __all__ = [
11
12
  "HelpScreen",
12
13
  "SettingsScreen",
13
14
  "ToolsScreen",
14
15
  "MCPInstallWizardScreen",
16
+ "AutosavePicker",
15
17
  ]