code-puppy 0.0.320__tar.gz → 0.0.338__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 (184) hide show
  1. {code_puppy-0.0.320 → code_puppy-0.0.338}/PKG-INFO +25 -49
  2. {code_puppy-0.0.320 → code_puppy-0.0.338}/README.md +23 -48
  3. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/agents/base_agent.py +200 -53
  4. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/claude_cache_client.py +46 -2
  5. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/cli_runner.py +107 -27
  6. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/command_line/add_model_menu.py +15 -0
  7. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/command_line/autosave_menu.py +5 -0
  8. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/command_line/colors_menu.py +5 -0
  9. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/command_line/config_commands.py +24 -1
  10. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/command_line/core_commands.py +51 -0
  11. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/command_line/diff_menu.py +5 -0
  12. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/command_line/mcp/custom_server_form.py +4 -0
  13. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/command_line/mcp/install_menu.py +5 -1
  14. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/command_line/model_settings_menu.py +5 -0
  15. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/command_line/motd.py +13 -7
  16. code_puppy-0.0.338/code_puppy/command_line/onboarding_slides.py +180 -0
  17. code_puppy-0.0.338/code_puppy/command_line/onboarding_wizard.py +340 -0
  18. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/config.py +3 -2
  19. code_puppy-0.0.338/code_puppy/http_utils.py +338 -0
  20. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/keymap.py +8 -0
  21. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/messaging/messages.py +3 -0
  22. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/messaging/rich_renderer.py +114 -22
  23. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/model_factory.py +102 -15
  24. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/models.json +4 -4
  25. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/plugins/__init__.py +12 -0
  26. code_puppy-0.0.338/code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  27. code_puppy-0.0.338/code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  28. code_puppy-0.0.338/code_puppy/plugins/antigravity_oauth/antigravity_model.py +668 -0
  29. code_puppy-0.0.338/code_puppy/plugins/antigravity_oauth/config.py +42 -0
  30. code_puppy-0.0.338/code_puppy/plugins/antigravity_oauth/constants.py +136 -0
  31. code_puppy-0.0.338/code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  32. code_puppy-0.0.338/code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
  33. code_puppy-0.0.338/code_puppy/plugins/antigravity_oauth/storage.py +271 -0
  34. code_puppy-0.0.338/code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  35. code_puppy-0.0.338/code_puppy/plugins/antigravity_oauth/token.py +167 -0
  36. code_puppy-0.0.338/code_puppy/plugins/antigravity_oauth/transport.py +664 -0
  37. code_puppy-0.0.338/code_puppy/plugins/antigravity_oauth/utils.py +169 -0
  38. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/plugins/chatgpt_oauth/register_callbacks.py +2 -0
  39. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/plugins/claude_code_oauth/register_callbacks.py +2 -0
  40. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/plugins/claude_code_oauth/utils.py +126 -7
  41. code_puppy-0.0.338/code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  42. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/plugins/shell_safety/register_callbacks.py +44 -3
  43. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/reopenable_async_client.py +8 -8
  44. code_puppy-0.0.338/code_puppy/terminal_utils.py +418 -0
  45. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/tools/command_runner.py +22 -6
  46. code_puppy-0.0.338/code_puppy/uvx_detection.py +242 -0
  47. {code_puppy-0.0.320 → code_puppy-0.0.338}/pyproject.toml +2 -1
  48. code_puppy-0.0.320/code_puppy/http_utils.py +0 -416
  49. code_puppy-0.0.320/code_puppy/plugins/shell_safety/agent_shell_safety.py +0 -186
  50. code_puppy-0.0.320/code_puppy/terminal_utils.py +0 -126
  51. {code_puppy-0.0.320 → code_puppy-0.0.338}/.gitignore +0 -0
  52. {code_puppy-0.0.320 → code_puppy-0.0.338}/LICENSE +0 -0
  53. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/__init__.py +0 -0
  54. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/__main__.py +0 -0
  55. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/agents/__init__.py +0 -0
  56. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/agents/agent_c_reviewer.py +0 -0
  57. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/agents/agent_code_puppy.py +0 -0
  58. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/agents/agent_code_reviewer.py +0 -0
  59. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/agents/agent_cpp_reviewer.py +0 -0
  60. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/agents/agent_creator_agent.py +0 -0
  61. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/agents/agent_golang_reviewer.py +0 -0
  62. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/agents/agent_javascript_reviewer.py +0 -0
  63. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/agents/agent_manager.py +0 -0
  64. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/agents/agent_planning.py +0 -0
  65. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/agents/agent_python_programmer.py +0 -0
  66. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/agents/agent_python_reviewer.py +0 -0
  67. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/agents/agent_qa_expert.py +0 -0
  68. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/agents/agent_qa_kitten.py +0 -0
  69. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/agents/agent_security_auditor.py +0 -0
  70. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/agents/agent_typescript_reviewer.py +0 -0
  71. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/agents/json_agent.py +0 -0
  72. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/agents/prompt_reviewer.py +0 -0
  73. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/callbacks.py +0 -0
  74. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/chatgpt_codex_client.py +0 -0
  75. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/command_line/__init__.py +0 -0
  76. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/command_line/attachments.py +0 -0
  77. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/command_line/command_handler.py +0 -0
  78. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/command_line/command_registry.py +0 -0
  79. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/command_line/file_path_completion.py +0 -0
  80. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/command_line/load_context_completion.py +0 -0
  81. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/command_line/mcp/__init__.py +0 -0
  82. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/command_line/mcp/add_command.py +0 -0
  83. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/command_line/mcp/base.py +0 -0
  84. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/command_line/mcp/catalog_server_installer.py +0 -0
  85. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/command_line/mcp/custom_server_installer.py +0 -0
  86. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/command_line/mcp/edit_command.py +0 -0
  87. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/command_line/mcp/handler.py +0 -0
  88. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/command_line/mcp/help_command.py +0 -0
  89. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/command_line/mcp/install_command.py +0 -0
  90. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/command_line/mcp/list_command.py +0 -0
  91. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/command_line/mcp/logs_command.py +0 -0
  92. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/command_line/mcp/remove_command.py +0 -0
  93. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/command_line/mcp/restart_command.py +0 -0
  94. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/command_line/mcp/search_command.py +0 -0
  95. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/command_line/mcp/start_all_command.py +0 -0
  96. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/command_line/mcp/start_command.py +0 -0
  97. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/command_line/mcp/status_command.py +0 -0
  98. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/command_line/mcp/stop_all_command.py +0 -0
  99. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/command_line/mcp/stop_command.py +0 -0
  100. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/command_line/mcp/test_command.py +0 -0
  101. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/command_line/mcp/utils.py +0 -0
  102. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/command_line/mcp/wizard_utils.py +0 -0
  103. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/command_line/mcp_completion.py +0 -0
  104. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/command_line/model_picker_completion.py +0 -0
  105. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/command_line/pin_command_completion.py +0 -0
  106. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/command_line/prompt_toolkit_completion.py +0 -0
  107. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/command_line/session_commands.py +0 -0
  108. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/command_line/utils.py +0 -0
  109. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/error_logging.py +0 -0
  110. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/gemini_code_assist.py +0 -0
  111. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/main.py +0 -0
  112. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/mcp_/__init__.py +0 -0
  113. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/mcp_/async_lifecycle.py +0 -0
  114. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/mcp_/blocking_startup.py +0 -0
  115. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/mcp_/captured_stdio_server.py +0 -0
  116. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/mcp_/circuit_breaker.py +0 -0
  117. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/mcp_/config_wizard.py +0 -0
  118. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/mcp_/dashboard.py +0 -0
  119. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/mcp_/error_isolation.py +0 -0
  120. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/mcp_/examples/retry_example.py +0 -0
  121. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/mcp_/health_monitor.py +0 -0
  122. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/mcp_/managed_server.py +0 -0
  123. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/mcp_/manager.py +0 -0
  124. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/mcp_/mcp_logs.py +0 -0
  125. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/mcp_/registry.py +0 -0
  126. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/mcp_/retry_manager.py +0 -0
  127. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/mcp_/server_registry_catalog.py +0 -0
  128. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/mcp_/status_tracker.py +0 -0
  129. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/mcp_/system_tools.py +0 -0
  130. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/messaging/__init__.py +0 -0
  131. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/messaging/bus.py +0 -0
  132. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/messaging/commands.py +0 -0
  133. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/messaging/markdown_patches.py +0 -0
  134. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/messaging/message_queue.py +0 -0
  135. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/messaging/queue_console.py +0 -0
  136. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/messaging/renderers.py +0 -0
  137. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/messaging/spinner/__init__.py +0 -0
  138. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/messaging/spinner/console_spinner.py +0 -0
  139. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/messaging/spinner/spinner_base.py +0 -0
  140. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/model_utils.py +0 -0
  141. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/models_dev_api.json +0 -0
  142. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/models_dev_parser.py +0 -0
  143. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/plugins/chatgpt_oauth/__init__.py +0 -0
  144. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/plugins/chatgpt_oauth/config.py +0 -0
  145. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/plugins/chatgpt_oauth/oauth_flow.py +0 -0
  146. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/plugins/chatgpt_oauth/test_plugin.py +0 -0
  147. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/plugins/chatgpt_oauth/utils.py +0 -0
  148. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/plugins/claude_code_oauth/README.md +0 -0
  149. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/plugins/claude_code_oauth/SETUP.md +0 -0
  150. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/plugins/claude_code_oauth/__init__.py +0 -0
  151. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/plugins/claude_code_oauth/config.py +0 -0
  152. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/plugins/claude_code_oauth/test_plugin.py +0 -0
  153. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/plugins/customizable_commands/__init__.py +0 -0
  154. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/plugins/customizable_commands/register_callbacks.py +0 -0
  155. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/plugins/example_custom_command/README.md +0 -0
  156. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/plugins/example_custom_command/register_callbacks.py +0 -0
  157. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/plugins/file_permission_handler/__init__.py +0 -0
  158. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/plugins/file_permission_handler/register_callbacks.py +0 -0
  159. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/plugins/oauth_puppy_html.py +0 -0
  160. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/plugins/shell_safety/__init__.py +0 -0
  161. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/plugins/shell_safety/command_cache.py +0 -0
  162. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/prompts/codex_system_prompt.md +0 -0
  163. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/pydantic_patches.py +0 -0
  164. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/round_robin_model.py +0 -0
  165. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/session_storage.py +0 -0
  166. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/status_display.py +0 -0
  167. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/summarization_agent.py +0 -0
  168. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/tools/__init__.py +0 -0
  169. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/tools/agent_tools.py +0 -0
  170. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/tools/browser/__init__.py +0 -0
  171. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/tools/browser/browser_control.py +0 -0
  172. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/tools/browser/browser_interactions.py +0 -0
  173. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/tools/browser/browser_locators.py +0 -0
  174. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/tools/browser/browser_navigation.py +0 -0
  175. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/tools/browser/browser_screenshot.py +0 -0
  176. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/tools/browser/browser_scripts.py +0 -0
  177. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/tools/browser/browser_workflows.py +0 -0
  178. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/tools/browser/camoufox_manager.py +0 -0
  179. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/tools/browser/vqa_agent.py +0 -0
  180. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/tools/common.py +0 -0
  181. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/tools/file_modifications.py +0 -0
  182. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/tools/file_operations.py +0 -0
  183. {code_puppy-0.0.320 → code_puppy-0.0.338}/code_puppy/tools/tools_content.py +0 -0
  184. {code_puppy-0.0.320 → code_puppy-0.0.338}/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.320
3
+ Version: 0.0.338
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
@@ -34,6 +34,7 @@ Requires-Dist: rich>=13.4.2
34
34
  Requires-Dist: ripgrep==14.1.0
35
35
  Requires-Dist: ruff>=0.11.11
36
36
  Requires-Dist: tenacity>=8.2.0
37
+ Requires-Dist: termflow-md>=0.1.6
37
38
  Requires-Dist: uvicorn>=0.30.0
38
39
  Description-Content-Type: text/markdown
39
40
 
@@ -100,66 +101,32 @@ uvx code-puppy -i
100
101
 
101
102
  ### UV (Recommended)
102
103
 
104
+ #### macOS / Linux
105
+
103
106
  ```bash
104
107
  # Install UV if you don't have it
105
108
  curl -LsSf https://astral.sh/uv/install.sh | sh
106
109
 
107
- # Set UV to always use managed Python (one-time setup)
108
- echo 'export UV_MANAGED_PYTHON=1' >> ~/.zshrc # or ~/.bashrc
109
- source ~/.zshrc # or ~/.bashrc
110
-
111
- # Install and run code-puppy
112
- uvx code-puppy -i
113
- ```
114
-
115
- UV will automatically download the latest compatible Python version (3.11+) if your system doesn't have one.
116
-
117
- ### pip (Alternative)
118
-
119
- ```bash
120
- pip install code-puppy
110
+ uvx code-puppy
121
111
  ```
122
112
 
123
- *Note: pip installation requires your system Python to be 3.11 or newer.*
113
+ #### Windows
124
114
 
125
- ### Permanent Python Management
115
+ On Windows, we recommend installing code-puppy as a global tool for the best experience with keyboard shortcuts (Ctrl+C/Ctrl+X cancellation):
126
116
 
127
- To make UV always use managed Python versions (recommended):
128
-
129
- ```bash
130
- # Set environment variable permanently
131
- echo 'export UV_MANAGED_PYTHON=1' >> ~/.zshrc # or ~/.bashrc
132
- source ~/.zshrc # or ~/.bashrc
117
+ ```powershell
118
+ # Install UV if you don't have it (run in PowerShell as Admin)
119
+ powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
133
120
 
134
- # Now all UV commands will prefer managed Python installations
135
- uvx code-puppy # No need for --managed-python flag anymore
121
+ uvx code-puppy
136
122
  ```
137
123
 
138
- ### Verifying Python Version
124
+ ## Changelog (By Kittylog!)
139
125
 
140
- ```bash
141
- # Check which Python UV will use
142
- uv python find
143
-
144
- # Or check the current project's Python
145
- uv run python --version
146
- ```
126
+ [📋 View the full changelog on Kittylog](https://kittylog.app/c/mpfaffenberger/code_puppy)
147
127
 
148
128
  ## Usage
149
129
 
150
- ### Custom Commands
151
- Create markdown files in `.claude/commands/`, `.github/prompts/`, or `.agents/commands/` to define custom slash commands. The filename becomes the command name and the content runs as a prompt.
152
-
153
- ```bash
154
- # Create a custom command
155
- echo "# Code Review
156
-
157
- Please review this code for security issues." > .claude/commands/review.md
158
-
159
- # Use it in Code Puppy
160
- /review with focus on authentication
161
- ```
162
-
163
130
  ### Adding Models from models.dev 🆕
164
131
 
165
132
  While there are several models configured right out of the box from providers like Synthetic, Cerebras, OpenAI, Google, and Anthropic, Code Puppy integrates with [models.dev](https://models.dev) to let you browse and add models from **65+ providers** with a single command:
@@ -227,6 +194,18 @@ The following environment variables control DBOS behavior:
227
194
  - `DBOS_SYSTEM_DATABASE_URL`: Database URL used by DBOS. Can point to a local SQLite file or a Postgres instance. Example: `postgresql://postgres:dbos@localhost:5432/postgres`. Default: `dbos_store.sqlite` file in the config directory.
228
195
  - `DBOS_APP_VERSION`: If set, Code Puppy uses it as the [DBOS application version](https://docs.dbos.dev/architecture#application-and-workflow-versions) and automatically tries to recover pending workflows for this version. Default: Code Puppy version + Unix timestamp in millisecond (disable automatic recovery).
229
196
 
197
+ ### Custom Commands
198
+ Create markdown files in `.claude/commands/`, `.github/prompts/`, or `.agents/commands/` to define custom slash commands. The filename becomes the command name and the content runs as a prompt.
199
+
200
+ ```bash
201
+ # Create a custom command
202
+ echo "# Code Review
203
+
204
+ Please review this code for security issues." > .claude/commands/review.md
205
+
206
+ # Use it in Code Puppy
207
+ /review with focus on authentication
208
+ ```
230
209
 
231
210
  ## Requirements
232
211
 
@@ -246,9 +225,6 @@ For examples and more information about agent rules, visit [https://agent.md](ht
246
225
 
247
226
  Use the `/mcp` command to manage MCP (list, start, stop, status, etc.)
248
227
 
249
- Watch this video for examples! https://www.youtube.com/watch?v=1t1zEetOqlo
250
-
251
-
252
228
  ## Round Robin Model Distribution
253
229
 
254
230
  Code Puppy supports **Round Robin model distribution** to help you overcome rate limits and distribute load across multiple AI models. This feature automatically cycles through configured models with each request, maximizing your API usage while staying within rate limits.
@@ -61,66 +61,32 @@ uvx code-puppy -i
61
61
 
62
62
  ### UV (Recommended)
63
63
 
64
+ #### macOS / Linux
65
+
64
66
  ```bash
65
67
  # Install UV if you don't have it
66
68
  curl -LsSf https://astral.sh/uv/install.sh | sh
67
69
 
68
- # Set UV to always use managed Python (one-time setup)
69
- echo 'export UV_MANAGED_PYTHON=1' >> ~/.zshrc # or ~/.bashrc
70
- source ~/.zshrc # or ~/.bashrc
71
-
72
- # Install and run code-puppy
73
- uvx code-puppy -i
74
- ```
75
-
76
- UV will automatically download the latest compatible Python version (3.11+) if your system doesn't have one.
77
-
78
- ### pip (Alternative)
79
-
80
- ```bash
81
- pip install code-puppy
70
+ uvx code-puppy
82
71
  ```
83
72
 
84
- *Note: pip installation requires your system Python to be 3.11 or newer.*
73
+ #### Windows
85
74
 
86
- ### Permanent Python Management
75
+ On Windows, we recommend installing code-puppy as a global tool for the best experience with keyboard shortcuts (Ctrl+C/Ctrl+X cancellation):
87
76
 
88
- To make UV always use managed Python versions (recommended):
89
-
90
- ```bash
91
- # Set environment variable permanently
92
- echo 'export UV_MANAGED_PYTHON=1' >> ~/.zshrc # or ~/.bashrc
93
- source ~/.zshrc # or ~/.bashrc
77
+ ```powershell
78
+ # Install UV if you don't have it (run in PowerShell as Admin)
79
+ powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
94
80
 
95
- # Now all UV commands will prefer managed Python installations
96
- uvx code-puppy # No need for --managed-python flag anymore
81
+ uvx code-puppy
97
82
  ```
98
83
 
99
- ### Verifying Python Version
84
+ ## Changelog (By Kittylog!)
100
85
 
101
- ```bash
102
- # Check which Python UV will use
103
- uv python find
104
-
105
- # Or check the current project's Python
106
- uv run python --version
107
- ```
86
+ [📋 View the full changelog on Kittylog](https://kittylog.app/c/mpfaffenberger/code_puppy)
108
87
 
109
88
  ## Usage
110
89
 
111
- ### Custom Commands
112
- Create markdown files in `.claude/commands/`, `.github/prompts/`, or `.agents/commands/` to define custom slash commands. The filename becomes the command name and the content runs as a prompt.
113
-
114
- ```bash
115
- # Create a custom command
116
- echo "# Code Review
117
-
118
- Please review this code for security issues." > .claude/commands/review.md
119
-
120
- # Use it in Code Puppy
121
- /review with focus on authentication
122
- ```
123
-
124
90
  ### Adding Models from models.dev 🆕
125
91
 
126
92
  While there are several models configured right out of the box from providers like Synthetic, Cerebras, OpenAI, Google, and Anthropic, Code Puppy integrates with [models.dev](https://models.dev) to let you browse and add models from **65+ providers** with a single command:
@@ -188,6 +154,18 @@ The following environment variables control DBOS behavior:
188
154
  - `DBOS_SYSTEM_DATABASE_URL`: Database URL used by DBOS. Can point to a local SQLite file or a Postgres instance. Example: `postgresql://postgres:dbos@localhost:5432/postgres`. Default: `dbos_store.sqlite` file in the config directory.
189
155
  - `DBOS_APP_VERSION`: If set, Code Puppy uses it as the [DBOS application version](https://docs.dbos.dev/architecture#application-and-workflow-versions) and automatically tries to recover pending workflows for this version. Default: Code Puppy version + Unix timestamp in millisecond (disable automatic recovery).
190
156
 
157
+ ### Custom Commands
158
+ Create markdown files in `.claude/commands/`, `.github/prompts/`, or `.agents/commands/` to define custom slash commands. The filename becomes the command name and the content runs as a prompt.
159
+
160
+ ```bash
161
+ # Create a custom command
162
+ echo "# Code Review
163
+
164
+ Please review this code for security issues." > .claude/commands/review.md
165
+
166
+ # Use it in Code Puppy
167
+ /review with focus on authentication
168
+ ```
191
169
 
192
170
  ## Requirements
193
171
 
@@ -207,9 +185,6 @@ For examples and more information about agent rules, visit [https://agent.md](ht
207
185
 
208
186
  Use the `/mcp` command to manage MCP (list, start, stop, status, etc.)
209
187
 
210
- Watch this video for examples! https://www.youtube.com/watch?v=1t1zEetOqlo
211
-
212
-
213
188
  ## Round Robin Model Distribution
214
189
 
215
190
  Code Puppy supports **Round Robin model distribution** to help you overcome rate limits and distribute load across multiple AI models. This feature automatically cycles through configured models with each request, maximizing your API usage while staying within rate limits.
@@ -8,7 +8,18 @@ import threading
8
8
  import uuid
9
9
  from abc import ABC, abstractmethod
10
10
  from collections.abc import AsyncIterable
11
- from typing import Any, Callable, Dict, List, Optional, Sequence, Set, Tuple, Union
11
+ from typing import (
12
+ Any,
13
+ Callable,
14
+ Dict,
15
+ List,
16
+ Optional,
17
+ Sequence,
18
+ Set,
19
+ Tuple,
20
+ Type,
21
+ Union,
22
+ )
12
23
 
13
24
  import mcp
14
25
  import pydantic
@@ -1230,6 +1241,74 @@ class BaseAgent(ABC):
1230
1241
  self._mcp_servers = mcp_servers
1231
1242
  return self._code_generation_agent
1232
1243
 
1244
+ def _create_agent_with_output_type(self, output_type: Type[Any]) -> PydanticAgent:
1245
+ """Create a temporary agent configured with a custom output_type.
1246
+
1247
+ This is used when structured output is requested via run_with_mcp.
1248
+ The agent is created fresh with the same configuration as the main agent
1249
+ but with the specified output_type instead of str.
1250
+
1251
+ Args:
1252
+ output_type: The Pydantic model or type for structured output.
1253
+
1254
+ Returns:
1255
+ A configured PydanticAgent (or DBOSAgent wrapper) with the custom output_type.
1256
+ """
1257
+ from code_puppy.model_utils import prepare_prompt_for_model
1258
+ from code_puppy.tools import register_tools_for_agent
1259
+
1260
+ model_name = self.get_model_name()
1261
+ models_config = ModelFactory.load_config()
1262
+ model, resolved_model_name = self._load_model_with_fallback(
1263
+ model_name, models_config, str(uuid.uuid4())
1264
+ )
1265
+
1266
+ instructions = self.get_system_prompt()
1267
+ puppy_rules = self.load_puppy_rules()
1268
+ if puppy_rules:
1269
+ instructions += f"\n{puppy_rules}"
1270
+
1271
+ mcp_servers = getattr(self, "_mcp_servers", []) or []
1272
+ model_settings = make_model_settings(resolved_model_name)
1273
+
1274
+ prepared = prepare_prompt_for_model(
1275
+ model_name, instructions, "", prepend_system_to_user=False
1276
+ )
1277
+ instructions = prepared.instructions
1278
+
1279
+ global _reload_count
1280
+ _reload_count += 1
1281
+
1282
+ if get_use_dbos():
1283
+ temp_agent = PydanticAgent(
1284
+ model=model,
1285
+ instructions=instructions,
1286
+ output_type=output_type,
1287
+ retries=3,
1288
+ toolsets=[],
1289
+ history_processors=[self.message_history_accumulator],
1290
+ model_settings=model_settings,
1291
+ )
1292
+ agent_tools = self.get_available_tools()
1293
+ register_tools_for_agent(temp_agent, agent_tools)
1294
+ dbos_agent = DBOSAgent(
1295
+ temp_agent, name=f"{self.name}-structured-{_reload_count}"
1296
+ )
1297
+ return dbos_agent
1298
+ else:
1299
+ temp_agent = PydanticAgent(
1300
+ model=model,
1301
+ instructions=instructions,
1302
+ output_type=output_type,
1303
+ retries=3,
1304
+ toolsets=mcp_servers,
1305
+ history_processors=[self.message_history_accumulator],
1306
+ model_settings=model_settings,
1307
+ )
1308
+ agent_tools = self.get_available_tools()
1309
+ register_tools_for_agent(temp_agent, agent_tools)
1310
+ return temp_agent
1311
+
1233
1312
  # It's okay to decorate it with DBOS.step even if not using DBOS; the decorator is a no-op in that case.
1234
1313
  @DBOS.step()
1235
1314
  def message_history_accumulator(self, ctx: RunContext, messages: List[Any]):
@@ -1260,17 +1339,20 @@ class BaseAgent(ABC):
1260
1339
  ) -> None:
1261
1340
  """Handle streaming events from the agent run.
1262
1341
 
1263
- This method processes streaming events and emits TextPart and ThinkingPart
1264
- content with styled banners as they stream in.
1342
+ This method processes streaming events and emits TextPart, ThinkingPart,
1343
+ and ToolCallPart content with styled banners/tokens as they stream in.
1265
1344
 
1266
1345
  Args:
1267
1346
  ctx: The run context.
1268
1347
  events: Async iterable of streaming events (PartStartEvent, PartDeltaEvent, etc.).
1269
1348
  """
1270
1349
  from pydantic_ai import PartDeltaEvent, PartStartEvent
1271
- from pydantic_ai.messages import TextPartDelta, ThinkingPartDelta
1350
+ from pydantic_ai.messages import (
1351
+ TextPartDelta,
1352
+ ThinkingPartDelta,
1353
+ ToolCallPartDelta,
1354
+ )
1272
1355
  from rich.console import Console
1273
- from rich.markdown import Markdown
1274
1356
  from rich.markup import escape
1275
1357
 
1276
1358
  from code_puppy.messaging.spinner import pause_all_spinners
@@ -1284,29 +1366,36 @@ class BaseAgent(ABC):
1284
1366
  # Fallback if console not set (shouldn't happen in normal use)
1285
1367
  console = Console()
1286
1368
 
1287
- # Track which part indices we're currently streaming (for Text/Thinking parts)
1369
+ # Track which part indices we're currently streaming (for Text/Thinking/Tool parts)
1288
1370
  streaming_parts: set[int] = set()
1289
1371
  thinking_parts: set[int] = (
1290
1372
  set()
1291
1373
  ) # Track which parts are thinking (for dim style)
1292
1374
  text_parts: set[int] = set() # Track which parts are text
1375
+ tool_parts: set[int] = set() # Track which parts are tool calls
1293
1376
  banner_printed: set[int] = set() # Track if banner was already printed
1294
- text_buffer: dict[int, list[str]] = {} # Buffer text for final markdown render
1295
- token_count: dict[int, int] = {} # Track token count per text part
1377
+ token_count: dict[int, int] = {} # Track token count per text/tool part
1296
1378
  did_stream_anything = False # Track if we streamed any content
1297
1379
 
1380
+ # Termflow streaming state for text parts
1381
+ from termflow import Parser as TermflowParser
1382
+ from termflow import Renderer as TermflowRenderer
1383
+
1384
+ termflow_parsers: dict[int, TermflowParser] = {}
1385
+ termflow_renderers: dict[int, TermflowRenderer] = {}
1386
+ termflow_line_buffers: dict[int, str] = {} # Buffer incomplete lines
1387
+
1298
1388
  def _print_thinking_banner() -> None:
1299
1389
  """Print the THINKING banner with spinner pause and line clear."""
1300
1390
  nonlocal did_stream_anything
1301
- import sys
1302
1391
  import time
1303
1392
 
1304
1393
  from code_puppy.config import get_banner_color
1305
1394
 
1306
1395
  pause_all_spinners()
1307
1396
  time.sleep(0.1) # Delay to let spinner fully clear
1308
- sys.stdout.write("\r\x1b[K") # Clear line
1309
- sys.stdout.flush()
1397
+ # Clear line and print newline before banner
1398
+ console.print(" " * 50, end="\r")
1310
1399
  console.print() # Newline before banner
1311
1400
  # Bold banner with configurable color and lightning bolt
1312
1401
  thinking_color = get_banner_color("thinking")
@@ -1316,21 +1405,19 @@ class BaseAgent(ABC):
1316
1405
  ),
1317
1406
  end="",
1318
1407
  )
1319
- sys.stdout.flush()
1320
1408
  did_stream_anything = True
1321
1409
 
1322
1410
  def _print_response_banner() -> None:
1323
1411
  """Print the AGENT RESPONSE banner with spinner pause and line clear."""
1324
1412
  nonlocal did_stream_anything
1325
- import sys
1326
1413
  import time
1327
1414
 
1328
1415
  from code_puppy.config import get_banner_color
1329
1416
 
1330
1417
  pause_all_spinners()
1331
1418
  time.sleep(0.1) # Delay to let spinner fully clear
1332
- sys.stdout.write("\r\x1b[K") # Clear line
1333
- sys.stdout.flush()
1419
+ # Clear line and print newline before banner
1420
+ console.print(" " * 50, end="\r")
1334
1421
  console.print() # Newline before banner
1335
1422
  response_color = get_banner_color("agent_response")
1336
1423
  console.print(
@@ -1338,7 +1425,6 @@ class BaseAgent(ABC):
1338
1425
  f"[bold white on {response_color}] AGENT RESPONSE [/bold white on {response_color}]"
1339
1426
  )
1340
1427
  )
1341
- sys.stdout.flush()
1342
1428
  did_stream_anything = True
1343
1429
 
1344
1430
  async for event in events:
@@ -1357,12 +1443,25 @@ class BaseAgent(ABC):
1357
1443
  elif isinstance(part, TextPart):
1358
1444
  streaming_parts.add(event.index)
1359
1445
  text_parts.add(event.index)
1360
- text_buffer[event.index] = [] # Initialize buffer
1361
- token_count[event.index] = 0 # Initialize token counter
1362
- # Buffer initial content if present
1446
+ # Initialize termflow streaming for this text part
1447
+ termflow_parsers[event.index] = TermflowParser()
1448
+ termflow_renderers[event.index] = TermflowRenderer(
1449
+ output=console.file, width=console.width
1450
+ )
1451
+ termflow_line_buffers[event.index] = ""
1452
+ # Handle initial content if present
1363
1453
  if part.content and part.content.strip():
1364
- text_buffer[event.index].append(part.content)
1365
- token_count[event.index] += 1
1454
+ _print_response_banner()
1455
+ banner_printed.add(event.index)
1456
+ termflow_line_buffers[event.index] = part.content
1457
+ elif isinstance(part, ToolCallPart):
1458
+ streaming_parts.add(event.index)
1459
+ tool_parts.add(event.index)
1460
+ token_count[event.index] = 0 # Initialize token counter
1461
+ # Track tool name for display
1462
+ banner_printed.add(
1463
+ event.index
1464
+ ) # Use banner_printed to track if we've shown tool info
1366
1465
 
1367
1466
  # PartDeltaEvent - stream the content as it arrives
1368
1467
  elif isinstance(event, PartDeltaEvent):
@@ -1370,23 +1469,29 @@ class BaseAgent(ABC):
1370
1469
  delta = event.delta
1371
1470
  if isinstance(delta, (TextPartDelta, ThinkingPartDelta)):
1372
1471
  if delta.content_delta:
1373
- # For text parts, show token counter then render at end
1472
+ # For text parts, stream markdown with termflow
1374
1473
  if event.index in text_parts:
1375
- import sys
1376
-
1377
1474
  # Print banner on first content
1378
1475
  if event.index not in banner_printed:
1379
1476
  _print_response_banner()
1380
1477
  banner_printed.add(event.index)
1381
- # Accumulate text for final markdown render
1382
- text_buffer[event.index].append(delta.content_delta)
1383
- token_count[event.index] += 1
1384
- # Update token counter in place (single line)
1385
- count = token_count[event.index]
1386
- sys.stdout.write(
1387
- f"\r\x1b[K ⏳ Receiving... {count} tokens"
1478
+
1479
+ # Add content to line buffer
1480
+ termflow_line_buffers[event.index] += (
1481
+ delta.content_delta
1388
1482
  )
1389
- sys.stdout.flush()
1483
+
1484
+ # Process complete lines
1485
+ parser = termflow_parsers[event.index]
1486
+ renderer = termflow_renderers[event.index]
1487
+ buffer = termflow_line_buffers[event.index]
1488
+
1489
+ while "\n" in buffer:
1490
+ line, buffer = buffer.split("\n", 1)
1491
+ events_to_render = parser.parse_line(line)
1492
+ renderer.render_all(events_to_render)
1493
+
1494
+ termflow_line_buffers[event.index] = buffer
1390
1495
  else:
1391
1496
  # For thinking parts, stream immediately (dim)
1392
1497
  if event.index not in banner_printed:
@@ -1394,44 +1499,72 @@ class BaseAgent(ABC):
1394
1499
  banner_printed.add(event.index)
1395
1500
  escaped = escape(delta.content_delta)
1396
1501
  console.print(f"[dim]{escaped}[/dim]", end="")
1502
+ elif isinstance(delta, ToolCallPartDelta):
1503
+ # For tool calls, count chunks received
1504
+ token_count[event.index] += 1
1505
+ # Get tool name if available
1506
+ tool_name = getattr(delta, "tool_name_delta", "")
1507
+ count = token_count[event.index]
1508
+ # Display with tool wrench icon and tool name
1509
+ if tool_name:
1510
+ console.print(
1511
+ f" 🔧 Calling {tool_name}... {count} chunks ",
1512
+ end="\r",
1513
+ )
1514
+ else:
1515
+ console.print(
1516
+ f" 🔧 Calling tool... {count} chunks ",
1517
+ end="\r",
1518
+ )
1397
1519
 
1398
1520
  # PartEndEvent - finish the streaming with a newline
1399
1521
  elif isinstance(event, PartEndEvent):
1400
1522
  if event.index in streaming_parts:
1401
- # For text parts, clear counter line and render markdown
1523
+ # For text parts, finalize termflow rendering
1402
1524
  if event.index in text_parts:
1403
- import sys
1404
-
1405
- # Clear the token counter line
1406
- sys.stdout.write("\r\x1b[K")
1407
- sys.stdout.flush()
1408
- # Render the final markdown nicely
1409
- if event.index in text_buffer:
1410
- try:
1411
- final_content = "".join(text_buffer[event.index])
1412
- if final_content.strip():
1413
- console.print(Markdown(final_content))
1414
- except Exception:
1415
- pass
1416
- del text_buffer[event.index]
1417
- # Clean up token count
1418
- token_count.pop(event.index, None)
1525
+ # Render any remaining buffered content
1526
+ if event.index in termflow_parsers:
1527
+ parser = termflow_parsers[event.index]
1528
+ renderer = termflow_renderers[event.index]
1529
+ remaining = termflow_line_buffers.get(event.index, "")
1530
+
1531
+ # Parse and render any remaining partial line
1532
+ if remaining.strip():
1533
+ events_to_render = parser.parse_line(remaining)
1534
+ renderer.render_all(events_to_render)
1535
+
1536
+ # Finalize the parser to close any open blocks
1537
+ final_events = parser.finalize()
1538
+ renderer.render_all(final_events)
1539
+
1540
+ # Clean up termflow state
1541
+ del termflow_parsers[event.index]
1542
+ del termflow_renderers[event.index]
1543
+ del termflow_line_buffers[event.index]
1544
+ # For tool parts, clear the chunk counter line
1545
+ elif event.index in tool_parts:
1546
+ # Clear the chunk counter line by printing spaces and returning
1547
+ console.print(" " * 50, end="\r")
1419
1548
  # For thinking parts, just print newline
1420
1549
  elif event.index in banner_printed:
1421
1550
  console.print() # Final newline after streaming
1551
+
1552
+ # Clean up token count
1553
+ token_count.pop(event.index, None)
1422
1554
  # Clean up all tracking sets
1423
1555
  streaming_parts.discard(event.index)
1424
1556
  thinking_parts.discard(event.index)
1425
1557
  text_parts.discard(event.index)
1558
+ tool_parts.discard(event.index)
1426
1559
  banner_printed.discard(event.index)
1427
1560
 
1428
- # Resume spinner if next part is NOT text/thinking (avoid race condition)
1429
- # If next part is a tool call or None, it's safe to resume
1561
+ # Resume spinner if next part is NOT text/thinking/tool (avoid race condition)
1562
+ # If next part is None or handled differently, it's safe to resume
1430
1563
  # Note: spinner itself handles blank line before appearing
1431
1564
  from code_puppy.messaging.spinner import resume_all_spinners
1432
1565
 
1433
1566
  next_kind = getattr(event, "next_part_kind", None)
1434
- if next_kind not in ("text", "thinking"):
1567
+ if next_kind not in ("text", "thinking", "tool-call"):
1435
1568
  resume_all_spinners()
1436
1569
 
1437
1570
  # Spinner is resumed in PartEndEvent when appropriate (based on next_part_kind)
@@ -1590,6 +1723,7 @@ class BaseAgent(ABC):
1590
1723
  *,
1591
1724
  attachments: Optional[Sequence[BinaryContent]] = None,
1592
1725
  link_attachments: Optional[Sequence[Union[ImageUrl, DocumentUrl]]] = None,
1726
+ output_type: Optional[Type[Any]] = None,
1593
1727
  **kwargs,
1594
1728
  ) -> Any:
1595
1729
  """Run the agent with MCP servers, attachments, and full cancellation support.
@@ -1598,10 +1732,13 @@ class BaseAgent(ABC):
1598
1732
  prompt: Primary user prompt text (may be empty when attachments present).
1599
1733
  attachments: Local binary payloads (e.g., dragged images) to include.
1600
1734
  link_attachments: Remote assets (image/document URLs) to include.
1735
+ output_type: Optional Pydantic model or type for structured output.
1736
+ When provided, creates a temporary agent configured to return
1737
+ this type instead of the default string output.
1601
1738
  **kwargs: Additional arguments forwarded to `pydantic_ai.Agent.run`.
1602
1739
 
1603
1740
  Returns:
1604
- The agent's response.
1741
+ The agent's response (typed according to output_type if specified).
1605
1742
 
1606
1743
  Raises:
1607
1744
  asyncio.CancelledError: When execution is cancelled by user.
@@ -1625,6 +1762,11 @@ class BaseAgent(ABC):
1625
1762
  pydantic_agent = (
1626
1763
  self._code_generation_agent or self.reload_code_generation_agent()
1627
1764
  )
1765
+
1766
+ # If a custom output_type is specified, create a temporary agent with that type
1767
+ if output_type is not None:
1768
+ pydantic_agent = self._create_agent_with_output_type(output_type)
1769
+
1628
1770
  # Handle claude-code and chatgpt-codex models: prepend system prompt to first user message
1629
1771
  from code_puppy.model_utils import is_chatgpt_codex_model, is_claude_code_model
1630
1772
 
@@ -1821,7 +1963,12 @@ class BaseAgent(ABC):
1821
1963
  def graceful_sigint_handler(_sig, _frame):
1822
1964
  # When using keyboard-based cancel, SIGINT should be a no-op
1823
1965
  # (just show a hint to user about the configured cancel key)
1966
+ # Also reset terminal to prevent bricking on Windows+uvx
1824
1967
  from code_puppy.keymap import get_cancel_agent_display_name
1968
+ from code_puppy.terminal_utils import reset_windows_terminal_full
1969
+
1970
+ # Reset terminal state first to prevent bricking
1971
+ reset_windows_terminal_full()
1825
1972
 
1826
1973
  cancel_key = get_cancel_agent_display_name()
1827
1974
  emit_info(f"Use {cancel_key} to cancel the agent task.")