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
@@ -5,7 +5,9 @@ MCP Stop Command - Stops a specific MCP server.
5
5
  import logging
6
6
  from typing import List, Optional
7
7
 
8
- from code_puppy.messaging import emit_info
8
+ from rich.text import Text
9
+
10
+ from code_puppy.messaging import emit_error, emit_info
9
11
 
10
12
  from ...agents import get_current_agent
11
13
  from .base import MCPCommandBase
@@ -35,7 +37,7 @@ class StopCommand(MCPCommandBase):
35
37
 
36
38
  if not args:
37
39
  emit_info(
38
- "[yellow]Usage: /mcp stop <server_name>[/yellow]",
40
+ Text.from_markup("[yellow]Usage: /mcp stop <server_name>[/yellow]"),
39
41
  message_group=group_id,
40
42
  )
41
43
  return
@@ -60,8 +62,10 @@ class StopCommand(MCPCommandBase):
60
62
  try:
61
63
  agent = get_current_agent()
62
64
  agent.reload_code_generation_agent()
65
+ # Update MCP tool cache immediately so token counts reflect the change
66
+ agent.update_mcp_tool_cache_sync()
63
67
  emit_info(
64
- "[dim]Agent reloaded with updated servers[/dim]",
68
+ "Agent reloaded with updated servers",
65
69
  message_group=group_id,
66
70
  )
67
71
  except Exception as e:
@@ -73,4 +77,4 @@ class StopCommand(MCPCommandBase):
73
77
 
74
78
  except Exception as e:
75
79
  logger.error(f"Error stopping server '{server_name}': {e}")
76
- emit_info(f"[red]Failed to stop server: {e}[/red]", message_group=group_id)
80
+ emit_error(f"Failed to stop server: {e}", message_group=group_id)
@@ -5,7 +5,7 @@ MCP Test Command - Tests connectivity to a specific MCP server.
5
5
  import logging
6
6
  from typing import List, Optional
7
7
 
8
- from code_puppy.messaging import emit_info
8
+ from code_puppy.messaging import emit_error, emit_info
9
9
 
10
10
  from .base import MCPCommandBase
11
11
  from .utils import find_server_id_by_name, suggest_similar_servers
@@ -104,4 +104,4 @@ class TestCommand(MCPCommandBase):
104
104
 
105
105
  except Exception as e:
106
106
  logger.error(f"Error testing server '{server_name}': {e}")
107
- emit_info(f"[red]Error testing server: {e}[/red]", message_group=group_id)
107
+ emit_error(f"Error testing server: {e}", message_group=group_id)
@@ -7,7 +7,9 @@ Provides interactive functionality for installing and configuring MCP servers.
7
7
  import logging
8
8
  from typing import Any, Dict, Optional
9
9
 
10
- from code_puppy.messaging import emit_info, emit_prompt
10
+ from rich.text import Text
11
+
12
+ from code_puppy.messaging import emit_error, emit_info, emit_prompt
11
13
 
12
14
  # Configure logging
13
15
  logger = logging.getLogger(__name__)
@@ -51,7 +53,7 @@ def run_interactive_install_wizard(manager, group_id: str) -> bool:
51
53
  required_env_vars = selected_server.get_environment_vars()
52
54
  if required_env_vars:
53
55
  emit_info(
54
- "\n[yellow]Required Environment Variables:[/yellow]",
56
+ Text.from_markup("\n[yellow]Required Environment Variables:[/yellow]"),
55
57
  message_group=group_id,
56
58
  )
57
59
  for var in required_env_vars:
@@ -61,7 +63,8 @@ def run_interactive_install_wizard(manager, group_id: str) -> bool:
61
63
  current_value = os.environ.get(var, "")
62
64
  if current_value:
63
65
  emit_info(
64
- f" {var}: [green]Already set[/green]", message_group=group_id
66
+ Text.from_markup(f" {var}: [green]Already set[/green]"),
67
+ message_group=group_id,
65
68
  )
66
69
  env_vars[var] = current_value
67
70
  else:
@@ -73,7 +76,8 @@ def run_interactive_install_wizard(manager, group_id: str) -> bool:
73
76
  required_cmd_args = selected_server.get_command_line_args()
74
77
  if required_cmd_args:
75
78
  emit_info(
76
- "\n[yellow]Command Line Arguments:[/yellow]", message_group=group_id
79
+ Text.from_markup("\n[yellow]Command Line Arguments:[/yellow]"),
80
+ message_group=group_id,
77
81
  )
78
82
  for arg_config in required_cmd_args:
79
83
  name = arg_config.get("name", "")
@@ -101,11 +105,11 @@ def run_interactive_install_wizard(manager, group_id: str) -> bool:
101
105
  )
102
106
 
103
107
  except ImportError:
104
- emit_info("[red]Server catalog not available[/red]", message_group=group_id)
108
+ emit_error("Server catalog not available", message_group=group_id)
105
109
  return False
106
110
  except Exception as e:
107
111
  logger.error(f"Error in interactive wizard: {e}")
108
- emit_info(f"[red]Wizard error: {e}[/red]", message_group=group_id)
112
+ emit_error(f"Wizard error: {e}", message_group=group_id)
109
113
  return False
110
114
 
111
115
 
@@ -122,9 +126,7 @@ def interactive_server_selection(group_id: str):
122
126
 
123
127
  servers = catalog.get_popular(10)
124
128
  if not servers:
125
- emit_info(
126
- "[red]No servers available in catalog[/red]", message_group=group_id
127
- )
129
+ emit_info("No servers available in catalog", message_group=group_id)
128
130
  return None
129
131
 
130
132
  emit_info("Popular MCP Servers:", message_group=group_id)
@@ -156,10 +158,10 @@ def interactive_server_selection(group_id: str):
156
158
  if 0 <= index < len(servers):
157
159
  return servers[index]
158
160
  else:
159
- emit_info("[red]Invalid selection[/red]", message_group=group_id)
161
+ emit_error("Invalid selection", message_group=group_id)
160
162
  return None
161
163
  except ValueError:
162
- emit_info("[red]Invalid input[/red]", message_group=group_id)
164
+ emit_error("Invalid input", message_group=group_id)
163
165
  return None
164
166
 
165
167
  except Exception as e:
@@ -215,7 +217,7 @@ def interactive_configure_server(
215
217
  if env_vars:
216
218
  emit_info("Environment Variables:", message_group=group_id)
217
219
  for var, value in env_vars.items():
218
- emit_info(f" {var}: [hidden]{value}[/hidden]", message_group=group_id)
220
+ emit_info(f" {var}: ***", message_group=group_id)
219
221
 
220
222
  if cmd_args:
221
223
  emit_info("Command Line Arguments:", message_group=group_id)
@@ -234,7 +236,7 @@ def interactive_configure_server(
234
236
 
235
237
  except Exception as e:
236
238
  logger.error(f"Error configuring server: {e}")
237
- emit_info(f"[red]Configuration error: {e}[/red]", message_group=group_id)
239
+ emit_error(f"Configuration error: {e}", message_group=group_id)
238
240
  return False
239
241
 
240
242
 
@@ -288,7 +290,7 @@ def install_server_from_catalog(
288
290
 
289
291
  if not server_id:
290
292
  emit_info(
291
- "[red]Failed to register server with manager[/red]",
293
+ "Failed to register server with manager",
292
294
  message_group=group_id,
293
295
  )
294
296
  return False
@@ -314,7 +316,9 @@ def install_server_from_catalog(
314
316
  json.dump(data, f, indent=2)
315
317
 
316
318
  emit_info(
317
- f"[green]✓ Successfully installed server: {server_name}[/green]",
319
+ Text.from_markup(
320
+ f"[green]✓ Successfully installed server: {server_name}[/green]"
321
+ ),
318
322
  message_group=group_id,
319
323
  )
320
324
  emit_info(
@@ -326,5 +330,5 @@ def install_server_from_catalog(
326
330
 
327
331
  except Exception as e:
328
332
  logger.error(f"Error installing server: {e}")
329
- emit_info(f"[red]Installation failed: {e}[/red]", message_group=group_id)
333
+ emit_error(f"Installation failed: {e}", message_group=group_id)
330
334
  return False
@@ -0,0 +1,174 @@
1
+ import logging
2
+ from typing import Iterable
3
+
4
+ from prompt_toolkit.completion import Completer, Completion
5
+ from prompt_toolkit.document import Document
6
+
7
+ # Configure logging
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ def load_server_names():
12
+ """Load server names from the MCP manager."""
13
+ try:
14
+ from code_puppy.mcp_.manager import MCPManager
15
+
16
+ manager = MCPManager()
17
+ servers = manager.list_servers()
18
+ return [server.name for server in servers]
19
+ except Exception as e:
20
+ logger.debug(f"Could not load server names: {e}")
21
+ return []
22
+
23
+
24
+ class MCPCompleter(Completer):
25
+ """
26
+ A completer that triggers on '/mcp' to show available MCP subcommands
27
+ and server names where appropriate.
28
+ """
29
+
30
+ def __init__(self, trigger: str = "/mcp"):
31
+ self.trigger = trigger
32
+
33
+ # Define all available MCP subcommands
34
+ # Subcommands that take server names as arguments
35
+ self.server_subcommands = {
36
+ "start": "Start a specific MCP server",
37
+ "stop": "Stop a specific MCP server",
38
+ "restart": "Restart a specific MCP server",
39
+ "status": "Show status of a specific MCP server",
40
+ "logs": "Show logs for a specific MCP server",
41
+ "edit": "Edit an existing MCP server config",
42
+ "remove": "Remove an MCP server",
43
+ }
44
+
45
+ # Subcommands that don't take server names
46
+ self.general_subcommands = {
47
+ "list": "List all registered MCP servers",
48
+ "start-all": "Start all MCP servers",
49
+ "stop-all": "Stop all MCP servers",
50
+ "test": "Test MCP server connection",
51
+ "add": "Add a new MCP server",
52
+ "install": "Install MCP servers from a list",
53
+ "search": "Search for available MCP servers",
54
+ "help": "Show help for MCP commands",
55
+ }
56
+
57
+ # All subcommands combined for completion when no subcommand is typed yet
58
+ self.all_subcommands = {**self.server_subcommands, **self.general_subcommands}
59
+
60
+ # Cache server names to avoid repeated lookups
61
+ self._server_names_cache = None
62
+ self._cache_timestamp = None
63
+
64
+ def _get_server_names(self):
65
+ """Get server names with caching."""
66
+ import time
67
+
68
+ # Cache for 30 seconds to avoid repeated manager calls
69
+ current_time = time.time()
70
+ if (
71
+ self._server_names_cache is None
72
+ or self._cache_timestamp is None
73
+ or current_time - self._cache_timestamp > 30
74
+ ):
75
+ self._server_names_cache = load_server_names()
76
+ self._cache_timestamp = current_time
77
+
78
+ return self._server_names_cache or []
79
+
80
+ def get_completions(
81
+ self, document: Document, complete_event
82
+ ) -> Iterable[Completion]:
83
+ text = document.text
84
+ cursor_position = document.cursor_position
85
+ text_before_cursor = text[:cursor_position]
86
+
87
+ # Only trigger if /mcp is at the very beginning of the line
88
+ stripped_text = text_before_cursor.lstrip()
89
+ if not stripped_text.startswith(self.trigger):
90
+ return
91
+
92
+ # Find where /mcp actually starts (after any leading whitespace)
93
+ mcp_pos = text_before_cursor.find(self.trigger)
94
+ mcp_end = mcp_pos + len(self.trigger)
95
+
96
+ # Require a space after /mcp before showing completions
97
+ if mcp_end >= len(text_before_cursor) or text_before_cursor[mcp_end] != " ":
98
+ return
99
+
100
+ # Extract everything after /mcp (and after the space)
101
+ after_mcp = text_before_cursor[mcp_end + 1 :].strip()
102
+
103
+ # If nothing after /mcp, show all available subcommands
104
+ if not after_mcp:
105
+ for subcommand, description in sorted(self.all_subcommands.items()):
106
+ yield Completion(
107
+ subcommand,
108
+ start_position=0,
109
+ display=subcommand,
110
+ display_meta=description,
111
+ )
112
+ return
113
+
114
+ # Parse what's been typed after /mcp
115
+ # Split by space but be careful with what we're currently typing
116
+ parts = after_mcp.split()
117
+
118
+ # Priority: Check for server name completion first when appropriate
119
+ # This handles cases like '/mcp start ' where the space indicates ready for server name
120
+ if len(parts) >= 1:
121
+ subcommand = parts[0].lower()
122
+
123
+ # Only complete server names for specific subcommands
124
+ if subcommand in self.server_subcommands:
125
+ # Case 1: Exactly the subcommand followed by a space (ready for server name)
126
+ if len(parts) == 1 and text.endswith(" "):
127
+ partial_server = ""
128
+ start_position = 0
129
+
130
+ server_names = self._get_server_names()
131
+ for server_name in sorted(server_names):
132
+ yield Completion(
133
+ server_name,
134
+ start_position=start_position,
135
+ display=server_name,
136
+ display_meta="MCP Server",
137
+ )
138
+ return
139
+
140
+ # Case 2: Subcommand + partial server name (require space after subcommand)
141
+ elif len(parts) == 2 and cursor_position > (
142
+ mcp_end + 1 + len(subcommand) + 1
143
+ ):
144
+ partial_server = parts[1]
145
+ start_position = -(len(partial_server))
146
+
147
+ server_names = self._get_server_names()
148
+ for server_name in sorted(server_names):
149
+ if server_name.lower().startswith(partial_server.lower()):
150
+ yield Completion(
151
+ server_name,
152
+ start_position=start_position,
153
+ display=server_name,
154
+ display_meta="MCP Server",
155
+ )
156
+ return
157
+
158
+ # If we only have one part and haven't returned above, show subcommand completions
159
+ # This includes cases like '/mcp start' where they might want 'start-all'
160
+ # But NOT when there's a space after the subcommand (which indicates they want arguments)
161
+ if len(parts) == 1 and not text.endswith(" "):
162
+ partial_subcommand = parts[0]
163
+ for subcommand, description in sorted(self.all_subcommands.items()):
164
+ if subcommand.startswith(partial_subcommand):
165
+ yield Completion(
166
+ subcommand,
167
+ start_position=-(len(partial_subcommand)),
168
+ display=subcommand,
169
+ display_meta=description,
170
+ )
171
+ return
172
+
173
+ # For general subcommands, we don't provide argument completion
174
+ # They may have their own specific completions in the future
@@ -28,6 +28,8 @@ def set_active_model(model_name: str):
28
28
  """
29
29
  Sets the active model name by updating the config (for persistence).
30
30
  """
31
+ from code_puppy.messaging import emit_info, emit_warning
32
+
31
33
  set_model_name(model_name)
32
34
  # Reload the currently active agent so the new model takes effect immediately
33
35
  try:
@@ -42,9 +44,9 @@ def set_active_model(model_name: str):
42
44
  # Non-fatal, continue to reload
43
45
  ...
44
46
  current_agent.reload_code_generation_agent()
45
- except Exception:
46
- # Swallow errors to avoid breaking the prompt flow; model persists for next run
47
- pass
47
+ emit_info("Active agent reloaded")
48
+ except Exception as e:
49
+ emit_warning(f"Model changed but agent reload failed: {e}")
48
50
 
49
51
 
50
52
  class ModelNameCompleter(Completer):
@@ -63,13 +65,31 @@ class ModelNameCompleter(Completer):
63
65
  text = document.text
64
66
  cursor_position = document.cursor_position
65
67
  text_before_cursor = text[:cursor_position]
66
- if self.trigger not in text_before_cursor:
68
+
69
+ # Only trigger if /model is at the very beginning of the line and has a space after it
70
+ stripped_text = text_before_cursor.lstrip()
71
+ if not stripped_text.startswith(self.trigger + " "):
67
72
  return
68
- symbol_pos = text_before_cursor.rfind(self.trigger)
69
- text_after_trigger = text_before_cursor[symbol_pos + len(self.trigger) :]
73
+
74
+ # Find where /model actually starts (after any leading whitespace)
75
+ symbol_pos = text_before_cursor.find(self.trigger)
76
+ text_after_trigger = text_before_cursor[
77
+ symbol_pos + len(self.trigger) + 1 :
78
+ ].lstrip()
70
79
  start_position = -(len(text_after_trigger))
80
+
81
+ # Filter model names based on what's typed after /model (case-insensitive)
71
82
  for model_name in self.model_names:
72
- meta = "Model (selected)" if model_name == get_active_model() else "Model"
83
+ if text_after_trigger and not model_name.lower().startswith(
84
+ text_after_trigger.lower()
85
+ ):
86
+ continue # Skip models that don't match the typed text
87
+
88
+ meta = (
89
+ "Model (selected)"
90
+ if model_name.lower() == get_active_model().lower()
91
+ else "Model"
92
+ )
73
93
  yield Completion(
74
94
  model_name,
75
95
  start_position=start_position,
@@ -81,32 +101,62 @@ class ModelNameCompleter(Completer):
81
101
  def update_model_in_input(text: str) -> Optional[str]:
82
102
  # If input starts with /model or /m and a model name, set model and strip it out
83
103
  content = text.strip()
84
-
85
- # Check for /model command
86
- if content.startswith("/model"):
87
- rest = content[6:].strip() # Remove '/model'
88
- for model in load_model_names():
89
- if rest == model:
104
+ model_names = load_model_names()
105
+
106
+ # Check for /model command (require space after /model, case-insensitive)
107
+ if content.lower().startswith("/model "):
108
+ # Find the actual /model command (case-insensitive)
109
+ model_cmd = content.split(" ", 1)[0] # Get the command part
110
+ rest = content[len(model_cmd) :].strip() # Remove the actual command
111
+
112
+ # Look for a model name at the start of rest (case-insensitive)
113
+ for model in model_names:
114
+ if rest.lower().startswith(model.lower()):
115
+ # Found a matching model - now extract it properly
90
116
  set_active_model(model)
91
- # Remove /model from the input
92
- idx = text.find("/model" + model)
117
+
118
+ # Find the actual model name in the original text (preserving case)
119
+ # We need to find where the model ends in the original rest string
120
+ model_end_idx = len(model)
121
+
122
+ # Build the full command+model part to remove
123
+ cmd_and_model_pattern = model_cmd + " " + rest[:model_end_idx]
124
+ idx = text.find(cmd_and_model_pattern)
93
125
  if idx != -1:
94
126
  new_text = (
95
- text[:idx] + text[idx + len("/model" + model) :]
127
+ text[:idx] + text[idx + len(cmd_and_model_pattern) :]
96
128
  ).strip()
97
129
  return new_text
98
-
99
- # Check for /m command
100
- elif content.startswith("/m "):
101
- rest = content[3:].strip() # Remove '/m '
102
- for model in load_model_names():
103
- if rest == model:
130
+ return None
131
+
132
+ # Check for /m command (case-insensitive)
133
+ elif content.lower().startswith("/m ") and not content.lower().startswith(
134
+ "/model "
135
+ ):
136
+ # Find the actual /m command (case-insensitive)
137
+ m_cmd = content.split(" ", 1)[0] # Get the command part
138
+ rest = content[len(m_cmd) :].strip() # Remove the actual command
139
+
140
+ # Look for a model name at the start of rest (case-insensitive)
141
+ for model in model_names:
142
+ if rest.lower().startswith(model.lower()):
143
+ # Found a matching model - now extract it properly
104
144
  set_active_model(model)
105
- # Remove /m from the input
106
- idx = text.find("/m " + model)
145
+
146
+ # Find the actual model name in the original text (preserving case)
147
+ # We need to find where the model ends in the original rest string
148
+ model_end_idx = len(model)
149
+
150
+ # Build the full command+model part to remove
151
+ # Handle space variations in the original text
152
+ cmd_and_model_pattern = m_cmd + " " + rest[:model_end_idx]
153
+ idx = text.find(cmd_and_model_pattern)
107
154
  if idx != -1:
108
- new_text = (text[:idx] + text[idx + len("/m " + model) :]).strip()
155
+ new_text = (
156
+ text[:idx] + text[idx + len(cmd_and_model_pattern) :]
157
+ ).strip()
109
158
  return new_text
159
+ return None
110
160
 
111
161
  return None
112
162