code-puppy 0.0.332__tar.gz → 0.0.333__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 (168) hide show
  1. {code_puppy-0.0.332 → code_puppy-0.0.333}/PKG-INFO +1 -1
  2. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/agents/base_agent.py +30 -72
  3. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/cli_runner.py +65 -6
  4. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/keymap.py +8 -0
  5. code_puppy-0.0.333/code_puppy/terminal_utils.py +291 -0
  6. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/tools/command_runner.py +4 -89
  7. code_puppy-0.0.333/code_puppy/uvx_detection.py +242 -0
  8. {code_puppy-0.0.332 → code_puppy-0.0.333}/pyproject.toml +1 -1
  9. code_puppy-0.0.332/code_puppy/terminal_utils.py +0 -130
  10. {code_puppy-0.0.332 → code_puppy-0.0.333}/.gitignore +0 -0
  11. {code_puppy-0.0.332 → code_puppy-0.0.333}/LICENSE +0 -0
  12. {code_puppy-0.0.332 → code_puppy-0.0.333}/README.md +0 -0
  13. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/__init__.py +0 -0
  14. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/__main__.py +0 -0
  15. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/agents/__init__.py +0 -0
  16. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/agents/agent_c_reviewer.py +0 -0
  17. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/agents/agent_code_puppy.py +0 -0
  18. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/agents/agent_code_reviewer.py +0 -0
  19. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/agents/agent_cpp_reviewer.py +0 -0
  20. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/agents/agent_creator_agent.py +0 -0
  21. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/agents/agent_golang_reviewer.py +0 -0
  22. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/agents/agent_javascript_reviewer.py +0 -0
  23. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/agents/agent_manager.py +0 -0
  24. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/agents/agent_planning.py +0 -0
  25. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/agents/agent_python_programmer.py +0 -0
  26. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/agents/agent_python_reviewer.py +0 -0
  27. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/agents/agent_qa_expert.py +0 -0
  28. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/agents/agent_qa_kitten.py +0 -0
  29. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/agents/agent_security_auditor.py +0 -0
  30. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/agents/agent_typescript_reviewer.py +0 -0
  31. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/agents/json_agent.py +0 -0
  32. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/agents/prompt_reviewer.py +0 -0
  33. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/callbacks.py +0 -0
  34. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/chatgpt_codex_client.py +0 -0
  35. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/claude_cache_client.py +0 -0
  36. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/command_line/__init__.py +0 -0
  37. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/command_line/add_model_menu.py +0 -0
  38. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/command_line/attachments.py +0 -0
  39. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/command_line/autosave_menu.py +0 -0
  40. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/command_line/colors_menu.py +0 -0
  41. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/command_line/command_handler.py +0 -0
  42. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/command_line/command_registry.py +0 -0
  43. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/command_line/config_commands.py +0 -0
  44. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/command_line/core_commands.py +0 -0
  45. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/command_line/diff_menu.py +0 -0
  46. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/command_line/file_path_completion.py +0 -0
  47. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/command_line/load_context_completion.py +0 -0
  48. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/command_line/mcp/__init__.py +0 -0
  49. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/command_line/mcp/add_command.py +0 -0
  50. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/command_line/mcp/base.py +0 -0
  51. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/command_line/mcp/catalog_server_installer.py +0 -0
  52. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/command_line/mcp/custom_server_form.py +0 -0
  53. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/command_line/mcp/custom_server_installer.py +0 -0
  54. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/command_line/mcp/edit_command.py +0 -0
  55. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/command_line/mcp/handler.py +0 -0
  56. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/command_line/mcp/help_command.py +0 -0
  57. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/command_line/mcp/install_command.py +0 -0
  58. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/command_line/mcp/install_menu.py +0 -0
  59. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/command_line/mcp/list_command.py +0 -0
  60. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/command_line/mcp/logs_command.py +0 -0
  61. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/command_line/mcp/remove_command.py +0 -0
  62. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/command_line/mcp/restart_command.py +0 -0
  63. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/command_line/mcp/search_command.py +0 -0
  64. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/command_line/mcp/start_all_command.py +0 -0
  65. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/command_line/mcp/start_command.py +0 -0
  66. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/command_line/mcp/status_command.py +0 -0
  67. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/command_line/mcp/stop_all_command.py +0 -0
  68. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/command_line/mcp/stop_command.py +0 -0
  69. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/command_line/mcp/test_command.py +0 -0
  70. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/command_line/mcp/utils.py +0 -0
  71. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/command_line/mcp/wizard_utils.py +0 -0
  72. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/command_line/mcp_completion.py +0 -0
  73. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/command_line/model_picker_completion.py +0 -0
  74. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/command_line/model_settings_menu.py +0 -0
  75. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/command_line/motd.py +0 -0
  76. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/command_line/pin_command_completion.py +0 -0
  77. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/command_line/prompt_toolkit_completion.py +0 -0
  78. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/command_line/session_commands.py +0 -0
  79. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/command_line/utils.py +0 -0
  80. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/config.py +0 -0
  81. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/error_logging.py +0 -0
  82. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/gemini_code_assist.py +0 -0
  83. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/http_utils.py +0 -0
  84. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/main.py +0 -0
  85. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/mcp_/__init__.py +0 -0
  86. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/mcp_/async_lifecycle.py +0 -0
  87. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/mcp_/blocking_startup.py +0 -0
  88. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/mcp_/captured_stdio_server.py +0 -0
  89. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/mcp_/circuit_breaker.py +0 -0
  90. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/mcp_/config_wizard.py +0 -0
  91. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/mcp_/dashboard.py +0 -0
  92. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/mcp_/error_isolation.py +0 -0
  93. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/mcp_/examples/retry_example.py +0 -0
  94. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/mcp_/health_monitor.py +0 -0
  95. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/mcp_/managed_server.py +0 -0
  96. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/mcp_/manager.py +0 -0
  97. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/mcp_/mcp_logs.py +0 -0
  98. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/mcp_/registry.py +0 -0
  99. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/mcp_/retry_manager.py +0 -0
  100. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/mcp_/server_registry_catalog.py +0 -0
  101. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/mcp_/status_tracker.py +0 -0
  102. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/mcp_/system_tools.py +0 -0
  103. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/messaging/__init__.py +0 -0
  104. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/messaging/bus.py +0 -0
  105. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/messaging/commands.py +0 -0
  106. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/messaging/markdown_patches.py +0 -0
  107. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/messaging/message_queue.py +0 -0
  108. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/messaging/messages.py +0 -0
  109. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/messaging/queue_console.py +0 -0
  110. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/messaging/renderers.py +0 -0
  111. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/messaging/rich_renderer.py +0 -0
  112. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/messaging/spinner/__init__.py +0 -0
  113. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/messaging/spinner/console_spinner.py +0 -0
  114. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/messaging/spinner/spinner_base.py +0 -0
  115. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/model_factory.py +0 -0
  116. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/model_utils.py +0 -0
  117. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/models.json +0 -0
  118. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/models_dev_api.json +0 -0
  119. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/models_dev_parser.py +0 -0
  120. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/plugins/__init__.py +0 -0
  121. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/plugins/chatgpt_oauth/__init__.py +0 -0
  122. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/plugins/chatgpt_oauth/config.py +0 -0
  123. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/plugins/chatgpt_oauth/oauth_flow.py +0 -0
  124. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/plugins/chatgpt_oauth/register_callbacks.py +0 -0
  125. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/plugins/chatgpt_oauth/test_plugin.py +0 -0
  126. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/plugins/chatgpt_oauth/utils.py +0 -0
  127. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/plugins/claude_code_oauth/README.md +0 -0
  128. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/plugins/claude_code_oauth/SETUP.md +0 -0
  129. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/plugins/claude_code_oauth/__init__.py +0 -0
  130. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/plugins/claude_code_oauth/config.py +0 -0
  131. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/plugins/claude_code_oauth/register_callbacks.py +0 -0
  132. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/plugins/claude_code_oauth/test_plugin.py +0 -0
  133. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/plugins/claude_code_oauth/utils.py +0 -0
  134. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/plugins/customizable_commands/__init__.py +0 -0
  135. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/plugins/customizable_commands/register_callbacks.py +0 -0
  136. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/plugins/example_custom_command/README.md +0 -0
  137. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/plugins/example_custom_command/register_callbacks.py +0 -0
  138. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/plugins/file_permission_handler/__init__.py +0 -0
  139. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/plugins/file_permission_handler/register_callbacks.py +0 -0
  140. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/plugins/oauth_puppy_html.py +0 -0
  141. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/plugins/shell_safety/__init__.py +0 -0
  142. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/plugins/shell_safety/agent_shell_safety.py +0 -0
  143. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/plugins/shell_safety/command_cache.py +0 -0
  144. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/plugins/shell_safety/register_callbacks.py +0 -0
  145. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/prompts/codex_system_prompt.md +0 -0
  146. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/pydantic_patches.py +0 -0
  147. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/reopenable_async_client.py +0 -0
  148. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/round_robin_model.py +0 -0
  149. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/session_storage.py +0 -0
  150. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/status_display.py +0 -0
  151. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/summarization_agent.py +0 -0
  152. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/tools/__init__.py +0 -0
  153. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/tools/agent_tools.py +0 -0
  154. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/tools/browser/__init__.py +0 -0
  155. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/tools/browser/browser_control.py +0 -0
  156. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/tools/browser/browser_interactions.py +0 -0
  157. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/tools/browser/browser_locators.py +0 -0
  158. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/tools/browser/browser_navigation.py +0 -0
  159. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/tools/browser/browser_screenshot.py +0 -0
  160. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/tools/browser/browser_scripts.py +0 -0
  161. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/tools/browser/browser_workflows.py +0 -0
  162. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/tools/browser/camoufox_manager.py +0 -0
  163. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/tools/browser/vqa_agent.py +0 -0
  164. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/tools/common.py +0 -0
  165. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/tools/file_modifications.py +0 -0
  166. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/tools/file_operations.py +0 -0
  167. {code_puppy-0.0.332 → code_puppy-0.0.333}/code_puppy/tools/tools_content.py +0 -0
  168. {code_puppy-0.0.332 → code_puppy-0.0.333}/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.332
3
+ Version: 0.0.333
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
@@ -75,8 +75,6 @@ from code_puppy.model_factory import ModelFactory, make_model_settings
75
75
  from code_puppy.summarization_agent import run_summarization_sync
76
76
  from code_puppy.tools.agent_tools import _active_subagent_tasks
77
77
  from code_puppy.tools.command_runner import (
78
- _add_windows_ctrl_handler,
79
- _remove_windows_ctrl_handler,
80
78
  is_awaiting_user_input,
81
79
  )
82
80
 
@@ -1384,15 +1382,14 @@ class BaseAgent(ABC):
1384
1382
  def _print_thinking_banner() -> None:
1385
1383
  """Print the THINKING banner with spinner pause and line clear."""
1386
1384
  nonlocal did_stream_anything
1387
- import sys
1388
1385
  import time
1389
1386
 
1390
1387
  from code_puppy.config import get_banner_color
1391
1388
 
1392
1389
  pause_all_spinners()
1393
1390
  time.sleep(0.1) # Delay to let spinner fully clear
1394
- sys.stdout.write("\r\x1b[K") # Clear line
1395
- sys.stdout.flush()
1391
+ # Clear line and print newline before banner
1392
+ console.print(" " * 50, end="\r")
1396
1393
  console.print() # Newline before banner
1397
1394
  # Bold banner with configurable color and lightning bolt
1398
1395
  thinking_color = get_banner_color("thinking")
@@ -1402,21 +1399,19 @@ class BaseAgent(ABC):
1402
1399
  ),
1403
1400
  end="",
1404
1401
  )
1405
- sys.stdout.flush()
1406
1402
  did_stream_anything = True
1407
1403
 
1408
1404
  def _print_response_banner() -> None:
1409
1405
  """Print the AGENT RESPONSE banner with spinner pause and line clear."""
1410
1406
  nonlocal did_stream_anything
1411
- import sys
1412
1407
  import time
1413
1408
 
1414
1409
  from code_puppy.config import get_banner_color
1415
1410
 
1416
1411
  pause_all_spinners()
1417
1412
  time.sleep(0.1) # Delay to let spinner fully clear
1418
- sys.stdout.write("\r\x1b[K") # Clear line
1419
- sys.stdout.flush()
1413
+ # Clear line and print newline before banner
1414
+ console.print(" " * 50, end="\r")
1420
1415
  console.print() # Newline before banner
1421
1416
  response_color = get_banner_color("agent_response")
1422
1417
  console.print(
@@ -1424,7 +1419,6 @@ class BaseAgent(ABC):
1424
1419
  f"[bold white on {response_color}] AGENT RESPONSE [/bold white on {response_color}]"
1425
1420
  )
1426
1421
  )
1427
- sys.stdout.flush()
1428
1422
  did_stream_anything = True
1429
1423
 
1430
1424
  async for event in events:
@@ -1448,8 +1442,8 @@ class BaseAgent(ABC):
1448
1442
  # Buffer initial content if present
1449
1443
  if part.content and part.content.strip():
1450
1444
  text_buffer[event.index].append(part.content)
1451
- # Use len(content) / 3 for token estimation (more accurate than chunk counting)
1452
- token_count[event.index] += len(part.content) // 3
1445
+ # Count chunks (each part counts as 1)
1446
+ token_count[event.index] += 1
1453
1447
  elif isinstance(part, ToolCallPart):
1454
1448
  streaming_parts.add(event.index)
1455
1449
  tool_parts.add(event.index)
@@ -1467,24 +1461,20 @@ class BaseAgent(ABC):
1467
1461
  if delta.content_delta:
1468
1462
  # For text parts, show token counter then render at end
1469
1463
  if event.index in text_parts:
1470
- import sys
1471
-
1472
1464
  # Print banner on first content
1473
1465
  if event.index not in banner_printed:
1474
1466
  _print_response_banner()
1475
1467
  banner_printed.add(event.index)
1476
1468
  # Accumulate text for final markdown render
1477
1469
  text_buffer[event.index].append(delta.content_delta)
1478
- # Use len(content) / 3 for token estimation
1479
- token_count[event.index] += (
1480
- len(delta.content_delta) // 3
1481
- )
1482
- # Update token counter in place (single line)
1470
+ # Count chunks received
1471
+ token_count[event.index] += 1
1472
+ # Update chunk counter in place (single line)
1483
1473
  count = token_count[event.index]
1484
- sys.stdout.write(
1485
- f"\r\x1b[K ⏳ Receiving... {count} tokens"
1474
+ console.print(
1475
+ f" ⏳ Receiving... {count} chunks ",
1476
+ end="\r",
1486
1477
  )
1487
- sys.stdout.flush()
1488
1478
  else:
1489
1479
  # For thinking parts, stream immediately (dim)
1490
1480
  if event.index not in banner_printed:
@@ -1493,34 +1483,30 @@ class BaseAgent(ABC):
1493
1483
  escaped = escape(delta.content_delta)
1494
1484
  console.print(f"[dim]{escaped}[/dim]", end="")
1495
1485
  elif isinstance(delta, ToolCallPartDelta):
1496
- import sys
1497
-
1498
- # For tool calls, show token counter (use string repr for estimation)
1499
- token_count[event.index] += len(str(delta)) // 3
1486
+ # For tool calls, count chunks received
1487
+ token_count[event.index] += 1
1500
1488
  # Get tool name if available
1501
1489
  tool_name = getattr(delta, "tool_name_delta", "")
1502
1490
  count = token_count[event.index]
1503
1491
  # Display with tool wrench icon and tool name
1504
1492
  if tool_name:
1505
- sys.stdout.write(
1506
- f"\r\x1b[K 🔧 Calling {tool_name}... {count} tokens"
1493
+ console.print(
1494
+ f" 🔧 Calling {tool_name}... {count} chunks ",
1495
+ end="\r",
1507
1496
  )
1508
1497
  else:
1509
- sys.stdout.write(
1510
- f"\r\x1b[K 🔧 Calling tool... {count} tokens"
1498
+ console.print(
1499
+ f" 🔧 Calling tool... {count} chunks ",
1500
+ end="\r",
1511
1501
  )
1512
- sys.stdout.flush()
1513
1502
 
1514
1503
  # PartEndEvent - finish the streaming with a newline
1515
1504
  elif isinstance(event, PartEndEvent):
1516
1505
  if event.index in streaming_parts:
1517
- import sys
1518
-
1519
1506
  # For text parts, clear counter line and render markdown
1520
1507
  if event.index in text_parts:
1521
- # Clear the token counter line
1522
- sys.stdout.write("\r\x1b[K")
1523
- sys.stdout.flush()
1508
+ # Clear the chunk counter line by printing spaces and returning
1509
+ console.print(" " * 50, end="\r")
1524
1510
  # Render the final markdown nicely
1525
1511
  if event.index in text_buffer:
1526
1512
  try:
@@ -1530,11 +1516,10 @@ class BaseAgent(ABC):
1530
1516
  except Exception:
1531
1517
  pass
1532
1518
  del text_buffer[event.index]
1533
- # For tool parts, clear the token counter line
1519
+ # For tool parts, clear the chunk counter line
1534
1520
  elif event.index in tool_parts:
1535
- # Clear the token counter line
1536
- sys.stdout.write("\r\x1b[K")
1537
- sys.stdout.flush()
1521
+ # Clear the chunk counter line by printing spaces and returning
1522
+ console.print(" " * 50, end="\r")
1538
1523
  # For thinking parts, just print newline
1539
1524
  elif event.index in banner_printed:
1540
1525
  console.print() # Final newline after streaming
@@ -1953,7 +1938,12 @@ class BaseAgent(ABC):
1953
1938
  def graceful_sigint_handler(_sig, _frame):
1954
1939
  # When using keyboard-based cancel, SIGINT should be a no-op
1955
1940
  # (just show a hint to user about the configured cancel key)
1941
+ # Also reset terminal to prevent bricking on Windows+uvx
1956
1942
  from code_puppy.keymap import get_cancel_agent_display_name
1943
+ from code_puppy.terminal_utils import reset_windows_terminal_full
1944
+
1945
+ # Reset terminal state first to prevent bricking
1946
+ reset_windows_terminal_full()
1957
1947
 
1958
1948
  cancel_key = get_cancel_agent_display_name()
1959
1949
  emit_info(f"Use {cancel_key} to cancel the agent task.")
@@ -1961,12 +1951,6 @@ class BaseAgent(ABC):
1961
1951
  original_handler = None
1962
1952
  key_listener_stop_event = None
1963
1953
  _key_listener_thread = None
1964
- windows_ctrl_handler = None
1965
-
1966
- # Check if we're on Windows
1967
- import sys
1968
-
1969
- is_windows = sys.platform.startswith("win")
1970
1954
 
1971
1955
  try:
1972
1956
  if cancel_agent_uses_signal():
@@ -1974,27 +1958,10 @@ class BaseAgent(ABC):
1974
1958
  original_handler = signal.signal(
1975
1959
  signal.SIGINT, keyboard_interrupt_handler
1976
1960
  )
1977
- # On Windows, also use SetConsoleCtrlHandler for reliable Ctrl+C with uvx
1978
- if is_windows:
1979
- windows_ctrl_handler = _add_windows_ctrl_handler(
1980
- schedule_agent_cancel
1981
- )
1982
1961
  else:
1983
1962
  # Use keyboard listener for agent cancellation
1984
1963
  # Set a graceful SIGINT handler that shows a hint
1985
1964
  original_handler = signal.signal(signal.SIGINT, graceful_sigint_handler)
1986
- # On Windows, SetConsoleCtrlHandler should also show the hint
1987
- if is_windows:
1988
-
1989
- def graceful_ctrl_handler():
1990
- from code_puppy.keymap import get_cancel_agent_display_name
1991
-
1992
- cancel_key = get_cancel_agent_display_name()
1993
- emit_info(f"Use {cancel_key} to cancel the agent task.")
1994
-
1995
- windows_ctrl_handler = _add_windows_ctrl_handler(
1996
- graceful_ctrl_handler
1997
- )
1998
1965
  # Spawn keyboard listener with the cancel agent callback
1999
1966
  key_listener_stop_event = threading.Event()
2000
1967
  _key_listener_thread = self._spawn_ctrl_x_key_listener(
@@ -2024,17 +1991,8 @@ class BaseAgent(ABC):
2024
1991
  # Stop keyboard listener if it was started
2025
1992
  if key_listener_stop_event is not None:
2026
1993
  key_listener_stop_event.set()
2027
- # Remove Windows console handler
2028
- if windows_ctrl_handler is not None:
2029
- _remove_windows_ctrl_handler(windows_ctrl_handler)
2030
1994
  # Restore original signal handler
2031
1995
  if (
2032
1996
  original_handler is not None
2033
1997
  ): # Explicit None check - SIG_DFL can be 0/falsy!
2034
1998
  signal.signal(signal.SIGINT, original_handler)
2035
- # Windows-specific: Reset terminal after Ctrl+C to prevent corruption
2036
- # This fixes the issue where Enter key shows as 'm' after interrupting with uvx
2037
- if is_windows:
2038
- from code_puppy.terminal_utils import reset_windows_terminal_full
2039
-
2040
- reset_windows_terminal_full()
@@ -171,6 +171,45 @@ async def main():
171
171
  emit_error(str(e))
172
172
  sys.exit(1)
173
173
 
174
+ # Show uvx detection notice if we're on Windows + uvx
175
+ # Also disable Ctrl+C at the console level to prevent terminal bricking
176
+ try:
177
+ from code_puppy.uvx_detection import should_use_alternate_cancel_key
178
+
179
+ if should_use_alternate_cancel_key():
180
+ from code_puppy.terminal_utils import (
181
+ disable_windows_ctrl_c,
182
+ set_keep_ctrl_c_disabled,
183
+ )
184
+
185
+ # Disable Ctrl+C at the console input level
186
+ # This prevents Ctrl+C from being processed as a signal at all
187
+ disable_windows_ctrl_c()
188
+
189
+ # Set flag to keep it disabled (prompt_toolkit may re-enable it)
190
+ set_keep_ctrl_c_disabled(True)
191
+
192
+ # Use print directly - emit_system_message can get cleared by ANSI codes
193
+ print(
194
+ "🔧 Detected uvx launch on Windows - using Ctrl+K for cancellation "
195
+ "(Ctrl+C is disabled to prevent terminal issues)"
196
+ )
197
+
198
+ # Also install a SIGINT handler as backup
199
+ import signal
200
+
201
+ from code_puppy.terminal_utils import reset_windows_terminal_full
202
+
203
+ def _uvx_protective_sigint_handler(_sig, _frame):
204
+ """Protective SIGINT handler for Windows+uvx."""
205
+ reset_windows_terminal_full()
206
+ # Re-disable Ctrl+C in case something re-enabled it
207
+ disable_windows_ctrl_c()
208
+
209
+ signal.signal(signal.SIGINT, _uvx_protective_sigint_handler)
210
+ except ImportError:
211
+ pass # uvx_detection module not available, ignore
212
+
174
213
  # Load API keys from puppy.cfg into environment variables
175
214
  from code_puppy.config import load_api_keys_to_environment
176
215
 
@@ -421,10 +460,6 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
421
460
  current_agent_task = None
422
461
 
423
462
  while True:
424
- # Windows-specific: Aggressively reset terminal at the start of every loop
425
- # This fixes terminal corruption after Ctrl+C, especially when running via uvx
426
- reset_windows_terminal_full()
427
-
428
463
  from code_puppy.agents.agent_manager import get_current_agent
429
464
  from code_puppy.messaging import emit_info
430
465
 
@@ -444,6 +479,15 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
444
479
  task = await get_input_with_combined_completion(
445
480
  get_prompt_with_active_model(), history_file=COMMAND_HISTORY_FILE
446
481
  )
482
+
483
+ # Windows+uvx: Re-disable Ctrl+C after prompt_toolkit
484
+ # (prompt_toolkit restores console mode which re-enables Ctrl+C)
485
+ try:
486
+ from code_puppy.terminal_utils import ensure_ctrl_c_disabled
487
+
488
+ ensure_ctrl_c_disabled()
489
+ except ImportError:
490
+ pass
447
491
  except ImportError:
448
492
  # Fall back to basic input if prompt_toolkit is not available
449
493
  task = input(">>> ")
@@ -608,8 +652,14 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
608
652
  # Check if the task was cancelled (but don't show message if we just killed processes)
609
653
  if result is None:
610
654
  # Windows-specific: Reset terminal state after cancellation
611
- # Use full reset (ANSI + console mode) to fix stdin after Ctrl+C
612
- reset_windows_terminal_full()
655
+ reset_windows_terminal_ansi()
656
+ # Re-disable Ctrl+C if needed (uvx mode)
657
+ try:
658
+ from code_puppy.terminal_utils import ensure_ctrl_c_disabled
659
+
660
+ ensure_ctrl_c_disabled()
661
+ except ImportError:
662
+ pass
613
663
  continue
614
664
  # Get the structured response
615
665
  agent_response = result.output
@@ -650,6 +700,15 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
650
700
 
651
701
  auto_save_session_if_enabled()
652
702
 
703
+ # Re-disable Ctrl+C if needed (uvx mode) - must be done after
704
+ # each iteration as various operations may restore console mode
705
+ try:
706
+ from code_puppy.terminal_utils import ensure_ctrl_c_disabled
707
+
708
+ ensure_ctrl_c_disabled()
709
+ except ImportError:
710
+ pass
711
+
653
712
 
654
713
  def prettier_code_blocks():
655
714
  """Configure Rich to use prettier code block rendering."""
@@ -55,11 +55,19 @@ class KeymapError(Exception):
55
55
  def get_cancel_agent_key() -> str:
56
56
  """Get the configured cancel agent key from config.
57
57
 
58
+ On Windows when launched via uvx, this automatically returns "ctrl+k"
59
+ to work around uvx capturing Ctrl+C before it reaches Python.
60
+
58
61
  Returns:
59
62
  The key name (e.g., "ctrl+c", "ctrl+k") from config,
60
63
  or the default if not configured.
61
64
  """
62
65
  from code_puppy.config import get_value
66
+ from code_puppy.uvx_detection import should_use_alternate_cancel_key
67
+
68
+ # On Windows + uvx, force ctrl+k to bypass uvx's SIGINT capture
69
+ if should_use_alternate_cancel_key():
70
+ return "ctrl+k"
63
71
 
64
72
  key = get_value("cancel_agent_key")
65
73
  if key is None or key.strip() == "":
@@ -0,0 +1,291 @@
1
+ """Terminal utilities for cross-platform terminal state management.
2
+
3
+ Handles Windows console mode resets and Unix terminal sanity restoration.
4
+ """
5
+
6
+ import platform
7
+ import subprocess
8
+ import sys
9
+ from typing import Callable, Optional
10
+
11
+ # Store the original console ctrl handler so we can restore it if needed
12
+ _original_ctrl_handler: Optional[Callable] = None
13
+
14
+
15
+ def reset_windows_terminal_ansi() -> None:
16
+ """Reset ANSI formatting on Windows stdout/stderr.
17
+
18
+ This is a lightweight reset that just clears ANSI escape sequences.
19
+ Use this for quick resets after output operations.
20
+ """
21
+ if platform.system() != "Windows":
22
+ return
23
+
24
+ try:
25
+ sys.stdout.write("\x1b[0m") # Reset ANSI formatting
26
+ sys.stdout.flush()
27
+ sys.stderr.write("\x1b[0m")
28
+ sys.stderr.flush()
29
+ except Exception:
30
+ pass # Silently ignore errors - best effort reset
31
+
32
+
33
+ def reset_windows_console_mode() -> None:
34
+ """Full Windows console mode reset using ctypes.
35
+
36
+ This resets both stdout and stdin console modes to restore proper
37
+ terminal behavior after interrupts (Ctrl+C, Ctrl+D). Without this,
38
+ the terminal can become unresponsive (can't type characters).
39
+ """
40
+ if platform.system() != "Windows":
41
+ return
42
+
43
+ try:
44
+ import ctypes
45
+
46
+ kernel32 = ctypes.windll.kernel32
47
+
48
+ # Reset stdout
49
+ STD_OUTPUT_HANDLE = -11
50
+ handle = kernel32.GetStdHandle(STD_OUTPUT_HANDLE)
51
+
52
+ # Enable virtual terminal processing and line input
53
+ mode = ctypes.c_ulong()
54
+ kernel32.GetConsoleMode(handle, ctypes.byref(mode))
55
+
56
+ # Console mode flags for stdout
57
+ ENABLE_PROCESSED_OUTPUT = 0x0001
58
+ ENABLE_WRAP_AT_EOL_OUTPUT = 0x0002
59
+ ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
60
+
61
+ new_mode = (
62
+ mode.value
63
+ | ENABLE_PROCESSED_OUTPUT
64
+ | ENABLE_WRAP_AT_EOL_OUTPUT
65
+ | ENABLE_VIRTUAL_TERMINAL_PROCESSING
66
+ )
67
+ kernel32.SetConsoleMode(handle, new_mode)
68
+
69
+ # Reset stdin
70
+ STD_INPUT_HANDLE = -10
71
+ stdin_handle = kernel32.GetStdHandle(STD_INPUT_HANDLE)
72
+
73
+ # Console mode flags for stdin
74
+ ENABLE_LINE_INPUT = 0x0002
75
+ ENABLE_ECHO_INPUT = 0x0004
76
+ ENABLE_PROCESSED_INPUT = 0x0001
77
+
78
+ stdin_mode = ctypes.c_ulong()
79
+ kernel32.GetConsoleMode(stdin_handle, ctypes.byref(stdin_mode))
80
+
81
+ new_stdin_mode = (
82
+ stdin_mode.value
83
+ | ENABLE_LINE_INPUT
84
+ | ENABLE_ECHO_INPUT
85
+ | ENABLE_PROCESSED_INPUT
86
+ )
87
+ kernel32.SetConsoleMode(stdin_handle, new_stdin_mode)
88
+
89
+ except Exception:
90
+ pass # Silently ignore errors - best effort reset
91
+
92
+
93
+ def flush_windows_keyboard_buffer() -> None:
94
+ """Flush the Windows keyboard buffer.
95
+
96
+ Clears any pending keyboard input that could interfere with
97
+ subsequent input operations after an interrupt.
98
+ """
99
+ if platform.system() != "Windows":
100
+ return
101
+
102
+ try:
103
+ import msvcrt
104
+
105
+ while msvcrt.kbhit():
106
+ msvcrt.getch()
107
+ except Exception:
108
+ pass # Silently ignore errors - best effort flush
109
+
110
+
111
+ def reset_windows_terminal_full() -> None:
112
+ """Perform a full Windows terminal reset (ANSI + console mode + keyboard buffer).
113
+
114
+ Combines ANSI reset, console mode reset, and keyboard buffer flush
115
+ for complete terminal state restoration after interrupts.
116
+ """
117
+ if platform.system() != "Windows":
118
+ return
119
+
120
+ reset_windows_terminal_ansi()
121
+ reset_windows_console_mode()
122
+ flush_windows_keyboard_buffer()
123
+
124
+
125
+ def reset_unix_terminal() -> None:
126
+ """Reset Unix/Linux/macOS terminal to sane state.
127
+
128
+ Uses the `reset` command to restore terminal sanity.
129
+ Silently fails if the command isn't available.
130
+ """
131
+ if platform.system() == "Windows":
132
+ return
133
+
134
+ try:
135
+ subprocess.run(["reset"], check=True, capture_output=True)
136
+ except (subprocess.CalledProcessError, FileNotFoundError):
137
+ pass # Silently fail if reset command isn't available
138
+
139
+
140
+ def reset_terminal() -> None:
141
+ """Cross-platform terminal reset.
142
+
143
+ Automatically detects the platform and performs the appropriate
144
+ terminal reset operation.
145
+ """
146
+ if platform.system() == "Windows":
147
+ reset_windows_terminal_full()
148
+ else:
149
+ reset_unix_terminal()
150
+
151
+
152
+ def disable_windows_ctrl_c() -> bool:
153
+ """Disable Ctrl+C processing at the Windows console input level.
154
+
155
+ This removes ENABLE_PROCESSED_INPUT from stdin, which prevents
156
+ Ctrl+C from being interpreted as a signal at all. Instead, it
157
+ becomes just a regular character (^C) that gets ignored.
158
+
159
+ This is more reliable than SetConsoleCtrlHandler because it
160
+ prevents Ctrl+C from being processed before it reaches any handler.
161
+
162
+ Returns:
163
+ True if successfully disabled, False otherwise.
164
+ """
165
+ global _original_ctrl_handler
166
+
167
+ if platform.system() != "Windows":
168
+ return False
169
+
170
+ try:
171
+ import ctypes
172
+
173
+ kernel32 = ctypes.windll.kernel32
174
+
175
+ # Get stdin handle
176
+ STD_INPUT_HANDLE = -10
177
+ stdin_handle = kernel32.GetStdHandle(STD_INPUT_HANDLE)
178
+
179
+ # Get current console mode
180
+ mode = ctypes.c_ulong()
181
+ if not kernel32.GetConsoleMode(stdin_handle, ctypes.byref(mode)):
182
+ return False
183
+
184
+ # Save original mode for potential restoration
185
+ _original_ctrl_handler = mode.value
186
+
187
+ # Console mode flags
188
+ ENABLE_PROCESSED_INPUT = 0x0001 # This makes Ctrl+C generate signals
189
+
190
+ # Remove ENABLE_PROCESSED_INPUT to disable Ctrl+C signal generation
191
+ new_mode = mode.value & ~ENABLE_PROCESSED_INPUT
192
+
193
+ if kernel32.SetConsoleMode(stdin_handle, new_mode):
194
+ return True
195
+ return False
196
+
197
+ except Exception:
198
+ return False
199
+
200
+
201
+ def enable_windows_ctrl_c() -> bool:
202
+ """Re-enable Ctrl+C at the Windows console level.
203
+
204
+ Restores the original console mode saved by disable_windows_ctrl_c().
205
+
206
+ Returns:
207
+ True if successfully re-enabled, False otherwise.
208
+ """
209
+ global _original_ctrl_handler
210
+
211
+ if platform.system() != "Windows":
212
+ return False
213
+
214
+ if _original_ctrl_handler is None:
215
+ return True # Nothing to restore
216
+
217
+ try:
218
+ import ctypes
219
+
220
+ kernel32 = ctypes.windll.kernel32
221
+
222
+ # Get stdin handle
223
+ STD_INPUT_HANDLE = -10
224
+ stdin_handle = kernel32.GetStdHandle(STD_INPUT_HANDLE)
225
+
226
+ # Restore original mode
227
+ if kernel32.SetConsoleMode(stdin_handle, _original_ctrl_handler):
228
+ _original_ctrl_handler = None
229
+ return True
230
+ return False
231
+
232
+ except Exception:
233
+ return False
234
+
235
+
236
+ # Flag to track if we should keep Ctrl+C disabled
237
+ _keep_ctrl_c_disabled: bool = False
238
+
239
+
240
+ def set_keep_ctrl_c_disabled(value: bool) -> None:
241
+ """Set whether Ctrl+C should be kept disabled.
242
+
243
+ When True, ensure_ctrl_c_disabled() will re-disable Ctrl+C
244
+ even if something else (like prompt_toolkit) re-enables it.
245
+ """
246
+ global _keep_ctrl_c_disabled
247
+ _keep_ctrl_c_disabled = value
248
+
249
+
250
+ def ensure_ctrl_c_disabled() -> bool:
251
+ """Ensure Ctrl+C is disabled if it should be.
252
+
253
+ Call this after operations that might restore console mode
254
+ (like prompt_toolkit input).
255
+
256
+ Returns:
257
+ True if Ctrl+C is now disabled (or wasn't needed), False on error.
258
+ """
259
+ if not _keep_ctrl_c_disabled:
260
+ return True
261
+
262
+ if platform.system() != "Windows":
263
+ return True
264
+
265
+ try:
266
+ import ctypes
267
+
268
+ kernel32 = ctypes.windll.kernel32
269
+
270
+ # Get stdin handle
271
+ STD_INPUT_HANDLE = -10
272
+ stdin_handle = kernel32.GetStdHandle(STD_INPUT_HANDLE)
273
+
274
+ # Get current console mode
275
+ mode = ctypes.c_ulong()
276
+ if not kernel32.GetConsoleMode(stdin_handle, ctypes.byref(mode)):
277
+ return False
278
+
279
+ # Console mode flags
280
+ ENABLE_PROCESSED_INPUT = 0x0001
281
+
282
+ # Check if Ctrl+C processing is enabled
283
+ if mode.value & ENABLE_PROCESSED_INPUT:
284
+ # Disable it
285
+ new_mode = mode.value & ~ENABLE_PROCESSED_INPUT
286
+ return bool(kernel32.SetConsoleMode(stdin_handle, new_mode))
287
+
288
+ return True # Already disabled
289
+
290
+ except Exception:
291
+ return False