code-puppy 0.0.314__tar.gz → 0.0.316__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.316}/PKG-INFO +1 -1
  2. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/agents/base_agent.py +237 -29
  3. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/chatgpt_codex_client.py +3 -1
  4. code_puppy-0.0.314/code_puppy/main.py → code_puppy-0.0.316/code_puppy/cli_runner.py +20 -32
  5. code_puppy-0.0.316/code_puppy/command_line/colors_menu.py +515 -0
  6. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/command_line/config_commands.py +31 -0
  7. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/command_line/model_settings_menu.py +46 -6
  8. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/command_line/prompt_toolkit_completion.py +4 -9
  9. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/config.py +100 -12
  10. code_puppy-0.0.316/code_puppy/main.py +10 -0
  11. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/mcp_/blocking_startup.py +11 -3
  12. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/mcp_/managed_server.py +22 -2
  13. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/mcp_/manager.py +65 -0
  14. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/messaging/rich_renderer.py +66 -43
  15. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/messaging/spinner/console_spinner.py +6 -0
  16. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/model_factory.py +2 -2
  17. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/models.json +4 -12
  18. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/plugins/chatgpt_oauth/utils.py +11 -0
  19. code_puppy-0.0.316/code_puppy/terminal_utils.py +126 -0
  20. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/tools/agent_tools.py +4 -1
  21. {code_puppy-0.0.314 → code_puppy-0.0.316}/pyproject.toml +2 -1
  22. {code_puppy-0.0.314 → code_puppy-0.0.316}/.gitignore +0 -0
  23. {code_puppy-0.0.314 → code_puppy-0.0.316}/LICENSE +0 -0
  24. {code_puppy-0.0.314 → code_puppy-0.0.316}/README.md +0 -0
  25. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/__init__.py +0 -0
  26. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/__main__.py +0 -0
  27. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/agents/__init__.py +0 -0
  28. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/agents/agent_c_reviewer.py +0 -0
  29. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/agents/agent_code_puppy.py +0 -0
  30. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/agents/agent_code_reviewer.py +0 -0
  31. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/agents/agent_cpp_reviewer.py +0 -0
  32. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/agents/agent_creator_agent.py +0 -0
  33. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/agents/agent_golang_reviewer.py +0 -0
  34. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/agents/agent_javascript_reviewer.py +0 -0
  35. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/agents/agent_manager.py +0 -0
  36. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/agents/agent_planning.py +0 -0
  37. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/agents/agent_python_programmer.py +0 -0
  38. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/agents/agent_python_reviewer.py +0 -0
  39. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/agents/agent_qa_expert.py +0 -0
  40. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/agents/agent_qa_kitten.py +0 -0
  41. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/agents/agent_security_auditor.py +0 -0
  42. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/agents/agent_typescript_reviewer.py +0 -0
  43. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/agents/json_agent.py +0 -0
  44. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/agents/prompt_reviewer.py +0 -0
  45. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/callbacks.py +0 -0
  46. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/claude_cache_client.py +0 -0
  47. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/command_line/__init__.py +0 -0
  48. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/command_line/add_model_menu.py +0 -0
  49. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/command_line/attachments.py +0 -0
  50. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/command_line/autosave_menu.py +0 -0
  51. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/command_line/command_handler.py +0 -0
  52. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/command_line/command_registry.py +0 -0
  53. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/command_line/core_commands.py +0 -0
  54. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/command_line/diff_menu.py +0 -0
  55. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/command_line/file_path_completion.py +0 -0
  56. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/command_line/load_context_completion.py +0 -0
  57. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/command_line/mcp/__init__.py +0 -0
  58. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/command_line/mcp/add_command.py +0 -0
  59. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/command_line/mcp/base.py +0 -0
  60. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/command_line/mcp/catalog_server_installer.py +0 -0
  61. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/command_line/mcp/custom_server_form.py +0 -0
  62. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/command_line/mcp/custom_server_installer.py +0 -0
  63. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/command_line/mcp/edit_command.py +0 -0
  64. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/command_line/mcp/handler.py +0 -0
  65. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/command_line/mcp/help_command.py +0 -0
  66. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/command_line/mcp/install_command.py +0 -0
  67. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/command_line/mcp/install_menu.py +0 -0
  68. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/command_line/mcp/list_command.py +0 -0
  69. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/command_line/mcp/logs_command.py +0 -0
  70. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/command_line/mcp/remove_command.py +0 -0
  71. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/command_line/mcp/restart_command.py +0 -0
  72. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/command_line/mcp/search_command.py +0 -0
  73. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/command_line/mcp/start_all_command.py +0 -0
  74. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/command_line/mcp/start_command.py +0 -0
  75. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/command_line/mcp/status_command.py +0 -0
  76. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/command_line/mcp/stop_all_command.py +0 -0
  77. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/command_line/mcp/stop_command.py +0 -0
  78. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/command_line/mcp/test_command.py +0 -0
  79. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/command_line/mcp/utils.py +0 -0
  80. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/command_line/mcp/wizard_utils.py +0 -0
  81. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/command_line/mcp_completion.py +0 -0
  82. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/command_line/model_picker_completion.py +0 -0
  83. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/command_line/motd.py +0 -0
  84. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/command_line/pin_command_completion.py +0 -0
  85. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/command_line/session_commands.py +0 -0
  86. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/command_line/utils.py +0 -0
  87. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/error_logging.py +0 -0
  88. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/gemini_code_assist.py +0 -0
  89. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/http_utils.py +0 -0
  90. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/keymap.py +0 -0
  91. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/mcp_/__init__.py +0 -0
  92. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/mcp_/async_lifecycle.py +0 -0
  93. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/mcp_/captured_stdio_server.py +0 -0
  94. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/mcp_/circuit_breaker.py +0 -0
  95. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/mcp_/config_wizard.py +0 -0
  96. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/mcp_/dashboard.py +0 -0
  97. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/mcp_/error_isolation.py +0 -0
  98. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/mcp_/examples/retry_example.py +0 -0
  99. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/mcp_/health_monitor.py +0 -0
  100. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/mcp_/registry.py +0 -0
  101. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/mcp_/retry_manager.py +0 -0
  102. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/mcp_/server_registry_catalog.py +0 -0
  103. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/mcp_/status_tracker.py +0 -0
  104. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/mcp_/system_tools.py +0 -0
  105. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/messaging/__init__.py +0 -0
  106. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/messaging/bus.py +0 -0
  107. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/messaging/commands.py +0 -0
  108. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/messaging/message_queue.py +0 -0
  109. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/messaging/messages.py +0 -0
  110. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/messaging/queue_console.py +0 -0
  111. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/messaging/renderers.py +0 -0
  112. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/messaging/spinner/__init__.py +0 -0
  113. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/messaging/spinner/spinner_base.py +0 -0
  114. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/model_utils.py +0 -0
  115. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/models_dev_api.json +0 -0
  116. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/models_dev_parser.py +0 -0
  117. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/plugins/__init__.py +0 -0
  118. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/plugins/chatgpt_oauth/__init__.py +0 -0
  119. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/plugins/chatgpt_oauth/config.py +0 -0
  120. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/plugins/chatgpt_oauth/oauth_flow.py +0 -0
  121. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/plugins/chatgpt_oauth/register_callbacks.py +0 -0
  122. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/plugins/chatgpt_oauth/test_plugin.py +0 -0
  123. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/plugins/claude_code_oauth/README.md +0 -0
  124. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/plugins/claude_code_oauth/SETUP.md +0 -0
  125. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/plugins/claude_code_oauth/__init__.py +0 -0
  126. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/plugins/claude_code_oauth/config.py +0 -0
  127. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/plugins/claude_code_oauth/register_callbacks.py +0 -0
  128. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/plugins/claude_code_oauth/test_plugin.py +0 -0
  129. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/plugins/claude_code_oauth/utils.py +0 -0
  130. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/plugins/customizable_commands/__init__.py +0 -0
  131. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/plugins/customizable_commands/register_callbacks.py +0 -0
  132. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/plugins/example_custom_command/README.md +0 -0
  133. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/plugins/example_custom_command/register_callbacks.py +0 -0
  134. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/plugins/file_permission_handler/__init__.py +0 -0
  135. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/plugins/file_permission_handler/register_callbacks.py +0 -0
  136. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/plugins/oauth_puppy_html.py +0 -0
  137. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/plugins/shell_safety/__init__.py +0 -0
  138. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/plugins/shell_safety/agent_shell_safety.py +0 -0
  139. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/plugins/shell_safety/command_cache.py +0 -0
  140. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/plugins/shell_safety/register_callbacks.py +0 -0
  141. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/prompts/codex_system_prompt.md +0 -0
  142. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/pydantic_patches.py +0 -0
  143. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/reopenable_async_client.py +0 -0
  144. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/round_robin_model.py +0 -0
  145. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/session_storage.py +0 -0
  146. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/status_display.py +0 -0
  147. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/summarization_agent.py +0 -0
  148. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/tools/__init__.py +0 -0
  149. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/tools/browser/__init__.py +0 -0
  150. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/tools/browser/browser_control.py +0 -0
  151. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/tools/browser/browser_interactions.py +0 -0
  152. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/tools/browser/browser_locators.py +0 -0
  153. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/tools/browser/browser_navigation.py +0 -0
  154. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/tools/browser/browser_screenshot.py +0 -0
  155. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/tools/browser/browser_scripts.py +0 -0
  156. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/tools/browser/browser_workflows.py +0 -0
  157. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/tools/browser/camoufox_manager.py +0 -0
  158. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/tools/browser/vqa_agent.py +0 -0
  159. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/tools/command_runner.py +0 -0
  160. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/tools/common.py +0 -0
  161. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/tools/file_modifications.py +0 -0
  162. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/tools/file_operations.py +0 -0
  163. {code_puppy-0.0.314 → code_puppy-0.0.316}/code_puppy/tools/tools_content.py +0 -0
  164. {code_puppy-0.0.314 → code_puppy-0.0.316}/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.316
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,
@@ -45,11 +47,10 @@ from code_puppy.config import (
45
47
  get_protected_token_count,
46
48
  get_use_dbos,
47
49
  get_value,
48
- load_mcp_server_configs,
49
50
  )
50
51
  from code_puppy.error_logging import log_error
51
52
  from code_puppy.keymap import cancel_agent_uses_signal, get_cancel_agent_char_code
52
- from code_puppy.mcp_ import ServerConfig, get_mcp_manager
53
+ from code_puppy.mcp_ import get_mcp_manager
53
54
  from code_puppy.messaging import (
54
55
  emit_error,
55
56
  emit_info,
@@ -987,45 +988,31 @@ class BaseAgent(ABC):
987
988
  return self._puppy_rules
988
989
 
989
990
  def load_mcp_servers(self, extra_headers: Optional[Dict[str, str]] = None):
990
- """Load MCP servers through the manager and return pydantic-ai compatible servers."""
991
+ """Load MCP servers through the manager and return pydantic-ai compatible servers.
992
+
993
+ Note: The manager automatically syncs from mcp_servers.json during initialization,
994
+ so we don't need to sync here. Use reload_mcp_servers() to force a re-sync.
995
+ """
991
996
 
992
997
  mcp_disabled = get_value("disable_mcp_servers")
993
998
  if mcp_disabled and str(mcp_disabled).lower() in ("1", "true", "yes", "on"):
994
999
  return []
995
1000
 
996
1001
  manager = get_mcp_manager()
997
- configs = load_mcp_server_configs()
998
- if not configs:
999
- existing_servers = manager.list_servers()
1000
- if not existing_servers:
1001
- return []
1002
- else:
1003
- for name, conf in configs.items():
1004
- try:
1005
- server_config = ServerConfig(
1006
- id=conf.get("id", f"{name}_{hash(name)}"),
1007
- name=name,
1008
- type=conf.get("type", "sse"),
1009
- enabled=conf.get("enabled", True),
1010
- config=conf,
1011
- )
1012
- existing = manager.get_server_by_name(name)
1013
- if not existing:
1014
- manager.register_server(server_config)
1015
- else:
1016
- if existing.config != server_config.config:
1017
- manager.update_server(existing.id, server_config)
1018
- except Exception:
1019
- continue
1020
-
1021
1002
  return manager.get_servers_for_agent()
1022
1003
 
1023
1004
  def reload_mcp_servers(self):
1024
- """Reload MCP servers and return updated servers."""
1005
+ """Reload MCP servers and return updated servers.
1006
+
1007
+ Forces a re-sync from mcp_servers.json to pick up any configuration changes.
1008
+ """
1025
1009
  # Clear the MCP tool cache when servers are reloaded
1026
1010
  self._mcp_tool_definitions_cache = []
1027
- self.load_mcp_servers()
1011
+
1012
+ # Force re-sync from mcp_servers.json
1028
1013
  manager = get_mcp_manager()
1014
+ manager.sync_from_config()
1015
+
1029
1016
  return manager.get_servers_for_agent()
1030
1017
 
1031
1018
  def _load_model_with_fallback(
@@ -1265,6 +1252,224 @@ class BaseAgent(ABC):
1265
1252
  self.set_message_history(result_messages_filtered_empty_thinking)
1266
1253
  return self.get_message_history()
1267
1254
 
1255
+ async def _event_stream_handler(
1256
+ self, ctx: RunContext, events: AsyncIterable[Any]
1257
+ ) -> None:
1258
+ """Handle streaming events from the agent run.
1259
+
1260
+ This method processes streaming events and emits TextPart and ThinkingPart
1261
+ content with styled banners as they stream in.
1262
+
1263
+ Args:
1264
+ ctx: The run context.
1265
+ events: Async iterable of streaming events (PartStartEvent, PartDeltaEvent, etc.).
1266
+ """
1267
+ import os
1268
+ import time as time_module
1269
+
1270
+ from pydantic_ai import PartDeltaEvent, PartStartEvent
1271
+ from pydantic_ai.messages import TextPartDelta, ThinkingPartDelta
1272
+ from rich.console import Console
1273
+ from rich.live import Live
1274
+ from rich.markdown import Markdown
1275
+ from rich.markup import escape
1276
+
1277
+ from code_puppy.messaging.spinner import pause_all_spinners
1278
+
1279
+ console = Console()
1280
+
1281
+ # Disable Live display in test mode or non-interactive environments
1282
+ # This fixes issues with pexpect PTY where Live() hangs
1283
+ use_live_display = (
1284
+ console.is_terminal
1285
+ and os.environ.get("CODE_PUPPY_TEST_FAST", "").lower() not in ("1", "true")
1286
+ and os.environ.get("CI", "").lower() not in ("1", "true")
1287
+ )
1288
+
1289
+ # Track which part indices we're currently streaming (for Text/Thinking parts)
1290
+ streaming_parts: set[int] = set()
1291
+ thinking_parts: set[int] = (
1292
+ set()
1293
+ ) # Track which parts are thinking (for dim style)
1294
+ text_parts: set[int] = set() # Track which parts are text
1295
+ banner_printed: set[int] = set() # Track if banner was already printed
1296
+ text_buffer: dict[int, list[str]] = {} # Buffer text for markdown
1297
+ live_displays: dict[int, Live] = {} # Live displays for streaming markdown
1298
+ did_stream_anything = False # Track if we streamed any content
1299
+ last_render_time: dict[int, float] = {} # Track last render time per part
1300
+ render_interval = 0.1 # Only re-render markdown every 100ms (throttle)
1301
+
1302
+ def _print_thinking_banner() -> None:
1303
+ """Print the THINKING banner with spinner pause and line clear."""
1304
+ nonlocal did_stream_anything
1305
+ import sys
1306
+ import time
1307
+
1308
+ from code_puppy.config import get_banner_color
1309
+
1310
+ pause_all_spinners()
1311
+ time.sleep(0.1) # Delay to let spinner fully clear
1312
+ sys.stdout.write("\r\x1b[K") # Clear line
1313
+ sys.stdout.flush()
1314
+ console.print() # Newline before banner
1315
+ # Bold banner with configurable color and lightning bolt
1316
+ thinking_color = get_banner_color("thinking")
1317
+ console.print(
1318
+ Text.from_markup(
1319
+ f"[bold white on {thinking_color}] THINKING [/bold white on {thinking_color}] [dim]⚡ "
1320
+ ),
1321
+ end="",
1322
+ )
1323
+ sys.stdout.flush()
1324
+ did_stream_anything = True
1325
+
1326
+ def _print_response_banner() -> None:
1327
+ """Print the AGENT RESPONSE banner with spinner pause and line clear."""
1328
+ nonlocal did_stream_anything
1329
+ import sys
1330
+ import time
1331
+
1332
+ from code_puppy.config import get_banner_color
1333
+
1334
+ pause_all_spinners()
1335
+ time.sleep(0.1) # Delay to let spinner fully clear
1336
+ sys.stdout.write("\r\x1b[K") # Clear line
1337
+ sys.stdout.flush()
1338
+ console.print() # Newline before banner
1339
+ response_color = get_banner_color("agent_response")
1340
+ console.print(
1341
+ Text.from_markup(
1342
+ f"[bold white on {response_color}] AGENT RESPONSE [/bold white on {response_color}]"
1343
+ )
1344
+ )
1345
+ sys.stdout.flush()
1346
+ did_stream_anything = True
1347
+
1348
+ async for event in events:
1349
+ # PartStartEvent - register the part but defer banner until content arrives
1350
+ if isinstance(event, PartStartEvent):
1351
+ part = event.part
1352
+ if isinstance(part, ThinkingPart):
1353
+ streaming_parts.add(event.index)
1354
+ thinking_parts.add(event.index)
1355
+ # If there's initial content, print banner + content now
1356
+ if part.content and part.content.strip():
1357
+ _print_thinking_banner()
1358
+ escaped = escape(part.content)
1359
+ console.print(f"[dim]{escaped}[/dim]", end="")
1360
+ banner_printed.add(event.index)
1361
+ elif isinstance(part, TextPart):
1362
+ streaming_parts.add(event.index)
1363
+ text_parts.add(event.index)
1364
+ text_buffer[event.index] = [] # Initialize buffer
1365
+ # Buffer initial content if present
1366
+ if part.content and part.content.strip():
1367
+ text_buffer[event.index].append(part.content)
1368
+
1369
+ # PartDeltaEvent - stream the content as it arrives
1370
+ elif isinstance(event, PartDeltaEvent):
1371
+ if event.index in streaming_parts:
1372
+ delta = event.delta
1373
+ if isinstance(delta, (TextPartDelta, ThinkingPartDelta)):
1374
+ if delta.content_delta:
1375
+ # For text parts, stream markdown with Live display
1376
+ if event.index in text_parts:
1377
+ # Print banner and start Live on first content
1378
+ if event.index not in banner_printed:
1379
+ _print_response_banner()
1380
+ banner_printed.add(event.index)
1381
+ # Only use Live display if enabled (disabled in test/CI)
1382
+ if use_live_display:
1383
+ live = Live(
1384
+ Markdown(""),
1385
+ console=console,
1386
+ refresh_per_second=10,
1387
+ vertical_overflow="visible", # Allow scrolling for long content
1388
+ )
1389
+ live.start()
1390
+ live_displays[event.index] = live
1391
+ # Accumulate text and throttle markdown rendering
1392
+ # (Markdown parsing is O(n), doing it on every token = O(n²) death)
1393
+ text_buffer[event.index].append(delta.content_delta)
1394
+ now = time_module.monotonic()
1395
+ last_render = last_render_time.get(event.index, 0)
1396
+
1397
+ # Only re-render if enough time has passed (throttle)
1398
+ # Skip Live updates when not using live display
1399
+ if (
1400
+ use_live_display
1401
+ and now - last_render >= render_interval
1402
+ ):
1403
+ content = "".join(text_buffer[event.index])
1404
+ if event.index in live_displays:
1405
+ try:
1406
+ live_displays[event.index].update(
1407
+ Markdown(content)
1408
+ )
1409
+ last_render_time[event.index] = now
1410
+ except Exception:
1411
+ pass
1412
+ else:
1413
+ # For thinking parts, stream immediately (dim)
1414
+ if event.index not in banner_printed:
1415
+ _print_thinking_banner()
1416
+ banner_printed.add(event.index)
1417
+ escaped = escape(delta.content_delta)
1418
+ console.print(f"[dim]{escaped}[/dim]", end="")
1419
+
1420
+ # PartEndEvent - finish the streaming with a newline
1421
+ elif isinstance(event, PartEndEvent):
1422
+ if event.index in streaming_parts:
1423
+ # For text parts, do final render then stop the Live display
1424
+ if event.index in text_parts:
1425
+ # Final render to ensure we show complete content
1426
+ # (throttling may have skipped the last few tokens)
1427
+ if event.index in live_displays and event.index in text_buffer:
1428
+ try:
1429
+ final_content = "".join(text_buffer[event.index])
1430
+ live_displays[event.index].update(
1431
+ Markdown(final_content)
1432
+ )
1433
+ except Exception:
1434
+ pass
1435
+ if event.index in live_displays:
1436
+ try:
1437
+ live_displays[event.index].stop()
1438
+ except Exception:
1439
+ pass
1440
+ del live_displays[event.index]
1441
+ # When not using Live display, print the final content as markdown
1442
+ elif event.index in text_buffer:
1443
+ try:
1444
+ final_content = "".join(text_buffer[event.index])
1445
+ if final_content.strip():
1446
+ console.print(Markdown(final_content))
1447
+ except Exception:
1448
+ pass
1449
+ if event.index in text_buffer:
1450
+ del text_buffer[event.index]
1451
+ # Clean up render time tracking
1452
+ last_render_time.pop(event.index, None)
1453
+ # For thinking parts, just print newline
1454
+ elif event.index in banner_printed:
1455
+ console.print() # Final newline after streaming
1456
+ # Clean up all tracking sets
1457
+ streaming_parts.discard(event.index)
1458
+ thinking_parts.discard(event.index)
1459
+ text_parts.discard(event.index)
1460
+ banner_printed.discard(event.index)
1461
+
1462
+ # Resume spinner if next part is NOT text/thinking (avoid race condition)
1463
+ # If next part is a tool call or None, it's safe to resume
1464
+ # Note: spinner itself handles blank line before appearing
1465
+ from code_puppy.messaging.spinner import resume_all_spinners
1466
+
1467
+ next_kind = getattr(event, "next_part_kind", None)
1468
+ if next_kind not in ("text", "thinking"):
1469
+ resume_all_spinners()
1470
+
1471
+ # Spinner is resumed in PartEndEvent when appropriate (based on next_part_kind)
1472
+
1268
1473
  def _spawn_ctrl_x_key_listener(
1269
1474
  self,
1270
1475
  stop_event: threading.Event,
@@ -1523,6 +1728,7 @@ class BaseAgent(ABC):
1523
1728
  prompt_payload,
1524
1729
  message_history=self.get_message_history(),
1525
1730
  usage_limits=usage_limits,
1731
+ event_stream_handler=self._event_stream_handler,
1526
1732
  **kwargs,
1527
1733
  )
1528
1734
  finally:
@@ -1535,6 +1741,7 @@ class BaseAgent(ABC):
1535
1741
  prompt_payload,
1536
1742
  message_history=self.get_message_history(),
1537
1743
  usage_limits=usage_limits,
1744
+ event_stream_handler=self._event_stream_handler,
1538
1745
  **kwargs,
1539
1746
  )
1540
1747
  else:
@@ -1543,6 +1750,7 @@ class BaseAgent(ABC):
1543
1750
  prompt_payload,
1544
1751
  message_history=self.get_message_history(),
1545
1752
  usage_limits=usage_limits,
1753
+ event_stream_handler=self._event_stream_handler,
1546
1754
  **kwargs,
1547
1755
  )
1548
1756
  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()