code-puppy 0.0.335__tar.gz → 0.0.336__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 (181) hide show
  1. {code_puppy-0.0.335 → code_puppy-0.0.336}/PKG-INFO +1 -1
  2. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/messaging/rich_renderer.py +101 -19
  3. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/plugins/antigravity_oauth/antigravity_model.py +50 -9
  4. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/plugins/antigravity_oauth/transport.py +127 -58
  5. {code_puppy-0.0.335 → code_puppy-0.0.336}/pyproject.toml +1 -1
  6. {code_puppy-0.0.335 → code_puppy-0.0.336}/.gitignore +0 -0
  7. {code_puppy-0.0.335 → code_puppy-0.0.336}/LICENSE +0 -0
  8. {code_puppy-0.0.335 → code_puppy-0.0.336}/README.md +0 -0
  9. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/__init__.py +0 -0
  10. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/__main__.py +0 -0
  11. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/agents/__init__.py +0 -0
  12. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/agents/agent_c_reviewer.py +0 -0
  13. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/agents/agent_code_puppy.py +0 -0
  14. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/agents/agent_code_reviewer.py +0 -0
  15. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/agents/agent_cpp_reviewer.py +0 -0
  16. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/agents/agent_creator_agent.py +0 -0
  17. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/agents/agent_golang_reviewer.py +0 -0
  18. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/agents/agent_javascript_reviewer.py +0 -0
  19. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/agents/agent_manager.py +0 -0
  20. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/agents/agent_planning.py +0 -0
  21. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/agents/agent_python_programmer.py +0 -0
  22. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/agents/agent_python_reviewer.py +0 -0
  23. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/agents/agent_qa_expert.py +0 -0
  24. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/agents/agent_qa_kitten.py +0 -0
  25. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/agents/agent_security_auditor.py +0 -0
  26. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/agents/agent_typescript_reviewer.py +0 -0
  27. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/agents/base_agent.py +0 -0
  28. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/agents/json_agent.py +0 -0
  29. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/agents/prompt_reviewer.py +0 -0
  30. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/callbacks.py +0 -0
  31. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/chatgpt_codex_client.py +0 -0
  32. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/claude_cache_client.py +0 -0
  33. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/cli_runner.py +0 -0
  34. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/command_line/__init__.py +0 -0
  35. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/command_line/add_model_menu.py +0 -0
  36. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/command_line/attachments.py +0 -0
  37. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/command_line/autosave_menu.py +0 -0
  38. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/command_line/colors_menu.py +0 -0
  39. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/command_line/command_handler.py +0 -0
  40. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/command_line/command_registry.py +0 -0
  41. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/command_line/config_commands.py +0 -0
  42. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/command_line/core_commands.py +0 -0
  43. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/command_line/diff_menu.py +0 -0
  44. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/command_line/file_path_completion.py +0 -0
  45. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/command_line/load_context_completion.py +0 -0
  46. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/command_line/mcp/__init__.py +0 -0
  47. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/command_line/mcp/add_command.py +0 -0
  48. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/command_line/mcp/base.py +0 -0
  49. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/command_line/mcp/catalog_server_installer.py +0 -0
  50. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/command_line/mcp/custom_server_form.py +0 -0
  51. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/command_line/mcp/custom_server_installer.py +0 -0
  52. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/command_line/mcp/edit_command.py +0 -0
  53. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/command_line/mcp/handler.py +0 -0
  54. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/command_line/mcp/help_command.py +0 -0
  55. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/command_line/mcp/install_command.py +0 -0
  56. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/command_line/mcp/install_menu.py +0 -0
  57. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/command_line/mcp/list_command.py +0 -0
  58. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/command_line/mcp/logs_command.py +0 -0
  59. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/command_line/mcp/remove_command.py +0 -0
  60. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/command_line/mcp/restart_command.py +0 -0
  61. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/command_line/mcp/search_command.py +0 -0
  62. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/command_line/mcp/start_all_command.py +0 -0
  63. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/command_line/mcp/start_command.py +0 -0
  64. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/command_line/mcp/status_command.py +0 -0
  65. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/command_line/mcp/stop_all_command.py +0 -0
  66. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/command_line/mcp/stop_command.py +0 -0
  67. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/command_line/mcp/test_command.py +0 -0
  68. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/command_line/mcp/utils.py +0 -0
  69. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/command_line/mcp/wizard_utils.py +0 -0
  70. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/command_line/mcp_completion.py +0 -0
  71. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/command_line/model_picker_completion.py +0 -0
  72. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/command_line/model_settings_menu.py +0 -0
  73. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/command_line/motd.py +0 -0
  74. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/command_line/onboarding_slides.py +0 -0
  75. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/command_line/onboarding_wizard.py +0 -0
  76. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/command_line/pin_command_completion.py +0 -0
  77. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/command_line/prompt_toolkit_completion.py +0 -0
  78. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/command_line/session_commands.py +0 -0
  79. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/command_line/utils.py +0 -0
  80. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/config.py +0 -0
  81. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/error_logging.py +0 -0
  82. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/gemini_code_assist.py +0 -0
  83. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/http_utils.py +0 -0
  84. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/keymap.py +0 -0
  85. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/main.py +0 -0
  86. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/mcp_/__init__.py +0 -0
  87. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/mcp_/async_lifecycle.py +0 -0
  88. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/mcp_/blocking_startup.py +0 -0
  89. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/mcp_/captured_stdio_server.py +0 -0
  90. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/mcp_/circuit_breaker.py +0 -0
  91. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/mcp_/config_wizard.py +0 -0
  92. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/mcp_/dashboard.py +0 -0
  93. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/mcp_/error_isolation.py +0 -0
  94. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/mcp_/examples/retry_example.py +0 -0
  95. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/mcp_/health_monitor.py +0 -0
  96. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/mcp_/managed_server.py +0 -0
  97. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/mcp_/manager.py +0 -0
  98. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/mcp_/mcp_logs.py +0 -0
  99. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/mcp_/registry.py +0 -0
  100. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/mcp_/retry_manager.py +0 -0
  101. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/mcp_/server_registry_catalog.py +0 -0
  102. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/mcp_/status_tracker.py +0 -0
  103. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/mcp_/system_tools.py +0 -0
  104. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/messaging/__init__.py +0 -0
  105. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/messaging/bus.py +0 -0
  106. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/messaging/commands.py +0 -0
  107. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/messaging/markdown_patches.py +0 -0
  108. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/messaging/message_queue.py +0 -0
  109. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/messaging/messages.py +0 -0
  110. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/messaging/queue_console.py +0 -0
  111. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/messaging/renderers.py +0 -0
  112. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/messaging/spinner/__init__.py +0 -0
  113. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/messaging/spinner/console_spinner.py +0 -0
  114. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/messaging/spinner/spinner_base.py +0 -0
  115. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/model_factory.py +0 -0
  116. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/model_utils.py +0 -0
  117. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/models.json +0 -0
  118. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/models_dev_api.json +0 -0
  119. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/models_dev_parser.py +0 -0
  120. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/plugins/__init__.py +0 -0
  121. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/plugins/antigravity_oauth/__init__.py +0 -0
  122. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/plugins/antigravity_oauth/accounts.py +0 -0
  123. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/plugins/antigravity_oauth/config.py +0 -0
  124. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/plugins/antigravity_oauth/constants.py +0 -0
  125. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/plugins/antigravity_oauth/oauth.py +0 -0
  126. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/plugins/antigravity_oauth/register_callbacks.py +0 -0
  127. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/plugins/antigravity_oauth/storage.py +0 -0
  128. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/plugins/antigravity_oauth/test_plugin.py +0 -0
  129. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/plugins/antigravity_oauth/token.py +0 -0
  130. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/plugins/antigravity_oauth/utils.py +0 -0
  131. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/plugins/chatgpt_oauth/__init__.py +0 -0
  132. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/plugins/chatgpt_oauth/config.py +0 -0
  133. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/plugins/chatgpt_oauth/oauth_flow.py +0 -0
  134. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/plugins/chatgpt_oauth/register_callbacks.py +0 -0
  135. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/plugins/chatgpt_oauth/test_plugin.py +0 -0
  136. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/plugins/chatgpt_oauth/utils.py +0 -0
  137. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/plugins/claude_code_oauth/README.md +0 -0
  138. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/plugins/claude_code_oauth/SETUP.md +0 -0
  139. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/plugins/claude_code_oauth/__init__.py +0 -0
  140. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/plugins/claude_code_oauth/config.py +0 -0
  141. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/plugins/claude_code_oauth/register_callbacks.py +0 -0
  142. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/plugins/claude_code_oauth/test_plugin.py +0 -0
  143. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/plugins/claude_code_oauth/utils.py +0 -0
  144. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/plugins/customizable_commands/__init__.py +0 -0
  145. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/plugins/customizable_commands/register_callbacks.py +0 -0
  146. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/plugins/example_custom_command/README.md +0 -0
  147. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/plugins/example_custom_command/register_callbacks.py +0 -0
  148. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/plugins/file_permission_handler/__init__.py +0 -0
  149. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/plugins/file_permission_handler/register_callbacks.py +0 -0
  150. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/plugins/oauth_puppy_html.py +0 -0
  151. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/plugins/shell_safety/__init__.py +0 -0
  152. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/plugins/shell_safety/agent_shell_safety.py +0 -0
  153. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/plugins/shell_safety/command_cache.py +0 -0
  154. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/plugins/shell_safety/register_callbacks.py +0 -0
  155. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/prompts/codex_system_prompt.md +0 -0
  156. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/pydantic_patches.py +0 -0
  157. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/reopenable_async_client.py +0 -0
  158. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/round_robin_model.py +0 -0
  159. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/session_storage.py +0 -0
  160. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/status_display.py +0 -0
  161. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/summarization_agent.py +0 -0
  162. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/terminal_utils.py +0 -0
  163. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/tools/__init__.py +0 -0
  164. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/tools/agent_tools.py +0 -0
  165. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/tools/browser/__init__.py +0 -0
  166. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/tools/browser/browser_control.py +0 -0
  167. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/tools/browser/browser_interactions.py +0 -0
  168. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/tools/browser/browser_locators.py +0 -0
  169. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/tools/browser/browser_navigation.py +0 -0
  170. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/tools/browser/browser_screenshot.py +0 -0
  171. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/tools/browser/browser_scripts.py +0 -0
  172. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/tools/browser/browser_workflows.py +0 -0
  173. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/tools/browser/camoufox_manager.py +0 -0
  174. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/tools/browser/vqa_agent.py +0 -0
  175. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/tools/command_runner.py +0 -0
  176. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/tools/common.py +0 -0
  177. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/tools/file_modifications.py +0 -0
  178. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/tools/file_operations.py +0 -0
  179. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/tools/tools_content.py +0 -0
  180. {code_puppy-0.0.335 → code_puppy-0.0.336}/code_puppy/uvx_detection.py +0 -0
  181. {code_puppy-0.0.335 → code_puppy-0.0.336}/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.335
3
+ Version: 0.0.336
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
@@ -348,7 +348,17 @@ class RichConsoleRenderer:
348
348
  # =========================================================================
349
349
 
350
350
  def _render_file_listing(self, msg: FileListingMessage) -> None:
351
- """Render a directory listing matching the old Rich-formatted output."""
351
+ """Render a compact directory listing with directory summaries.
352
+
353
+ Instead of listing every file, we group by directory and show:
354
+ - Directory name
355
+ - Number of files
356
+ - Total size
357
+ - Number of subdirectories
358
+ """
359
+ import os
360
+ from collections import defaultdict
361
+
352
362
  # Header on single line
353
363
  rec_flag = f"(recursive={msg.recursive})"
354
364
  banner = self._format_banner("directory_listing", "DIRECTORY LISTING")
@@ -357,32 +367,104 @@ class RichConsoleRenderer:
357
367
  f"📂 [bold cyan]{msg.directory}[/bold cyan] [dim]{rec_flag}[/dim]\n"
358
368
  )
359
369
 
360
- # Directory header
361
- dir_name = msg.directory.rstrip("/").split("/")[-1] or msg.directory
362
- self._console.print(f"📁 [bold blue]{dir_name}[/bold blue]")
370
+ # Build a tree structure: {parent_path: {files: [], dirs: set(), size: int}}
371
+ # Each key is a directory path, value contains direct children stats
372
+ dir_stats: dict = defaultdict(
373
+ lambda: {"files": [], "subdirs": set(), "total_size": 0}
374
+ )
375
+
376
+ # Root directory is represented as ""
377
+ root_key = ""
363
378
 
364
- # Build tree structure from flat list
365
379
  for entry in msg.files:
366
- # Calculate indentation based on depth
367
- prefix = ""
368
- for d in range(entry.depth + 1):
369
- if d == entry.depth:
370
- prefix += "└── "
371
- else:
372
- prefix += " "
380
+ path = entry.path
381
+ parent = os.path.dirname(path) if os.path.dirname(path) else root_key
373
382
 
374
383
  if entry.type == "dir":
375
- self._console.print(f"{prefix}📁 [bold blue]{entry.path}/[/bold blue]")
384
+ # Register this dir as a subdir of its parent
385
+ dir_stats[parent]["subdirs"].add(path)
386
+ # Ensure the dir itself exists in stats (even if empty)
387
+ _ = dir_stats[path]
376
388
  else:
377
- icon = self._get_file_icon(entry.path)
378
- if entry.size > 0:
379
- size_str = f" [dim]({self._format_size(entry.size)})[/dim]"
380
- else:
381
- size_str = ""
389
+ # It's a file - add to parent's stats
390
+ dir_stats[parent]["files"].append(entry)
391
+ dir_stats[parent]["total_size"] += entry.size
392
+
393
+ def render_dir_tree(dir_path: str, depth: int = 0) -> None:
394
+ """Recursively render directory with compact summary."""
395
+ stats = dir_stats.get(
396
+ dir_path, {"files": [], "subdirs": set(), "total_size": 0}
397
+ )
398
+ files = stats["files"]
399
+ subdirs = sorted(stats["subdirs"])
400
+
401
+ # Calculate total size including subdirectories (recursive)
402
+ def get_recursive_size(d: str) -> int:
403
+ s = dir_stats.get(d, {"files": [], "subdirs": set(), "total_size": 0})
404
+ size = s["total_size"]
405
+ for sub in s["subdirs"]:
406
+ size += get_recursive_size(sub)
407
+ return size
408
+
409
+ def get_recursive_file_count(d: str) -> int:
410
+ s = dir_stats.get(d, {"files": [], "subdirs": set(), "total_size": 0})
411
+ count = len(s["files"])
412
+ for sub in s["subdirs"]:
413
+ count += get_recursive_file_count(sub)
414
+ return count
415
+
416
+ indent = " " * depth
417
+
418
+ # For root level, just show contents
419
+ if dir_path == root_key:
420
+ # Show files at root level (depth 0)
421
+ for f in sorted(files, key=lambda x: x.path):
422
+ icon = self._get_file_icon(f.path)
423
+ name = os.path.basename(f.path)
424
+ size_str = (
425
+ f" [dim]({self._format_size(f.size)})[/dim]"
426
+ if f.size > 0
427
+ else ""
428
+ )
429
+ self._console.print(
430
+ f"{indent}{icon} [green]{name}[/green]{size_str}"
431
+ )
432
+
433
+ # Show subdirs at root level
434
+ for subdir in subdirs:
435
+ render_dir_tree(subdir, depth)
436
+ else:
437
+ # Show directory with summary
438
+ dir_name = os.path.basename(dir_path)
439
+ rec_size = get_recursive_size(dir_path)
440
+ rec_file_count = get_recursive_file_count(dir_path)
441
+ subdir_count = len(subdirs)
442
+
443
+ # Build summary parts
444
+ parts = []
445
+ if rec_file_count > 0:
446
+ parts.append(
447
+ f"{rec_file_count} file{'s' if rec_file_count != 1 else ''}"
448
+ )
449
+ if subdir_count > 0:
450
+ parts.append(
451
+ f"{subdir_count} subdir{'s' if subdir_count != 1 else ''}"
452
+ )
453
+ if rec_size > 0:
454
+ parts.append(self._format_size(rec_size))
455
+
456
+ summary = f" [dim]({', '.join(parts)})[/dim]" if parts else ""
382
457
  self._console.print(
383
- f"{prefix}{icon} [green]{entry.path}[/green]{size_str}"
458
+ f"{indent}📁 [bold blue]{dir_name}/[/bold blue]{summary}"
384
459
  )
385
460
 
461
+ # Recursively show subdirectories
462
+ for subdir in subdirs:
463
+ render_dir_tree(subdir, depth + 1)
464
+
465
+ # Render the tree starting from root
466
+ render_dir_tree(root_key, 0)
467
+
386
468
  # Summary
387
469
  self._console.print("\n[bold cyan]Summary:[/bold cyan]")
388
470
  self._console.print(
@@ -54,7 +54,15 @@ class AntigravityModel(GoogleModel):
54
54
  messages: list[ModelMessage],
55
55
  model_request_parameters: ModelRequestParameters,
56
56
  ) -> tuple[ContentDict | None, list[dict]]:
57
- """Map messages to Google GenAI format, preserving thinking signatures."""
57
+ """Map messages to Google GenAI format, preserving thinking signatures.
58
+
59
+ IMPORTANT: For Gemini with parallel function calls, the API expects:
60
+ - Model message: [FC1 + signature, FC2, ...] (all function calls together)
61
+ - User message: [FR1, FR2, ...] (all function responses together)
62
+
63
+ If messages are interleaved (FC1, FR1, FC2, FR2), the API returns 400.
64
+ This method merges consecutive same-role messages to fix this.
65
+ """
58
66
  contents: list[dict] = []
59
67
  system_parts: list[PartDict] = []
60
68
 
@@ -95,7 +103,12 @@ class AntigravityModel(GoogleModel):
95
103
  assert_never(part)
96
104
 
97
105
  if message_parts:
98
- contents.append({"role": "user", "parts": message_parts})
106
+ # Merge with previous user message if exists (for parallel function responses)
107
+ if contents and contents[-1].get("role") == "user":
108
+ contents[-1]["parts"].extend(message_parts)
109
+ else:
110
+ contents.append({"role": "user", "parts": message_parts})
111
+
99
112
  elif isinstance(m, ModelResponse):
100
113
  # USE CUSTOM HELPER HERE
101
114
  # Pass model name so we can handle Claude vs Gemini signature placement
@@ -103,7 +116,11 @@ class AntigravityModel(GoogleModel):
103
116
  m, self.system, self._model_name
104
117
  )
105
118
  if maybe_content:
106
- contents.append(maybe_content)
119
+ # Merge with previous model message if exists (for parallel function calls)
120
+ if contents and contents[-1].get("role") == "model":
121
+ contents[-1]["parts"].extend(maybe_content["parts"])
122
+ else:
123
+ contents.append(maybe_content)
107
124
  else:
108
125
  assert_never(m)
109
126
 
@@ -435,6 +452,13 @@ class AntigravityStreamingResponse(StreamedResponse):
435
452
  return self._timestamp_val
436
453
 
437
454
 
455
+ # Bypass signature for when no real thought signature is available.
456
+ # Gemini API requires EVERY function call to have a thoughtSignature field.
457
+ # When there's no thinking block or no signature was captured, we use this bypass.
458
+ # This specific key is the official bypass token for Gemini 3 Pro.
459
+ BYPASS_THOUGHT_SIGNATURE = "context_engineering_is_the_way_to_go"
460
+
461
+
438
462
  def _antigravity_content_model_response(
439
463
  m: ModelResponse, provider_name: str, model_name: str = ""
440
464
  ) -> ContentDict | None:
@@ -443,6 +467,10 @@ def _antigravity_content_model_response(
443
467
  Handles different signature protocols:
444
468
  - Claude models: signature goes ON the thinking block itself
445
469
  - Gemini models: signature goes on the NEXT part (function_call or text) after thinking
470
+
471
+ IMPORTANT: For Gemini, EVERY function call MUST have a thoughtSignature field.
472
+ If no real signature is available (no preceding ThinkingPart, or ThinkingPart
473
+ had no signature), we use BYPASS_THOUGHT_SIGNATURE as a fallback.
446
474
  """
447
475
  parts: list[PartDict] = []
448
476
 
@@ -451,6 +479,7 @@ def _antigravity_content_model_response(
451
479
  is_gemini = "gemini" in model_name.lower()
452
480
 
453
481
  # For Gemini: save signature from ThinkingPart to attach to next part
482
+ # Initialize to None - we'll use BYPASS_THOUGHT_SIGNATURE if still None when needed
454
483
  pending_signature: str | None = None
455
484
 
456
485
  for item in m.parts:
@@ -462,16 +491,24 @@ def _antigravity_content_model_response(
462
491
  )
463
492
  part["function_call"] = function_call
464
493
 
465
- # For Gemini: attach pending signature to function call
466
- if is_gemini and pending_signature:
467
- part["thoughtSignature"] = pending_signature
468
- pending_signature = None
494
+ # For Gemini: ALWAYS attach a thoughtSignature to function calls.
495
+ # Use the real signature if available, otherwise use bypass.
496
+ # NOTE: Do NOT clear pending_signature here! Multiple tool calls
497
+ # in a row (e.g., parallel function calls) all need the same
498
+ # signature from the preceding ThinkingPart.
499
+ if is_gemini:
500
+ part["thoughtSignature"] = (
501
+ pending_signature
502
+ if pending_signature is not None
503
+ else BYPASS_THOUGHT_SIGNATURE
504
+ )
469
505
 
470
506
  elif isinstance(item, TextPart):
471
507
  part["text"] = item.content
472
508
 
473
- # For Gemini: attach pending signature to text part
474
- if is_gemini and pending_signature:
509
+ # For Gemini: attach pending signature to text part if available
510
+ # Clear signature after text since text typically ends a response
511
+ if is_gemini and pending_signature is not None:
475
512
  part["thoughtSignature"] = pending_signature
476
513
  pending_signature = None
477
514
 
@@ -490,6 +527,10 @@ def _antigravity_content_model_response(
490
527
  else:
491
528
  # Default: try both (put on thinking block)
492
529
  part["thoughtSignature"] = item.signature
530
+ elif is_gemini:
531
+ # ThinkingPart exists but has no signature - use bypass
532
+ # This ensures subsequent tool calls still get a signature
533
+ pending_signature = BYPASS_THOUGHT_SIGNATURE
493
534
 
494
535
  elif isinstance(item, BuiltinToolCallPart):
495
536
  # Skip code execution for now
@@ -422,6 +422,8 @@ class AntigravityClient(httpx.AsyncClient):
422
422
 
423
423
  async def send(self, request: httpx.Request, **kwargs: Any) -> httpx.Response:
424
424
  """Override send to intercept at the lowest level with endpoint fallback."""
425
+ import asyncio
426
+
425
427
  # Transform POST requests to Antigravity format
426
428
  if request.method == "POST" and request.content:
427
429
  new_content, new_path, new_query, is_claude_thinking = self._wrap_request(
@@ -467,25 +469,21 @@ class AntigravityClient(httpx.AsyncClient):
467
469
  else:
468
470
  new_headers["anthropic-beta"] = interleaved_header
469
471
 
470
- # Try each endpoint in fallback order
472
+ # Try each endpoint with rate limit retry logic
471
473
  last_response = None
472
-
473
- # Check for rate limit
474
- import asyncio
475
-
476
- # Try endpoints logic...
477
- max_retries = 3
478
-
479
- for attempt in range(max_retries):
480
- for endpoint in ANTIGRAVITY_ENDPOINT_FALLBACKS:
481
- # Build URL with current endpoint
482
- new_url = httpx.URL(
483
- scheme="https",
484
- host=endpoint.replace("https://", ""),
485
- path=new_path,
486
- query=new_query.encode() if new_query else b"",
487
- )
488
-
474
+ max_rate_limit_retries = 5 # Max retries for 429s per endpoint
475
+
476
+ for endpoint in ANTIGRAVITY_ENDPOINT_FALLBACKS:
477
+ # Build URL with current endpoint
478
+ new_url = httpx.URL(
479
+ scheme="https",
480
+ host=endpoint.replace("https://", ""),
481
+ path=new_path,
482
+ query=new_query.encode() if new_query else b"",
483
+ )
484
+
485
+ # Retry loop for rate limits on this endpoint
486
+ for rate_limit_attempt in range(max_rate_limit_retries):
489
487
  req = httpx.Request(
490
488
  method=request.method,
491
489
  url=new_url,
@@ -498,45 +496,35 @@ class AntigravityClient(httpx.AsyncClient):
498
496
 
499
497
  # Handle rate limit (429)
500
498
  if response.status_code == 429:
501
- try:
502
- # Need to read response to get reset time
503
- await response.aread()
504
- error_data = json.loads(response.content)
505
-
506
- # Extract reset delay from metadata
507
- reset_delay = 2.0 # Default fallback
508
- if isinstance(error_data, dict):
509
- error_info = error_data.get("error", {})
510
- if isinstance(error_info, dict):
511
- details = error_info.get("details", [])
512
- for detail in details:
513
- metadata = detail.get("metadata", {})
514
- if "quotaResetDelay" in metadata:
515
- delay_str = metadata["quotaResetDelay"]
516
- if delay_str.endswith("s"):
517
- reset_delay = float(delay_str[:-1])
518
- break
519
-
520
- # Only retry if delay is reasonable (< 60s)
521
- if reset_delay < 60:
522
- # Add small buffer
523
- wait_time = reset_delay + 0.5
499
+ wait_time = await self._extract_rate_limit_delay(response)
500
+
501
+ if wait_time is not None and wait_time < 60:
502
+ # Add small buffer to wait time
503
+ wait_time = wait_time + 0.1
504
+ try:
524
505
  from code_puppy.messaging import emit_warning
525
506
 
526
507
  emit_warning(
527
- f"⏳ Rate limited on {endpoint}. Waiting {wait_time:.1f}s..."
508
+ f"⏳ Rate limited (attempt {rate_limit_attempt + 1}/{max_rate_limit_retries}). "
509
+ f"Waiting {wait_time:.2f}s..."
528
510
  )
529
- await asyncio.sleep(wait_time)
530
- continue # Retry same endpoint
531
- else:
532
- emit_warning(
533
- f"❌ Rate limit reset too long ({reset_delay}s). Aborting."
511
+ except ImportError:
512
+ logger.warning(
513
+ "Rate limited, waiting %.2fs...", wait_time
534
514
  )
535
- return UnwrappedResponse(response)
536
- except Exception:
537
- pass # Failed to parse error, treat as normal error
538
515
 
539
- # Retry on 403, 404, 5xx errors
516
+ await asyncio.sleep(wait_time)
517
+ continue # Retry same endpoint
518
+ else:
519
+ # Wait time too long or couldn't parse, try next endpoint
520
+ logger.debug(
521
+ "Rate limit wait too long (%.1fs) on %s, trying next endpoint...",
522
+ wait_time or 0,
523
+ endpoint,
524
+ )
525
+ break # Break inner loop, try next endpoint
526
+
527
+ # Retry on 403, 404, 5xx errors - try next endpoint
540
528
  if (
541
529
  response.status_code in (403, 404)
542
530
  or response.status_code >= 500
@@ -546,9 +534,9 @@ class AntigravityClient(httpx.AsyncClient):
546
534
  endpoint,
547
535
  response.status_code,
548
536
  )
549
- continue
537
+ break # Try next endpoint
550
538
 
551
- # Success or non-retriable error
539
+ # Success or non-retriable error (4xx except 429)
552
540
  # Wrap response to unwrap Antigravity format
553
541
  if "alt=sse" in new_query:
554
542
  return UnwrappedSSEResponse(response)
@@ -558,15 +546,96 @@ class AntigravityClient(httpx.AsyncClient):
558
546
  await response.aread()
559
547
  return UnwrappedResponse(response)
560
548
 
561
- # All endpoints failed, return last response
562
- if last_response and last_response.status_code == 429:
563
- await last_response.aread()
549
+ # All endpoints/retries exhausted, return last response
550
+ if last_response:
551
+ # Ensure response is read for proper error handling
552
+ if not last_response.is_stream_consumed:
553
+ try:
554
+ await last_response.aread()
555
+ except Exception:
556
+ pass
564
557
  return UnwrappedResponse(last_response)
565
558
 
566
- return last_response
567
-
568
559
  return await super().send(request, **kwargs)
569
560
 
561
+ async def _extract_rate_limit_delay(self, response: httpx.Response) -> float | None:
562
+ """Extract the retry delay from a 429 rate limit response.
563
+
564
+ Parses the Antigravity/Google API error format to find:
565
+ - retryDelay from RetryInfo (e.g., "0.088325827s")
566
+ - quotaResetDelay from ErrorInfo metadata (e.g., "88.325827ms")
567
+
568
+ Returns the delay in seconds, or None if parsing fails.
569
+ """
570
+ try:
571
+ # Read response body if not already read
572
+ if not response.is_stream_consumed:
573
+ await response.aread()
574
+
575
+ error_data = json.loads(response.content)
576
+
577
+ if not isinstance(error_data, dict):
578
+ return 2.0 # Default fallback
579
+
580
+ error_info = error_data.get("error", {})
581
+ if not isinstance(error_info, dict):
582
+ return 2.0
583
+
584
+ details = error_info.get("details", [])
585
+ if not isinstance(details, list):
586
+ return 2.0
587
+
588
+ # Look for RetryInfo first (most precise)
589
+ for detail in details:
590
+ if not isinstance(detail, dict):
591
+ continue
592
+
593
+ detail_type = detail.get("@type", "")
594
+
595
+ # Check for RetryInfo (e.g., "0.088325827s")
596
+ if "RetryInfo" in detail_type:
597
+ retry_delay = detail.get("retryDelay", "")
598
+ parsed = self._parse_duration(retry_delay)
599
+ if parsed is not None:
600
+ return parsed
601
+
602
+ # Check for ErrorInfo with quotaResetDelay in metadata
603
+ if "ErrorInfo" in detail_type:
604
+ metadata = detail.get("metadata", {})
605
+ if isinstance(metadata, dict):
606
+ quota_delay = metadata.get("quotaResetDelay", "")
607
+ parsed = self._parse_duration(quota_delay)
608
+ if parsed is not None:
609
+ return parsed
610
+
611
+ return 2.0 # Default if no delay found
612
+
613
+ except (json.JSONDecodeError, Exception) as e:
614
+ logger.debug("Failed to parse rate limit response: %s", e)
615
+ return 2.0 # Default fallback
616
+
617
+ def _parse_duration(self, duration_str: str) -> float | None:
618
+ """Parse a duration string like '0.088s' or '88.325827ms' to seconds."""
619
+ if not duration_str or not isinstance(duration_str, str):
620
+ return None
621
+
622
+ duration_str = duration_str.strip()
623
+
624
+ try:
625
+ # Handle milliseconds (e.g., "88.325827ms")
626
+ if duration_str.endswith("ms"):
627
+ return float(duration_str[:-2]) / 1000.0
628
+
629
+ # Handle seconds (e.g., "0.088325827s")
630
+ if duration_str.endswith("s"):
631
+ return float(duration_str[:-1])
632
+
633
+ # Try parsing as raw number (assume seconds)
634
+ return float(duration_str)
635
+
636
+ except ValueError:
637
+ return None
638
+
570
639
 
571
640
  def create_antigravity_client(
572
641
  access_token: str,
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "code-puppy"
7
- version = "0.0.335"
7
+ version = "0.0.336"
8
8
  description = "Code generation agent"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11,<3.14"
File without changes
File without changes
File without changes