code-puppy 0.0.370__tar.gz → 0.0.372__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. {code_puppy-0.0.370 → code_puppy-0.0.372}/PKG-INFO +1 -1
  2. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/agents/__init__.py +6 -0
  3. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/agents/agent_manager.py +205 -1
  4. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/command_line/add_model_menu.py +13 -4
  5. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/command_line/agent_menu.py +281 -14
  6. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/command_line/core_commands.py +3 -3
  7. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/command_line/model_picker_completion.py +3 -20
  8. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/command_line/model_settings_menu.py +15 -3
  9. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/config.py +44 -8
  10. code_puppy-0.0.372/code_puppy/model_switching.py +63 -0
  11. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/models.json +12 -12
  12. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/plugins/antigravity_oauth/register_callbacks.py +15 -8
  13. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/plugins/chatgpt_oauth/register_callbacks.py +2 -2
  14. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/plugins/claude_code_oauth/register_callbacks.py +2 -30
  15. {code_puppy-0.0.370 → code_puppy-0.0.372}/pyproject.toml +1 -1
  16. {code_puppy-0.0.370 → code_puppy-0.0.372}/.gitignore +0 -0
  17. {code_puppy-0.0.370 → code_puppy-0.0.372}/LICENSE +0 -0
  18. {code_puppy-0.0.370 → code_puppy-0.0.372}/README.md +0 -0
  19. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/__init__.py +0 -0
  20. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/__main__.py +0 -0
  21. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/agents/agent_c_reviewer.py +0 -0
  22. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/agents/agent_code_puppy.py +0 -0
  23. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/agents/agent_code_reviewer.py +0 -0
  24. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/agents/agent_cpp_reviewer.py +0 -0
  25. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/agents/agent_creator_agent.py +0 -0
  26. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/agents/agent_golang_reviewer.py +0 -0
  27. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/agents/agent_javascript_reviewer.py +0 -0
  28. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/agents/agent_pack_leader.py +0 -0
  29. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/agents/agent_planning.py +0 -0
  30. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/agents/agent_python_programmer.py +0 -0
  31. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/agents/agent_python_reviewer.py +0 -0
  32. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/agents/agent_qa_expert.py +0 -0
  33. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/agents/agent_qa_kitten.py +0 -0
  34. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/agents/agent_security_auditor.py +0 -0
  35. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/agents/agent_terminal_qa.py +0 -0
  36. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/agents/agent_typescript_reviewer.py +0 -0
  37. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/agents/base_agent.py +0 -0
  38. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/agents/event_stream_handler.py +0 -0
  39. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/agents/json_agent.py +0 -0
  40. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/agents/pack/__init__.py +0 -0
  41. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/agents/pack/bloodhound.py +0 -0
  42. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/agents/pack/husky.py +0 -0
  43. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/agents/pack/retriever.py +0 -0
  44. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/agents/pack/shepherd.py +0 -0
  45. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/agents/pack/terrier.py +0 -0
  46. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/agents/pack/watchdog.py +0 -0
  47. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/agents/prompt_reviewer.py +0 -0
  48. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/agents/subagent_stream_handler.py +0 -0
  49. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/api/__init__.py +0 -0
  50. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/api/app.py +0 -0
  51. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/api/main.py +0 -0
  52. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/api/pty_manager.py +0 -0
  53. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/api/routers/__init__.py +0 -0
  54. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/api/routers/agents.py +0 -0
  55. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/api/routers/commands.py +0 -0
  56. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/api/routers/config.py +0 -0
  57. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/api/routers/sessions.py +0 -0
  58. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/api/templates/terminal.html +0 -0
  59. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/api/websocket.py +0 -0
  60. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/callbacks.py +0 -0
  61. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/chatgpt_codex_client.py +0 -0
  62. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/claude_cache_client.py +0 -0
  63. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/cli_runner.py +0 -0
  64. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/command_line/__init__.py +0 -0
  65. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/command_line/attachments.py +0 -0
  66. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/command_line/autosave_menu.py +0 -0
  67. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/command_line/clipboard.py +0 -0
  68. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/command_line/colors_menu.py +0 -0
  69. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/command_line/command_handler.py +0 -0
  70. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/command_line/command_registry.py +0 -0
  71. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/command_line/config_commands.py +0 -0
  72. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/command_line/diff_menu.py +0 -0
  73. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/command_line/file_path_completion.py +0 -0
  74. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/command_line/load_context_completion.py +0 -0
  75. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/command_line/mcp/__init__.py +0 -0
  76. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/command_line/mcp/base.py +0 -0
  77. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/command_line/mcp/catalog_server_installer.py +0 -0
  78. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/command_line/mcp/custom_server_form.py +0 -0
  79. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/command_line/mcp/custom_server_installer.py +0 -0
  80. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/command_line/mcp/edit_command.py +0 -0
  81. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/command_line/mcp/handler.py +0 -0
  82. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/command_line/mcp/help_command.py +0 -0
  83. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/command_line/mcp/install_command.py +0 -0
  84. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/command_line/mcp/install_menu.py +0 -0
  85. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/command_line/mcp/list_command.py +0 -0
  86. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/command_line/mcp/logs_command.py +0 -0
  87. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/command_line/mcp/remove_command.py +0 -0
  88. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/command_line/mcp/restart_command.py +0 -0
  89. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/command_line/mcp/search_command.py +0 -0
  90. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/command_line/mcp/start_all_command.py +0 -0
  91. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/command_line/mcp/start_command.py +0 -0
  92. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/command_line/mcp/status_command.py +0 -0
  93. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/command_line/mcp/stop_all_command.py +0 -0
  94. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/command_line/mcp/stop_command.py +0 -0
  95. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/command_line/mcp/test_command.py +0 -0
  96. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/command_line/mcp/utils.py +0 -0
  97. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/command_line/mcp/wizard_utils.py +0 -0
  98. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/command_line/mcp_completion.py +0 -0
  99. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/command_line/motd.py +0 -0
  100. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/command_line/onboarding_slides.py +0 -0
  101. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/command_line/onboarding_wizard.py +0 -0
  102. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/command_line/pin_command_completion.py +0 -0
  103. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/command_line/prompt_toolkit_completion.py +0 -0
  104. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/command_line/session_commands.py +0 -0
  105. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/command_line/utils.py +0 -0
  106. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/error_logging.py +0 -0
  107. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/gemini_code_assist.py +0 -0
  108. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/gemini_model.py +0 -0
  109. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/http_utils.py +0 -0
  110. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/keymap.py +0 -0
  111. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/main.py +0 -0
  112. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/mcp_/__init__.py +0 -0
  113. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/mcp_/async_lifecycle.py +0 -0
  114. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/mcp_/blocking_startup.py +0 -0
  115. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/mcp_/captured_stdio_server.py +0 -0
  116. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/mcp_/circuit_breaker.py +0 -0
  117. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/mcp_/config_wizard.py +0 -0
  118. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/mcp_/dashboard.py +0 -0
  119. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/mcp_/error_isolation.py +0 -0
  120. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/mcp_/examples/retry_example.py +0 -0
  121. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/mcp_/health_monitor.py +0 -0
  122. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/mcp_/managed_server.py +0 -0
  123. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/mcp_/manager.py +0 -0
  124. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/mcp_/mcp_logs.py +0 -0
  125. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/mcp_/registry.py +0 -0
  126. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/mcp_/retry_manager.py +0 -0
  127. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/mcp_/server_registry_catalog.py +0 -0
  128. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/mcp_/status_tracker.py +0 -0
  129. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/mcp_/system_tools.py +0 -0
  130. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/messaging/__init__.py +0 -0
  131. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/messaging/bus.py +0 -0
  132. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/messaging/commands.py +0 -0
  133. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/messaging/markdown_patches.py +0 -0
  134. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/messaging/message_queue.py +0 -0
  135. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/messaging/messages.py +0 -0
  136. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/messaging/queue_console.py +0 -0
  137. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/messaging/renderers.py +0 -0
  138. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/messaging/rich_renderer.py +0 -0
  139. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/messaging/spinner/__init__.py +0 -0
  140. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/messaging/spinner/console_spinner.py +0 -0
  141. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/messaging/spinner/spinner_base.py +0 -0
  142. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/messaging/subagent_console.py +0 -0
  143. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/model_factory.py +0 -0
  144. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/model_utils.py +0 -0
  145. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/models_dev_api.json +0 -0
  146. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/models_dev_parser.py +0 -0
  147. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/plugins/__init__.py +0 -0
  148. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/plugins/antigravity_oauth/__init__.py +0 -0
  149. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/plugins/antigravity_oauth/accounts.py +0 -0
  150. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/plugins/antigravity_oauth/antigravity_model.py +0 -0
  151. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/plugins/antigravity_oauth/config.py +0 -0
  152. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/plugins/antigravity_oauth/constants.py +0 -0
  153. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/plugins/antigravity_oauth/oauth.py +0 -0
  154. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/plugins/antigravity_oauth/storage.py +0 -0
  155. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/plugins/antigravity_oauth/test_plugin.py +0 -0
  156. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/plugins/antigravity_oauth/token.py +0 -0
  157. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/plugins/antigravity_oauth/transport.py +0 -0
  158. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/plugins/antigravity_oauth/utils.py +0 -0
  159. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/plugins/chatgpt_oauth/__init__.py +0 -0
  160. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/plugins/chatgpt_oauth/config.py +0 -0
  161. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/plugins/chatgpt_oauth/oauth_flow.py +0 -0
  162. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/plugins/chatgpt_oauth/test_plugin.py +0 -0
  163. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/plugins/chatgpt_oauth/utils.py +0 -0
  164. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/plugins/claude_code_oauth/README.md +0 -0
  165. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/plugins/claude_code_oauth/SETUP.md +0 -0
  166. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/plugins/claude_code_oauth/__init__.py +0 -0
  167. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/plugins/claude_code_oauth/config.py +0 -0
  168. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/plugins/claude_code_oauth/test_plugin.py +0 -0
  169. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/plugins/claude_code_oauth/utils.py +0 -0
  170. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/plugins/customizable_commands/__init__.py +0 -0
  171. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/plugins/customizable_commands/register_callbacks.py +0 -0
  172. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/plugins/example_custom_command/README.md +0 -0
  173. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/plugins/example_custom_command/register_callbacks.py +0 -0
  174. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/plugins/file_permission_handler/__init__.py +0 -0
  175. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/plugins/file_permission_handler/register_callbacks.py +0 -0
  176. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/plugins/frontend_emitter/__init__.py +0 -0
  177. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/plugins/frontend_emitter/emitter.py +0 -0
  178. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/plugins/frontend_emitter/register_callbacks.py +0 -0
  179. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/plugins/oauth_puppy_html.py +0 -0
  180. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/plugins/shell_safety/__init__.py +0 -0
  181. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/plugins/shell_safety/agent_shell_safety.py +0 -0
  182. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/plugins/shell_safety/command_cache.py +0 -0
  183. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/plugins/shell_safety/register_callbacks.py +0 -0
  184. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/prompts/antigravity_system_prompt.md +0 -0
  185. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/pydantic_patches.py +0 -0
  186. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/reopenable_async_client.py +0 -0
  187. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/round_robin_model.py +0 -0
  188. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/session_storage.py +0 -0
  189. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/status_display.py +0 -0
  190. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/summarization_agent.py +0 -0
  191. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/terminal_utils.py +0 -0
  192. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/tools/__init__.py +0 -0
  193. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/tools/agent_tools.py +0 -0
  194. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/tools/browser/__init__.py +0 -0
  195. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/tools/browser/browser_control.py +0 -0
  196. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/tools/browser/browser_interactions.py +0 -0
  197. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/tools/browser/browser_locators.py +0 -0
  198. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/tools/browser/browser_manager.py +0 -0
  199. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/tools/browser/browser_navigation.py +0 -0
  200. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/tools/browser/browser_screenshot.py +0 -0
  201. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/tools/browser/browser_scripts.py +0 -0
  202. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/tools/browser/browser_workflows.py +0 -0
  203. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/tools/browser/chromium_terminal_manager.py +0 -0
  204. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/tools/browser/terminal_command_tools.py +0 -0
  205. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/tools/browser/terminal_screenshot_tools.py +0 -0
  206. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/tools/browser/terminal_tools.py +0 -0
  207. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/tools/command_runner.py +0 -0
  208. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/tools/common.py +0 -0
  209. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/tools/display.py +0 -0
  210. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/tools/file_modifications.py +0 -0
  211. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/tools/file_operations.py +0 -0
  212. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/tools/subagent_context.py +0 -0
  213. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/tools/tools_content.py +0 -0
  214. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/uvx_detection.py +0 -0
  215. {code_puppy-0.0.370 → code_puppy-0.0.372}/code_puppy/version_checker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-puppy
3
- Version: 0.0.370
3
+ Version: 0.0.372
4
4
  Summary: Code generation agent
5
5
  Project-URL: repository, https://github.com/mpfaffenberger/code_puppy
6
6
  Project-URL: HomePage, https://github.com/mpfaffenberger/code_puppy
@@ -5,9 +5,12 @@ configurations, each with their own system prompts and tool sets.
5
5
  """
6
6
 
7
7
  from .agent_manager import (
8
+ clone_agent,
9
+ delete_clone_agent,
8
10
  get_agent_descriptions,
9
11
  get_available_agents,
10
12
  get_current_agent,
13
+ is_clone_agent_name,
11
14
  load_agent,
12
15
  refresh_agents,
13
16
  set_current_agent,
@@ -15,8 +18,11 @@ from .agent_manager import (
15
18
  from .subagent_stream_handler import subagent_stream_handler
16
19
 
17
20
  __all__ = [
21
+ "clone_agent",
22
+ "delete_clone_agent",
18
23
  "get_available_agents",
19
24
  "get_current_agent",
25
+ "is_clone_agent_name",
20
26
  "set_current_agent",
21
27
  "load_agent",
22
28
  "get_agent_descriptions",
@@ -4,6 +4,7 @@ import importlib
4
4
  import json
5
5
  import os
6
6
  import pkgutil
7
+ import re
7
8
  import uuid
8
9
  from pathlib import Path
9
10
  from typing import Dict, List, Optional, Type, Union
@@ -13,7 +14,7 @@ from pydantic_ai.messages import ModelMessage
13
14
  from code_puppy.agents.base_agent import BaseAgent
14
15
  from code_puppy.agents.json_agent import JSONAgent, discover_json_agents
15
16
  from code_puppy.callbacks import on_agent_reload
16
- from code_puppy.messaging import emit_warning
17
+ from code_puppy.messaging import emit_success, emit_warning
17
18
 
18
19
  # Registry of available agents (Python classes and JSON file paths)
19
20
  _AGENT_REGISTRY: Dict[str, Union[Type[BaseAgent], str]] = {}
@@ -467,3 +468,206 @@ def refresh_agents():
467
468
  # Generate a message group ID for agent refreshing
468
469
  message_group_id = str(uuid.uuid4())
469
470
  _discover_agents(message_group_id=message_group_id)
471
+
472
+
473
+ _CLONE_NAME_PATTERN = re.compile(r"^(?P<base>.+)-clone-(?P<index>\d+)$")
474
+ _CLONE_DISPLAY_PATTERN = re.compile(r"\s*\(Clone\s+\d+\)$", re.IGNORECASE)
475
+
476
+
477
+ def _strip_clone_suffix(agent_name: str) -> str:
478
+ """Strip a trailing -clone-N suffix from a name if present."""
479
+ match = _CLONE_NAME_PATTERN.match(agent_name)
480
+ return match.group("base") if match else agent_name
481
+
482
+
483
+ def _strip_clone_display_suffix(display_name: str) -> str:
484
+ """Remove a trailing "(Clone N)" suffix from display names."""
485
+ cleaned = _CLONE_DISPLAY_PATTERN.sub("", display_name).strip()
486
+ return cleaned or display_name
487
+
488
+
489
+ def is_clone_agent_name(agent_name: str) -> bool:
490
+ """Return True if the agent name looks like a clone."""
491
+ return bool(_CLONE_NAME_PATTERN.match(agent_name))
492
+
493
+
494
+ def _default_display_name(agent_name: str) -> str:
495
+ """Build a default display name from an agent name."""
496
+ title = agent_name.title()
497
+ return f"{title} 🤖"
498
+
499
+
500
+ def _build_clone_display_name(display_name: str, clone_index: int) -> str:
501
+ """Build a clone display name based on the source display name."""
502
+ base_name = _strip_clone_display_suffix(display_name)
503
+ return f"{base_name} (Clone {clone_index})"
504
+
505
+
506
+ def _filter_available_tools(tool_names: List[str]) -> List[str]:
507
+ """Filter a tool list to only available tool names."""
508
+ from code_puppy.tools import get_available_tool_names
509
+
510
+ available_tools = set(get_available_tool_names())
511
+ return [tool for tool in tool_names if tool in available_tools]
512
+
513
+
514
+ def _next_clone_index(
515
+ base_name: str, existing_names: set[str], agents_dir: Path
516
+ ) -> int:
517
+ """Compute the next clone index for a base name."""
518
+ clone_pattern = re.compile(rf"^{re.escape(base_name)}-clone-(\\d+)$")
519
+ indices = []
520
+ for name in existing_names:
521
+ match = clone_pattern.match(name)
522
+ if match:
523
+ indices.append(int(match.group(1)))
524
+
525
+ next_index = max(indices, default=0) + 1
526
+ while True:
527
+ clone_name = f"{base_name}-clone-{next_index}"
528
+ clone_path = agents_dir / f"{clone_name}.json"
529
+ if clone_name not in existing_names and not clone_path.exists():
530
+ return next_index
531
+ next_index += 1
532
+
533
+
534
+ def clone_agent(agent_name: str) -> Optional[str]:
535
+ """Clone an agent definition into the user agents directory.
536
+
537
+ Args:
538
+ agent_name: Source agent name to clone.
539
+
540
+ Returns:
541
+ The cloned agent name, or None if cloning failed.
542
+ """
543
+ # Generate a message group ID for agent cloning
544
+ message_group_id = str(uuid.uuid4())
545
+ _discover_agents(message_group_id=message_group_id)
546
+
547
+ agent_ref = _AGENT_REGISTRY.get(agent_name)
548
+ if agent_ref is None:
549
+ emit_warning(f"Agent '{agent_name}' not found for cloning.")
550
+ return None
551
+
552
+ from ..config import get_agent_pinned_model, get_user_agents_directory
553
+
554
+ agents_dir = Path(get_user_agents_directory())
555
+ base_name = _strip_clone_suffix(agent_name)
556
+ existing_names = set(_AGENT_REGISTRY.keys())
557
+ clone_index = _next_clone_index(base_name, existing_names, agents_dir)
558
+ clone_name = f"{base_name}-clone-{clone_index}"
559
+ clone_path = agents_dir / f"{clone_name}.json"
560
+
561
+ try:
562
+ if isinstance(agent_ref, str):
563
+ with open(agent_ref, "r", encoding="utf-8") as f:
564
+ source_config = json.load(f)
565
+
566
+ source_display_name = source_config.get("display_name")
567
+ if not source_display_name:
568
+ source_display_name = _default_display_name(base_name)
569
+
570
+ clone_config = dict(source_config)
571
+ clone_config["name"] = clone_name
572
+ clone_config["display_name"] = _build_clone_display_name(
573
+ source_display_name, clone_index
574
+ )
575
+
576
+ tools = source_config.get("tools", [])
577
+ clone_config["tools"] = (
578
+ _filter_available_tools(tools) if isinstance(tools, list) else []
579
+ )
580
+
581
+ if not clone_config.get("model"):
582
+ clone_config.pop("model", None)
583
+ else:
584
+ agent_instance = agent_ref()
585
+ clone_config = {
586
+ "name": clone_name,
587
+ "display_name": _build_clone_display_name(
588
+ agent_instance.display_name, clone_index
589
+ ),
590
+ "description": agent_instance.description,
591
+ "system_prompt": agent_instance.get_system_prompt(),
592
+ "tools": _filter_available_tools(agent_instance.get_available_tools()),
593
+ }
594
+
595
+ user_prompt = agent_instance.get_user_prompt()
596
+ if user_prompt is not None:
597
+ clone_config["user_prompt"] = user_prompt
598
+
599
+ tools_config = agent_instance.get_tools_config()
600
+ if tools_config is not None:
601
+ clone_config["tools_config"] = tools_config
602
+
603
+ pinned_model = get_agent_pinned_model(agent_instance.name)
604
+ if pinned_model:
605
+ clone_config["model"] = pinned_model
606
+ except Exception as exc:
607
+ emit_warning(f"Failed to build clone for '{agent_name}': {exc}")
608
+ return None
609
+
610
+ if clone_path.exists():
611
+ emit_warning(f"Clone target '{clone_name}' already exists.")
612
+ return None
613
+
614
+ try:
615
+ with open(clone_path, "w", encoding="utf-8") as f:
616
+ json.dump(clone_config, f, indent=2, ensure_ascii=False)
617
+ emit_success(f"Cloned '{agent_name}' to '{clone_name}'.")
618
+ return clone_name
619
+ except Exception as exc:
620
+ emit_warning(f"Failed to write clone file '{clone_path}': {exc}")
621
+ return None
622
+
623
+
624
+ def delete_clone_agent(agent_name: str) -> bool:
625
+ """Delete a cloned JSON agent definition.
626
+
627
+ Args:
628
+ agent_name: Clone agent name to delete.
629
+
630
+ Returns:
631
+ True if the clone was deleted, False otherwise.
632
+ """
633
+ message_group_id = str(uuid.uuid4())
634
+ _discover_agents(message_group_id=message_group_id)
635
+
636
+ if not is_clone_agent_name(agent_name):
637
+ emit_warning(f"Agent '{agent_name}' is not a clone.")
638
+ return False
639
+
640
+ if get_current_agent_name() == agent_name:
641
+ emit_warning("Cannot delete the active agent. Switch agents first.")
642
+ return False
643
+
644
+ agent_ref = _AGENT_REGISTRY.get(agent_name)
645
+ if agent_ref is None:
646
+ emit_warning(f"Clone '{agent_name}' not found.")
647
+ return False
648
+
649
+ if not isinstance(agent_ref, str):
650
+ emit_warning(f"Clone '{agent_name}' is not a JSON agent.")
651
+ return False
652
+
653
+ clone_path = Path(agent_ref)
654
+ if not clone_path.exists():
655
+ emit_warning(f"Clone file for '{agent_name}' does not exist.")
656
+ return False
657
+
658
+ from ..config import get_user_agents_directory
659
+
660
+ agents_dir = Path(get_user_agents_directory()).resolve()
661
+ if clone_path.resolve().parent != agents_dir:
662
+ emit_warning(f"Refusing to delete non-user clone '{agent_name}'.")
663
+ return False
664
+
665
+ try:
666
+ clone_path.unlink()
667
+ emit_success(f"Deleted clone '{agent_name}'.")
668
+ _AGENT_REGISTRY.pop(agent_name, None)
669
+ _AGENT_HISTORIES.pop(agent_name, None)
670
+ return True
671
+ except Exception as exc:
672
+ emit_warning(f"Failed to delete clone '{agent_name}': {exc}")
673
+ return False
@@ -626,12 +626,21 @@ class AddModelMenu:
626
626
  elif model_type == "openai" and "gpt-5" in model.model_id:
627
627
  # GPT-5 models have special settings
628
628
  if "codex" in model.model_id:
629
- config["supported_settings"] = ["reasoning_effort"]
629
+ config["supported_settings"] = [
630
+ "temperature",
631
+ "top_p",
632
+ "reasoning_effort",
633
+ ]
630
634
  else:
631
- config["supported_settings"] = ["reasoning_effort", "verbosity"]
635
+ config["supported_settings"] = [
636
+ "temperature",
637
+ "top_p",
638
+ "reasoning_effort",
639
+ "verbosity",
640
+ ]
632
641
  else:
633
- # Default settings for most models (no top_p)
634
- config["supported_settings"] = ["temperature", "seed"]
642
+ # Default settings for most models
643
+ config["supported_settings"] = ["temperature", "seed", "top_p"]
635
644
 
636
645
  return config
637
646
 
@@ -16,11 +16,22 @@ from prompt_toolkit.layout.controls import FormattedTextControl
16
16
  from prompt_toolkit.widgets import Frame
17
17
 
18
18
  from code_puppy.agents import (
19
+ clone_agent,
20
+ delete_clone_agent,
19
21
  get_agent_descriptions,
20
22
  get_available_agents,
21
23
  get_current_agent,
24
+ is_clone_agent_name,
22
25
  )
26
+ from code_puppy.command_line.model_picker_completion import load_model_names
27
+ from code_puppy.config import (
28
+ clear_agent_pinned_model,
29
+ get_agent_pinned_model,
30
+ set_agent_pinned_model,
31
+ )
32
+ from code_puppy.messaging import emit_info, emit_success, emit_warning
23
33
  from code_puppy.tools.command_runner import set_awaiting_user_input
34
+ from code_puppy.tools.common import arrow_select_async
24
35
 
25
36
  PAGE_SIZE = 10 # Agents per page
26
37
 
@@ -87,6 +98,166 @@ def _sanitize_display_text(text: str) -> str:
87
98
  return cleaned
88
99
 
89
100
 
101
+ def _get_pinned_model(agent_name: str) -> Optional[str]:
102
+ """Return the pinned model for an agent, if any.
103
+
104
+ Checks both built-in agent config and JSON agent files.
105
+ """
106
+ import json
107
+
108
+ # First check built-in agent config
109
+ try:
110
+ pinned = get_agent_pinned_model(agent_name)
111
+ if pinned:
112
+ return pinned
113
+ except Exception:
114
+ pass # Continue to check JSON agents
115
+
116
+ # Check if it's a JSON agent
117
+ try:
118
+ from code_puppy.agents.json_agent import discover_json_agents
119
+
120
+ json_agents = discover_json_agents()
121
+ if agent_name in json_agents:
122
+ agent_file_path = json_agents[agent_name]
123
+ with open(agent_file_path, "r", encoding="utf-8") as f:
124
+ agent_config = json.load(f)
125
+ model = agent_config.get("model")
126
+ return model if model else None
127
+ except Exception:
128
+ pass # Return None if we can't read the JSON file
129
+
130
+ return None
131
+
132
+
133
+ def _build_model_picker_choices(
134
+ pinned_model: Optional[str],
135
+ model_names: List[str],
136
+ ) -> List[str]:
137
+ """Build model picker choices with pinned/unpin indicators."""
138
+ choices = ["✓ (unpin)" if not pinned_model else " (unpin)"]
139
+
140
+ for model_name in model_names:
141
+ if model_name == pinned_model:
142
+ choices.append(f"✓ {model_name} (pinned)")
143
+ else:
144
+ choices.append(f" {model_name}")
145
+
146
+ return choices
147
+
148
+
149
+ def _normalize_model_choice(choice: str) -> str:
150
+ """Normalize a picker choice into a model name or '(unpin)' string."""
151
+ cleaned = choice.strip()
152
+ if cleaned.startswith("✓"):
153
+ cleaned = cleaned.lstrip("✓").strip()
154
+ if cleaned.endswith(" (pinned)"):
155
+ cleaned = cleaned[: -len(" (pinned)")].strip()
156
+ return cleaned
157
+
158
+
159
+ async def _select_pinned_model(agent_name: str) -> Optional[str]:
160
+ """Prompt for a model to pin to the agent."""
161
+ try:
162
+ model_names = load_model_names() or []
163
+ except Exception as exc:
164
+ emit_warning(f"Failed to load models: {exc}")
165
+ return None
166
+
167
+ pinned_model = _get_pinned_model(agent_name)
168
+ choices = _build_model_picker_choices(pinned_model, model_names)
169
+ if not choices:
170
+ emit_warning("No models available to pin.")
171
+ return None
172
+
173
+ try:
174
+ choice = await arrow_select_async(
175
+ f"Select a model to pin for '{agent_name}'",
176
+ choices,
177
+ )
178
+ except KeyboardInterrupt:
179
+ emit_info("Model pinning cancelled")
180
+ return None
181
+
182
+ return _normalize_model_choice(choice)
183
+
184
+
185
+ def _reload_agent_if_current(
186
+ agent_name: str,
187
+ pinned_model: Optional[str],
188
+ ) -> None:
189
+ """Reload the current agent when its pinned model changes."""
190
+ current_agent = get_current_agent()
191
+ if not current_agent or current_agent.name != agent_name:
192
+ return
193
+
194
+ try:
195
+ if hasattr(current_agent, "refresh_config"):
196
+ current_agent.refresh_config()
197
+ current_agent.reload_code_generation_agent()
198
+ if pinned_model:
199
+ emit_info(f"Active agent reloaded with pinned model '{pinned_model}'")
200
+ else:
201
+ emit_info("Active agent reloaded with default model")
202
+ except Exception as exc:
203
+ emit_warning(f"Pinned model applied but reload failed: {exc}")
204
+
205
+
206
+ def _apply_pinned_model(agent_name: str, model_choice: str) -> None:
207
+ """Persist a pinned model selection for an agent.
208
+
209
+ Handles both built-in agents (via config) and JSON agents (via JSON file).
210
+ """
211
+ import json
212
+
213
+ # Check if this is a JSON agent or a built-in agent
214
+ try:
215
+ from code_puppy.agents.json_agent import discover_json_agents
216
+
217
+ json_agents = discover_json_agents()
218
+ is_json_agent = agent_name in json_agents
219
+ except Exception:
220
+ is_json_agent = False
221
+
222
+ try:
223
+ if is_json_agent:
224
+ # Handle JSON agent - modify the JSON file
225
+ agent_file_path = json_agents[agent_name]
226
+
227
+ with open(agent_file_path, "r", encoding="utf-8") as f:
228
+ agent_config = json.load(f)
229
+
230
+ if model_choice == "(unpin)":
231
+ # Remove the model key if it exists
232
+ if "model" in agent_config:
233
+ del agent_config["model"]
234
+ emit_success(f"Model pin cleared for '{agent_name}'")
235
+ pinned_model = None
236
+ else:
237
+ # Set the model
238
+ agent_config["model"] = model_choice
239
+ emit_success(f"Pinned '{model_choice}' to '{agent_name}'")
240
+ pinned_model = model_choice
241
+
242
+ # Save the updated configuration
243
+ with open(agent_file_path, "w", encoding="utf-8") as f:
244
+ json.dump(agent_config, f, indent=2, ensure_ascii=False)
245
+ else:
246
+ # Handle built-in Python agent - use config functions
247
+ if model_choice == "(unpin)":
248
+ clear_agent_pinned_model(agent_name)
249
+ emit_success(f"Model pin cleared for '{agent_name}'")
250
+ pinned_model = None
251
+ else:
252
+ set_agent_pinned_model(agent_name, model_choice)
253
+ emit_success(f"Pinned '{model_choice}' to '{agent_name}'")
254
+ pinned_model = model_choice
255
+
256
+ _reload_agent_if_current(agent_name, pinned_model)
257
+ except Exception as exc:
258
+ emit_warning(f"Failed to apply pinned model: {exc}")
259
+
260
+
90
261
  def _get_agent_entries() -> List[Tuple[str, str, str]]:
91
262
  """Get all agents with their display names and descriptions.
92
263
 
@@ -141,6 +312,7 @@ def _render_menu_panel(
141
312
  name, display_name, _ = entries[i]
142
313
  is_selected = i == selected_idx
143
314
  is_current = name == current_agent_name
315
+ pinned_model = _get_pinned_model(name)
144
316
 
145
317
  # Sanitize display name to avoid emoji rendering issues
146
318
  safe_display_name = _sanitize_display_text(display_name)
@@ -153,6 +325,10 @@ def _render_menu_panel(
153
325
  lines.append(("", " "))
154
326
  lines.append(("", safe_display_name))
155
327
 
328
+ if pinned_model:
329
+ safe_pinned_model = _sanitize_display_text(pinned_model)
330
+ lines.append(("fg:ansiyellow", f" → {safe_pinned_model}"))
331
+
156
332
  # Add current marker
157
333
  if is_current:
158
334
  lines.append(("fg:ansicyan", " ← current"))
@@ -167,6 +343,12 @@ def _render_menu_panel(
167
343
  lines.append(("", "Page\n"))
168
344
  lines.append(("fg:green", " Enter "))
169
345
  lines.append(("", "Select\n"))
346
+ lines.append(("fg:ansibrightblack", " P "))
347
+ lines.append(("", "Pin model\n"))
348
+ lines.append(("fg:ansibrightblack", " C "))
349
+ lines.append(("", "Clone\n"))
350
+ lines.append(("fg:ansibrightblack", " D "))
351
+ lines.append(("", "Delete clone\n"))
170
352
  lines.append(("fg:ansibrightred", " Ctrl+C "))
171
353
  lines.append(("", "Cancel"))
172
354
 
@@ -198,6 +380,7 @@ def _render_preview_panel(
198
380
 
199
381
  name, display_name, description = entry
200
382
  is_current = name == current_agent_name
383
+ pinned_model = _get_pinned_model(name)
201
384
 
202
385
  # Sanitize text to avoid emoji rendering issues
203
386
  safe_display_name = _sanitize_display_text(display_name)
@@ -213,6 +396,15 @@ def _render_preview_panel(
213
396
  lines.append(("fg:ansicyan", safe_display_name))
214
397
  lines.append(("", "\n\n"))
215
398
 
399
+ # Pinned model
400
+ lines.append(("bold", "Pinned Model: "))
401
+ if pinned_model:
402
+ safe_pinned_model = _sanitize_display_text(pinned_model)
403
+ lines.append(("fg:ansiyellow", safe_pinned_model))
404
+ else:
405
+ lines.append(("fg:ansibrightblack", "default"))
406
+ lines.append(("", "\n\n"))
407
+
216
408
  # Description
217
409
  lines.append(("bold", "Description:"))
218
410
  lines.append(("", "\n"))
@@ -261,8 +453,6 @@ async def interactive_agent_picker() -> Optional[str]:
261
453
  current_agent_name = current_agent.name if current_agent else ""
262
454
 
263
455
  if not entries:
264
- from code_puppy.messaging import emit_info
265
-
266
456
  emit_info("No agents found.")
267
457
  return None
268
458
 
@@ -270,14 +460,38 @@ async def interactive_agent_picker() -> Optional[str]:
270
460
  selected_idx = [0] # Current selection (global index)
271
461
  current_page = [0] # Current page
272
462
  result = [None] # Selected agent name
463
+ pending_action = [None] # 'pin', 'clone', 'delete', or None
273
464
 
274
- total_pages = (len(entries) + PAGE_SIZE - 1) // PAGE_SIZE
465
+ total_pages = [max(1, (len(entries) + PAGE_SIZE - 1) // PAGE_SIZE)]
275
466
 
276
467
  def get_current_entry() -> Optional[Tuple[str, str, str]]:
277
468
  if 0 <= selected_idx[0] < len(entries):
278
469
  return entries[selected_idx[0]]
279
470
  return None
280
471
 
472
+ def refresh_entries(selected_name: Optional[str] = None) -> None:
473
+ nonlocal entries
474
+
475
+ entries = _get_agent_entries()
476
+ total_pages[0] = max(1, (len(entries) + PAGE_SIZE - 1) // PAGE_SIZE)
477
+
478
+ if not entries:
479
+ selected_idx[0] = 0
480
+ current_page[0] = 0
481
+ return
482
+
483
+ if selected_name:
484
+ for idx, (name, _, _) in enumerate(entries):
485
+ if name == selected_name:
486
+ selected_idx[0] = idx
487
+ break
488
+ else:
489
+ selected_idx[0] = min(selected_idx[0], len(entries) - 1)
490
+ else:
491
+ selected_idx[0] = min(selected_idx[0], len(entries) - 1)
492
+
493
+ current_page[0] = selected_idx[0] // PAGE_SIZE
494
+
281
495
  # Build UI
282
496
  menu_control = FormattedTextControl(text="")
283
497
  preview_control = FormattedTextControl(text="")
@@ -336,11 +550,29 @@ async def interactive_agent_picker() -> Optional[str]:
336
550
 
337
551
  @kb.add("right")
338
552
  def _(event):
339
- if current_page[0] < total_pages - 1:
553
+ if current_page[0] < total_pages[0] - 1:
340
554
  current_page[0] += 1
341
555
  selected_idx[0] = current_page[0] * PAGE_SIZE
342
556
  update_display()
343
557
 
558
+ @kb.add("p")
559
+ def _(event):
560
+ if get_current_entry():
561
+ pending_action[0] = "pin"
562
+ event.app.exit()
563
+
564
+ @kb.add("c")
565
+ def _(event):
566
+ if get_current_entry():
567
+ pending_action[0] = "clone"
568
+ event.app.exit()
569
+
570
+ @kb.add("d")
571
+ def _(event):
572
+ if get_current_entry():
573
+ pending_action[0] = "delete"
574
+ event.app.exit()
575
+
344
576
  @kb.add("enter")
345
577
  def _(event):
346
578
  entry = get_current_entry()
@@ -370,15 +602,52 @@ async def interactive_agent_picker() -> Optional[str]:
370
602
  time.sleep(0.05)
371
603
 
372
604
  try:
373
- # Initial display
374
- update_display()
375
-
376
- # Clear the current buffer
377
- sys.stdout.write("\033[2J\033[H")
378
- sys.stdout.flush()
605
+ while True:
606
+ pending_action[0] = None
607
+ result[0] = None
608
+ update_display()
379
609
 
380
- # Run application
381
- await app.run_async()
610
+ # Clear the current buffer
611
+ sys.stdout.write("\033[2J\033[H")
612
+ sys.stdout.flush()
613
+
614
+ # Run application
615
+ await app.run_async()
616
+
617
+ if pending_action[0] == "pin":
618
+ entry = get_current_entry()
619
+ if entry:
620
+ selected_model = await _select_pinned_model(entry[0])
621
+ if selected_model:
622
+ _apply_pinned_model(entry[0], selected_model)
623
+ continue
624
+
625
+ if pending_action[0] == "clone":
626
+ entry = get_current_entry()
627
+ selected_name = None
628
+ if entry:
629
+ cloned_name = clone_agent(entry[0])
630
+ selected_name = cloned_name or entry[0]
631
+ refresh_entries(selected_name=selected_name)
632
+ continue
633
+
634
+ if pending_action[0] == "delete":
635
+ entry = get_current_entry()
636
+ selected_name = None
637
+ if entry:
638
+ agent_name = entry[0]
639
+ selected_name = agent_name
640
+ if not is_clone_agent_name(agent_name):
641
+ emit_warning("Only cloned agents can be deleted.")
642
+ elif agent_name == current_agent_name:
643
+ emit_warning("Cannot delete the active agent. Switch first.")
644
+ else:
645
+ if delete_clone_agent(agent_name):
646
+ selected_name = None
647
+ refresh_entries(selected_name=selected_name)
648
+ continue
649
+
650
+ break
382
651
 
383
652
  finally:
384
653
  # Exit alternate screen buffer once at end
@@ -388,8 +657,6 @@ async def interactive_agent_picker() -> Optional[str]:
388
657
  set_awaiting_user_input(False)
389
658
 
390
659
  # Clear exit message
391
- from code_puppy.messaging import emit_info
392
-
393
660
  emit_info("✓ Exited agent picker")
394
661
 
395
662
  return result[0]