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
@@ -0,0 +1,792 @@
1
+ """Command handlers for Code Puppy - CORE commands.
2
+
3
+ This module contains @register_command decorated handlers that are automatically
4
+ discovered by the command registry system.
5
+ """
6
+
7
+ import os
8
+
9
+ from code_puppy.command_line.agent_menu import interactive_agent_picker
10
+ from code_puppy.command_line.command_registry import register_command
11
+ from code_puppy.command_line.model_picker_completion import update_model_in_input
12
+ from code_puppy.command_line.motd import print_motd
13
+ from code_puppy.command_line.utils import make_directory_table
14
+ from code_puppy.config import finalize_autosave_session
15
+ from code_puppy.messaging import emit_error, emit_info
16
+ from code_puppy.tools.tools_content import tools_content
17
+
18
+
19
+ # Import get_commands_help from command_handler to avoid circular imports
20
+ # This will be defined in command_handler.py
21
+ def get_commands_help():
22
+ """Lazy import to avoid circular dependency."""
23
+ from code_puppy.command_line.command_handler import get_commands_help as _gch
24
+
25
+ return _gch()
26
+
27
+
28
+ @register_command(
29
+ name="help",
30
+ description="Show this help message",
31
+ usage="/help, /h",
32
+ aliases=["h"],
33
+ category="core",
34
+ )
35
+ def handle_help_command(command: str) -> bool:
36
+ """Show commands help."""
37
+ import uuid
38
+
39
+ from code_puppy.messaging import emit_info
40
+
41
+ group_id = str(uuid.uuid4())
42
+ help_text = get_commands_help()
43
+ emit_info(help_text, message_group_id=group_id)
44
+ return True
45
+
46
+
47
+ @register_command(
48
+ name="cd",
49
+ description="Change directory or show directories",
50
+ usage="/cd <dir>",
51
+ category="core",
52
+ )
53
+ def handle_cd_command(command: str) -> bool:
54
+ """Change directory or list current directory."""
55
+ # Use shlex.split to handle quoted paths properly
56
+ import shlex
57
+
58
+ from code_puppy.messaging import emit_error, emit_info, emit_success
59
+
60
+ try:
61
+ tokens = shlex.split(command)
62
+ except ValueError:
63
+ # Fallback to simple split if shlex fails
64
+ tokens = command.split()
65
+ if len(tokens) == 1:
66
+ try:
67
+ table = make_directory_table()
68
+ emit_info(table)
69
+ except Exception as e:
70
+ emit_error(f"Error listing directory: {e}")
71
+ return True
72
+ elif len(tokens) == 2:
73
+ dirname = tokens[1]
74
+ target = os.path.expanduser(dirname)
75
+ if not os.path.isabs(target):
76
+ target = os.path.join(os.getcwd(), target)
77
+ if os.path.isdir(target):
78
+ os.chdir(target)
79
+ emit_success(f"Changed directory to: {target}")
80
+ else:
81
+ emit_error(f"Not a directory: {dirname}")
82
+ return True
83
+ return True
84
+
85
+
86
+ @register_command(
87
+ name="tools",
88
+ description="Show available tools and capabilities",
89
+ usage="/tools",
90
+ category="core",
91
+ )
92
+ def handle_tools_command(command: str) -> bool:
93
+ """Display available tools."""
94
+ from rich.markdown import Markdown
95
+
96
+ from code_puppy.messaging import emit_info
97
+
98
+ markdown_content = Markdown(tools_content)
99
+ emit_info(markdown_content)
100
+ return True
101
+
102
+
103
+ @register_command(
104
+ name="motd",
105
+ description="Show the latest message of the day (MOTD)",
106
+ usage="/motd",
107
+ category="core",
108
+ )
109
+ def handle_motd_command(command: str) -> bool:
110
+ """Show message of the day."""
111
+ try:
112
+ print_motd(force=True)
113
+ except Exception:
114
+ # Handle printing errors gracefully
115
+ pass
116
+ return True
117
+
118
+
119
+ @register_command(
120
+ name="paste",
121
+ description="Paste image from clipboard (same as F3, or Ctrl+V with image)",
122
+ usage="/paste, /clipboard, /cb",
123
+ aliases=["clipboard", "cb"],
124
+ category="core",
125
+ )
126
+ def handle_paste_command(command: str) -> bool:
127
+ """Paste an image from the clipboard into the pending attachments."""
128
+ from code_puppy.command_line.clipboard import (
129
+ capture_clipboard_image_to_pending,
130
+ get_clipboard_manager,
131
+ has_image_in_clipboard,
132
+ )
133
+ from code_puppy.messaging import emit_info, emit_success, emit_warning
134
+
135
+ if not has_image_in_clipboard():
136
+ emit_warning("No image found in clipboard")
137
+ emit_info("Copy an image (screenshot, from browser, etc.) and try again")
138
+ return True
139
+
140
+ placeholder = capture_clipboard_image_to_pending()
141
+ if placeholder:
142
+ manager = get_clipboard_manager()
143
+ count = manager.get_pending_count()
144
+ emit_success(f"📋 {placeholder}")
145
+ emit_info(f"Total pending clipboard images: {count}")
146
+ emit_info("Type your prompt and press Enter to send with the image(s)")
147
+ else:
148
+ emit_warning("Failed to capture clipboard image")
149
+
150
+ return True
151
+
152
+
153
+ @register_command(
154
+ name="tutorial",
155
+ description="Run the interactive tutorial wizard",
156
+ usage="/tutorial",
157
+ category="core",
158
+ )
159
+ def handle_tutorial_command(command: str) -> bool:
160
+ """Run the interactive tutorial wizard.
161
+
162
+ Usage:
163
+ /tutorial - Run the tutorial (can be run anytime)
164
+ """
165
+ import asyncio
166
+ import concurrent.futures
167
+
168
+ from code_puppy.command_line.onboarding_wizard import (
169
+ reset_onboarding,
170
+ run_onboarding_wizard,
171
+ )
172
+ from code_puppy.config import set_model_name
173
+
174
+ # Always reset so user can re-run the tutorial anytime
175
+ reset_onboarding()
176
+
177
+ # Run the async wizard in a thread pool (same pattern as agent picker)
178
+ with concurrent.futures.ThreadPoolExecutor() as executor:
179
+ future = executor.submit(lambda: asyncio.run(run_onboarding_wizard()))
180
+ result = future.result(timeout=300) # 5 min timeout
181
+
182
+ if result == "chatgpt":
183
+ emit_info("🔐 Starting ChatGPT OAuth flow...")
184
+ from code_puppy.plugins.chatgpt_oauth.oauth_flow import run_oauth_flow
185
+
186
+ run_oauth_flow()
187
+ set_model_name("chatgpt-gpt-5.2-codex")
188
+ elif result == "claude":
189
+ emit_info("🔐 Starting Claude Code OAuth flow...")
190
+ from code_puppy.plugins.claude_code_oauth.register_callbacks import (
191
+ _perform_authentication,
192
+ )
193
+
194
+ _perform_authentication()
195
+ set_model_name("claude-code-claude-opus-4-5-20251101")
196
+ elif result == "completed":
197
+ emit_info("🎉 Tutorial complete! Happy coding!")
198
+ elif result == "skipped":
199
+ emit_info("⏭️ Tutorial skipped. Run /tutorial anytime!")
200
+
201
+ return True
202
+
203
+
204
+ @register_command(
205
+ name="exit",
206
+ description="Exit interactive mode",
207
+ usage="/exit, /quit",
208
+ aliases=["quit"],
209
+ category="core",
210
+ )
211
+ def handle_exit_command(command: str) -> bool:
212
+ """Exit the interactive session."""
213
+ from code_puppy.messaging import emit_success
214
+
215
+ try:
216
+ emit_success("Goodbye!")
217
+ except Exception:
218
+ # Handle emit errors gracefully
219
+ pass
220
+ # Signal to the main app that we want to exit
221
+ # The actual exit handling is done in main.py
222
+ return True
223
+
224
+
225
+ @register_command(
226
+ name="agent",
227
+ description="Switch to a different agent or show available agents",
228
+ usage="/agent <name>, /a <name>",
229
+ aliases=["a"],
230
+ category="core",
231
+ )
232
+ def handle_agent_command(command: str) -> bool:
233
+ """Handle agent switching."""
234
+ from rich.text import Text
235
+
236
+ from code_puppy.agents import (
237
+ get_agent_descriptions,
238
+ get_available_agents,
239
+ get_current_agent,
240
+ set_current_agent,
241
+ )
242
+ from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
243
+
244
+ tokens = command.split()
245
+
246
+ if len(tokens) == 1:
247
+ # Show interactive agent picker
248
+ try:
249
+ # Run the async picker using asyncio utilities
250
+ # Since we're called from an async context but this function is sync,
251
+ # we need to carefully schedule and wait for the coroutine
252
+ import asyncio
253
+ import concurrent.futures
254
+ import uuid
255
+
256
+ # Create a new event loop in a thread and run the picker there
257
+ with concurrent.futures.ThreadPoolExecutor() as executor:
258
+ future = executor.submit(
259
+ lambda: asyncio.run(interactive_agent_picker())
260
+ )
261
+ selected_agent = future.result(timeout=300) # 5 min timeout
262
+
263
+ if selected_agent:
264
+ current_agent = get_current_agent()
265
+ # Check if we're already using this agent
266
+ if current_agent.name == selected_agent:
267
+ group_id = str(uuid.uuid4())
268
+ emit_info(
269
+ f"Already using agent: {current_agent.display_name}",
270
+ message_group=group_id,
271
+ )
272
+ return True
273
+
274
+ # Switch to the new agent
275
+ group_id = str(uuid.uuid4())
276
+ new_session_id = finalize_autosave_session()
277
+ if not set_current_agent(selected_agent):
278
+ emit_warning(
279
+ "Agent switch failed after autosave rotation. Your context was preserved.",
280
+ message_group=group_id,
281
+ )
282
+ return True
283
+
284
+ new_agent = get_current_agent()
285
+ new_agent.reload_code_generation_agent()
286
+ emit_success(
287
+ f"Switched to agent: {new_agent.display_name}",
288
+ message_group=group_id,
289
+ )
290
+ emit_info(f"{new_agent.description}", message_group=group_id)
291
+ emit_info(
292
+ Text.from_markup(
293
+ f"[dim]Auto-save session rotated to: {new_session_id}[/dim]"
294
+ ),
295
+ message_group=group_id,
296
+ )
297
+ else:
298
+ emit_warning("Agent selection cancelled")
299
+ return True
300
+ except Exception as e:
301
+ # Fallback to old behavior if picker fails
302
+ import traceback
303
+ import uuid
304
+
305
+ emit_warning(f"Interactive picker failed: {e}")
306
+ emit_warning(f"Traceback: {traceback.format_exc()}")
307
+
308
+ # Show current agent and available agents
309
+ current_agent = get_current_agent()
310
+ available_agents = get_available_agents()
311
+ descriptions = get_agent_descriptions()
312
+
313
+ # Generate a group ID for all messages in this command
314
+ group_id = str(uuid.uuid4())
315
+
316
+ emit_info(
317
+ Text.from_markup(
318
+ f"[bold green]Current Agent:[/bold green] {current_agent.display_name}"
319
+ ),
320
+ message_group=group_id,
321
+ )
322
+ emit_info(
323
+ Text.from_markup(f"[dim]{current_agent.description}[/dim]\n"),
324
+ message_group=group_id,
325
+ )
326
+
327
+ emit_info(
328
+ Text.from_markup("[bold magenta]Available Agents:[/bold magenta]"),
329
+ message_group=group_id,
330
+ )
331
+ for name, display_name in available_agents.items():
332
+ description = descriptions.get(name, "No description")
333
+ current_marker = (
334
+ " [green]← current[/green]" if name == current_agent.name else ""
335
+ )
336
+ emit_info(
337
+ Text.from_markup(
338
+ f" [cyan]{name:<12}[/cyan] {display_name}{current_marker}"
339
+ ),
340
+ message_group=group_id,
341
+ )
342
+ emit_info(f" {description}", message_group=group_id)
343
+
344
+ emit_info(
345
+ Text.from_markup("\n[yellow]Usage:[/yellow] /agent <agent-name>"),
346
+ message_group=group_id,
347
+ )
348
+ return True
349
+
350
+ elif len(tokens) == 2:
351
+ agent_name = tokens[1].lower()
352
+
353
+ # Generate a group ID for all messages in this command
354
+ import uuid
355
+
356
+ group_id = str(uuid.uuid4())
357
+ available_agents = get_available_agents()
358
+
359
+ if agent_name not in 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
+
367
+ current_agent = get_current_agent()
368
+ if current_agent.name == agent_name:
369
+ emit_info(
370
+ f"Already using agent: {current_agent.display_name}",
371
+ message_group=group_id,
372
+ )
373
+ return True
374
+
375
+ new_session_id = finalize_autosave_session()
376
+ if not set_current_agent(agent_name):
377
+ emit_warning(
378
+ "Agent switch failed after autosave rotation. Your context was preserved.",
379
+ message_group=group_id,
380
+ )
381
+ return True
382
+
383
+ new_agent = get_current_agent()
384
+ new_agent.reload_code_generation_agent()
385
+ emit_success(
386
+ f"Switched to agent: {new_agent.display_name}",
387
+ message_group=group_id,
388
+ )
389
+ emit_info(f"{new_agent.description}", message_group=group_id)
390
+ emit_info(
391
+ Text.from_markup(
392
+ f"[dim]Auto-save session rotated to: {new_session_id}[/dim]"
393
+ ),
394
+ message_group=group_id,
395
+ )
396
+ return True
397
+ else:
398
+ emit_warning("Usage: /agent [agent-name]")
399
+ return True
400
+
401
+
402
+ async def interactive_model_picker() -> str | None:
403
+ """Show an interactive arrow-key selector to pick a model (async version).
404
+
405
+ Returns:
406
+ The selected model name, or None if cancelled
407
+ """
408
+ import sys
409
+ import time
410
+
411
+ from rich.console import Console
412
+ from rich.panel import Panel
413
+ from rich.text import Text
414
+
415
+ from code_puppy.command_line.model_picker_completion import (
416
+ get_active_model,
417
+ load_model_names,
418
+ )
419
+ from code_puppy.tools.command_runner import set_awaiting_user_input
420
+ from code_puppy.tools.common import arrow_select_async
421
+
422
+ # Load available models
423
+ model_names = load_model_names()
424
+ current_model = get_active_model()
425
+
426
+ # Build choices with current model indicator
427
+ choices = []
428
+ for model_name in model_names:
429
+ if model_name == current_model:
430
+ choices.append(f"✓ {model_name} (current)")
431
+ else:
432
+ choices.append(f" {model_name}")
433
+
434
+ # Create panel content
435
+ panel_content = Text()
436
+ panel_content.append("🤖 Select a model to use\n", style="bold cyan")
437
+ panel_content.append("Current model: ", style="dim")
438
+ panel_content.append(current_model, style="bold green")
439
+
440
+ # Display panel
441
+ panel = Panel(
442
+ panel_content,
443
+ title="[bold white]Model Selection[/bold white]",
444
+ border_style="cyan",
445
+ padding=(1, 2),
446
+ )
447
+
448
+ # Pause spinners BEFORE showing panel
449
+ set_awaiting_user_input(True)
450
+ time.sleep(0.3) # Let spinners fully stop
451
+
452
+ local_console = Console()
453
+ emit_info("")
454
+ local_console.print(panel)
455
+ emit_info("")
456
+
457
+ # Flush output before prompt_toolkit takes control
458
+ sys.stdout.flush()
459
+ sys.stderr.flush()
460
+ time.sleep(0.1)
461
+
462
+ selected_model = None
463
+
464
+ try:
465
+ # Final flush
466
+ sys.stdout.flush()
467
+
468
+ # Show arrow-key selector (async version)
469
+ choice = await arrow_select_async(
470
+ "💭 Which model would you like to use?",
471
+ choices,
472
+ )
473
+
474
+ # Extract model name from choice (remove prefix and suffix)
475
+ if choice:
476
+ # Remove the "✓ " or " " prefix and " (current)" suffix if present
477
+ selected_model = choice.strip().lstrip("✓").strip()
478
+ if selected_model.endswith(" (current)"):
479
+ selected_model = selected_model[:-10].strip()
480
+
481
+ except (KeyboardInterrupt, EOFError):
482
+ emit_error("Cancelled by user")
483
+ selected_model = None
484
+
485
+ finally:
486
+ set_awaiting_user_input(False)
487
+
488
+ return selected_model
489
+
490
+
491
+ @register_command(
492
+ name="model",
493
+ description="Set active model",
494
+ usage="/model, /m <model>",
495
+ aliases=["m"],
496
+ category="core",
497
+ )
498
+ def handle_model_command(command: str) -> bool:
499
+ """Set the active model."""
500
+ import asyncio
501
+
502
+ from code_puppy.command_line.model_picker_completion import (
503
+ get_active_model,
504
+ load_model_names,
505
+ set_active_model,
506
+ )
507
+ from code_puppy.messaging import emit_success, emit_warning
508
+
509
+ tokens = command.split()
510
+
511
+ # If just /model or /m with no args, show interactive picker
512
+ if len(tokens) == 1:
513
+ try:
514
+ # Run the async picker using asyncio utilities
515
+ # Since we're called from an async context but this function is sync,
516
+ # we need to carefully schedule and wait for the coroutine
517
+ import concurrent.futures
518
+
519
+ # Create a new event loop in a thread and run the picker there
520
+ with concurrent.futures.ThreadPoolExecutor() as executor:
521
+ future = executor.submit(
522
+ lambda: asyncio.run(interactive_model_picker())
523
+ )
524
+ selected_model = future.result(timeout=300) # 5 min timeout
525
+
526
+ if selected_model:
527
+ set_active_model(selected_model)
528
+ emit_success(f"Active model set and loaded: {selected_model}")
529
+ else:
530
+ emit_warning("Model selection cancelled")
531
+ return True
532
+ except Exception as e:
533
+ # Fallback to old behavior if picker fails
534
+ import traceback
535
+
536
+ emit_warning(f"Interactive picker failed: {e}")
537
+ emit_warning(f"Traceback: {traceback.format_exc()}")
538
+ model_names = load_model_names()
539
+ emit_warning("Usage: /model <model-name> or /m <model-name>")
540
+ emit_warning(f"Available models: {', '.join(model_names)}")
541
+ return True
542
+
543
+ # Handle both /model and /m for backward compatibility
544
+ model_command = command
545
+ if command.startswith("/model"):
546
+ # Convert /model to /m for internal processing
547
+ model_command = command.replace("/model", "/m", 1)
548
+
549
+ # If model matched, set it
550
+ new_input = update_model_in_input(model_command)
551
+ if new_input is not None:
552
+ model = get_active_model()
553
+ emit_success(f"Active model set and loaded: {model}")
554
+ return True
555
+
556
+ # If no model matched, show error
557
+ model_names = load_model_names()
558
+ emit_warning("Usage: /model <model-name> or /m <model-name>")
559
+ emit_warning(f"Available models: {', '.join(model_names)}")
560
+ return True
561
+
562
+
563
+ @register_command(
564
+ name="add_model",
565
+ description="Browse and add models from models.dev catalog",
566
+ usage="/add_model",
567
+ category="core",
568
+ )
569
+ def handle_add_model_command(command: str) -> bool:
570
+ """Launch interactive model browser TUI."""
571
+ from code_puppy.command_line.add_model_menu import interactive_model_picker
572
+ from code_puppy.tools.command_runner import set_awaiting_user_input
573
+
574
+ set_awaiting_user_input(True)
575
+ try:
576
+ # interactive_model_picker is now synchronous - no async complications!
577
+ result = interactive_model_picker()
578
+
579
+ if result:
580
+ emit_info("Successfully added model configuration")
581
+ return True
582
+ except KeyboardInterrupt:
583
+ # User cancelled - this is expected behavior
584
+ return True
585
+ except Exception as e:
586
+ emit_error(f"Failed to launch model browser: {e}")
587
+ return False
588
+ finally:
589
+ set_awaiting_user_input(False)
590
+
591
+
592
+ @register_command(
593
+ name="model_settings",
594
+ description="Configure per-model settings (temperature, seed, etc.)",
595
+ usage="/model_settings [--show [model_name]]",
596
+ aliases=["ms"],
597
+ category="config",
598
+ )
599
+ def handle_model_settings_command(command: str) -> bool:
600
+ """Launch interactive model settings TUI.
601
+
602
+ Opens a TUI showing all available models. Select a model to configure
603
+ its settings (temperature, seed, etc.). ESC closes the TUI.
604
+
605
+ Use --show [model_name] to display current settings without the TUI.
606
+ """
607
+ from code_puppy.command_line.model_settings_menu import (
608
+ interactive_model_settings,
609
+ show_model_settings_summary,
610
+ )
611
+ from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
612
+ from code_puppy.tools.command_runner import set_awaiting_user_input
613
+
614
+ tokens = command.split()
615
+
616
+ # Check for --show flag to just display current settings
617
+ if "--show" in tokens:
618
+ model_name = None
619
+ for t in tokens[1:]:
620
+ if not t.startswith("--"):
621
+ model_name = t
622
+ break
623
+ show_model_settings_summary(model_name)
624
+ return True
625
+
626
+ set_awaiting_user_input(True)
627
+ try:
628
+ result = interactive_model_settings()
629
+
630
+ if result:
631
+ emit_success("Model settings updated successfully")
632
+
633
+ # Always reload the active agent so settings take effect
634
+ from code_puppy.agents import get_current_agent
635
+
636
+ try:
637
+ current_agent = get_current_agent()
638
+ current_agent.reload_code_generation_agent()
639
+ emit_info("Active agent reloaded")
640
+ except Exception as reload_error:
641
+ emit_warning(f"Agent reload failed: {reload_error}")
642
+
643
+ return True
644
+ except KeyboardInterrupt:
645
+ return True
646
+ except Exception as e:
647
+ emit_error(f"Failed to launch model settings: {e}")
648
+ return False
649
+ finally:
650
+ set_awaiting_user_input(False)
651
+
652
+
653
+ @register_command(
654
+ name="mcp",
655
+ description="Manage MCP servers (list, start, stop, status, etc.)",
656
+ usage="/mcp",
657
+ category="core",
658
+ )
659
+ def handle_mcp_command(command: str) -> bool:
660
+ """Handle MCP server management."""
661
+ from code_puppy.command_line.mcp import MCPCommandHandler
662
+
663
+ handler = MCPCommandHandler()
664
+ return handler.handle_mcp_command(command)
665
+
666
+
667
+ @register_command(
668
+ name="api",
669
+ description="Manage the Code Puppy API server",
670
+ usage="/api [start|stop|status]",
671
+ category="core",
672
+ detailed_help="Start, stop, or check status of the local FastAPI server for GUI integration.",
673
+ )
674
+ def handle_api_command(command: str) -> bool:
675
+ """Handle the /api command."""
676
+ import os
677
+ import signal
678
+ import subprocess
679
+ import sys
680
+ from pathlib import Path
681
+
682
+ from code_puppy.config import STATE_DIR
683
+ from code_puppy.messaging import emit_error, emit_info, emit_success
684
+
685
+ parts = command.split()
686
+ subcommand = parts[1] if len(parts) > 1 else "status"
687
+
688
+ pid_file = Path(STATE_DIR) / "api_server.pid"
689
+
690
+ if subcommand == "start":
691
+ # Check if already running
692
+ if pid_file.exists():
693
+ try:
694
+ pid = int(pid_file.read_text().strip())
695
+ os.kill(pid, 0) # Check if process exists
696
+ emit_info(f"API server already running (PID {pid})")
697
+ return True
698
+ except (OSError, ValueError):
699
+ pid_file.unlink(missing_ok=True) # Stale PID file
700
+
701
+ # Start the server in background
702
+ emit_info("Starting API server on http://127.0.0.1:8765 ...")
703
+ proc = subprocess.Popen(
704
+ [sys.executable, "-m", "code_puppy.api.main"],
705
+ stdout=subprocess.DEVNULL,
706
+ stderr=subprocess.DEVNULL,
707
+ start_new_session=True,
708
+ )
709
+ pid_file.parent.mkdir(parents=True, exist_ok=True)
710
+ pid_file.write_text(str(proc.pid))
711
+ emit_success(f"API server started (PID {proc.pid})")
712
+ emit_info("Docs available at http://127.0.0.1:8765/docs")
713
+ return True
714
+
715
+ elif subcommand == "stop":
716
+ if not pid_file.exists():
717
+ emit_info("API server is not running")
718
+ return True
719
+
720
+ try:
721
+ pid = int(pid_file.read_text().strip())
722
+ os.kill(pid, signal.SIGTERM)
723
+ pid_file.unlink()
724
+ emit_success(f"API server stopped (PID {pid})")
725
+ except (OSError, ValueError) as e:
726
+ pid_file.unlink(missing_ok=True)
727
+ emit_error(f"Error stopping server: {e}")
728
+ return True
729
+
730
+ elif subcommand == "status":
731
+ if not pid_file.exists():
732
+ emit_info("API server is not running")
733
+ return True
734
+
735
+ try:
736
+ pid = int(pid_file.read_text().strip())
737
+ os.kill(pid, 0) # Check if process exists
738
+ emit_success(f"API server is running (PID {pid})")
739
+ emit_info("URL: http://127.0.0.1:8765")
740
+ emit_info("Docs: http://127.0.0.1:8765/docs")
741
+ except (OSError, ValueError):
742
+ pid_file.unlink(missing_ok=True)
743
+ emit_info("API server is not running (stale PID file removed)")
744
+ return True
745
+
746
+ else:
747
+ emit_error(f"Unknown subcommand: {subcommand}")
748
+ emit_info("Usage: /api [start|stop|status]")
749
+ return True
750
+
751
+
752
+ @register_command(
753
+ name="generate-pr-description",
754
+ description="Generate comprehensive PR description",
755
+ usage="/generate-pr-description [@dir]",
756
+ category="core",
757
+ )
758
+ def handle_generate_pr_description_command(command: str) -> str:
759
+ """Generate a PR description."""
760
+ # Parse directory argument (e.g., /generate-pr-description @some/dir)
761
+ tokens = command.split()
762
+ directory_context = ""
763
+ for t in tokens:
764
+ if t.startswith("@"):
765
+ directory_context = f" Please work in the directory: {t[1:]}"
766
+ break
767
+
768
+ # Hard-coded prompt from user requirements
769
+ pr_prompt = f"""Generate a comprehensive PR description for my current branch changes. Follow these steps:
770
+
771
+ 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.
772
+ 2 Analyze the code: Read and analyze all modified files to understand:
773
+ • What functionality was added/changed/removed
774
+ • The technical approach and implementation details
775
+ • Any architectural or design pattern changes
776
+ • Dependencies added/removed/updated
777
+ 3 Generate a structured PR description with these sections:
778
+ • Title: Concise, descriptive title (50 chars max)
779
+ • Summary: Brief overview of what this PR accomplishes
780
+ • Changes Made: Detailed bullet points of specific changes
781
+ • Technical Details: Implementation approach, design decisions, patterns used
782
+ • Files Modified: List of key files with brief description of changes
783
+ • Testing: What was tested and how (if applicable)
784
+ • Breaking Changes: Any breaking changes (if applicable)
785
+ • Additional Notes: Any other relevant information
786
+ 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
787
+ description field. Use proper markdown syntax with headers, bullet points, code blocks, and formatting.
788
+ 5 Make it review-ready: Ensure the description helps reviewers understand the context, approach, and impact of the changes.
789
+ 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}"""
790
+
791
+ # Return the prompt to be processed by the main chat system
792
+ return pr_prompt