code-puppy 0.0.169__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 (243) hide show
  1. code_puppy/__init__.py +7 -1
  2. code_puppy/agents/__init__.py +8 -8
  3. code_puppy/agents/agent_c_reviewer.py +155 -0
  4. code_puppy/agents/agent_code_puppy.py +9 -2
  5. code_puppy/agents/agent_code_reviewer.py +90 -0
  6. code_puppy/agents/agent_cpp_reviewer.py +132 -0
  7. code_puppy/agents/agent_creator_agent.py +48 -9
  8. code_puppy/agents/agent_golang_reviewer.py +151 -0
  9. code_puppy/agents/agent_javascript_reviewer.py +160 -0
  10. code_puppy/agents/agent_manager.py +146 -199
  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 +90 -0
  15. code_puppy/agents/agent_qa_expert.py +163 -0
  16. code_puppy/agents/agent_qa_kitten.py +208 -0
  17. code_puppy/agents/agent_security_auditor.py +181 -0
  18. code_puppy/agents/agent_terminal_qa.py +323 -0
  19. code_puppy/agents/agent_typescript_reviewer.py +166 -0
  20. code_puppy/agents/base_agent.py +1713 -1
  21. code_puppy/agents/event_stream_handler.py +350 -0
  22. code_puppy/agents/json_agent.py +12 -1
  23. code_puppy/agents/pack/__init__.py +34 -0
  24. code_puppy/agents/pack/bloodhound.py +304 -0
  25. code_puppy/agents/pack/husky.py +321 -0
  26. code_puppy/agents/pack/retriever.py +393 -0
  27. code_puppy/agents/pack/shepherd.py +348 -0
  28. code_puppy/agents/pack/terrier.py +287 -0
  29. code_puppy/agents/pack/watchdog.py +367 -0
  30. code_puppy/agents/prompt_reviewer.py +145 -0
  31. code_puppy/agents/subagent_stream_handler.py +276 -0
  32. code_puppy/api/__init__.py +13 -0
  33. code_puppy/api/app.py +169 -0
  34. code_puppy/api/main.py +21 -0
  35. code_puppy/api/pty_manager.py +446 -0
  36. code_puppy/api/routers/__init__.py +12 -0
  37. code_puppy/api/routers/agents.py +36 -0
  38. code_puppy/api/routers/commands.py +217 -0
  39. code_puppy/api/routers/config.py +74 -0
  40. code_puppy/api/routers/sessions.py +232 -0
  41. code_puppy/api/templates/terminal.html +361 -0
  42. code_puppy/api/websocket.py +154 -0
  43. code_puppy/callbacks.py +174 -4
  44. code_puppy/chatgpt_codex_client.py +283 -0
  45. code_puppy/claude_cache_client.py +586 -0
  46. code_puppy/cli_runner.py +916 -0
  47. code_puppy/command_line/add_model_menu.py +1079 -0
  48. code_puppy/command_line/agent_menu.py +395 -0
  49. code_puppy/command_line/attachments.py +395 -0
  50. code_puppy/command_line/autosave_menu.py +605 -0
  51. code_puppy/command_line/clipboard.py +527 -0
  52. code_puppy/command_line/colors_menu.py +520 -0
  53. code_puppy/command_line/command_handler.py +233 -627
  54. code_puppy/command_line/command_registry.py +150 -0
  55. code_puppy/command_line/config_commands.py +715 -0
  56. code_puppy/command_line/core_commands.py +792 -0
  57. code_puppy/command_line/diff_menu.py +863 -0
  58. code_puppy/command_line/load_context_completion.py +15 -22
  59. code_puppy/command_line/mcp/base.py +1 -4
  60. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  61. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  62. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  63. code_puppy/command_line/mcp/edit_command.py +148 -0
  64. code_puppy/command_line/mcp/handler.py +9 -4
  65. code_puppy/command_line/mcp/help_command.py +6 -5
  66. code_puppy/command_line/mcp/install_command.py +16 -27
  67. code_puppy/command_line/mcp/install_menu.py +685 -0
  68. code_puppy/command_line/mcp/list_command.py +3 -3
  69. code_puppy/command_line/mcp/logs_command.py +174 -65
  70. code_puppy/command_line/mcp/remove_command.py +2 -2
  71. code_puppy/command_line/mcp/restart_command.py +12 -4
  72. code_puppy/command_line/mcp/search_command.py +17 -11
  73. code_puppy/command_line/mcp/start_all_command.py +22 -13
  74. code_puppy/command_line/mcp/start_command.py +50 -31
  75. code_puppy/command_line/mcp/status_command.py +6 -7
  76. code_puppy/command_line/mcp/stop_all_command.py +11 -8
  77. code_puppy/command_line/mcp/stop_command.py +11 -10
  78. code_puppy/command_line/mcp/test_command.py +2 -2
  79. code_puppy/command_line/mcp/utils.py +1 -1
  80. code_puppy/command_line/mcp/wizard_utils.py +22 -18
  81. code_puppy/command_line/mcp_completion.py +174 -0
  82. code_puppy/command_line/model_picker_completion.py +89 -30
  83. code_puppy/command_line/model_settings_menu.py +884 -0
  84. code_puppy/command_line/motd.py +14 -8
  85. code_puppy/command_line/onboarding_slides.py +179 -0
  86. code_puppy/command_line/onboarding_wizard.py +340 -0
  87. code_puppy/command_line/pin_command_completion.py +329 -0
  88. code_puppy/command_line/prompt_toolkit_completion.py +626 -75
  89. code_puppy/command_line/session_commands.py +296 -0
  90. code_puppy/command_line/utils.py +54 -0
  91. code_puppy/config.py +1181 -51
  92. code_puppy/error_logging.py +118 -0
  93. code_puppy/gemini_code_assist.py +385 -0
  94. code_puppy/gemini_model.py +602 -0
  95. code_puppy/http_utils.py +220 -104
  96. code_puppy/keymap.py +128 -0
  97. code_puppy/main.py +5 -594
  98. code_puppy/{mcp → mcp_}/__init__.py +17 -0
  99. code_puppy/{mcp → mcp_}/async_lifecycle.py +35 -4
  100. code_puppy/{mcp → mcp_}/blocking_startup.py +70 -43
  101. code_puppy/{mcp → mcp_}/captured_stdio_server.py +2 -2
  102. code_puppy/{mcp → mcp_}/config_wizard.py +5 -5
  103. code_puppy/{mcp → mcp_}/dashboard.py +15 -6
  104. code_puppy/{mcp → mcp_}/examples/retry_example.py +4 -1
  105. code_puppy/{mcp → mcp_}/managed_server.py +66 -39
  106. code_puppy/{mcp → mcp_}/manager.py +146 -52
  107. code_puppy/mcp_/mcp_logs.py +224 -0
  108. code_puppy/{mcp → mcp_}/registry.py +6 -6
  109. code_puppy/{mcp → mcp_}/server_registry_catalog.py +25 -8
  110. code_puppy/messaging/__init__.py +199 -2
  111. code_puppy/messaging/bus.py +610 -0
  112. code_puppy/messaging/commands.py +167 -0
  113. code_puppy/messaging/markdown_patches.py +57 -0
  114. code_puppy/messaging/message_queue.py +17 -48
  115. code_puppy/messaging/messages.py +500 -0
  116. code_puppy/messaging/queue_console.py +1 -24
  117. code_puppy/messaging/renderers.py +43 -146
  118. code_puppy/messaging/rich_renderer.py +1027 -0
  119. code_puppy/messaging/spinner/__init__.py +33 -5
  120. code_puppy/messaging/spinner/console_spinner.py +92 -52
  121. code_puppy/messaging/spinner/spinner_base.py +29 -0
  122. code_puppy/messaging/subagent_console.py +461 -0
  123. code_puppy/model_factory.py +686 -80
  124. code_puppy/model_utils.py +167 -0
  125. code_puppy/models.json +86 -104
  126. code_puppy/models_dev_api.json +1 -0
  127. code_puppy/models_dev_parser.py +592 -0
  128. code_puppy/plugins/__init__.py +164 -10
  129. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  130. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  131. code_puppy/plugins/antigravity_oauth/antigravity_model.py +704 -0
  132. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  133. code_puppy/plugins/antigravity_oauth/constants.py +136 -0
  134. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  135. code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
  136. code_puppy/plugins/antigravity_oauth/storage.py +271 -0
  137. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  138. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  139. code_puppy/plugins/antigravity_oauth/transport.py +767 -0
  140. code_puppy/plugins/antigravity_oauth/utils.py +169 -0
  141. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  142. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  143. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
  144. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
  145. code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
  146. code_puppy/plugins/chatgpt_oauth/utils.py +489 -0
  147. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  148. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  149. code_puppy/plugins/claude_code_oauth/__init__.py +6 -0
  150. code_puppy/plugins/claude_code_oauth/config.py +50 -0
  151. code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
  152. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  153. code_puppy/plugins/claude_code_oauth/utils.py +518 -0
  154. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  155. code_puppy/plugins/customizable_commands/register_callbacks.py +169 -0
  156. code_puppy/plugins/example_custom_command/README.md +280 -0
  157. code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
  158. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  159. code_puppy/plugins/file_permission_handler/register_callbacks.py +523 -0
  160. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  161. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  162. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  163. code_puppy/plugins/oauth_puppy_html.py +228 -0
  164. code_puppy/plugins/shell_safety/__init__.py +6 -0
  165. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  166. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  167. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  168. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  169. code_puppy/prompts/codex_system_prompt.md +310 -0
  170. code_puppy/pydantic_patches.py +131 -0
  171. code_puppy/reopenable_async_client.py +8 -8
  172. code_puppy/round_robin_model.py +10 -15
  173. code_puppy/session_storage.py +294 -0
  174. code_puppy/status_display.py +21 -4
  175. code_puppy/summarization_agent.py +52 -14
  176. code_puppy/terminal_utils.py +418 -0
  177. code_puppy/tools/__init__.py +139 -6
  178. code_puppy/tools/agent_tools.py +548 -49
  179. code_puppy/tools/browser/__init__.py +37 -0
  180. code_puppy/tools/browser/browser_control.py +289 -0
  181. code_puppy/tools/browser/browser_interactions.py +545 -0
  182. code_puppy/tools/browser/browser_locators.py +640 -0
  183. code_puppy/tools/browser/browser_manager.py +316 -0
  184. code_puppy/tools/browser/browser_navigation.py +251 -0
  185. code_puppy/tools/browser/browser_screenshot.py +179 -0
  186. code_puppy/tools/browser/browser_scripts.py +462 -0
  187. code_puppy/tools/browser/browser_workflows.py +221 -0
  188. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  189. code_puppy/tools/browser/terminal_command_tools.py +521 -0
  190. code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
  191. code_puppy/tools/browser/terminal_tools.py +525 -0
  192. code_puppy/tools/command_runner.py +941 -153
  193. code_puppy/tools/common.py +1146 -6
  194. code_puppy/tools/display.py +84 -0
  195. code_puppy/tools/file_modifications.py +288 -89
  196. code_puppy/tools/file_operations.py +352 -266
  197. code_puppy/tools/subagent_context.py +158 -0
  198. code_puppy/uvx_detection.py +242 -0
  199. code_puppy/version_checker.py +30 -11
  200. code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
  201. code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
  202. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/METADATA +184 -67
  203. code_puppy-0.0.366.dist-info/RECORD +217 -0
  204. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
  205. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +1 -0
  206. code_puppy/agent.py +0 -231
  207. code_puppy/agents/agent_orchestrator.json +0 -26
  208. code_puppy/agents/runtime_manager.py +0 -272
  209. code_puppy/command_line/mcp/add_command.py +0 -183
  210. code_puppy/command_line/meta_command_handler.py +0 -153
  211. code_puppy/message_history_processor.py +0 -490
  212. code_puppy/messaging/spinner/textual_spinner.py +0 -101
  213. code_puppy/state_management.py +0 -200
  214. code_puppy/tui/__init__.py +0 -10
  215. code_puppy/tui/app.py +0 -986
  216. code_puppy/tui/components/__init__.py +0 -21
  217. code_puppy/tui/components/chat_view.py +0 -550
  218. code_puppy/tui/components/command_history_modal.py +0 -218
  219. code_puppy/tui/components/copy_button.py +0 -139
  220. code_puppy/tui/components/custom_widgets.py +0 -63
  221. code_puppy/tui/components/human_input_modal.py +0 -175
  222. code_puppy/tui/components/input_area.py +0 -167
  223. code_puppy/tui/components/sidebar.py +0 -309
  224. code_puppy/tui/components/status_bar.py +0 -182
  225. code_puppy/tui/messages.py +0 -27
  226. code_puppy/tui/models/__init__.py +0 -8
  227. code_puppy/tui/models/chat_message.py +0 -25
  228. code_puppy/tui/models/command_history.py +0 -89
  229. code_puppy/tui/models/enums.py +0 -24
  230. code_puppy/tui/screens/__init__.py +0 -15
  231. code_puppy/tui/screens/help.py +0 -130
  232. code_puppy/tui/screens/mcp_install_wizard.py +0 -803
  233. code_puppy/tui/screens/settings.py +0 -290
  234. code_puppy/tui/screens/tools.py +0 -74
  235. code_puppy-0.0.169.data/data/code_puppy/models.json +0 -128
  236. code_puppy-0.0.169.dist-info/RECORD +0 -112
  237. /code_puppy/{mcp → mcp_}/circuit_breaker.py +0 -0
  238. /code_puppy/{mcp → mcp_}/error_isolation.py +0 -0
  239. /code_puppy/{mcp → mcp_}/health_monitor.py +0 -0
  240. /code_puppy/{mcp → mcp_}/retry_manager.py +0 -0
  241. /code_puppy/{mcp → mcp_}/status_tracker.py +0 -0
  242. /code_puppy/{mcp → mcp_}/system_tools.py +0 -0
  243. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/licenses/LICENSE +0 -0
@@ -1,667 +1,271 @@
1
- import os
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
2
5
 
3
- from code_puppy.command_line.model_picker_completion import update_model_in_input
4
- from code_puppy.command_line.motd import print_motd
5
- from code_puppy.command_line.utils import make_directory_table
6
- from code_puppy.config import get_config_keys
7
- from code_puppy.tools.tools_content import tools_content
6
+ # Global flag to track if plugins have been loaded
7
+ _PLUGINS_LOADED = False
8
8
 
9
9
 
10
10
  def get_commands_help():
11
- """Generate commands help using Rich Text objects to avoid markup conflicts."""
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
+ """
12
16
  from rich.text import Text
13
17
 
14
- # Build help text programmatically
15
- help_lines = []
16
-
17
- # Title
18
- help_lines.append(Text("Commands Help", style="bold magenta"))
19
-
20
- # Commands - build each line programmatically
21
- help_lines.append(
22
- Text("/help, /h", style="cyan") + Text(" Show this help message")
23
- )
24
- help_lines.append(
25
- Text("/cd", style="cyan")
26
- + Text(" <dir> Change directory or show directories")
27
- )
28
- help_lines.append(
29
- Text("/agent", style="cyan")
30
- + Text(" <name> Switch to a different agent or show available agents")
31
- )
32
- help_lines.append(
33
- Text("/exit, /quit", style="cyan") + Text(" Exit interactive mode")
34
- )
35
- help_lines.append(
36
- Text("/generate-pr-description", style="cyan")
37
- + Text(" [@dir] Generate comprehensive PR description")
38
- )
39
- help_lines.append(
40
- Text("/model, /m", style="cyan") + Text(" <model> Set active model")
41
- )
42
- help_lines.append(
43
- Text("/pin_model", style="cyan")
44
- + Text(" <agent> <model> Pin a specific model to an agent")
45
- )
46
- help_lines.append(
47
- Text("/mcp", style="cyan")
48
- + Text(" Manage MCP servers (list, start, stop, status, etc.)")
49
- )
50
- help_lines.append(
51
- Text("/motd", style="cyan")
52
- + Text(" Show the latest message of the day (MOTD)")
53
- )
54
- help_lines.append(
55
- Text("/show", style="cyan")
56
- + Text(" Show puppy config key-values")
57
- )
58
- help_lines.append(
59
- Text("/compact", style="cyan")
60
- + Text(
61
- " Summarize and compact current chat history (uses compaction_strategy config)"
62
- )
63
- )
64
- help_lines.append(
65
- Text("/dump_context", style="cyan")
66
- + Text(" <name> Save current message history to file")
67
- )
68
- help_lines.append(
69
- Text("/load_context", style="cyan")
70
- + Text(" <name> Load message history from file")
71
- )
72
- help_lines.append(
73
- Text("/set", style="cyan")
74
- + Text(
75
- " Set puppy config key-values (e.g., /set yolo_mode true, /set compaction_strategy truncation)"
76
- )
77
- )
78
- help_lines.append(
79
- Text("/tools", style="cyan")
80
- + Text(" Show available tools and capabilities")
81
- )
82
- help_lines.append(
83
- Text("/truncate", style="cyan")
84
- + Text(" <N> Truncate message history to N most recent messages (keeping system message)")
85
- )
86
- help_lines.append(
87
- Text("/<unknown>", style="cyan")
88
- + Text(" Show unknown command warning")
89
- )
90
-
91
- # Combine all lines
18
+ from code_puppy.command_line.command_registry import get_unique_commands
19
+
20
+ # Ensure plugins are loaded so custom help can register
21
+ _ensure_plugins_loaded()
22
+
23
+ lines: list[Text] = []
24
+ # No global header needed - user already knows they're viewing help
25
+
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))
33
+
34
+ # Get custom commands from plugins
35
+ custom_entries: list[tuple[str, str]] = []
36
+ try:
37
+ from code_puppy import callbacks
38
+
39
+ custom_help_results = callbacks.on_custom_command_help()
40
+ for res in custom_help_results:
41
+ if not res:
42
+ continue
43
+ # Format 1: Tuple with (command_name, description)
44
+ if isinstance(res, tuple) and len(res) == 2:
45
+ cmd_name = str(res[0])
46
+ custom_entries.append((f"/{cmd_name}", str(res[1])))
47
+ # Format 2: List of tuples or strings
48
+ elif isinstance(res, list):
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))
64
+ except Exception:
65
+ pass
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
+
92
108
  final_text = Text()
93
- for i, line in enumerate(help_lines):
109
+ for i, line in enumerate(lines):
94
110
  if i > 0:
95
111
  final_text.append("\n")
96
112
  final_text.append_text(line)
97
113
 
98
- return final_text
114
+ # Add trailing newline for spacing before next prompt
115
+ final_text.append("\n")
99
116
 
117
+ return final_text
100
118
 
101
- def handle_command(command: str):
102
- from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
103
-
104
- """
105
- Handle commands prefixed with '/'.
106
119
 
107
- Args:
108
- command: The command string to handle
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.
109
125
 
110
- Returns:
111
- True if the command was handled, False if not, or a string to be processed as user input
112
- """
113
- command = command.strip()
126
+ # ============================================================================
127
+ # UTILITY FUNCTIONS
128
+ # ============================================================================
114
129
 
115
- if command.strip().startswith("/motd"):
116
- print_motd(force=True)
117
- return True
118
130
 
119
- if command.strip().startswith("/compact"):
120
- from code_puppy.config import get_compaction_strategy
121
- from code_puppy.message_history_processor import (
122
- estimate_tokens_for_message,
123
- get_protected_token_count,
124
- summarize_messages,
125
- truncation,
126
- )
127
- from code_puppy.messaging import (
128
- emit_error,
129
- emit_info,
130
- emit_success,
131
- emit_warning,
132
- )
133
- from code_puppy.state_management import get_message_history, set_message_history
131
+ def _ensure_plugins_loaded() -> None:
132
+ global _PLUGINS_LOADED
133
+ if _PLUGINS_LOADED:
134
+ return
135
+ try:
136
+ from code_puppy import plugins
134
137
 
138
+ plugins.load_plugin_callbacks()
139
+ _PLUGINS_LOADED = True
140
+ except Exception as e:
141
+ # If plugins fail to load, continue gracefully but note it
135
142
  try:
136
- history = get_message_history()
137
- if not history:
138
- emit_warning("No history to compact yet. Ask me something first!")
139
- return True
140
-
141
- before_tokens = sum(estimate_tokens_for_message(m) for m in history)
142
- compaction_strategy = get_compaction_strategy()
143
- protected_tokens = get_protected_token_count()
144
- emit_info(
145
- f"🤔 Compacting {len(history)} messages using {compaction_strategy} strategy... (~{before_tokens} tokens)"
146
- )
147
-
148
- if compaction_strategy == "truncation":
149
- compacted = truncation(history, protected_tokens)
150
- summarized_messages = [] # No summarization in truncation mode
151
- else:
152
- # Default to summarization
153
- compacted, summarized_messages = summarize_messages(
154
- history, with_protection=True
155
- )
156
-
157
- if not compacted:
158
- emit_error("Compaction failed. History unchanged.")
159
- return True
160
-
161
- set_message_history(compacted)
162
-
163
- after_tokens = sum(estimate_tokens_for_message(m) for m in compacted)
164
- reduction_pct = (
165
- ((before_tokens - after_tokens) / before_tokens * 100)
166
- if before_tokens > 0
167
- else 0
168
- )
169
-
170
- strategy_info = (
171
- f"using {compaction_strategy} strategy"
172
- if compaction_strategy == "truncation"
173
- else "via summarization"
174
- )
175
- emit_success(
176
- f"✨ Done! History: {len(history)} → {len(compacted)} messages {strategy_info}\n"
177
- f"🏦 Tokens: {before_tokens:,} → {after_tokens:,} ({reduction_pct:.1f}% reduction)"
178
- )
179
- return True
180
- except Exception as e:
181
- emit_error(f"/compact error: {e}")
182
- return True
143
+ from code_puppy.messaging import emit_warning
183
144
 
184
- if command.startswith("/cd"):
185
- tokens = command.split()
186
- if len(tokens) == 1:
187
- try:
188
- table = make_directory_table()
189
- emit_info(table)
190
- except Exception as e:
191
- emit_error(f"Error listing directory: {e}")
192
- return True
193
- elif len(tokens) == 2:
194
- dirname = tokens[1]
195
- target = os.path.expanduser(dirname)
196
- if not os.path.isabs(target):
197
- target = os.path.join(os.getcwd(), target)
198
- if os.path.isdir(target):
199
- os.chdir(target)
200
- emit_success(f"Changed directory to: {target}")
201
- else:
202
- emit_error(f"Not a directory: {dirname}")
203
- return True
204
-
205
- if command.strip().startswith("/show"):
206
- from code_puppy.agents import get_current_agent_config
207
- from code_puppy.command_line.model_picker_completion import get_active_model
208
- from code_puppy.config import (
209
- get_compaction_strategy,
210
- get_compaction_threshold,
211
- get_owner_name,
212
- get_protected_token_count,
213
- get_puppy_name,
214
- get_yolo_mode,
215
- )
216
-
217
- puppy_name = get_puppy_name()
218
- owner_name = get_owner_name()
219
- model = get_active_model()
220
- yolo_mode = get_yolo_mode()
221
- protected_tokens = get_protected_token_count()
222
- compaction_threshold = get_compaction_threshold()
223
- compaction_strategy = get_compaction_strategy()
224
-
225
- # Get current agent info
226
- current_agent = get_current_agent_config()
227
-
228
- status_msg = f"""[bold magenta]🐶 Puppy Status[/bold magenta]
229
-
230
- [bold]puppy_name:[/bold] [cyan]{puppy_name}[/cyan]
231
- [bold]owner_name:[/bold] [cyan]{owner_name}[/cyan]
232
- [bold]current_agent:[/bold] [magenta]{current_agent.display_name}[/magenta]
233
- [bold]model:[/bold] [green]{model}[/green]
234
- [bold]YOLO_MODE:[/bold] {"[red]ON[/red]" if yolo_mode else "[yellow]off[/yellow]"}
235
- [bold]protected_tokens:[/bold] [cyan]{protected_tokens:,}[/cyan] recent tokens preserved
236
- [bold]compaction_threshold:[/bold] [cyan]{compaction_threshold:.1%}[/cyan] context usage triggers compaction
237
- [bold]compaction_strategy:[/bold] [cyan]{compaction_strategy}[/cyan] (summarization or truncation)
238
-
239
- """
240
- emit_info(status_msg)
241
- return True
242
-
243
- if command.startswith("/set"):
244
- # Syntax: /set KEY=VALUE or /set KEY VALUE
245
- from code_puppy.config import set_config_value
246
-
247
- tokens = command.split(None, 2)
248
- argstr = command[len("/set") :].strip()
249
- key = None
250
- value = None
251
- if "=" in argstr:
252
- key, value = argstr.split("=", 1)
253
- key = key.strip()
254
- value = value.strip()
255
- elif len(tokens) >= 3:
256
- key = tokens[1]
257
- value = tokens[2]
258
- elif len(tokens) == 2:
259
- key = tokens[1]
260
- value = ""
261
- else:
262
- config_keys = get_config_keys()
263
- if "compaction_strategy" not in config_keys:
264
- config_keys.append("compaction_strategy")
265
- emit_warning(
266
- 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]"
267
- )
268
- return True
269
- if key:
270
- set_config_value(key, value)
271
- emit_success(f'🌶 Set {key} = "{value}" in puppy.cfg!')
272
- else:
273
- emit_error("You must supply a key.")
274
- return True
275
-
276
- if command.startswith("/tools"):
277
- # Display the tools_content.py file content with markdown formatting
278
- from rich.markdown import Markdown
279
-
280
- markdown_content = Markdown(tools_content)
281
- emit_info(markdown_content)
282
- return True
283
-
284
- if command.startswith("/agent"):
285
- # Handle agent switching
286
- from code_puppy.agents import (
287
- get_agent_descriptions,
288
- get_available_agents,
289
- get_current_agent_config,
290
- set_current_agent,
291
- )
292
- from code_puppy.agents.runtime_manager import get_runtime_agent_manager
293
-
294
- tokens = command.split()
295
-
296
- if len(tokens) == 1:
297
- # Show current agent and available agents
298
- current_agent = get_current_agent_config()
299
- available_agents = get_available_agents()
300
- descriptions = get_agent_descriptions()
145
+ emit_warning(f"Plugin load error: {e}")
146
+ except Exception:
147
+ pass
148
+ _PLUGINS_LOADED = True
301
149
 
302
- # Generate a group ID for all messages in this command
303
- import uuid
304
150
 
305
- group_id = str(uuid.uuid4())
151
+ # All command handlers moved to builtin_commands.py
152
+ # The import above triggers their registration
306
153
 
307
- emit_info(
308
- f"[bold green]Current Agent:[/bold green] {current_agent.display_name}",
309
- message_group=group_id,
310
- )
311
- emit_info(
312
- f"[dim]{current_agent.description}[/dim]\n", message_group=group_id
313
- )
314
-
315
- emit_info(
316
- "[bold magenta]Available Agents:[/bold magenta]", message_group=group_id
317
- )
318
- for name, display_name in available_agents.items():
319
- description = descriptions.get(name, "No description")
320
- current_marker = (
321
- " [green]← current[/green]" if name == current_agent.name else ""
322
- )
323
- emit_info(
324
- f" [cyan]{name:<12}[/cyan] {display_name}{current_marker}",
325
- message_group=group_id,
326
- )
327
- emit_info(f" [dim]{description}[/dim]", message_group=group_id)
154
+ # ============================================================================
155
+ # MAIN COMMAND DISPATCHER
156
+ # ============================================================================
328
157
 
329
- emit_info(
330
- "\n[yellow]Usage:[/yellow] /agent <agent-name>", message_group=group_id
331
- )
332
- return True
158
+ # _show_color_options has been moved to builtin_commands.py
333
159
 
334
- elif len(tokens) == 2:
335
- agent_name = tokens[1].lower()
336
160
 
337
- # Generate a group ID for all messages in this command
338
- import uuid
339
-
340
- group_id = str(uuid.uuid4())
341
-
342
- if set_current_agent(agent_name):
343
- # Reload the agent with new configuration
344
- manager = get_runtime_agent_manager()
345
- manager.reload_agent()
346
- new_agent = get_current_agent_config()
347
- emit_success(
348
- f"Switched to agent: {new_agent.display_name}",
349
- message_group=group_id,
350
- )
351
- emit_info(f"[dim]{new_agent.description}[/dim]", message_group=group_id)
352
- return True
353
- else:
354
- # Generate a group ID for all messages in this command
355
- import uuid
356
-
357
- group_id = str(uuid.uuid4())
358
-
359
- available_agents = get_available_agents()
360
- emit_error(f"Agent '{agent_name}' not found", message_group=group_id)
361
- emit_warning(
362
- f"Available agents: {', '.join(available_agents.keys())}",
363
- message_group=group_id,
364
- )
365
- return True
366
- else:
367
- emit_warning("Usage: /agent [agent-name]")
368
- return True
369
-
370
- if command.startswith("/model") or command.startswith("/m "):
371
- # Try setting model and show confirmation
372
- # Handle both /model and /m for backward compatibility
373
- model_command = command
374
- if command.startswith("/model"):
375
- # Convert /model to /m for internal processing
376
- model_command = command.replace("/model", "/m", 1)
377
-
378
- # If no model matched, show available models
379
- from code_puppy.command_line.model_picker_completion import load_model_names
380
-
381
- new_input = update_model_in_input(model_command)
382
- if new_input is not None:
383
- from code_puppy.agents.runtime_manager import get_runtime_agent_manager
384
- from code_puppy.command_line.model_picker_completion import get_active_model
385
-
386
- model = get_active_model()
387
- # Make sure this is called for the test
388
- manager = get_runtime_agent_manager()
389
- manager.reload_agent()
390
- emit_success(f"Active model set and loaded: {model}")
391
- return True
392
- model_names = load_model_names()
393
- emit_warning("Usage: /model <model-name> or /m <model-name>")
394
- emit_warning(f"Available models: {', '.join(model_names)}")
395
- return True
396
-
397
- if command.startswith("/mcp"):
398
- from code_puppy.command_line.mcp import MCPCommandHandler
399
-
400
- handler = MCPCommandHandler()
401
- return handler.handle_mcp_command(command)
402
- if command in ("/help", "/h"):
403
- import uuid
404
-
405
- group_id = str(uuid.uuid4())
406
- help_text = get_commands_help()
407
- emit_info(help_text, message_group_id=group_id)
408
- return True
409
-
410
- if command.startswith("/pin_model"):
411
- # Handle agent model pinning
412
- from code_puppy.agents.json_agent import discover_json_agents
413
- from code_puppy.command_line.model_picker_completion import load_model_names
414
- import json
415
-
416
- tokens = command.split()
417
-
418
- if len(tokens) != 3:
419
- emit_warning("Usage: /pin_model <agent-name> <model-name>")
420
-
421
- # Show available models and JSON agents
422
- available_models = load_model_names()
423
- json_agents = discover_json_agents()
424
-
425
- emit_info("Available models:")
426
- for model in available_models:
427
- emit_info(f" [cyan]{model}[/cyan]")
428
-
429
- if json_agents:
430
- emit_info("\nAvailable JSON agents:")
431
- for agent_name, agent_path in json_agents.items():
432
- emit_info(f" [cyan]{agent_name}[/cyan] ({agent_path})")
433
- return True
434
-
435
- agent_name = tokens[1].lower()
436
- model_name = tokens[2]
437
-
438
- # Check if model exists
439
- available_models = load_model_names()
440
- if model_name not in available_models:
441
- emit_error(f"Model '{model_name}' not found")
442
- emit_warning(f"Available models: {', '.join(available_models)}")
443
- return True
444
-
445
- # Check that we're modifying a JSON agent (not a built-in Python agent)
446
- json_agents = discover_json_agents()
447
- if agent_name not in json_agents:
448
- emit_error(f"JSON agent '{agent_name}' not found")
449
-
450
- # Show available JSON agents
451
- if json_agents:
452
- emit_info("Available JSON agents:")
453
- for name, path in json_agents.items():
454
- emit_info(f" [cyan]{name}[/cyan] ({path})")
455
- return True
456
-
457
- agent_file_path = json_agents[agent_name]
458
-
459
- # Load, modify, and save the agent configuration
460
- try:
461
- with open(agent_file_path, "r", encoding="utf-8") as f:
462
- agent_config = json.load(f)
463
-
464
- # Set the model
465
- agent_config["model"] = model_name
466
-
467
- # Save the updated configuration
468
- with open(agent_file_path, "w", encoding="utf-8") as f:
469
- json.dump(agent_config, f, indent=2, ensure_ascii=False)
470
-
471
- emit_success(f"Model '{model_name}' pinned to agent '{agent_name}'")
472
-
473
- # If this is the current agent, reload it to use the new model
474
- from code_puppy.agents import get_current_agent_config
475
- from code_puppy.agents.runtime_manager import get_runtime_agent_manager
161
+ def handle_command(command: str):
162
+ """
163
+ Handle commands prefixed with '/'.
476
164
 
477
- current_agent = get_current_agent_config()
478
- if current_agent.name == agent_name:
479
- manager = get_runtime_agent_manager()
480
- manager.reload_agent()
481
- emit_info(f"Active agent reloaded with pinned model '{model_name}'")
165
+ Args:
166
+ command: The command string to handle
482
167
 
483
- return True
168
+ Returns:
169
+ True if the command was handled, False if not, or a string to be processed as user input
170
+ """
171
+ from rich.text import Text
484
172
 
485
- except Exception as e:
486
- emit_error(f"Failed to pin model to agent '{agent_name}': {e}")
487
- return True
488
-
489
- if command.startswith("/generate-pr-description"):
490
- # Parse directory argument (e.g., /generate-pr-description @some/dir)
491
- tokens = command.split()
492
- directory_context = ""
493
- for t in tokens:
494
- if t.startswith("@"):
495
- directory_context = f" Please work in the directory: {t[1:]}"
496
- break
497
-
498
- # Hard-coded prompt from user requirements
499
- pr_prompt = f"""Generate a comprehensive PR description for my current branch changes. Follow these steps:
500
-
501
- 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.
502
- 2 Analyze the code: Read and analyze all modified files to understand:
503
- • What functionality was added/changed/removed
504
- • The technical approach and implementation details
505
- • Any architectural or design pattern changes
506
- • Dependencies added/removed/updated
507
- 3 Generate a structured PR description with these sections:
508
- • Title: Concise, descriptive title (50 chars max)
509
- • Summary: Brief overview of what this PR accomplishes
510
- • Changes Made: Detailed bullet points of specific changes
511
- • Technical Details: Implementation approach, design decisions, patterns used
512
- • Files Modified: List of key files with brief description of changes
513
- • Testing: What was tested and how (if applicable)
514
- • Breaking Changes: Any breaking changes (if applicable)
515
- • Additional Notes: Any other relevant information
516
- 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
517
- description field. Use proper markdown syntax with headers, bullet points, code blocks, and formatting.
518
- 5 Make it review-ready: Ensure the description helps reviewers understand the context, approach, and impact of the changes.
519
- 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}"""
520
-
521
- # Return the prompt to be processed by the main chat system
522
- return pr_prompt
523
-
524
- if command.startswith("/dump_context"):
525
- import json
526
- import pickle
527
- from datetime import datetime
528
- from pathlib import Path
529
-
530
- from code_puppy.config import CONFIG_DIR
531
- from code_puppy.message_history_processor import estimate_tokens_for_message
532
- from code_puppy.state_management import get_message_history
533
-
534
- tokens = command.split()
535
- if len(tokens) != 2:
536
- emit_warning("Usage: /dump_context <session_name>")
537
- return True
538
-
539
- session_name = tokens[1]
540
- history = get_message_history()
541
-
542
- if not history:
543
- emit_warning("No message history to dump!")
544
- return True
545
-
546
- # Create contexts directory inside CONFIG_DIR if it doesn't exist
547
- contexts_dir = Path(CONFIG_DIR) / "contexts"
548
- contexts_dir.mkdir(parents=True, exist_ok=True)
173
+ from code_puppy.command_line.command_registry import get_command
174
+ from code_puppy.messaging import emit_info, emit_warning
549
175
 
550
- try:
551
- # Save as pickle for exact preservation
552
- pickle_file = contexts_dir / f"{session_name}.pkl"
553
- with open(pickle_file, "wb") as f:
554
- pickle.dump(history, f)
555
-
556
- # Also save metadata as JSON for readability
557
- meta_file = contexts_dir / f"{session_name}_meta.json"
558
- metadata = {
559
- "session_name": session_name,
560
- "timestamp": datetime.now().isoformat(),
561
- "message_count": len(history),
562
- "total_tokens": sum(estimate_tokens_for_message(m) for m in history),
563
- "file_path": str(pickle_file),
564
- }
565
-
566
- with open(meta_file, "w") as f:
567
- json.dump(metadata, f, indent=2)
568
-
569
- emit_success(
570
- f"✅ Context saved: {len(history)} messages ({metadata['total_tokens']} tokens)\n"
571
- f"📁 Files: {pickle_file}, {meta_file}"
572
- )
573
- return True
176
+ _ensure_plugins_loaded()
574
177
 
575
- except Exception as e:
576
- emit_error(f"Failed to dump context: {e}")
577
- return True
578
-
579
- if command.startswith("/load_context"):
580
- import pickle
581
- from pathlib import Path
582
-
583
- from code_puppy.config import CONFIG_DIR
584
- from code_puppy.message_history_processor import estimate_tokens_for_message
585
- from code_puppy.state_management import set_message_history
586
-
587
- tokens = command.split()
588
- if len(tokens) != 2:
589
- emit_warning("Usage: /load_context <session_name>")
590
- return True
591
-
592
- session_name = tokens[1]
593
- contexts_dir = Path(CONFIG_DIR) / "contexts"
594
- pickle_file = contexts_dir / f"{session_name}.pkl"
595
-
596
- if not pickle_file.exists():
597
- emit_error(f"Context file not found: {pickle_file}")
598
- # List available contexts
599
- available = list(contexts_dir.glob("*.pkl"))
600
- if available:
601
- names = [f.stem for f in available]
602
- emit_info(f"Available contexts: {', '.join(names)}")
603
- return True
178
+ command = command.strip()
604
179
 
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.
227
+
228
+ # Try plugin-provided custom commands before unknown warning
229
+ if command.startswith("/"):
230
+ # Extract command name without leading slash and arguments intact
231
+ name = command[1:].split()[0] if len(command) > 1 else ""
605
232
  try:
606
- with open(pickle_file, "rb") as f:
607
- history = pickle.load(f)
608
-
609
- set_message_history(history)
610
- total_tokens = sum(estimate_tokens_for_message(m) for m in history)
611
-
612
- emit_success(
613
- f"✅ Context loaded: {len(history)} messages ({total_tokens} tokens)\n"
614
- f"📁 From: {pickle_file}"
615
- )
616
- return True
233
+ from code_puppy import callbacks
617
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
+
243
+ results = callbacks.on_custom_command(command=command, name=name)
244
+ # Iterate through callback results; treat str as handled (no model run)
245
+ for res in results:
246
+ if res is True:
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
253
+ if isinstance(res, str):
254
+ # Display returned text to the user and treat as handled
255
+ try:
256
+ emit_info(res)
257
+ except Exception:
258
+ pass
259
+ return True
618
260
  except Exception as e:
619
- emit_error(f"Failed to load context: {e}")
620
- return True
621
-
622
- if command.startswith("/truncate"):
623
- tokens = command.split()
624
- if len(tokens) != 2:
625
- emit_error("Usage: /truncate <N> (where N is the number of messages to keep)")
626
- return True
627
-
628
- try:
629
- n = int(tokens[1])
630
- if n < 1:
631
- emit_error("N must be a positive integer")
632
- return True
633
- except ValueError:
634
- emit_error("N must be a valid integer")
635
- return True
636
-
637
- from code_puppy.state_management import get_message_history, set_message_history
638
-
639
- history = get_message_history()
640
- if not history:
641
- emit_warning("No history to truncate yet. Ask me something first!")
642
- return True
643
-
644
- if len(history) <= n:
645
- emit_info(f"History already has {len(history)} messages, which is <= {n}. Nothing to truncate.")
646
- return True
647
-
648
- # Always keep the first message (system message) and then keep the N-1 most recent messages
649
- truncated_history = [history[0]] + history[-(n-1):] if n > 1 else [history[0]]
650
-
651
- set_message_history(truncated_history)
652
- emit_success(f"Truncated message history from {len(history)} to {len(truncated_history)} messages (keeping system message and {n-1} most recent)")
653
- return True
261
+ # Log via emit_error but do not block default handling
262
+ emit_warning(f"Custom command hook error: {e}")
654
263
 
655
- if command in ("/exit", "/quit"):
656
- emit_success("Goodbye!")
657
- # Signal to the main app that we want to exit
658
- # The actual exit handling is done in main.py
659
- return True
660
- if command.startswith("/"):
661
- name = command[1:].split()[0] if len(command) > 1 else ""
662
264
  if name:
663
265
  emit_warning(
664
- 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
+ )
665
269
  )
666
270
  else:
667
271
  # Show current model ONLY here
@@ -669,7 +273,9 @@ def handle_command(command: str):
669
273
 
670
274
  current_model = get_active_model()
671
275
  emit_info(
672
- 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
+ )
673
279
  )
674
280
  return True
675
281