code-puppy 0.0.339__tar.gz → 0.0.341__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 (183) hide show
  1. {code_puppy-0.0.339 → code_puppy-0.0.341}/PKG-INFO +1 -1
  2. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/agents/base_agent.py +24 -1
  3. code_puppy-0.0.341/code_puppy/claude_cache_client.py +371 -0
  4. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/cli_runner.py +6 -2
  5. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/autosave_menu.py +18 -24
  6. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/prompt_toolkit_completion.py +23 -17
  7. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/mcp_/managed_server.py +7 -11
  8. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/tools/common.py +3 -9
  9. {code_puppy-0.0.339 → code_puppy-0.0.341}/pyproject.toml +1 -1
  10. code_puppy-0.0.339/code_puppy/claude_cache_client.py +0 -209
  11. {code_puppy-0.0.339 → code_puppy-0.0.341}/.gitignore +0 -0
  12. {code_puppy-0.0.339 → code_puppy-0.0.341}/LICENSE +0 -0
  13. {code_puppy-0.0.339 → code_puppy-0.0.341}/README.md +0 -0
  14. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/__init__.py +0 -0
  15. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/__main__.py +0 -0
  16. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/agents/__init__.py +0 -0
  17. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/agents/agent_c_reviewer.py +0 -0
  18. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/agents/agent_code_puppy.py +0 -0
  19. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/agents/agent_code_reviewer.py +0 -0
  20. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/agents/agent_cpp_reviewer.py +0 -0
  21. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/agents/agent_creator_agent.py +0 -0
  22. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/agents/agent_golang_reviewer.py +0 -0
  23. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/agents/agent_javascript_reviewer.py +0 -0
  24. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/agents/agent_manager.py +0 -0
  25. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/agents/agent_planning.py +0 -0
  26. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/agents/agent_python_programmer.py +0 -0
  27. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/agents/agent_python_reviewer.py +0 -0
  28. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/agents/agent_qa_expert.py +0 -0
  29. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/agents/agent_qa_kitten.py +0 -0
  30. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/agents/agent_security_auditor.py +0 -0
  31. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/agents/agent_typescript_reviewer.py +0 -0
  32. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/agents/json_agent.py +0 -0
  33. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/agents/prompt_reviewer.py +0 -0
  34. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/callbacks.py +0 -0
  35. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/chatgpt_codex_client.py +0 -0
  36. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/__init__.py +0 -0
  37. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/add_model_menu.py +0 -0
  38. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/attachments.py +0 -0
  39. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/clipboard.py +0 -0
  40. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/colors_menu.py +0 -0
  41. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/command_handler.py +0 -0
  42. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/command_registry.py +0 -0
  43. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/config_commands.py +0 -0
  44. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/core_commands.py +0 -0
  45. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/diff_menu.py +0 -0
  46. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/file_path_completion.py +0 -0
  47. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/load_context_completion.py +0 -0
  48. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/__init__.py +0 -0
  49. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/add_command.py +0 -0
  50. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/base.py +0 -0
  51. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/catalog_server_installer.py +0 -0
  52. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/custom_server_form.py +0 -0
  53. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/custom_server_installer.py +0 -0
  54. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/edit_command.py +0 -0
  55. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/handler.py +0 -0
  56. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/help_command.py +0 -0
  57. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/install_command.py +0 -0
  58. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/install_menu.py +0 -0
  59. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/list_command.py +0 -0
  60. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/logs_command.py +0 -0
  61. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/remove_command.py +0 -0
  62. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/restart_command.py +0 -0
  63. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/search_command.py +0 -0
  64. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/start_all_command.py +0 -0
  65. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/start_command.py +0 -0
  66. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/status_command.py +0 -0
  67. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/stop_all_command.py +0 -0
  68. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/stop_command.py +0 -0
  69. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/test_command.py +0 -0
  70. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/utils.py +0 -0
  71. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/wizard_utils.py +0 -0
  72. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp_completion.py +0 -0
  73. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/model_picker_completion.py +0 -0
  74. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/model_settings_menu.py +0 -0
  75. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/motd.py +0 -0
  76. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/onboarding_slides.py +0 -0
  77. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/onboarding_wizard.py +0 -0
  78. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/pin_command_completion.py +0 -0
  79. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/session_commands.py +0 -0
  80. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/utils.py +0 -0
  81. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/config.py +0 -0
  82. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/error_logging.py +0 -0
  83. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/gemini_code_assist.py +0 -0
  84. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/http_utils.py +0 -0
  85. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/keymap.py +0 -0
  86. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/main.py +0 -0
  87. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/mcp_/__init__.py +0 -0
  88. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/mcp_/async_lifecycle.py +0 -0
  89. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/mcp_/blocking_startup.py +0 -0
  90. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/mcp_/captured_stdio_server.py +0 -0
  91. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/mcp_/circuit_breaker.py +0 -0
  92. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/mcp_/config_wizard.py +0 -0
  93. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/mcp_/dashboard.py +0 -0
  94. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/mcp_/error_isolation.py +0 -0
  95. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/mcp_/examples/retry_example.py +0 -0
  96. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/mcp_/health_monitor.py +0 -0
  97. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/mcp_/manager.py +0 -0
  98. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/mcp_/mcp_logs.py +0 -0
  99. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/mcp_/registry.py +0 -0
  100. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/mcp_/retry_manager.py +0 -0
  101. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/mcp_/server_registry_catalog.py +0 -0
  102. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/mcp_/status_tracker.py +0 -0
  103. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/mcp_/system_tools.py +0 -0
  104. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/messaging/__init__.py +0 -0
  105. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/messaging/bus.py +0 -0
  106. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/messaging/commands.py +0 -0
  107. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/messaging/markdown_patches.py +0 -0
  108. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/messaging/message_queue.py +0 -0
  109. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/messaging/messages.py +0 -0
  110. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/messaging/queue_console.py +0 -0
  111. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/messaging/renderers.py +0 -0
  112. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/messaging/rich_renderer.py +0 -0
  113. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/messaging/spinner/__init__.py +0 -0
  114. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/messaging/spinner/console_spinner.py +0 -0
  115. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/messaging/spinner/spinner_base.py +0 -0
  116. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/model_factory.py +0 -0
  117. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/model_utils.py +0 -0
  118. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/models.json +0 -0
  119. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/models_dev_api.json +0 -0
  120. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/models_dev_parser.py +0 -0
  121. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/__init__.py +0 -0
  122. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/antigravity_oauth/__init__.py +0 -0
  123. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/antigravity_oauth/accounts.py +0 -0
  124. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/antigravity_oauth/antigravity_model.py +0 -0
  125. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/antigravity_oauth/config.py +0 -0
  126. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/antigravity_oauth/constants.py +0 -0
  127. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/antigravity_oauth/oauth.py +0 -0
  128. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/antigravity_oauth/register_callbacks.py +0 -0
  129. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/antigravity_oauth/storage.py +0 -0
  130. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/antigravity_oauth/test_plugin.py +0 -0
  131. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/antigravity_oauth/token.py +0 -0
  132. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/antigravity_oauth/transport.py +0 -0
  133. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/antigravity_oauth/utils.py +0 -0
  134. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/chatgpt_oauth/__init__.py +0 -0
  135. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/chatgpt_oauth/config.py +0 -0
  136. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/chatgpt_oauth/oauth_flow.py +0 -0
  137. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/chatgpt_oauth/register_callbacks.py +0 -0
  138. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/chatgpt_oauth/test_plugin.py +0 -0
  139. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/chatgpt_oauth/utils.py +0 -0
  140. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/claude_code_oauth/README.md +0 -0
  141. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/claude_code_oauth/SETUP.md +0 -0
  142. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/claude_code_oauth/__init__.py +0 -0
  143. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/claude_code_oauth/config.py +0 -0
  144. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/claude_code_oauth/register_callbacks.py +0 -0
  145. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/claude_code_oauth/test_plugin.py +0 -0
  146. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/claude_code_oauth/utils.py +0 -0
  147. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/customizable_commands/__init__.py +0 -0
  148. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/customizable_commands/register_callbacks.py +0 -0
  149. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/example_custom_command/README.md +0 -0
  150. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/example_custom_command/register_callbacks.py +0 -0
  151. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/file_permission_handler/__init__.py +0 -0
  152. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/file_permission_handler/register_callbacks.py +0 -0
  153. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/oauth_puppy_html.py +0 -0
  154. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/shell_safety/__init__.py +0 -0
  155. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/shell_safety/agent_shell_safety.py +0 -0
  156. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/shell_safety/command_cache.py +0 -0
  157. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/shell_safety/register_callbacks.py +0 -0
  158. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/prompts/codex_system_prompt.md +0 -0
  159. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/pydantic_patches.py +0 -0
  160. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/reopenable_async_client.py +0 -0
  161. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/round_robin_model.py +0 -0
  162. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/session_storage.py +0 -0
  163. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/status_display.py +0 -0
  164. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/summarization_agent.py +0 -0
  165. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/terminal_utils.py +0 -0
  166. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/tools/__init__.py +0 -0
  167. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/tools/agent_tools.py +0 -0
  168. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/tools/browser/__init__.py +0 -0
  169. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/tools/browser/browser_control.py +0 -0
  170. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/tools/browser/browser_interactions.py +0 -0
  171. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/tools/browser/browser_locators.py +0 -0
  172. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/tools/browser/browser_navigation.py +0 -0
  173. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/tools/browser/browser_screenshot.py +0 -0
  174. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/tools/browser/browser_scripts.py +0 -0
  175. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/tools/browser/browser_workflows.py +0 -0
  176. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/tools/browser/camoufox_manager.py +0 -0
  177. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/tools/browser/vqa_agent.py +0 -0
  178. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/tools/command_runner.py +0 -0
  179. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/tools/file_modifications.py +0 -0
  180. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/tools/file_operations.py +0 -0
  181. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/tools/tools_content.py +0 -0
  182. {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/uvx_detection.py +0 -0
  183. {code_puppy-0.0.339 → code_puppy-0.0.341}/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.339
3
+ Version: 0.0.341
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
@@ -913,6 +913,11 @@ class BaseAgent(ABC):
913
913
  """
914
914
  Truncate message history to manage token usage.
915
915
 
916
+ Protects:
917
+ - The first message (system prompt) - always kept
918
+ - The second message if it contains a ThinkingPart (extended thinking context)
919
+ - The most recent messages up to protected_tokens
920
+
916
921
  Args:
917
922
  messages: List of messages to truncate
918
923
  protected_tokens: Number of tokens to protect
@@ -924,12 +929,30 @@ class BaseAgent(ABC):
924
929
 
925
930
  emit_info("Truncating message history to manage token usage")
926
931
  result = [messages[0]] # Always keep the first message (system prompt)
932
+
933
+ # Check if second message exists and contains a ThinkingPart
934
+ # If so, protect it (extended thinking context shouldn't be lost)
935
+ skip_second = False
936
+ if len(messages) > 1:
937
+ second_msg = messages[1]
938
+ has_thinking = any(
939
+ isinstance(part, ThinkingPart) for part in second_msg.parts
940
+ )
941
+ if has_thinking:
942
+ result.append(second_msg)
943
+ skip_second = True
944
+
927
945
  num_tokens = 0
928
946
  stack = queue.LifoQueue()
929
947
 
948
+ # Determine which messages to consider for the recent-tokens window
949
+ # Skip first message (already added), and skip second if it has thinking
950
+ start_idx = 2 if skip_second else 1
951
+ messages_to_scan = messages[start_idx:]
952
+
930
953
  # Put messages in reverse order (most recent first) into the stack
931
954
  # but break when we exceed protected_tokens
932
- for idx, msg in enumerate(reversed(messages[1:])): # Skip the first message
955
+ for msg in reversed(messages_to_scan):
933
956
  num_tokens += self.estimate_tokens_for_message(msg)
934
957
  if num_tokens > protected_tokens:
935
958
  break
@@ -0,0 +1,371 @@
1
+ """Cache helpers for Claude Code / Anthropic.
2
+
3
+ ClaudeCacheAsyncClient: httpx client that tries to patch /v1/messages bodies.
4
+
5
+ We now also expose `patch_anthropic_client_messages` which monkey-patches
6
+ AsyncAnthropic.messages.create() so we can inject cache_control BEFORE
7
+ serialization, avoiding httpx/Pydantic internals.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import base64
13
+ import json
14
+ import logging
15
+ import time
16
+ from typing import Any, Callable, MutableMapping
17
+
18
+ import httpx
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ # Refresh token if it's older than 1 hour (3600 seconds)
23
+ TOKEN_MAX_AGE_SECONDS = 3600
24
+
25
+ try:
26
+ from anthropic import AsyncAnthropic
27
+ except ImportError: # pragma: no cover - optional dep
28
+ AsyncAnthropic = None # type: ignore
29
+
30
+
31
+ class ClaudeCacheAsyncClient(httpx.AsyncClient):
32
+ def _get_jwt_age_seconds(self, token: str | None) -> float | None:
33
+ """Decode a JWT and return its age in seconds.
34
+
35
+ Returns None if the token can't be decoded or has no timestamp claims.
36
+ Uses 'iat' (issued at) if available, otherwise calculates from 'exp'.
37
+ """
38
+ if not token:
39
+ return None
40
+
41
+ try:
42
+ # JWT format: header.payload.signature
43
+ # We only need the payload (second part)
44
+ parts = token.split(".")
45
+ if len(parts) != 3:
46
+ return None
47
+
48
+ # Decode the payload (base64url encoded)
49
+ payload_b64 = parts[1]
50
+ # Add padding if needed (base64url doesn't require padding)
51
+ padding = 4 - len(payload_b64) % 4
52
+ if padding != 4:
53
+ payload_b64 += "=" * padding
54
+
55
+ payload_bytes = base64.urlsafe_b64decode(payload_b64)
56
+ payload = json.loads(payload_bytes.decode("utf-8"))
57
+
58
+ now = time.time()
59
+
60
+ # Prefer 'iat' (issued at) claim if available
61
+ if "iat" in payload:
62
+ iat = float(payload["iat"])
63
+ age = now - iat
64
+ return age
65
+
66
+ # Fall back to calculating from 'exp' claim
67
+ # Assume tokens are typically valid for 1 hour
68
+ if "exp" in payload:
69
+ exp = float(payload["exp"])
70
+ # If exp is in the future, calculate how long until expiry
71
+ # and assume the token was issued 1 hour before expiry
72
+ time_until_exp = exp - now
73
+ # If token has less than 1 hour left, it's "old"
74
+ age = TOKEN_MAX_AGE_SECONDS - time_until_exp
75
+ return max(0, age)
76
+
77
+ return None
78
+ except Exception as exc:
79
+ logger.debug("Failed to decode JWT age: %s", exc)
80
+ return None
81
+
82
+ def _extract_bearer_token(self, request: httpx.Request) -> str | None:
83
+ """Extract the bearer token from request headers."""
84
+ auth_header = request.headers.get("Authorization") or request.headers.get(
85
+ "authorization"
86
+ )
87
+ if auth_header and auth_header.lower().startswith("bearer "):
88
+ return auth_header[7:] # Strip "Bearer " prefix
89
+ return None
90
+
91
+ def _should_refresh_token(self, request: httpx.Request) -> bool:
92
+ """Check if the token in the request is older than 1 hour."""
93
+ token = self._extract_bearer_token(request)
94
+ if not token:
95
+ return False
96
+
97
+ age = self._get_jwt_age_seconds(token)
98
+ if age is None:
99
+ return False
100
+
101
+ should_refresh = age >= TOKEN_MAX_AGE_SECONDS
102
+ if should_refresh:
103
+ logger.info(
104
+ "JWT token is %.1f seconds old (>= %d), will refresh proactively",
105
+ age,
106
+ TOKEN_MAX_AGE_SECONDS,
107
+ )
108
+ return should_refresh
109
+
110
+ async def send(
111
+ self, request: httpx.Request, *args: Any, **kwargs: Any
112
+ ) -> httpx.Response: # type: ignore[override]
113
+ # Proactive token refresh: check JWT age before every request
114
+ if not request.extensions.get("claude_oauth_refresh_attempted"):
115
+ try:
116
+ if self._should_refresh_token(request):
117
+ refreshed_token = self._refresh_claude_oauth_token()
118
+ if refreshed_token:
119
+ logger.info("Proactively refreshed token before request")
120
+ # Rebuild request with new token
121
+ headers = dict(request.headers)
122
+ self._update_auth_headers(headers, refreshed_token)
123
+ body_bytes = self._extract_body_bytes(request)
124
+ request = self.build_request(
125
+ method=request.method,
126
+ url=request.url,
127
+ headers=headers,
128
+ content=body_bytes,
129
+ )
130
+ request.extensions["claude_oauth_refresh_attempted"] = True
131
+ except Exception as exc:
132
+ logger.debug("Error during proactive token refresh check: %s", exc)
133
+
134
+ try:
135
+ if request.url.path.endswith("/v1/messages"):
136
+ body_bytes = self._extract_body_bytes(request)
137
+ if body_bytes:
138
+ updated = self._inject_cache_control(body_bytes)
139
+ if updated is not None:
140
+ # Rebuild a request with the updated body and transplant internals
141
+ try:
142
+ rebuilt = self.build_request(
143
+ method=request.method,
144
+ url=request.url,
145
+ headers=request.headers,
146
+ content=updated,
147
+ )
148
+
149
+ # Copy core internals so httpx uses the modified body/stream
150
+ if hasattr(rebuilt, "_content"):
151
+ setattr(request, "_content", rebuilt._content) # type: ignore[attr-defined]
152
+ if hasattr(rebuilt, "stream"):
153
+ request.stream = rebuilt.stream
154
+ if hasattr(rebuilt, "extensions"):
155
+ request.extensions = rebuilt.extensions
156
+
157
+ # Ensure Content-Length matches the new body
158
+ request.headers["Content-Length"] = str(len(updated))
159
+
160
+ except Exception:
161
+ # Swallow instrumentation errors; do not break real calls.
162
+ pass
163
+ except Exception:
164
+ # Swallow wrapper errors; do not break real calls.
165
+ pass
166
+ response = await super().send(request, *args, **kwargs)
167
+ try:
168
+ # Check for both 401 and 400 - Anthropic/Cloudflare may return 400 for auth errors
169
+ # Also check if it's a Cloudflare HTML error response
170
+ if response.status_code in (400, 401) and not request.extensions.get(
171
+ "claude_oauth_refresh_attempted"
172
+ ):
173
+ # Determine if this is an auth error (including Cloudflare HTML errors)
174
+ is_auth_error = response.status_code == 401
175
+
176
+ if response.status_code == 400:
177
+ # Check if this is a Cloudflare HTML error
178
+ is_auth_error = self._is_cloudflare_html_error(response)
179
+ if is_auth_error:
180
+ logger.info(
181
+ "Detected Cloudflare 400 error (likely auth-related), attempting token refresh"
182
+ )
183
+
184
+ if is_auth_error:
185
+ refreshed_token = self._refresh_claude_oauth_token()
186
+ if refreshed_token:
187
+ logger.info("Token refreshed successfully, retrying request")
188
+ await response.aclose()
189
+ body_bytes = self._extract_body_bytes(request)
190
+ headers = dict(request.headers)
191
+ self._update_auth_headers(headers, refreshed_token)
192
+ retry_request = self.build_request(
193
+ method=request.method,
194
+ url=request.url,
195
+ headers=headers,
196
+ content=body_bytes,
197
+ )
198
+ retry_request.extensions["claude_oauth_refresh_attempted"] = (
199
+ True
200
+ )
201
+ return await super().send(retry_request, *args, **kwargs)
202
+ else:
203
+ logger.warning("Token refresh failed, returning original error")
204
+ except Exception as exc:
205
+ logger.debug("Error during token refresh attempt: %s", exc)
206
+ return response
207
+
208
+ @staticmethod
209
+ def _extract_body_bytes(request: httpx.Request) -> bytes | None:
210
+ # Try public content first
211
+ try:
212
+ content = request.content
213
+ if content:
214
+ return content
215
+ except Exception:
216
+ pass
217
+
218
+ # Fallback to private attr if necessary
219
+ try:
220
+ content = getattr(request, "_content", None)
221
+ if content:
222
+ return content
223
+ except Exception:
224
+ pass
225
+
226
+ return None
227
+
228
+ @staticmethod
229
+ def _update_auth_headers(
230
+ headers: MutableMapping[str, str], access_token: str
231
+ ) -> None:
232
+ bearer_value = f"Bearer {access_token}"
233
+ if "Authorization" in headers or "authorization" in headers:
234
+ headers["Authorization"] = bearer_value
235
+ elif "x-api-key" in headers or "X-API-Key" in headers:
236
+ headers["x-api-key"] = access_token
237
+ else:
238
+ headers["Authorization"] = bearer_value
239
+
240
+ @staticmethod
241
+ def _is_cloudflare_html_error(response: httpx.Response) -> bool:
242
+ """Check if this is a Cloudflare HTML error response.
243
+
244
+ Cloudflare often returns HTML error pages with status 400 when
245
+ there are authentication issues.
246
+ """
247
+ # Check content type
248
+ content_type = response.headers.get("content-type", "")
249
+ if "text/html" not in content_type.lower():
250
+ return False
251
+
252
+ # Check if body contains Cloudflare markers
253
+ try:
254
+ # Read response body if not already consumed
255
+ if hasattr(response, "_content") and response._content:
256
+ body = response._content.decode("utf-8", errors="ignore")
257
+ else:
258
+ # Try to read the text (this might be already consumed)
259
+ try:
260
+ body = response.text
261
+ except Exception:
262
+ return False
263
+
264
+ # Look for Cloudflare and 400 Bad Request markers
265
+ body_lower = body.lower()
266
+ return "cloudflare" in body_lower and "400 bad request" in body_lower
267
+ except Exception as exc:
268
+ logger.debug("Error checking for Cloudflare error: %s", exc)
269
+ return False
270
+
271
+ def _refresh_claude_oauth_token(self) -> str | None:
272
+ try:
273
+ from code_puppy.plugins.claude_code_oauth.utils import refresh_access_token
274
+
275
+ logger.info("Attempting to refresh Claude Code OAuth token...")
276
+ refreshed_token = refresh_access_token(force=True)
277
+ if refreshed_token:
278
+ self._update_auth_headers(self.headers, refreshed_token)
279
+ logger.info("Successfully refreshed Claude Code OAuth token")
280
+ else:
281
+ logger.warning("Token refresh returned None")
282
+ return refreshed_token
283
+ except Exception as exc:
284
+ logger.error("Exception during token refresh: %s", exc)
285
+ return None
286
+
287
+ @staticmethod
288
+ def _inject_cache_control(body: bytes) -> bytes | None:
289
+ try:
290
+ data = json.loads(body.decode("utf-8"))
291
+ except Exception:
292
+ return None
293
+
294
+ if not isinstance(data, dict):
295
+ return None
296
+
297
+ modified = False
298
+
299
+ # Minimal, deterministic strategy:
300
+ # Add cache_control only on the single most recent block:
301
+ # the last dict content block of the last message (if any).
302
+ messages = data.get("messages")
303
+ if isinstance(messages, list) and messages:
304
+ last = messages[-1]
305
+ if isinstance(last, dict):
306
+ content = last.get("content")
307
+ if isinstance(content, list) and content:
308
+ last_block = content[-1]
309
+ if (
310
+ isinstance(last_block, dict)
311
+ and "cache_control" not in last_block
312
+ ):
313
+ last_block["cache_control"] = {"type": "ephemeral"}
314
+ modified = True
315
+
316
+ if not modified:
317
+ return None
318
+
319
+ return json.dumps(data).encode("utf-8")
320
+
321
+
322
+ def _inject_cache_control_in_payload(payload: dict[str, Any]) -> None:
323
+ """In-place cache_control injection on Anthropic messages.create payload."""
324
+
325
+ messages = payload.get("messages")
326
+ if isinstance(messages, list) and messages:
327
+ last = messages[-1]
328
+ if isinstance(last, dict):
329
+ content = last.get("content")
330
+ if isinstance(content, list) and content:
331
+ last_block = content[-1]
332
+ if isinstance(last_block, dict) and "cache_control" not in last_block:
333
+ last_block["cache_control"] = {"type": "ephemeral"}
334
+
335
+ # No extra markers in production mode; keep payload clean.
336
+ # (Function kept for potential future use.)
337
+ return
338
+
339
+
340
+ def patch_anthropic_client_messages(client: Any) -> None:
341
+ """Monkey-patch AsyncAnthropic.messages.create to inject cache_control.
342
+
343
+ This operates at the highest level: just before Anthropic SDK serializes
344
+ the request into HTTP. That means no httpx / Pydantic shenanigans can
345
+ undo it.
346
+ """
347
+
348
+ if AsyncAnthropic is None or not isinstance(client, AsyncAnthropic): # type: ignore[arg-type]
349
+ return
350
+
351
+ try:
352
+ messages_obj = getattr(client, "messages", None)
353
+ if messages_obj is None:
354
+ return
355
+ original_create: Callable[..., Any] = messages_obj.create
356
+ except Exception: # pragma: no cover - defensive
357
+ return
358
+
359
+ async def wrapped_create(*args: Any, **kwargs: Any):
360
+ # Anthropic messages.create takes a mix of positional/kw args.
361
+ # The payload is usually in kwargs for the Python SDK.
362
+ if kwargs:
363
+ _inject_cache_control_in_payload(kwargs)
364
+ elif args:
365
+ maybe_payload = args[-1]
366
+ if isinstance(maybe_payload, dict):
367
+ _inject_cache_control_in_payload(maybe_payload)
368
+
369
+ return await original_create(*args, **kwargs)
370
+
371
+ messages_obj.create = wrapped_create # type: ignore[assignment]
@@ -144,8 +144,8 @@ async def main():
144
144
  except ImportError:
145
145
  emit_system_message("🐶 Code Puppy is Loading...")
146
146
 
147
- # Check for truecolor support and warn if not available
148
- print_truecolor_warning(display_console)
147
+ # Truecolor warning moved to interactive_mode() so it prints LAST
148
+ # after all the help stuff - max visibility for the ugly red box!
149
149
 
150
150
  available_port = find_available_port()
151
151
  if available_port is None:
@@ -382,6 +382,10 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
382
382
 
383
383
  emit_warning(f"MOTD error: {e}")
384
384
 
385
+ # Print truecolor warning LAST so it's the most visible thing on startup
386
+ # Big ugly red box should be impossible to miss! 🔴
387
+ print_truecolor_warning(display_console)
388
+
385
389
  # Initialize the runtime agent manager
386
390
  if initial_command:
387
391
  from code_puppy.agents import get_current_agent
@@ -69,12 +69,21 @@ def _get_session_entries(base_dir: Path) -> List[Tuple[str, dict]]:
69
69
 
70
70
 
71
71
  def _extract_last_user_message(history: list) -> str:
72
- """Extract the most recent user message from history."""
72
+ """Extract the most recent user message from history.
73
+
74
+ Joins all content parts from the message since messages can have
75
+ multiple parts (e.g., text + attachments, multi-part prompts).
76
+ """
73
77
  # Walk backwards through history to find last user message
74
78
  for msg in reversed(history):
79
+ content_parts = []
75
80
  for part in msg.parts:
76
81
  if hasattr(part, "content"):
77
- return part.content
82
+ content = part.content
83
+ if isinstance(content, str) and content.strip():
84
+ content_parts.append(content)
85
+ if content_parts:
86
+ return "\n\n".join(content_parts)
78
87
  return "[No messages found]"
79
88
 
80
89
 
@@ -298,19 +307,13 @@ def _render_message_browser_panel(
298
307
  # Don't override Rich's ANSI styling - use empty style
299
308
  text_color = ""
300
309
 
301
- # Truncate if too long (max 35 lines)
302
- message_lines = rendered.split("\n")[:35]
303
- is_truncated = len(rendered.split("\n")) > 35
310
+ # Show full message without truncation
311
+ message_lines = rendered.split("\n")
304
312
 
305
313
  for line in message_lines:
306
314
  lines.append((text_color, f" {line}"))
307
315
  lines.append(("", "\n"))
308
316
 
309
- if is_truncated:
310
- lines.append(("", "\n"))
311
- lines.append(("fg:yellow", " ... truncated (message too long)"))
312
- lines.append(("", "\n"))
313
-
314
317
  except Exception as e:
315
318
  lines.append(("fg:red", f" Error rendering message: {e}"))
316
319
  lines.append(("", "\n"))
@@ -359,7 +362,7 @@ def _render_preview_panel(base_dir: Path, entry: Optional[Tuple[str, dict]]) ->
359
362
  lines.append(("", "\n\n"))
360
363
 
361
364
  lines.append(("bold", " Last Message:"))
362
- lines.append(("fg:ansibrightblack", " (press 'e' to browse all)"))
365
+ lines.append(("fg:ansibrightblack", " (press 'e' to browse full history)"))
363
366
  lines.append(("", "\n"))
364
367
 
365
368
  # Try to load and preview the last message
@@ -367,15 +370,11 @@ def _render_preview_panel(base_dir: Path, entry: Optional[Tuple[str, dict]]) ->
367
370
  history = load_session(session_name, base_dir)
368
371
  last_message = _extract_last_user_message(history)
369
372
 
370
- # Check if original message is long (before Rich processing)
371
- original_lines = last_message.split("\n") if last_message else []
372
- is_long = len(original_lines) > 30
373
-
374
- # Render markdown with rich but strip ANSI codes
373
+ # Render markdown with rich
375
374
  console = Console(
376
375
  file=StringIO(),
377
376
  legacy_windows=False,
378
- no_color=False, # Disable ANSI color codes
377
+ no_color=False,
379
378
  force_terminal=False,
380
379
  width=76,
381
380
  )
@@ -383,19 +382,14 @@ def _render_preview_panel(base_dir: Path, entry: Optional[Tuple[str, dict]]) ->
383
382
  console.print(md)
384
383
  rendered = console.file.getvalue()
385
384
 
386
- # Truncate if too long (max 30 lines for bigger preview)
387
- message_lines = rendered.split("\n")[:30]
385
+ # Show full message without truncation
386
+ message_lines = rendered.split("\n")
388
387
 
389
388
  for line in message_lines:
390
389
  # Rich already rendered the markdown, just display it dimmed
391
390
  lines.append(("fg:ansibrightblack", f" {line}"))
392
391
  lines.append(("", "\n"))
393
392
 
394
- if is_long:
395
- lines.append(("", "\n"))
396
- lines.append(("fg:yellow", " ... truncated"))
397
- lines.append(("", "\n"))
398
-
399
393
  except Exception as e:
400
394
  lines.append(("fg:red", f" Error loading preview: {e}"))
401
395
  lines.append(("", "\n"))
@@ -29,7 +29,6 @@ from code_puppy.command_line.attachments import (
29
29
  )
30
30
  from code_puppy.command_line.clipboard import (
31
31
  capture_clipboard_image_to_pending,
32
- get_clipboard_manager,
33
32
  has_image_in_clipboard,
34
33
  )
35
34
  from code_puppy.command_line.command_registry import get_unique_commands
@@ -663,16 +662,18 @@ async def get_input_with_combined_completion(
663
662
  placeholder = capture_clipboard_image_to_pending()
664
663
  if placeholder:
665
664
  event.app.current_buffer.insert_text(placeholder + " ")
666
- count = get_clipboard_manager().get_pending_count()
667
- sys.stdout.write(f"\033[36m📋 +image ({count})\033[0m ")
668
- sys.stdout.flush()
665
+ # The placeholder itself is visible feedback - no need for extra output
666
+ # Use bell for audible feedback (works in most terminals)
667
+ event.app.output.bell()
669
668
  return # Don't also paste the text data
670
669
  except Exception:
671
670
  pass
672
671
 
673
- # No image - insert the pasted text as normal
672
+ # No image - insert the pasted text as normal, sanitizing Windows newlines
674
673
  if pasted_data:
675
- event.app.current_buffer.insert_text(pasted_data)
674
+ # Normalize Windows line endings to Unix style
675
+ sanitized_data = pasted_data.replace("\r\n", "\n").replace("\r", "\n")
676
+ event.app.current_buffer.insert_text(sanitized_data)
676
677
 
677
678
  # Fallback Ctrl+V for terminals without bracketed paste support
678
679
  @bindings.add("c-v", eager=True)
@@ -684,9 +685,9 @@ async def get_input_with_combined_completion(
684
685
  placeholder = capture_clipboard_image_to_pending()
685
686
  if placeholder:
686
687
  event.app.current_buffer.insert_text(placeholder + " ")
687
- count = get_clipboard_manager().get_pending_count()
688
- sys.stdout.write(f"\033[36m📋 +image ({count})\033[0m ")
689
- sys.stdout.flush()
688
+ # The placeholder itself is visible feedback - no need for extra output
689
+ # Use bell for audible feedback (works in most terminals)
690
+ event.app.output.bell()
690
691
  return # Don't also paste text
691
692
  except Exception:
692
693
  pass # Fall through to text paste on any error
@@ -715,7 +716,7 @@ async def get_input_with_combined_completion(
715
716
  timeout=2,
716
717
  )
717
718
  if result.returncode == 0:
718
- text = result.stdout.rstrip("\r\n")
719
+ text = result.stdout
719
720
  else: # Linux
720
721
  # Try xclip first, then xsel
721
722
  for cmd in [
@@ -733,6 +734,10 @@ async def get_input_with_combined_completion(
733
734
  continue
734
735
 
735
736
  if text:
737
+ # Normalize Windows line endings to Unix style
738
+ text = text.replace("\r\n", "\n").replace("\r", "\n")
739
+ # Strip trailing newline that clipboard tools often add
740
+ text = text.rstrip("\n")
736
741
  event.app.current_buffer.insert_text(text)
737
742
  except Exception:
738
743
  pass # Silently fail if text paste doesn't work
@@ -746,15 +751,16 @@ async def get_input_with_combined_completion(
746
751
  placeholder = capture_clipboard_image_to_pending()
747
752
  if placeholder:
748
753
  event.app.current_buffer.insert_text(placeholder + " ")
749
- count = get_clipboard_manager().get_pending_count()
750
- sys.stdout.write(f"\033[36m📋 +image ({count})\033[0m ")
751
- sys.stdout.flush()
754
+ # The placeholder itself is visible feedback
755
+ # Use bell for audible feedback (works in most terminals)
756
+ event.app.output.bell()
752
757
  else:
753
- sys.stdout.write("\033[33m⚠️ no image\033[0m ")
754
- sys.stdout.flush()
758
+ # Insert a transient message that user can delete
759
+ event.app.current_buffer.insert_text("[⚠️ no image in clipboard] ")
760
+ event.app.output.bell()
755
761
  except Exception:
756
- sys.stdout.write("\033[31m❌ clipboard error\033[0m ")
757
- sys.stdout.flush()
762
+ event.app.current_buffer.insert_text("[❌ clipboard error] ")
763
+ event.app.output.bell()
758
764
 
759
765
  session = PromptSession(
760
766
  completer=completer,
@@ -222,18 +222,14 @@ class ManagedMCPServer:
222
222
  http_kwargs["timeout"] = config["timeout"]
223
223
  if "read_timeout" in config:
224
224
  http_kwargs["read_timeout"] = config["read_timeout"]
225
- if "headers" in config:
226
- # Expand environment variables in headers
227
- headers = config.get("headers")
228
- resolved_headers = {}
229
- if isinstance(headers, dict):
230
- for k, v in headers.items():
231
- if isinstance(v, str):
232
- resolved_headers[k] = os.path.expandvars(v)
233
- else:
234
- resolved_headers[k] = v
235
- http_kwargs["headers"] = resolved_headers
225
+
226
+ # Handle http_client vs headers (mutually exclusive)
227
+ if "http_client" in config:
228
+ # Use provided http_client
229
+ http_kwargs["http_client"] = config["http_client"]
230
+ elif config.get("headers"):
236
231
  # Create HTTP client if headers are provided but no client specified
232
+ http_kwargs["http_client"] = self._get_http_client()
237
233
 
238
234
  self._pydantic_server = MCPServerStreamableHTTP(
239
235
  **http_kwargs, process_tool_call=process_tool_call
@@ -727,15 +727,9 @@ def _format_diff_with_syntax_highlighting(
727
727
  result.append("\n")
728
728
  continue
729
729
 
730
- # Handle diff headers specially
731
- if line.startswith("---"):
732
- result.append(line, style="yellow")
733
- elif line.startswith("+++"):
734
- result.append(line, style="yellow")
735
- elif line.startswith("@@"):
736
- result.append(line, style="cyan")
737
- elif line.startswith(("diff ", "index ")):
738
- result.append(line, style="dim")
730
+ # Skip diff headers - they're redundant noise since we show the filename in the banner
731
+ if line.startswith(("---", "+++", "@@", "diff ", "index ")):
732
+ continue
739
733
  else:
740
734
  # Determine line type and extract code content
741
735
  if line.startswith("-"):