code-puppy 0.0.314__tar.gz → 0.0.315__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 (164) hide show
  1. {code_puppy-0.0.314 → code_puppy-0.0.315}/PKG-INFO +1 -1
  2. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/agents/base_agent.py +223 -0
  3. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/chatgpt_codex_client.py +3 -1
  4. code_puppy-0.0.314/code_puppy/main.py → code_puppy-0.0.315/code_puppy/cli_runner.py +20 -32
  5. code_puppy-0.0.315/code_puppy/command_line/colors_menu.py +515 -0
  6. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/command_line/config_commands.py +31 -0
  7. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/command_line/model_settings_menu.py +46 -6
  8. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/command_line/prompt_toolkit_completion.py +4 -9
  9. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/config.py +100 -12
  10. code_puppy-0.0.315/code_puppy/main.py +10 -0
  11. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/messaging/rich_renderer.py +66 -43
  12. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/messaging/spinner/console_spinner.py +6 -0
  13. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/model_factory.py +2 -2
  14. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/models.json +4 -12
  15. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/plugins/chatgpt_oauth/utils.py +11 -0
  16. code_puppy-0.0.315/code_puppy/terminal_utils.py +126 -0
  17. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/tools/agent_tools.py +4 -1
  18. {code_puppy-0.0.314 → code_puppy-0.0.315}/pyproject.toml +2 -1
  19. {code_puppy-0.0.314 → code_puppy-0.0.315}/.gitignore +0 -0
  20. {code_puppy-0.0.314 → code_puppy-0.0.315}/LICENSE +0 -0
  21. {code_puppy-0.0.314 → code_puppy-0.0.315}/README.md +0 -0
  22. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/__init__.py +0 -0
  23. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/__main__.py +0 -0
  24. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/agents/__init__.py +0 -0
  25. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/agents/agent_c_reviewer.py +0 -0
  26. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/agents/agent_code_puppy.py +0 -0
  27. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/agents/agent_code_reviewer.py +0 -0
  28. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/agents/agent_cpp_reviewer.py +0 -0
  29. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/agents/agent_creator_agent.py +0 -0
  30. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/agents/agent_golang_reviewer.py +0 -0
  31. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/agents/agent_javascript_reviewer.py +0 -0
  32. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/agents/agent_manager.py +0 -0
  33. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/agents/agent_planning.py +0 -0
  34. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/agents/agent_python_programmer.py +0 -0
  35. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/agents/agent_python_reviewer.py +0 -0
  36. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/agents/agent_qa_expert.py +0 -0
  37. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/agents/agent_qa_kitten.py +0 -0
  38. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/agents/agent_security_auditor.py +0 -0
  39. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/agents/agent_typescript_reviewer.py +0 -0
  40. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/agents/json_agent.py +0 -0
  41. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/agents/prompt_reviewer.py +0 -0
  42. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/callbacks.py +0 -0
  43. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/claude_cache_client.py +0 -0
  44. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/command_line/__init__.py +0 -0
  45. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/command_line/add_model_menu.py +0 -0
  46. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/command_line/attachments.py +0 -0
  47. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/command_line/autosave_menu.py +0 -0
  48. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/command_line/command_handler.py +0 -0
  49. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/command_line/command_registry.py +0 -0
  50. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/command_line/core_commands.py +0 -0
  51. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/command_line/diff_menu.py +0 -0
  52. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/command_line/file_path_completion.py +0 -0
  53. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/command_line/load_context_completion.py +0 -0
  54. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/command_line/mcp/__init__.py +0 -0
  55. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/command_line/mcp/add_command.py +0 -0
  56. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/command_line/mcp/base.py +0 -0
  57. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/command_line/mcp/catalog_server_installer.py +0 -0
  58. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/command_line/mcp/custom_server_form.py +0 -0
  59. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/command_line/mcp/custom_server_installer.py +0 -0
  60. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/command_line/mcp/edit_command.py +0 -0
  61. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/command_line/mcp/handler.py +0 -0
  62. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/command_line/mcp/help_command.py +0 -0
  63. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/command_line/mcp/install_command.py +0 -0
  64. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/command_line/mcp/install_menu.py +0 -0
  65. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/command_line/mcp/list_command.py +0 -0
  66. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/command_line/mcp/logs_command.py +0 -0
  67. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/command_line/mcp/remove_command.py +0 -0
  68. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/command_line/mcp/restart_command.py +0 -0
  69. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/command_line/mcp/search_command.py +0 -0
  70. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/command_line/mcp/start_all_command.py +0 -0
  71. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/command_line/mcp/start_command.py +0 -0
  72. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/command_line/mcp/status_command.py +0 -0
  73. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/command_line/mcp/stop_all_command.py +0 -0
  74. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/command_line/mcp/stop_command.py +0 -0
  75. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/command_line/mcp/test_command.py +0 -0
  76. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/command_line/mcp/utils.py +0 -0
  77. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/command_line/mcp/wizard_utils.py +0 -0
  78. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/command_line/mcp_completion.py +0 -0
  79. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/command_line/model_picker_completion.py +0 -0
  80. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/command_line/motd.py +0 -0
  81. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/command_line/pin_command_completion.py +0 -0
  82. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/command_line/session_commands.py +0 -0
  83. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/command_line/utils.py +0 -0
  84. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/error_logging.py +0 -0
  85. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/gemini_code_assist.py +0 -0
  86. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/http_utils.py +0 -0
  87. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/keymap.py +0 -0
  88. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/mcp_/__init__.py +0 -0
  89. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/mcp_/async_lifecycle.py +0 -0
  90. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/mcp_/blocking_startup.py +0 -0
  91. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/mcp_/captured_stdio_server.py +0 -0
  92. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/mcp_/circuit_breaker.py +0 -0
  93. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/mcp_/config_wizard.py +0 -0
  94. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/mcp_/dashboard.py +0 -0
  95. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/mcp_/error_isolation.py +0 -0
  96. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/mcp_/examples/retry_example.py +0 -0
  97. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/mcp_/health_monitor.py +0 -0
  98. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/mcp_/managed_server.py +0 -0
  99. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/mcp_/manager.py +0 -0
  100. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/mcp_/registry.py +0 -0
  101. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/mcp_/retry_manager.py +0 -0
  102. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/mcp_/server_registry_catalog.py +0 -0
  103. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/mcp_/status_tracker.py +0 -0
  104. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/mcp_/system_tools.py +0 -0
  105. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/messaging/__init__.py +0 -0
  106. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/messaging/bus.py +0 -0
  107. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/messaging/commands.py +0 -0
  108. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/messaging/message_queue.py +0 -0
  109. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/messaging/messages.py +0 -0
  110. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/messaging/queue_console.py +0 -0
  111. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/messaging/renderers.py +0 -0
  112. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/messaging/spinner/__init__.py +0 -0
  113. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/messaging/spinner/spinner_base.py +0 -0
  114. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/model_utils.py +0 -0
  115. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/models_dev_api.json +0 -0
  116. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/models_dev_parser.py +0 -0
  117. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/plugins/__init__.py +0 -0
  118. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/plugins/chatgpt_oauth/__init__.py +0 -0
  119. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/plugins/chatgpt_oauth/config.py +0 -0
  120. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/plugins/chatgpt_oauth/oauth_flow.py +0 -0
  121. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/plugins/chatgpt_oauth/register_callbacks.py +0 -0
  122. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/plugins/chatgpt_oauth/test_plugin.py +0 -0
  123. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/plugins/claude_code_oauth/README.md +0 -0
  124. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/plugins/claude_code_oauth/SETUP.md +0 -0
  125. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/plugins/claude_code_oauth/__init__.py +0 -0
  126. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/plugins/claude_code_oauth/config.py +0 -0
  127. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/plugins/claude_code_oauth/register_callbacks.py +0 -0
  128. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/plugins/claude_code_oauth/test_plugin.py +0 -0
  129. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/plugins/claude_code_oauth/utils.py +0 -0
  130. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/plugins/customizable_commands/__init__.py +0 -0
  131. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/plugins/customizable_commands/register_callbacks.py +0 -0
  132. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/plugins/example_custom_command/README.md +0 -0
  133. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/plugins/example_custom_command/register_callbacks.py +0 -0
  134. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/plugins/file_permission_handler/__init__.py +0 -0
  135. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/plugins/file_permission_handler/register_callbacks.py +0 -0
  136. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/plugins/oauth_puppy_html.py +0 -0
  137. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/plugins/shell_safety/__init__.py +0 -0
  138. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/plugins/shell_safety/agent_shell_safety.py +0 -0
  139. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/plugins/shell_safety/command_cache.py +0 -0
  140. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/plugins/shell_safety/register_callbacks.py +0 -0
  141. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/prompts/codex_system_prompt.md +0 -0
  142. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/pydantic_patches.py +0 -0
  143. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/reopenable_async_client.py +0 -0
  144. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/round_robin_model.py +0 -0
  145. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/session_storage.py +0 -0
  146. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/status_display.py +0 -0
  147. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/summarization_agent.py +0 -0
  148. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/tools/__init__.py +0 -0
  149. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/tools/browser/__init__.py +0 -0
  150. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/tools/browser/browser_control.py +0 -0
  151. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/tools/browser/browser_interactions.py +0 -0
  152. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/tools/browser/browser_locators.py +0 -0
  153. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/tools/browser/browser_navigation.py +0 -0
  154. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/tools/browser/browser_screenshot.py +0 -0
  155. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/tools/browser/browser_scripts.py +0 -0
  156. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/tools/browser/browser_workflows.py +0 -0
  157. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/tools/browser/camoufox_manager.py +0 -0
  158. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/tools/browser/vqa_agent.py +0 -0
  159. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/tools/command_runner.py +0 -0
  160. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/tools/common.py +0 -0
  161. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/tools/file_modifications.py +0 -0
  162. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/tools/file_operations.py +0 -0
  163. {code_puppy-0.0.314 → code_puppy-0.0.315}/code_puppy/tools/tools_content.py +0 -0
  164. {code_puppy-0.0.314 → code_puppy-0.0.315}/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.314
3
+ Version: 0.0.315
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
@@ -7,6 +7,7 @@ import signal
7
7
  import threading
8
8
  import uuid
9
9
  from abc import ABC, abstractmethod
10
+ from collections.abc import AsyncIterable
10
11
  from typing import Any, Callable, Dict, List, Optional, Sequence, Set, Tuple, Union
11
12
 
12
13
  import mcp
@@ -18,6 +19,7 @@ from pydantic_ai import (
18
19
  BinaryContent,
19
20
  DocumentUrl,
20
21
  ImageUrl,
22
+ PartEndEvent,
21
23
  RunContext,
22
24
  UsageLimitExceeded,
23
25
  UsageLimits,
@@ -1265,6 +1267,224 @@ class BaseAgent(ABC):
1265
1267
  self.set_message_history(result_messages_filtered_empty_thinking)
1266
1268
  return self.get_message_history()
1267
1269
 
1270
+ async def _event_stream_handler(
1271
+ self, ctx: RunContext, events: AsyncIterable[Any]
1272
+ ) -> None:
1273
+ """Handle streaming events from the agent run.
1274
+
1275
+ This method processes streaming events and emits TextPart and ThinkingPart
1276
+ content with styled banners as they stream in.
1277
+
1278
+ Args:
1279
+ ctx: The run context.
1280
+ events: Async iterable of streaming events (PartStartEvent, PartDeltaEvent, etc.).
1281
+ """
1282
+ import os
1283
+ import time as time_module
1284
+
1285
+ from pydantic_ai import PartDeltaEvent, PartStartEvent
1286
+ from pydantic_ai.messages import TextPartDelta, ThinkingPartDelta
1287
+ from rich.console import Console
1288
+ from rich.live import Live
1289
+ from rich.markdown import Markdown
1290
+ from rich.markup import escape
1291
+
1292
+ from code_puppy.messaging.spinner import pause_all_spinners
1293
+
1294
+ console = Console()
1295
+
1296
+ # Disable Live display in test mode or non-interactive environments
1297
+ # This fixes issues with pexpect PTY where Live() hangs
1298
+ use_live_display = (
1299
+ console.is_terminal
1300
+ and os.environ.get("CODE_PUPPY_TEST_FAST", "").lower() not in ("1", "true")
1301
+ and os.environ.get("CI", "").lower() not in ("1", "true")
1302
+ )
1303
+
1304
+ # Track which part indices we're currently streaming (for Text/Thinking parts)
1305
+ streaming_parts: set[int] = set()
1306
+ thinking_parts: set[int] = (
1307
+ set()
1308
+ ) # Track which parts are thinking (for dim style)
1309
+ text_parts: set[int] = set() # Track which parts are text
1310
+ banner_printed: set[int] = set() # Track if banner was already printed
1311
+ text_buffer: dict[int, list[str]] = {} # Buffer text for markdown
1312
+ live_displays: dict[int, Live] = {} # Live displays for streaming markdown
1313
+ did_stream_anything = False # Track if we streamed any content
1314
+ last_render_time: dict[int, float] = {} # Track last render time per part
1315
+ render_interval = 0.1 # Only re-render markdown every 100ms (throttle)
1316
+
1317
+ def _print_thinking_banner() -> None:
1318
+ """Print the THINKING banner with spinner pause and line clear."""
1319
+ nonlocal did_stream_anything
1320
+ import sys
1321
+ import time
1322
+
1323
+ from code_puppy.config import get_banner_color
1324
+
1325
+ pause_all_spinners()
1326
+ time.sleep(0.1) # Delay to let spinner fully clear
1327
+ sys.stdout.write("\r\x1b[K") # Clear line
1328
+ sys.stdout.flush()
1329
+ console.print() # Newline before banner
1330
+ # Bold banner with configurable color and lightning bolt
1331
+ thinking_color = get_banner_color("thinking")
1332
+ console.print(
1333
+ Text.from_markup(
1334
+ f"[bold white on {thinking_color}] THINKING [/bold white on {thinking_color}] [dim]⚡ "
1335
+ ),
1336
+ end="",
1337
+ )
1338
+ sys.stdout.flush()
1339
+ did_stream_anything = True
1340
+
1341
+ def _print_response_banner() -> None:
1342
+ """Print the AGENT RESPONSE banner with spinner pause and line clear."""
1343
+ nonlocal did_stream_anything
1344
+ import sys
1345
+ import time
1346
+
1347
+ from code_puppy.config import get_banner_color
1348
+
1349
+ pause_all_spinners()
1350
+ time.sleep(0.1) # Delay to let spinner fully clear
1351
+ sys.stdout.write("\r\x1b[K") # Clear line
1352
+ sys.stdout.flush()
1353
+ console.print() # Newline before banner
1354
+ response_color = get_banner_color("agent_response")
1355
+ console.print(
1356
+ Text.from_markup(
1357
+ f"[bold white on {response_color}] AGENT RESPONSE [/bold white on {response_color}]"
1358
+ )
1359
+ )
1360
+ sys.stdout.flush()
1361
+ did_stream_anything = True
1362
+
1363
+ async for event in events:
1364
+ # PartStartEvent - register the part but defer banner until content arrives
1365
+ if isinstance(event, PartStartEvent):
1366
+ part = event.part
1367
+ if isinstance(part, ThinkingPart):
1368
+ streaming_parts.add(event.index)
1369
+ thinking_parts.add(event.index)
1370
+ # If there's initial content, print banner + content now
1371
+ if part.content and part.content.strip():
1372
+ _print_thinking_banner()
1373
+ escaped = escape(part.content)
1374
+ console.print(f"[dim]{escaped}[/dim]", end="")
1375
+ banner_printed.add(event.index)
1376
+ elif isinstance(part, TextPart):
1377
+ streaming_parts.add(event.index)
1378
+ text_parts.add(event.index)
1379
+ text_buffer[event.index] = [] # Initialize buffer
1380
+ # Buffer initial content if present
1381
+ if part.content and part.content.strip():
1382
+ text_buffer[event.index].append(part.content)
1383
+
1384
+ # PartDeltaEvent - stream the content as it arrives
1385
+ elif isinstance(event, PartDeltaEvent):
1386
+ if event.index in streaming_parts:
1387
+ delta = event.delta
1388
+ if isinstance(delta, (TextPartDelta, ThinkingPartDelta)):
1389
+ if delta.content_delta:
1390
+ # For text parts, stream markdown with Live display
1391
+ if event.index in text_parts:
1392
+ # Print banner and start Live on first content
1393
+ if event.index not in banner_printed:
1394
+ _print_response_banner()
1395
+ banner_printed.add(event.index)
1396
+ # Only use Live display if enabled (disabled in test/CI)
1397
+ if use_live_display:
1398
+ live = Live(
1399
+ Markdown(""),
1400
+ console=console,
1401
+ refresh_per_second=10,
1402
+ vertical_overflow="visible", # Allow scrolling for long content
1403
+ )
1404
+ live.start()
1405
+ live_displays[event.index] = live
1406
+ # Accumulate text and throttle markdown rendering
1407
+ # (Markdown parsing is O(n), doing it on every token = O(n²) death)
1408
+ text_buffer[event.index].append(delta.content_delta)
1409
+ now = time_module.monotonic()
1410
+ last_render = last_render_time.get(event.index, 0)
1411
+
1412
+ # Only re-render if enough time has passed (throttle)
1413
+ # Skip Live updates when not using live display
1414
+ if (
1415
+ use_live_display
1416
+ and now - last_render >= render_interval
1417
+ ):
1418
+ content = "".join(text_buffer[event.index])
1419
+ if event.index in live_displays:
1420
+ try:
1421
+ live_displays[event.index].update(
1422
+ Markdown(content)
1423
+ )
1424
+ last_render_time[event.index] = now
1425
+ except Exception:
1426
+ pass
1427
+ else:
1428
+ # For thinking parts, stream immediately (dim)
1429
+ if event.index not in banner_printed:
1430
+ _print_thinking_banner()
1431
+ banner_printed.add(event.index)
1432
+ escaped = escape(delta.content_delta)
1433
+ console.print(f"[dim]{escaped}[/dim]", end="")
1434
+
1435
+ # PartEndEvent - finish the streaming with a newline
1436
+ elif isinstance(event, PartEndEvent):
1437
+ if event.index in streaming_parts:
1438
+ # For text parts, do final render then stop the Live display
1439
+ if event.index in text_parts:
1440
+ # Final render to ensure we show complete content
1441
+ # (throttling may have skipped the last few tokens)
1442
+ if event.index in live_displays and event.index in text_buffer:
1443
+ try:
1444
+ final_content = "".join(text_buffer[event.index])
1445
+ live_displays[event.index].update(
1446
+ Markdown(final_content)
1447
+ )
1448
+ except Exception:
1449
+ pass
1450
+ if event.index in live_displays:
1451
+ try:
1452
+ live_displays[event.index].stop()
1453
+ except Exception:
1454
+ pass
1455
+ del live_displays[event.index]
1456
+ # When not using Live display, print the final content as markdown
1457
+ elif event.index in text_buffer:
1458
+ try:
1459
+ final_content = "".join(text_buffer[event.index])
1460
+ if final_content.strip():
1461
+ console.print(Markdown(final_content))
1462
+ except Exception:
1463
+ pass
1464
+ if event.index in text_buffer:
1465
+ del text_buffer[event.index]
1466
+ # Clean up render time tracking
1467
+ last_render_time.pop(event.index, None)
1468
+ # For thinking parts, just print newline
1469
+ elif event.index in banner_printed:
1470
+ console.print() # Final newline after streaming
1471
+ # Clean up all tracking sets
1472
+ streaming_parts.discard(event.index)
1473
+ thinking_parts.discard(event.index)
1474
+ text_parts.discard(event.index)
1475
+ banner_printed.discard(event.index)
1476
+
1477
+ # Resume spinner if next part is NOT text/thinking (avoid race condition)
1478
+ # If next part is a tool call or None, it's safe to resume
1479
+ # Note: spinner itself handles blank line before appearing
1480
+ from code_puppy.messaging.spinner import resume_all_spinners
1481
+
1482
+ next_kind = getattr(event, "next_part_kind", None)
1483
+ if next_kind not in ("text", "thinking"):
1484
+ resume_all_spinners()
1485
+
1486
+ # Spinner is resumed in PartEndEvent when appropriate (based on next_part_kind)
1487
+
1268
1488
  def _spawn_ctrl_x_key_listener(
1269
1489
  self,
1270
1490
  stop_event: threading.Event,
@@ -1523,6 +1743,7 @@ class BaseAgent(ABC):
1523
1743
  prompt_payload,
1524
1744
  message_history=self.get_message_history(),
1525
1745
  usage_limits=usage_limits,
1746
+ event_stream_handler=self._event_stream_handler,
1526
1747
  **kwargs,
1527
1748
  )
1528
1749
  finally:
@@ -1535,6 +1756,7 @@ class BaseAgent(ABC):
1535
1756
  prompt_payload,
1536
1757
  message_history=self.get_message_history(),
1537
1758
  usage_limits=usage_limits,
1759
+ event_stream_handler=self._event_stream_handler,
1538
1760
  **kwargs,
1539
1761
  )
1540
1762
  else:
@@ -1543,6 +1765,7 @@ class BaseAgent(ABC):
1543
1765
  prompt_payload,
1544
1766
  message_history=self.get_message_history(),
1545
1767
  usage_limits=usage_limits,
1768
+ event_stream_handler=self._event_stream_handler,
1546
1769
  **kwargs,
1547
1770
  )
1548
1771
  return result_
@@ -140,9 +140,11 @@ class ChatGPTCodexAsyncClient(httpx.AsyncClient):
140
140
  modified = True
141
141
 
142
142
  # CRITICAL: ChatGPT Codex backend requires stream=true
143
+ # If stream is already true (e.g., pydantic-ai with event_stream_handler),
144
+ # don't force conversion - let streaming events flow through naturally
143
145
  if data.get("stream") is not True:
144
146
  data["stream"] = True
145
- forced_stream = True
147
+ forced_stream = True # Only convert if WE forced streaming
146
148
  modified = True
147
149
 
148
150
  # Add reasoning settings for reasoning models (gpt-5.2, o-series, etc.)
@@ -1,3 +1,8 @@
1
+ """CLI runner for Code Puppy.
2
+
3
+ Contains the main application logic, interactive mode, and entry point.
4
+ """
5
+
1
6
  # Apply pydantic-ai patches BEFORE any pydantic-ai imports
2
7
  from code_puppy.pydantic_patches import apply_all_patches
3
8
 
@@ -6,8 +11,6 @@ apply_all_patches()
6
11
  import argparse
7
12
  import asyncio
8
13
  import os
9
- import platform
10
- import subprocess
11
14
  import sys
12
15
  import time
13
16
  import traceback
@@ -39,15 +42,19 @@ from code_puppy.keymap import (
39
42
  validate_cancel_agent_key,
40
43
  )
41
44
  from code_puppy.messaging import emit_info
45
+ from code_puppy.terminal_utils import (
46
+ reset_unix_terminal,
47
+ reset_windows_terminal_ansi,
48
+ reset_windows_terminal_full,
49
+ )
42
50
  from code_puppy.tools.common import console
43
-
44
- # message_history_accumulator and prune_interrupted_tool_calls have been moved to BaseAgent class
45
51
  from code_puppy.version_checker import default_version_mismatch_behavior
46
52
 
47
53
  plugins.load_plugin_callbacks()
48
54
 
49
55
 
50
56
  async def main():
57
+ """Main async entry point for Code Puppy CLI."""
51
58
  parser = argparse.ArgumentParser(description="Code Puppy - A code generation agent")
52
59
  parser.add_argument(
53
60
  "--version",
@@ -295,11 +302,9 @@ async def main():
295
302
  DBOS.destroy()
296
303
 
297
304
 
298
- # Add the file handling functionality for interactive mode
299
305
  async def interactive_mode(message_renderer, initial_command: str = None) -> None:
300
- from code_puppy.command_line.command_handler import handle_command
301
-
302
306
  """Run the agent in interactive mode."""
307
+ from code_puppy.command_line.command_handler import handle_command
303
308
 
304
309
  display_console = message_renderer.console
305
310
  from code_puppy.messaging import emit_info, emit_system_message
@@ -429,12 +434,7 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
429
434
  # Use prompt_toolkit for enhanced input with path completion
430
435
  try:
431
436
  # Windows-specific: Reset terminal state before prompting
432
- if platform.system() == "Windows":
433
- try:
434
- sys.stdout.write("\x1b[0m") # Reset ANSI formatting
435
- sys.stdout.flush()
436
- except Exception:
437
- pass
437
+ reset_windows_terminal_ansi()
438
438
 
439
439
  # Use the async version of get_input_with_combined_completion
440
440
  task = await get_input_with_combined_completion(
@@ -446,6 +446,9 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
446
446
 
447
447
  except (KeyboardInterrupt, EOFError):
448
448
  # Handle Ctrl+C or Ctrl+D
449
+ # Windows-specific: Reset terminal state after interrupt to prevent
450
+ # the terminal from becoming unresponsive (can't type characters)
451
+ reset_windows_terminal_full()
449
452
  from code_puppy.messaging import emit_warning
450
453
 
451
454
  emit_warning("\nInput cancelled")
@@ -601,14 +604,7 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
601
604
  # Check if the task was cancelled (but don't show message if we just killed processes)
602
605
  if result is None:
603
606
  # Windows-specific: Reset terminal state after cancellation
604
- if platform.system() == "Windows":
605
- try:
606
- sys.stdout.write("\x1b[0m") # Reset ANSI formatting
607
- sys.stdout.flush()
608
- sys.stderr.write("\x1b[0m")
609
- sys.stderr.flush()
610
- except Exception:
611
- pass
607
+ reset_windows_terminal_ansi()
612
608
  continue
613
609
  # Get the structured response
614
610
  agent_response = result.output
@@ -651,6 +647,8 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
651
647
 
652
648
 
653
649
  def prettier_code_blocks():
650
+ """Configure Rich to use prettier code block rendering."""
651
+
654
652
  class SimpleCodeBlock(CodeBlock):
655
653
  def __rich_console__(
656
654
  self, console: Console, options: ConsoleOptions
@@ -787,14 +785,4 @@ def main_entry():
787
785
  return 0
788
786
  finally:
789
787
  # Reset terminal on Unix-like systems (not Windows)
790
- if platform.system() != "Windows":
791
- try:
792
- # Reset terminal to sanity state
793
- subprocess.run(["reset"], check=True, capture_output=True)
794
- except (subprocess.CalledProcessError, FileNotFoundError):
795
- # Silently fail if reset command isn't available
796
- pass
797
-
798
-
799
- if __name__ == "__main__":
800
- main_entry()
788
+ reset_unix_terminal()