code-puppy 0.0.214__py3-none-any.whl → 0.0.366__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. code_puppy/__init__.py +7 -1
  2. code_puppy/agents/__init__.py +2 -0
  3. code_puppy/agents/agent_c_reviewer.py +59 -6
  4. code_puppy/agents/agent_code_puppy.py +7 -1
  5. code_puppy/agents/agent_code_reviewer.py +12 -2
  6. code_puppy/agents/agent_cpp_reviewer.py +73 -6
  7. code_puppy/agents/agent_creator_agent.py +45 -4
  8. code_puppy/agents/agent_golang_reviewer.py +92 -3
  9. code_puppy/agents/agent_javascript_reviewer.py +101 -8
  10. code_puppy/agents/agent_manager.py +81 -4
  11. code_puppy/agents/agent_pack_leader.py +383 -0
  12. code_puppy/agents/agent_planning.py +163 -0
  13. code_puppy/agents/agent_python_programmer.py +165 -0
  14. code_puppy/agents/agent_python_reviewer.py +28 -6
  15. code_puppy/agents/agent_qa_expert.py +98 -6
  16. code_puppy/agents/agent_qa_kitten.py +12 -7
  17. code_puppy/agents/agent_security_auditor.py +113 -3
  18. code_puppy/agents/agent_terminal_qa.py +323 -0
  19. code_puppy/agents/agent_typescript_reviewer.py +106 -7
  20. code_puppy/agents/base_agent.py +802 -176
  21. code_puppy/agents/event_stream_handler.py +350 -0
  22. code_puppy/agents/pack/__init__.py +34 -0
  23. code_puppy/agents/pack/bloodhound.py +304 -0
  24. code_puppy/agents/pack/husky.py +321 -0
  25. code_puppy/agents/pack/retriever.py +393 -0
  26. code_puppy/agents/pack/shepherd.py +348 -0
  27. code_puppy/agents/pack/terrier.py +287 -0
  28. code_puppy/agents/pack/watchdog.py +367 -0
  29. code_puppy/agents/prompt_reviewer.py +145 -0
  30. code_puppy/agents/subagent_stream_handler.py +276 -0
  31. code_puppy/api/__init__.py +13 -0
  32. code_puppy/api/app.py +169 -0
  33. code_puppy/api/main.py +21 -0
  34. code_puppy/api/pty_manager.py +446 -0
  35. code_puppy/api/routers/__init__.py +12 -0
  36. code_puppy/api/routers/agents.py +36 -0
  37. code_puppy/api/routers/commands.py +217 -0
  38. code_puppy/api/routers/config.py +74 -0
  39. code_puppy/api/routers/sessions.py +232 -0
  40. code_puppy/api/templates/terminal.html +361 -0
  41. code_puppy/api/websocket.py +154 -0
  42. code_puppy/callbacks.py +142 -4
  43. code_puppy/chatgpt_codex_client.py +283 -0
  44. code_puppy/claude_cache_client.py +586 -0
  45. code_puppy/cli_runner.py +916 -0
  46. code_puppy/command_line/add_model_menu.py +1079 -0
  47. code_puppy/command_line/agent_menu.py +395 -0
  48. code_puppy/command_line/attachments.py +10 -5
  49. code_puppy/command_line/autosave_menu.py +605 -0
  50. code_puppy/command_line/clipboard.py +527 -0
  51. code_puppy/command_line/colors_menu.py +520 -0
  52. code_puppy/command_line/command_handler.py +176 -738
  53. code_puppy/command_line/command_registry.py +150 -0
  54. code_puppy/command_line/config_commands.py +715 -0
  55. code_puppy/command_line/core_commands.py +792 -0
  56. code_puppy/command_line/diff_menu.py +863 -0
  57. code_puppy/command_line/load_context_completion.py +15 -22
  58. code_puppy/command_line/mcp/base.py +0 -3
  59. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  60. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  61. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  62. code_puppy/command_line/mcp/edit_command.py +148 -0
  63. code_puppy/command_line/mcp/handler.py +9 -4
  64. code_puppy/command_line/mcp/help_command.py +6 -5
  65. code_puppy/command_line/mcp/install_command.py +15 -26
  66. code_puppy/command_line/mcp/install_menu.py +685 -0
  67. code_puppy/command_line/mcp/list_command.py +2 -2
  68. code_puppy/command_line/mcp/logs_command.py +174 -65
  69. code_puppy/command_line/mcp/remove_command.py +2 -2
  70. code_puppy/command_line/mcp/restart_command.py +12 -4
  71. code_puppy/command_line/mcp/search_command.py +16 -10
  72. code_puppy/command_line/mcp/start_all_command.py +18 -6
  73. code_puppy/command_line/mcp/start_command.py +47 -25
  74. code_puppy/command_line/mcp/status_command.py +4 -5
  75. code_puppy/command_line/mcp/stop_all_command.py +7 -1
  76. code_puppy/command_line/mcp/stop_command.py +8 -4
  77. code_puppy/command_line/mcp/test_command.py +2 -2
  78. code_puppy/command_line/mcp/wizard_utils.py +20 -16
  79. code_puppy/command_line/mcp_completion.py +174 -0
  80. code_puppy/command_line/model_picker_completion.py +75 -25
  81. code_puppy/command_line/model_settings_menu.py +884 -0
  82. code_puppy/command_line/motd.py +14 -8
  83. code_puppy/command_line/onboarding_slides.py +179 -0
  84. code_puppy/command_line/onboarding_wizard.py +340 -0
  85. code_puppy/command_line/pin_command_completion.py +329 -0
  86. code_puppy/command_line/prompt_toolkit_completion.py +463 -63
  87. code_puppy/command_line/session_commands.py +296 -0
  88. code_puppy/command_line/utils.py +54 -0
  89. code_puppy/config.py +898 -112
  90. code_puppy/error_logging.py +118 -0
  91. code_puppy/gemini_code_assist.py +385 -0
  92. code_puppy/gemini_model.py +602 -0
  93. code_puppy/http_utils.py +210 -148
  94. code_puppy/keymap.py +128 -0
  95. code_puppy/main.py +5 -698
  96. code_puppy/mcp_/__init__.py +17 -0
  97. code_puppy/mcp_/async_lifecycle.py +35 -4
  98. code_puppy/mcp_/blocking_startup.py +70 -43
  99. code_puppy/mcp_/captured_stdio_server.py +2 -2
  100. code_puppy/mcp_/config_wizard.py +4 -4
  101. code_puppy/mcp_/dashboard.py +15 -6
  102. code_puppy/mcp_/managed_server.py +65 -38
  103. code_puppy/mcp_/manager.py +146 -52
  104. code_puppy/mcp_/mcp_logs.py +224 -0
  105. code_puppy/mcp_/registry.py +6 -6
  106. code_puppy/mcp_/server_registry_catalog.py +24 -5
  107. code_puppy/messaging/__init__.py +199 -2
  108. code_puppy/messaging/bus.py +610 -0
  109. code_puppy/messaging/commands.py +167 -0
  110. code_puppy/messaging/markdown_patches.py +57 -0
  111. code_puppy/messaging/message_queue.py +17 -48
  112. code_puppy/messaging/messages.py +500 -0
  113. code_puppy/messaging/queue_console.py +1 -24
  114. code_puppy/messaging/renderers.py +43 -146
  115. code_puppy/messaging/rich_renderer.py +1027 -0
  116. code_puppy/messaging/spinner/__init__.py +21 -5
  117. code_puppy/messaging/spinner/console_spinner.py +86 -51
  118. code_puppy/messaging/subagent_console.py +461 -0
  119. code_puppy/model_factory.py +634 -83
  120. code_puppy/model_utils.py +167 -0
  121. code_puppy/models.json +66 -68
  122. code_puppy/models_dev_api.json +1 -0
  123. code_puppy/models_dev_parser.py +592 -0
  124. code_puppy/plugins/__init__.py +164 -10
  125. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  126. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  127. code_puppy/plugins/antigravity_oauth/antigravity_model.py +704 -0
  128. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  129. code_puppy/plugins/antigravity_oauth/constants.py +136 -0
  130. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  131. code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
  132. code_puppy/plugins/antigravity_oauth/storage.py +271 -0
  133. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  134. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  135. code_puppy/plugins/antigravity_oauth/transport.py +767 -0
  136. code_puppy/plugins/antigravity_oauth/utils.py +169 -0
  137. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  138. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  139. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
  140. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
  141. code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
  142. code_puppy/plugins/chatgpt_oauth/utils.py +489 -0
  143. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  144. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  145. code_puppy/plugins/claude_code_oauth/__init__.py +6 -0
  146. code_puppy/plugins/claude_code_oauth/config.py +50 -0
  147. code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
  148. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  149. code_puppy/plugins/claude_code_oauth/utils.py +518 -0
  150. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  151. code_puppy/plugins/customizable_commands/register_callbacks.py +169 -0
  152. code_puppy/plugins/example_custom_command/README.md +280 -0
  153. code_puppy/plugins/example_custom_command/register_callbacks.py +2 -2
  154. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  155. code_puppy/plugins/file_permission_handler/register_callbacks.py +523 -0
  156. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  157. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  158. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  159. code_puppy/plugins/oauth_puppy_html.py +228 -0
  160. code_puppy/plugins/shell_safety/__init__.py +6 -0
  161. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  162. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  163. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  164. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  165. code_puppy/prompts/codex_system_prompt.md +310 -0
  166. code_puppy/pydantic_patches.py +131 -0
  167. code_puppy/reopenable_async_client.py +8 -8
  168. code_puppy/round_robin_model.py +9 -12
  169. code_puppy/session_storage.py +2 -1
  170. code_puppy/status_display.py +21 -4
  171. code_puppy/summarization_agent.py +41 -13
  172. code_puppy/terminal_utils.py +418 -0
  173. code_puppy/tools/__init__.py +37 -1
  174. code_puppy/tools/agent_tools.py +536 -52
  175. code_puppy/tools/browser/__init__.py +37 -0
  176. code_puppy/tools/browser/browser_control.py +19 -23
  177. code_puppy/tools/browser/browser_interactions.py +41 -48
  178. code_puppy/tools/browser/browser_locators.py +36 -38
  179. code_puppy/tools/browser/browser_manager.py +316 -0
  180. code_puppy/tools/browser/browser_navigation.py +16 -16
  181. code_puppy/tools/browser/browser_screenshot.py +79 -143
  182. code_puppy/tools/browser/browser_scripts.py +32 -42
  183. code_puppy/tools/browser/browser_workflows.py +44 -27
  184. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  185. code_puppy/tools/browser/terminal_command_tools.py +521 -0
  186. code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
  187. code_puppy/tools/browser/terminal_tools.py +525 -0
  188. code_puppy/tools/command_runner.py +930 -147
  189. code_puppy/tools/common.py +1113 -5
  190. code_puppy/tools/display.py +84 -0
  191. code_puppy/tools/file_modifications.py +288 -89
  192. code_puppy/tools/file_operations.py +226 -154
  193. code_puppy/tools/subagent_context.py +158 -0
  194. code_puppy/uvx_detection.py +242 -0
  195. code_puppy/version_checker.py +30 -11
  196. code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
  197. code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
  198. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/METADATA +149 -75
  199. code_puppy-0.0.366.dist-info/RECORD +217 -0
  200. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
  201. code_puppy/command_line/mcp/add_command.py +0 -183
  202. code_puppy/messaging/spinner/textual_spinner.py +0 -106
  203. code_puppy/tools/browser/camoufox_manager.py +0 -216
  204. code_puppy/tools/browser/vqa_agent.py +0 -70
  205. code_puppy/tui/__init__.py +0 -10
  206. code_puppy/tui/app.py +0 -1105
  207. code_puppy/tui/components/__init__.py +0 -21
  208. code_puppy/tui/components/chat_view.py +0 -551
  209. code_puppy/tui/components/command_history_modal.py +0 -218
  210. code_puppy/tui/components/copy_button.py +0 -139
  211. code_puppy/tui/components/custom_widgets.py +0 -63
  212. code_puppy/tui/components/human_input_modal.py +0 -175
  213. code_puppy/tui/components/input_area.py +0 -167
  214. code_puppy/tui/components/sidebar.py +0 -309
  215. code_puppy/tui/components/status_bar.py +0 -185
  216. code_puppy/tui/messages.py +0 -27
  217. code_puppy/tui/models/__init__.py +0 -8
  218. code_puppy/tui/models/chat_message.py +0 -25
  219. code_puppy/tui/models/command_history.py +0 -89
  220. code_puppy/tui/models/enums.py +0 -24
  221. code_puppy/tui/screens/__init__.py +0 -17
  222. code_puppy/tui/screens/autosave_picker.py +0 -175
  223. code_puppy/tui/screens/help.py +0 -130
  224. code_puppy/tui/screens/mcp_install_wizard.py +0 -803
  225. code_puppy/tui/screens/settings.py +0 -306
  226. code_puppy/tui/screens/tools.py +0 -74
  227. code_puppy/tui_state.py +0 -55
  228. code_puppy-0.0.214.data/data/code_puppy/models.json +0 -112
  229. code_puppy-0.0.214.dist-info/RECORD +0 -131
  230. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +0 -0
  231. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/licenses/LICENSE +0 -0
@@ -1,118 +1,131 @@
1
- import os
2
- from datetime import datetime
3
- from pathlib import Path
4
-
5
- from code_puppy.command_line.model_picker_completion import update_model_in_input
6
- from code_puppy.command_line.motd import print_motd
7
- from code_puppy.command_line.utils import make_directory_table
8
- from code_puppy.config import (
9
- CONTEXTS_DIR,
10
- finalize_autosave_session,
11
- get_config_keys,
12
- )
13
- from code_puppy.session_storage import list_sessions, load_session, save_session
14
- from code_puppy.tools.tools_content import tools_content
1
+ # Import to trigger command registration
2
+ import code_puppy.command_line.config_commands # noqa: F401
3
+ import code_puppy.command_line.core_commands # noqa: F401
4
+ import code_puppy.command_line.session_commands # noqa: F401
5
+
6
+ # Global flag to track if plugins have been loaded
7
+ _PLUGINS_LOADED = False
15
8
 
16
9
 
17
10
  def get_commands_help():
18
- """Generate aligned commands help using Rich Text for safe markup."""
11
+ """Generate aligned commands help using Rich Text for safe markup.
12
+
13
+ Now dynamically generates help from the command registry!
14
+ Only shows two sections: Built-in Commands and Custom Commands.
15
+ """
19
16
  from rich.text import Text
20
17
 
18
+ from code_puppy.command_line.command_registry import get_unique_commands
19
+
21
20
  # Ensure plugins are loaded so custom help can register
22
21
  _ensure_plugins_loaded()
23
22
 
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
- (
37
- "/reasoning <low|medium|high>",
38
- "Set OpenAI reasoning effort for GPT-5 models",
39
- ),
40
- ("/pin_model <agent> <model>", "Pin a specific model to an agent"),
41
- ("/mcp", "Manage MCP servers (list, start, stop, status, etc.)"),
42
- ("/motd", "Show the latest message of the day (MOTD)"),
43
- ("/show", "Show puppy config key-values"),
44
- (
45
- "/compact",
46
- "Summarize and compact current chat history (uses compaction_strategy config)",
47
- ),
48
- ("/dump_context <name>", "Save current message history to file"),
49
- ("/load_context <name>", "Load message history from file"),
50
- (
51
- "/set",
52
- "Set puppy config (e.g., /set yolo_mode true, /set auto_save_session true)",
53
- ),
54
- ("/tools", "Show available tools and capabilities"),
55
- (
56
- "/truncate <N>",
57
- "Truncate history to N most recent messages (keeping system message)",
58
- ),
59
- ("/<unknown>", "Show unknown command warning"),
60
- ]
61
-
62
- # Determine padding width for the left column
63
- left_width = max(len(cmd) for cmd, _ in core_cmds) + 2 # add spacing
64
-
65
23
  lines: list[Text] = []
66
- lines.append(Text("Commands Help", style="bold magenta"))
24
+ # No global header needed - user already knows they're viewing help
67
25
 
68
- for cmd, desc in core_cmds:
69
- left = Text(cmd.ljust(left_width), style="cyan")
70
- right = Text(desc)
71
- line = Text()
72
- line.append_text(left)
73
- line.append_text(right)
74
- lines.append(line)
26
+ # Collect all built-in commands (registered + legacy)
27
+ builtin_cmds: list[tuple[str, str]] = []
28
+
29
+ # Get registered commands (all categories are built-in)
30
+ registered_commands = get_unique_commands()
31
+ for cmd_info in sorted(registered_commands, key=lambda c: c.name):
32
+ builtin_cmds.append((cmd_info.usage, cmd_info.description))
75
33
 
76
- # Add custom commands from plugins (if any)
34
+ # Get custom commands from plugins
35
+ custom_entries: list[tuple[str, str]] = []
77
36
  try:
78
37
  from code_puppy import callbacks
79
38
 
80
39
  custom_help_results = callbacks.on_custom_command_help()
81
- custom_entries: list[tuple[str, str]] = []
82
40
  for res in custom_help_results:
83
41
  if not res:
84
42
  continue
43
+ # Format 1: Tuple with (command_name, description)
85
44
  if isinstance(res, tuple) and len(res) == 2:
86
- custom_entries.append((str(res[0]), str(res[1])))
45
+ cmd_name = str(res[0])
46
+ custom_entries.append((f"/{cmd_name}", str(res[1])))
47
+ # Format 2: List of tuples or strings
87
48
  elif isinstance(res, list):
88
- for item in res:
89
- if isinstance(item, tuple) and len(item) == 2:
90
- custom_entries.append((str(item[0]), str(item[1])))
91
- if custom_entries:
92
- lines.append(Text("", style="dim"))
93
- lines.append(Text("Custom Commands", style="bold magenta"))
94
- # Compute padding for custom commands as well
95
- custom_left_width = max(len(name) for name, _ in custom_entries) + 3
96
- for name, desc in custom_entries:
97
- left = Text(f"/{name}".ljust(custom_left_width), style="cyan")
98
- right = Text(desc)
99
- line = Text()
100
- line.append_text(left)
101
- line.append_text(right)
102
- lines.append(line)
49
+ # Check if it's a list of tuples (preferred format)
50
+ if res and isinstance(res[0], tuple) and len(res[0]) == 2:
51
+ for item in res:
52
+ if isinstance(item, tuple) and len(item) == 2:
53
+ cmd_name = str(item[0])
54
+ custom_entries.append((f"/{cmd_name}", str(item[1])))
55
+ # Format 3: List of strings (legacy format)
56
+ # Extract command from first line like "/command_name - Description"
57
+ elif res and isinstance(res[0], str) and res[0].startswith("/"):
58
+ first_line = res[0]
59
+ if " - " in first_line:
60
+ parts = first_line.split(" - ", 1)
61
+ cmd_name = parts[0].lstrip("/").strip()
62
+ description = parts[1].strip()
63
+ custom_entries.append((f"/{cmd_name}", description))
103
64
  except Exception:
104
65
  pass
105
66
 
67
+ # Calculate global column width (longest command across ALL sections + padding)
68
+ all_commands = builtin_cmds + custom_entries
69
+ if all_commands:
70
+ max_cmd_width = max(len(cmd) for cmd, _ in all_commands)
71
+ column_width = max_cmd_width + 4 # Add 4 spaces padding
72
+ else:
73
+ column_width = 30
74
+
75
+ # Maximum description width before truncation (to prevent line wrapping)
76
+ max_desc_width = 80
77
+
78
+ def truncate_desc(desc: str, max_width: int) -> str:
79
+ """Truncate description if too long, add ellipsis."""
80
+ if len(desc) <= max_width:
81
+ return desc
82
+ return desc[: max_width - 3] + "..."
83
+
84
+ # Display Built-in Commands section (starts immediately, no blank line)
85
+ lines.append(Text("Built-in Commands", style="bold magenta"))
86
+ for cmd, desc in sorted(builtin_cmds, key=lambda x: x[0]):
87
+ truncated_desc = truncate_desc(desc, max_desc_width)
88
+ left = Text(cmd.ljust(column_width), style="cyan")
89
+ right = Text(truncated_desc)
90
+ line = Text()
91
+ line.append_text(left)
92
+ line.append_text(right)
93
+ lines.append(line)
94
+
95
+ # Display Custom Commands section (if any)
96
+ if custom_entries:
97
+ lines.append(Text(""))
98
+ lines.append(Text("Custom Commands", style="bold magenta"))
99
+ for cmd, desc in sorted(custom_entries, key=lambda x: x[0]):
100
+ truncated_desc = truncate_desc(desc, max_desc_width)
101
+ left = Text(cmd.ljust(column_width), style="cyan")
102
+ right = Text(truncated_desc)
103
+ line = Text()
104
+ line.append_text(left)
105
+ line.append_text(right)
106
+ lines.append(line)
107
+
106
108
  final_text = Text()
107
109
  for i, line in enumerate(lines):
108
110
  if i > 0:
109
111
  final_text.append("\n")
110
112
  final_text.append_text(line)
111
113
 
114
+ # Add trailing newline for spacing before next prompt
115
+ final_text.append("\n")
116
+
112
117
  return final_text
113
118
 
114
119
 
115
- _PLUGINS_LOADED = False
120
+ # ============================================================================
121
+ # IMPORT BUILT-IN COMMAND HANDLERS
122
+ # ============================================================================
123
+ # All built-in command handlers have been split into category-specific files.
124
+ # These imports trigger their registration via @register_command decorators.
125
+
126
+ # ============================================================================
127
+ # UTILITY FUNCTIONS
128
+ # ============================================================================
116
129
 
117
130
 
118
131
  def _ensure_plugins_loaded() -> None:
@@ -135,11 +148,17 @@ def _ensure_plugins_loaded() -> None:
135
148
  _PLUGINS_LOADED = True
136
149
 
137
150
 
138
- def handle_command(command: str):
139
- from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
151
+ # All command handlers moved to builtin_commands.py
152
+ # The import above triggers their registration
140
153
 
141
- _ensure_plugins_loaded()
154
+ # ============================================================================
155
+ # MAIN COMMAND DISPATCHER
156
+ # ============================================================================
157
+
158
+ # _show_color_options has been moved to builtin_commands.py
142
159
 
160
+
161
+ def handle_command(command: str):
143
162
  """
144
163
  Handle commands prefixed with '/'.
145
164
 
@@ -149,660 +168,62 @@ def handle_command(command: str):
149
168
  Returns:
150
169
  True if the command was handled, False if not, or a string to be processed as user input
151
170
  """
152
- command = command.strip()
153
-
154
- if command.strip().startswith("/motd"):
155
- print_motd(force=True)
156
- return True
157
-
158
- if command.strip().startswith("/compact"):
159
- # Functions have been moved to BaseAgent class
160
- from code_puppy.agents.agent_manager import get_current_agent
161
- from code_puppy.config import get_compaction_strategy, get_protected_token_count
162
- from code_puppy.messaging import (
163
- emit_error,
164
- emit_info,
165
- emit_success,
166
- emit_warning,
167
- )
168
-
169
- try:
170
- agent = get_current_agent()
171
- history = agent.get_message_history()
172
- if not history:
173
- emit_warning("No history to compact yet. Ask me something first!")
174
- return True
175
-
176
- current_agent = get_current_agent()
177
- before_tokens = sum(
178
- current_agent.estimate_tokens_for_message(m) for m in history
179
- )
180
- compaction_strategy = get_compaction_strategy()
181
- protected_tokens = get_protected_token_count()
182
- emit_info(
183
- f"🤔 Compacting {len(history)} messages using {compaction_strategy} strategy... (~{before_tokens} tokens)"
184
- )
185
-
186
- current_agent = get_current_agent()
187
- if compaction_strategy == "truncation":
188
- compacted = current_agent.truncation(history, protected_tokens)
189
- summarized_messages = [] # No summarization in truncation mode
190
- else:
191
- # Default to summarization
192
- compacted, summarized_messages = current_agent.summarize_messages(
193
- history, with_protection=True
194
- )
195
-
196
- if not compacted:
197
- emit_error("Compaction failed. History unchanged.")
198
- return True
199
-
200
- agent.set_message_history(compacted)
201
-
202
- current_agent = get_current_agent()
203
- after_tokens = sum(
204
- current_agent.estimate_tokens_for_message(m) for m in compacted
205
- )
206
- reduction_pct = (
207
- ((before_tokens - after_tokens) / before_tokens * 100)
208
- if before_tokens > 0
209
- else 0
210
- )
211
-
212
- strategy_info = (
213
- f"using {compaction_strategy} strategy"
214
- if compaction_strategy == "truncation"
215
- else "via summarization"
216
- )
217
- emit_success(
218
- f"✨ Done! History: {len(history)} → {len(compacted)} messages {strategy_info}\n"
219
- f"🏦 Tokens: {before_tokens:,} → {after_tokens:,} ({reduction_pct:.1f}% reduction)"
220
- )
221
- return True
222
- except Exception as e:
223
- emit_error(f"/compact error: {e}")
224
- return True
225
-
226
- if command.startswith("/cd"):
227
- tokens = command.split()
228
- if len(tokens) == 1:
229
- try:
230
- table = make_directory_table()
231
- emit_info(table)
232
- except Exception as e:
233
- emit_error(f"Error listing directory: {e}")
234
- return True
235
- elif len(tokens) == 2:
236
- dirname = tokens[1]
237
- target = os.path.expanduser(dirname)
238
- if not os.path.isabs(target):
239
- target = os.path.join(os.getcwd(), target)
240
- if os.path.isdir(target):
241
- os.chdir(target)
242
- emit_success(f"Changed directory to: {target}")
243
- else:
244
- emit_error(f"Not a directory: {dirname}")
245
- return True
246
-
247
- if command.strip().startswith("/show"):
248
- from code_puppy.agents import get_current_agent
249
- from code_puppy.command_line.model_picker_completion import get_active_model
250
- from code_puppy.config import (
251
- get_compaction_strategy,
252
- get_compaction_threshold,
253
- get_openai_reasoning_effort,
254
- get_owner_name,
255
- get_protected_token_count,
256
- get_puppy_name,
257
- get_use_dbos,
258
- get_yolo_mode,
259
- )
260
-
261
- puppy_name = get_puppy_name()
262
- owner_name = get_owner_name()
263
- model = get_active_model()
264
- yolo_mode = get_yolo_mode()
265
- protected_tokens = get_protected_token_count()
266
- compaction_threshold = get_compaction_threshold()
267
- compaction_strategy = get_compaction_strategy()
268
-
269
- # Get current agent info
270
- current_agent = get_current_agent()
271
-
272
- status_msg = f"""[bold magenta]🐶 Puppy Status[/bold magenta]
273
-
274
- [bold]puppy_name:[/bold] [cyan]{puppy_name}[/cyan]
275
- [bold]owner_name:[/bold] [cyan]{owner_name}[/cyan]
276
- [bold]current_agent:[/bold] [magenta]{current_agent.display_name}[/magenta]
277
- [bold]model:[/bold] [green]{model}[/green]
278
- [bold]YOLO_MODE:[/bold] {"[red]ON[/red]" if yolo_mode else "[yellow]off[/yellow]"}
279
- [bold]DBOS:[/bold] {"[green]enabled[/green]" if get_use_dbos() else "[yellow]disabled[/yellow]"} (toggle: /set enable_dbos true|false)
280
- [bold]protected_tokens:[/bold] [cyan]{protected_tokens:,}[/cyan] recent tokens preserved
281
- [bold]compaction_threshold:[/bold] [cyan]{compaction_threshold:.1%}[/cyan] context usage triggers compaction
282
- [bold]compaction_strategy:[/bold] [cyan]{compaction_strategy}[/cyan] (summarization or truncation)
283
- [bold]reasoning_effort:[/bold] [cyan]{get_openai_reasoning_effort()}[/cyan]
284
-
285
- """
286
- emit_info(status_msg)
287
- return True
288
-
289
- if command.startswith("/reasoning"):
290
- tokens = command.split()
291
- if len(tokens) != 2:
292
- emit_warning("Usage: /reasoning <low|medium|high>")
293
- return True
294
-
295
- effort = tokens[1]
296
- try:
297
- from code_puppy.config import set_openai_reasoning_effort
298
-
299
- set_openai_reasoning_effort(effort)
300
- except ValueError as exc:
301
- emit_error(str(exc))
302
- return True
303
-
304
- from code_puppy.config import get_openai_reasoning_effort
305
-
306
- normalized_effort = get_openai_reasoning_effort()
307
-
308
- from code_puppy.agents.agent_manager import get_current_agent
309
-
310
- agent = get_current_agent()
311
- agent.reload_code_generation_agent()
312
- emit_success(
313
- f"Reasoning effort set to '{normalized_effort}' and active agent reloaded"
314
- )
315
- return True
316
-
317
- if command.startswith("/session"):
318
- # /session id -> show current autosave id
319
- # /session new -> rotate autosave id
320
- tokens = command.split()
321
- from code_puppy.config import (
322
- AUTOSAVE_DIR,
323
- get_current_autosave_id,
324
- get_current_autosave_session_name,
325
- rotate_autosave_id,
326
- )
327
-
328
- if len(tokens) == 1 or tokens[1] == "id":
329
- sid = get_current_autosave_id()
330
- emit_info(
331
- f"[bold magenta]Autosave Session[/bold magenta]: {sid}\n"
332
- f"Files prefix: {Path(AUTOSAVE_DIR) / get_current_autosave_session_name()}"
333
- )
334
- return True
335
- if tokens[1] == "new":
336
- new_sid = rotate_autosave_id()
337
- emit_success(f"New autosave session id: {new_sid}")
338
- return True
339
- emit_warning("Usage: /session [id|new]")
340
- return True
341
-
342
- if command.startswith("/set"):
343
- # Syntax: /set KEY=VALUE or /set KEY VALUE
344
- from code_puppy.config import set_config_value
345
-
346
- tokens = command.split(None, 2)
347
- argstr = command[len("/set") :].strip()
348
- key = None
349
- value = None
350
- if "=" in argstr:
351
- key, value = argstr.split("=", 1)
352
- key = key.strip()
353
- value = value.strip()
354
- elif len(tokens) >= 3:
355
- key = tokens[1]
356
- value = tokens[2]
357
- elif len(tokens) == 2:
358
- key = tokens[1]
359
- value = ""
360
- else:
361
- config_keys = get_config_keys()
362
- if "compaction_strategy" not in config_keys:
363
- config_keys.append("compaction_strategy")
364
- session_help = (
365
- "\n[yellow]Session Management[/yellow]"
366
- "\n [cyan]auto_save_session[/cyan] Auto-save chat after every response (true/false)"
367
- )
368
- emit_warning(
369
- 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}"
370
- )
371
- return True
372
- if key:
373
- # Check if we're toggling DBOS enablement
374
- if key == "enable_dbos":
375
- emit_info(
376
- "[yellow]⚠️ DBOS configuration changed. Please restart Code Puppy for this change to take effect.[/yellow]"
377
- )
378
-
379
- set_config_value(key, value)
380
- emit_success(f'Set {key} = "{value}" in puppy.cfg!')
381
- else:
382
- emit_error("You must supply a key.")
383
- return True
384
-
385
- if command.startswith("/tools"):
386
- # Display the tools_content.py file content with markdown formatting
387
- from rich.markdown import Markdown
388
-
389
- markdown_content = Markdown(tools_content)
390
- emit_info(markdown_content)
391
- return True
392
-
393
- if command.startswith("/agent"):
394
- # Handle agent switching
395
- from code_puppy.agents import (
396
- get_agent_descriptions,
397
- get_available_agents,
398
- get_current_agent,
399
- set_current_agent,
400
- )
401
-
402
- tokens = command.split()
403
-
404
- if len(tokens) == 1:
405
- # Show current agent and available agents
406
- current_agent = get_current_agent()
407
- available_agents = get_available_agents()
408
- descriptions = get_agent_descriptions()
409
-
410
- # Generate a group ID for all messages in this command
411
- import uuid
412
-
413
- group_id = str(uuid.uuid4())
414
-
415
- emit_info(
416
- f"[bold green]Current Agent:[/bold green] {current_agent.display_name}",
417
- message_group=group_id,
418
- )
419
- emit_info(
420
- f"[dim]{current_agent.description}[/dim]\n", message_group=group_id
421
- )
422
-
423
- emit_info(
424
- "[bold magenta]Available Agents:[/bold magenta]", message_group=group_id
425
- )
426
- for name, display_name in available_agents.items():
427
- description = descriptions.get(name, "No description")
428
- current_marker = (
429
- " [green]← current[/green]" if name == current_agent.name else ""
430
- )
431
- emit_info(
432
- f" [cyan]{name:<12}[/cyan] {display_name}{current_marker}",
433
- message_group=group_id,
434
- )
435
- emit_info(f" [dim]{description}[/dim]", message_group=group_id)
436
-
437
- emit_info(
438
- "\n[yellow]Usage:[/yellow] /agent <agent-name>", message_group=group_id
439
- )
440
- return True
441
-
442
- elif len(tokens) == 2:
443
- agent_name = tokens[1].lower()
444
-
445
- # Generate a group ID for all messages in this command
446
- import uuid
447
-
448
- group_id = str(uuid.uuid4())
449
- available_agents = get_available_agents()
450
-
451
- if agent_name not in available_agents:
452
- emit_error(f"Agent '{agent_name}' not found", message_group=group_id)
453
- emit_warning(
454
- f"Available agents: {', '.join(available_agents.keys())}",
455
- message_group=group_id,
456
- )
457
- return True
458
-
459
- current_agent = get_current_agent()
460
- if current_agent.name == agent_name:
461
- emit_info(
462
- f"Already using agent: {current_agent.display_name}",
463
- message_group=group_id,
464
- )
465
- return True
466
-
467
- new_session_id = finalize_autosave_session()
468
- if not set_current_agent(agent_name):
469
- emit_warning(
470
- "Agent switch failed after autosave rotation. Your context was preserved.",
471
- message_group=group_id,
472
- )
473
- return True
474
-
475
- new_agent = get_current_agent()
476
- new_agent.reload_code_generation_agent()
477
- emit_success(
478
- f"Switched to agent: {new_agent.display_name}",
479
- message_group=group_id,
480
- )
481
- emit_info(f"[dim]{new_agent.description}[/dim]", message_group=group_id)
482
- emit_info(
483
- f"[dim]Auto-save session rotated to: {new_session_id}[/dim]",
484
- message_group=group_id,
485
- )
486
- return True
487
- else:
488
- emit_warning("Usage: /agent [agent-name]")
489
- return True
490
-
491
- if command.startswith("/model") or command.startswith("/m "):
492
- # Try setting model and show confirmation
493
- # Handle both /model and /m for backward compatibility
494
- model_command = command
495
- if command.startswith("/model"):
496
- # Convert /model to /m for internal processing
497
- model_command = command.replace("/model", "/m", 1)
498
-
499
- # If no model matched, show available models
500
- from code_puppy.command_line.model_picker_completion import load_model_names
501
-
502
- new_input = update_model_in_input(model_command)
503
- if new_input is not None:
504
- from code_puppy.command_line.model_picker_completion import get_active_model
505
-
506
- model = get_active_model()
507
- # Make sure this is called for the test
508
- emit_success(f"Active model set and loaded: {model}")
509
- return True
510
- model_names = load_model_names()
511
- emit_warning("Usage: /model <model-name> or /m <model-name>")
512
- emit_warning(f"Available models: {', '.join(model_names)}")
513
- return True
514
-
515
- if command.startswith("/mcp"):
516
- from code_puppy.command_line.mcp import MCPCommandHandler
517
-
518
- handler = MCPCommandHandler()
519
- return handler.handle_mcp_command(command)
520
-
521
- # Built-in help
522
- if command in ("/help", "/h"):
523
- import uuid
524
-
525
- group_id = str(uuid.uuid4())
526
- help_text = get_commands_help()
527
- emit_info(help_text, message_group_id=group_id)
528
- return True
529
-
530
- if command.startswith("/pin_model"):
531
- # Handle agent model pinning
532
- import json
533
-
534
- from code_puppy.agents.json_agent import discover_json_agents
535
- from code_puppy.command_line.model_picker_completion import load_model_names
536
-
537
- tokens = command.split()
538
-
539
- if len(tokens) != 3:
540
- emit_warning("Usage: /pin_model <agent-name> <model-name>")
541
-
542
- # Show available models and agents
543
- available_models = load_model_names()
544
- json_agents = discover_json_agents()
545
-
546
- # Get built-in agents
547
- from code_puppy.agents.agent_manager import get_agent_descriptions
548
-
549
- builtin_agents = get_agent_descriptions()
550
-
551
- emit_info("Available models:")
552
- for model in available_models:
553
- emit_info(f" [cyan]{model}[/cyan]")
554
-
555
- if builtin_agents:
556
- emit_info("\nAvailable built-in agents:")
557
- for agent_name, description in builtin_agents.items():
558
- emit_info(f" [cyan]{agent_name}[/cyan] - {description}")
559
-
560
- if json_agents:
561
- emit_info("\nAvailable JSON agents:")
562
- for agent_name, agent_path in json_agents.items():
563
- emit_info(f" [cyan]{agent_name}[/cyan] ({agent_path})")
564
- return True
565
-
566
- agent_name = tokens[1].lower()
567
- model_name = tokens[2]
568
-
569
- # Check if model exists
570
- available_models = load_model_names()
571
- if model_name not in available_models:
572
- emit_error(f"Model '{model_name}' not found")
573
- emit_warning(f"Available models: {', '.join(available_models)}")
574
- return True
575
-
576
- # Check if this is a JSON agent or a built-in Python agent
577
- json_agents = discover_json_agents()
578
-
579
- # Get list of available built-in agents
580
- from code_puppy.agents.agent_manager import get_agent_descriptions
581
-
582
- builtin_agents = get_agent_descriptions()
583
-
584
- is_json_agent = agent_name in json_agents
585
- is_builtin_agent = agent_name in builtin_agents
586
-
587
- if not is_json_agent and not is_builtin_agent:
588
- emit_error(f"Agent '{agent_name}' not found")
589
-
590
- # Show available agents
591
- if builtin_agents:
592
- emit_info("Available built-in agents:")
593
- for name, desc in builtin_agents.items():
594
- emit_info(f" [cyan]{name}[/cyan] - {desc}")
595
-
596
- if json_agents:
597
- emit_info("\nAvailable JSON agents:")
598
- for name, path in json_agents.items():
599
- emit_info(f" [cyan]{name}[/cyan] ({path})")
600
- return True
601
-
602
- # Handle different agent types
603
- try:
604
- if is_json_agent:
605
- # Handle JSON agent - modify the JSON file
606
- agent_file_path = json_agents[agent_name]
607
-
608
- with open(agent_file_path, "r", encoding="utf-8") as f:
609
- agent_config = json.load(f)
610
-
611
- # Set the model
612
- agent_config["model"] = model_name
613
-
614
- # Save the updated configuration
615
- with open(agent_file_path, "w", encoding="utf-8") as f:
616
- json.dump(agent_config, f, indent=2, ensure_ascii=False)
617
-
618
- else:
619
- # Handle built-in Python agent - store in config
620
- from code_puppy.config import set_agent_pinned_model
621
-
622
- set_agent_pinned_model(agent_name, model_name)
623
-
624
- emit_success(f"Model '{model_name}' pinned to agent '{agent_name}'")
625
-
626
- # If this is the current agent, refresh it so the prompt updates immediately
627
- from code_puppy.agents import get_current_agent
628
-
629
- current_agent = get_current_agent()
630
- if current_agent.name == agent_name:
631
- try:
632
- if is_json_agent and hasattr(current_agent, "refresh_config"):
633
- current_agent.refresh_config()
634
- current_agent.reload_code_generation_agent()
635
- emit_info(f"Active agent reloaded with pinned model '{model_name}'")
636
- except Exception as reload_error:
637
- emit_warning(
638
- f"Pinned model applied but reload failed: {reload_error}"
639
- )
640
-
641
- return True
642
-
643
- except Exception as e:
644
- emit_error(f"Failed to pin model to agent '{agent_name}': {e}")
645
- return True
646
-
647
- if command.startswith("/generate-pr-description"):
648
- # Parse directory argument (e.g., /generate-pr-description @some/dir)
649
- tokens = command.split()
650
- directory_context = ""
651
- for t in tokens:
652
- if t.startswith("@"):
653
- directory_context = f" Please work in the directory: {t[1:]}"
654
- break
655
-
656
- # Hard-coded prompt from user requirements
657
- pr_prompt = f"""Generate a comprehensive PR description for my current branch changes. Follow these steps:
658
-
659
- 1 Discover the changes: Use git CLI to find the base branch (usually main/master/develop) and get the list of changed files, commits, and diffs.
660
- 2 Analyze the code: Read and analyze all modified files to understand:
661
- • What functionality was added/changed/removed
662
- • The technical approach and implementation details
663
- • Any architectural or design pattern changes
664
- • Dependencies added/removed/updated
665
- 3 Generate a structured PR description with these sections:
666
- • Title: Concise, descriptive title (50 chars max)
667
- • Summary: Brief overview of what this PR accomplishes
668
- • Changes Made: Detailed bullet points of specific changes
669
- • Technical Details: Implementation approach, design decisions, patterns used
670
- • Files Modified: List of key files with brief description of changes
671
- • Testing: What was tested and how (if applicable)
672
- • Breaking Changes: Any breaking changes (if applicable)
673
- • Additional Notes: Any other relevant information
674
- 4 Create a markdown file: Generate a PR_DESCRIPTION.md file with proper GitHub markdown formatting that I can directly copy-paste into GitHub's PR
675
- description field. Use proper markdown syntax with headers, bullet points, code blocks, and formatting.
676
- 5 Make it review-ready: Ensure the description helps reviewers understand the context, approach, and impact of the changes.
677
- 6. If you have Github MCP, or gh cli is installed and authenticated then find the PR for the branch we analyzed and update the PR description there and then delete the PR_DESCRIPTION.md file. (If you have a better name (title) for the PR, go ahead and update the title too.{directory_context}"""
678
-
679
- # Return the prompt to be processed by the main chat system
680
- return pr_prompt
681
-
682
- if command.startswith("/dump_context"):
683
- from code_puppy.agents.agent_manager import get_current_agent
684
-
685
- tokens = command.split()
686
- if len(tokens) != 2:
687
- emit_warning("Usage: /dump_context <session_name>")
688
- return True
689
-
690
- session_name = tokens[1]
691
- agent = get_current_agent()
692
- history = agent.get_message_history()
693
-
694
- if not history:
695
- emit_warning("No message history to dump!")
696
- return True
697
-
698
- try:
699
- metadata = save_session(
700
- history=history,
701
- session_name=session_name,
702
- base_dir=Path(CONTEXTS_DIR),
703
- timestamp=datetime.now().isoformat(),
704
- token_estimator=agent.estimate_tokens_for_message,
705
- )
706
- emit_success(
707
- f"✅ Context saved: {metadata.message_count} messages ({metadata.total_tokens} tokens)\n"
708
- f"📁 Files: {metadata.pickle_path}, {metadata.metadata_path}"
709
- )
710
- return True
711
-
712
- except Exception as exc:
713
- emit_error(f"Failed to dump context: {exc}")
714
- return True
715
-
716
- if command.startswith("/load_context"):
717
- from code_puppy.agents.agent_manager import get_current_agent
718
-
719
- tokens = command.split()
720
- if len(tokens) != 2:
721
- emit_warning("Usage: /load_context <session_name>")
722
- return True
723
-
724
- session_name = tokens[1]
725
- contexts_dir = Path(CONTEXTS_DIR)
726
- session_path = contexts_dir / f"{session_name}.pkl"
727
-
728
- try:
729
- history = load_session(session_name, contexts_dir)
730
- except FileNotFoundError:
731
- emit_error(f"Context file not found: {session_path}")
732
- available = list_sessions(contexts_dir)
733
- if available:
734
- emit_info(f"Available contexts: {', '.join(available)}")
735
- return True
736
- except Exception as exc:
737
- emit_error(f"Failed to load context: {exc}")
738
- return True
739
-
740
- agent = get_current_agent()
741
- agent.set_message_history(history)
742
- total_tokens = sum(agent.estimate_tokens_for_message(m) for m in history)
743
-
744
- # Rotate autosave id to avoid overwriting any existing autosave
745
- try:
746
- from code_puppy.config import rotate_autosave_id
747
-
748
- new_id = rotate_autosave_id()
749
- autosave_info = f"\n[dim]Autosave session rotated to: {new_id}[/dim]"
750
- except Exception:
751
- autosave_info = ""
752
-
753
- emit_success(
754
- f"✅ Context loaded: {len(history)} messages ({total_tokens} tokens)\n"
755
- f"📁 From: {session_path}{autosave_info}"
756
- )
757
- return True
758
-
759
- if command.startswith("/truncate"):
760
- from code_puppy.agents.agent_manager import get_current_agent
761
-
762
- tokens = command.split()
763
- if len(tokens) != 2:
764
- emit_error(
765
- "Usage: /truncate <N> (where N is the number of messages to keep)"
766
- )
767
- return True
171
+ from rich.text import Text
768
172
 
769
- try:
770
- n = int(tokens[1])
771
- if n < 1:
772
- emit_error("N must be a positive integer")
773
- return True
774
- except ValueError:
775
- emit_error("N must be a valid integer")
776
- return True
777
-
778
- agent = get_current_agent()
779
- history = agent.get_message_history()
780
- if not history:
781
- emit_warning("No history to truncate yet. Ask me something first!")
782
- return True
783
-
784
- if len(history) <= n:
785
- emit_info(
786
- f"History already has {len(history)} messages, which is <= {n}. Nothing to truncate."
787
- )
788
- return True
173
+ from code_puppy.command_line.command_registry import get_command
174
+ from code_puppy.messaging import emit_info, emit_warning
789
175
 
790
- # Always keep the first message (system message) and then keep the N-1 most recent messages
791
- truncated_history = (
792
- [history[0]] + history[-(n - 1) :] if n > 1 else [history[0]]
793
- )
176
+ _ensure_plugins_loaded()
794
177
 
795
- agent.set_message_history(truncated_history)
796
- emit_success(
797
- f"Truncated message history from {len(history)} to {len(truncated_history)} messages (keeping system message and {n - 1} most recent)"
798
- )
799
- return True
178
+ command = command.strip()
800
179
 
801
- if command in ("/exit", "/quit"):
802
- emit_success("Goodbye!")
803
- # Signal to the main app that we want to exit
804
- # The actual exit handling is done in main.py
805
- return True
180
+ # Check if this is a registered command
181
+ if command.startswith("/"):
182
+ # Extract command name (first word after /)
183
+ cmd_name = command[1:].split()[0] if len(command) > 1 else ""
184
+
185
+ # Try to find in registry
186
+ cmd_info = get_command(cmd_name)
187
+ if cmd_info:
188
+ # Execute the registered handler
189
+ return cmd_info.handler(command)
190
+
191
+ # ========================================================================
192
+ # LEGACY COMMAND FALLBACK
193
+ # ========================================================================
194
+ # This section is kept as a fallback mechanism for commands added in other
195
+ # branches that haven't been migrated to the registry system yet.
196
+ #
197
+ # All current commands are registered above using @register_command, so
198
+ # they won't fall through to this section.
199
+ #
200
+ # If you're rebasing and your branch adds a new command using the old
201
+ # if/elif style, it will still work! Just add your if block below.
202
+ #
203
+ # EXAMPLE: How to add a legacy command:
204
+ #
205
+ # if command.startswith("/mycommand"):
206
+ # from code_puppy.messaging import emit_info
207
+ # emit_info("My command executed!")
208
+ # return True
209
+ #
210
+ # NOTE: For new commands, please use @register_command instead (see above).
211
+ # ========================================================================
212
+
213
+ # Legacy commands from other branches/rebases go here:
214
+ # (All current commands are in the registry above)
215
+
216
+ # Example placeholder (remove this and add your command if needed):
217
+ # if command.startswith("/my_new_command"):
218
+ # from code_puppy.messaging import emit_info
219
+ # emit_info("Command executed!")
220
+ # return True
221
+
222
+ # End of legacy fallback section
223
+ # ========================================================================
224
+
225
+ # All legacy command implementations have been moved to @register_command handlers above.
226
+ # If you're adding a new command via rebase, add your if block here.
806
227
 
807
228
  # Try plugin-provided custom commands before unknown warning
808
229
  if command.startswith("/"):
@@ -811,11 +232,24 @@ def handle_command(command: str):
811
232
  try:
812
233
  from code_puppy import callbacks
813
234
 
235
+ # Import the special result class for markdown commands
236
+ try:
237
+ from code_puppy.plugins.customizable_commands.register_callbacks import (
238
+ MarkdownCommandResult,
239
+ )
240
+ except ImportError:
241
+ MarkdownCommandResult = None
242
+
814
243
  results = callbacks.on_custom_command(command=command, name=name)
815
244
  # Iterate through callback results; treat str as handled (no model run)
816
245
  for res in results:
817
246
  if res is True:
818
247
  return True
248
+ if MarkdownCommandResult and isinstance(res, MarkdownCommandResult):
249
+ # Special case: markdown command that should be processed as input
250
+ # Replace the command with the markdown content and let it be processed
251
+ # This is handled by the caller, so return the content as string
252
+ return res.content
819
253
  if isinstance(res, str):
820
254
  # Display returned text to the user and treat as handled
821
255
  try:
@@ -829,7 +263,9 @@ def handle_command(command: str):
829
263
 
830
264
  if name:
831
265
  emit_warning(
832
- f"Unknown command: {command}\n[dim]Type /help for options.[/dim]"
266
+ Text.from_markup(
267
+ f"Unknown command: {command}\n[dim]Type /help for options.[/dim]"
268
+ )
833
269
  )
834
270
  else:
835
271
  # Show current model ONLY here
@@ -837,7 +273,9 @@ def handle_command(command: str):
837
273
 
838
274
  current_model = get_active_model()
839
275
  emit_info(
840
- f"[bold green]Current Model:[/bold green] [cyan]{current_model}[/cyan]"
276
+ Text.from_markup(
277
+ f"[bold green]Current Model:[/bold green] [cyan]{current_model}[/cyan]"
278
+ )
841
279
  )
842
280
  return True
843
281