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.
- {code_puppy-0.0.339 → code_puppy-0.0.341}/PKG-INFO +1 -1
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/agents/base_agent.py +24 -1
- code_puppy-0.0.341/code_puppy/claude_cache_client.py +371 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/cli_runner.py +6 -2
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/autosave_menu.py +18 -24
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/prompt_toolkit_completion.py +23 -17
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/mcp_/managed_server.py +7 -11
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/tools/common.py +3 -9
- {code_puppy-0.0.339 → code_puppy-0.0.341}/pyproject.toml +1 -1
- code_puppy-0.0.339/code_puppy/claude_cache_client.py +0 -209
- {code_puppy-0.0.339 → code_puppy-0.0.341}/.gitignore +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/LICENSE +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/README.md +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/__init__.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/__main__.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/agents/__init__.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/agents/agent_c_reviewer.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/agents/agent_code_puppy.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/agents/agent_code_reviewer.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/agents/agent_cpp_reviewer.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/agents/agent_creator_agent.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/agents/agent_golang_reviewer.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/agents/agent_javascript_reviewer.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/agents/agent_manager.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/agents/agent_planning.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/agents/agent_python_programmer.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/agents/agent_python_reviewer.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/agents/agent_qa_expert.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/agents/agent_qa_kitten.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/agents/agent_security_auditor.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/agents/agent_typescript_reviewer.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/agents/json_agent.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/agents/prompt_reviewer.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/callbacks.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/chatgpt_codex_client.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/__init__.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/add_model_menu.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/attachments.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/clipboard.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/colors_menu.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/command_handler.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/command_registry.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/config_commands.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/core_commands.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/diff_menu.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/file_path_completion.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/load_context_completion.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/__init__.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/add_command.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/base.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/catalog_server_installer.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/custom_server_form.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/custom_server_installer.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/edit_command.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/handler.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/help_command.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/install_command.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/install_menu.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/list_command.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/logs_command.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/remove_command.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/restart_command.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/search_command.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/start_all_command.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/start_command.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/status_command.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/stop_all_command.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/stop_command.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/test_command.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/utils.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp/wizard_utils.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/mcp_completion.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/model_picker_completion.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/model_settings_menu.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/motd.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/onboarding_slides.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/onboarding_wizard.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/pin_command_completion.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/session_commands.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/utils.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/config.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/error_logging.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/gemini_code_assist.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/http_utils.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/keymap.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/main.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/mcp_/__init__.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/mcp_/async_lifecycle.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/mcp_/blocking_startup.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/mcp_/captured_stdio_server.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/mcp_/circuit_breaker.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/mcp_/config_wizard.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/mcp_/dashboard.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/mcp_/error_isolation.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/mcp_/examples/retry_example.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/mcp_/health_monitor.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/mcp_/manager.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/mcp_/mcp_logs.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/mcp_/registry.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/mcp_/retry_manager.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/mcp_/server_registry_catalog.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/mcp_/status_tracker.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/mcp_/system_tools.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/messaging/__init__.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/messaging/bus.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/messaging/commands.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/messaging/markdown_patches.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/messaging/message_queue.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/messaging/messages.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/messaging/queue_console.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/messaging/renderers.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/messaging/rich_renderer.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/messaging/spinner/__init__.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/messaging/spinner/console_spinner.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/messaging/spinner/spinner_base.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/model_factory.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/model_utils.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/models.json +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/models_dev_parser.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/__init__.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/antigravity_oauth/__init__.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/antigravity_oauth/accounts.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/antigravity_oauth/antigravity_model.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/antigravity_oauth/config.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/antigravity_oauth/constants.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/antigravity_oauth/oauth.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/antigravity_oauth/register_callbacks.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/antigravity_oauth/storage.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/antigravity_oauth/test_plugin.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/antigravity_oauth/token.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/antigravity_oauth/transport.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/antigravity_oauth/utils.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/chatgpt_oauth/__init__.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/chatgpt_oauth/config.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/chatgpt_oauth/oauth_flow.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/chatgpt_oauth/register_callbacks.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/chatgpt_oauth/test_plugin.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/chatgpt_oauth/utils.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/claude_code_oauth/README.md +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/claude_code_oauth/SETUP.md +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/claude_code_oauth/__init__.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/claude_code_oauth/config.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/claude_code_oauth/register_callbacks.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/claude_code_oauth/test_plugin.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/claude_code_oauth/utils.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/customizable_commands/__init__.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/customizable_commands/register_callbacks.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/example_custom_command/README.md +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/example_custom_command/register_callbacks.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/file_permission_handler/__init__.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/file_permission_handler/register_callbacks.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/oauth_puppy_html.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/shell_safety/__init__.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/shell_safety/agent_shell_safety.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/shell_safety/command_cache.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/plugins/shell_safety/register_callbacks.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/prompts/codex_system_prompt.md +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/pydantic_patches.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/reopenable_async_client.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/round_robin_model.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/session_storage.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/status_display.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/summarization_agent.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/terminal_utils.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/tools/__init__.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/tools/agent_tools.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/tools/browser/__init__.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/tools/browser/browser_control.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/tools/browser/browser_interactions.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/tools/browser/browser_locators.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/tools/browser/browser_navigation.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/tools/browser/browser_screenshot.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/tools/browser/browser_scripts.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/tools/browser/browser_workflows.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/tools/browser/camoufox_manager.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/tools/browser/vqa_agent.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/tools/command_runner.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/tools/file_modifications.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/tools/file_operations.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/tools/tools_content.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/uvx_detection.py +0 -0
- {code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/version_checker.py +0 -0
|
@@ -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
|
|
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
|
-
#
|
|
148
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
302
|
-
message_lines = rendered.split("\n")
|
|
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
|
|
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
|
-
#
|
|
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,
|
|
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
|
-
#
|
|
387
|
-
message_lines = rendered.split("\n")
|
|
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"))
|
{code_puppy-0.0.339 → code_puppy-0.0.341}/code_puppy/command_line/prompt_toolkit_completion.py
RENAMED
|
@@ -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
|
-
|
|
667
|
-
|
|
668
|
-
|
|
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
|
-
|
|
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
|
-
|
|
688
|
-
|
|
689
|
-
|
|
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
|
|
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
|
-
|
|
750
|
-
|
|
751
|
-
|
|
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
|
-
|
|
754
|
-
|
|
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
|
-
|
|
757
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
#
|
|
731
|
-
if line.startswith("---"):
|
|
732
|
-
|
|
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("-"):
|