code-puppy 0.0.315__tar.gz → 0.0.323__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.315 → code_puppy-0.0.323}/PKG-INFO +1 -1
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/agents/base_agent.py +205 -113
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/cli_runner.py +8 -1
- code_puppy-0.0.323/code_puppy/command_line/mcp/logs_command.py +235 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/model_settings_menu.py +6 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/keymap.py +8 -2
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/mcp_/__init__.py +17 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/mcp_/blocking_startup.py +61 -32
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/mcp_/managed_server.py +23 -3
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/mcp_/manager.py +65 -0
- code_puppy-0.0.323/code_puppy/mcp_/mcp_logs.py +224 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/messaging/__init__.py +9 -0
- code_puppy-0.0.323/code_puppy/messaging/markdown_patches.py +57 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/model_factory.py +54 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/models.json +1 -1
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/claude_code_oauth/utils.py +1 -0
- code_puppy-0.0.323/code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/shell_safety/register_callbacks.py +44 -3
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/tools/command_runner.py +48 -21
- {code_puppy-0.0.315 → code_puppy-0.0.323}/pyproject.toml +2 -1
- code_puppy-0.0.315/code_puppy/command_line/mcp/logs_command.py +0 -126
- code_puppy-0.0.315/code_puppy/plugins/shell_safety/agent_shell_safety.py +0 -186
- {code_puppy-0.0.315 → code_puppy-0.0.323}/.gitignore +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/LICENSE +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/README.md +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/__init__.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/__main__.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/agents/__init__.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/agents/agent_c_reviewer.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/agents/agent_code_puppy.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/agents/agent_code_reviewer.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/agents/agent_cpp_reviewer.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/agents/agent_creator_agent.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/agents/agent_golang_reviewer.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/agents/agent_javascript_reviewer.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/agents/agent_manager.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/agents/agent_planning.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/agents/agent_python_programmer.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/agents/agent_python_reviewer.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/agents/agent_qa_expert.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/agents/agent_qa_kitten.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/agents/agent_security_auditor.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/agents/agent_typescript_reviewer.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/agents/json_agent.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/agents/prompt_reviewer.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/callbacks.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/chatgpt_codex_client.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/claude_cache_client.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/__init__.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/add_model_menu.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/attachments.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/autosave_menu.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/colors_menu.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/command_handler.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/command_registry.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/config_commands.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/core_commands.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/diff_menu.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/file_path_completion.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/load_context_completion.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp/__init__.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp/add_command.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp/base.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp/catalog_server_installer.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp/custom_server_form.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp/custom_server_installer.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp/edit_command.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp/handler.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp/help_command.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp/install_command.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp/install_menu.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp/list_command.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp/remove_command.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp/restart_command.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp/search_command.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp/start_all_command.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp/start_command.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp/status_command.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp/stop_all_command.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp/stop_command.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp/test_command.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp/utils.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp/wizard_utils.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/mcp_completion.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/model_picker_completion.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/motd.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/pin_command_completion.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/prompt_toolkit_completion.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/session_commands.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/command_line/utils.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/config.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/error_logging.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/gemini_code_assist.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/http_utils.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/main.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/mcp_/async_lifecycle.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/mcp_/captured_stdio_server.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/mcp_/circuit_breaker.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/mcp_/config_wizard.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/mcp_/dashboard.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/mcp_/error_isolation.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/mcp_/examples/retry_example.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/mcp_/health_monitor.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/mcp_/registry.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/mcp_/retry_manager.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/mcp_/server_registry_catalog.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/mcp_/status_tracker.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/mcp_/system_tools.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/messaging/bus.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/messaging/commands.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/messaging/message_queue.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/messaging/messages.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/messaging/queue_console.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/messaging/renderers.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/messaging/rich_renderer.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/messaging/spinner/__init__.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/messaging/spinner/console_spinner.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/messaging/spinner/spinner_base.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/model_utils.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/models_dev_parser.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/__init__.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/chatgpt_oauth/__init__.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/chatgpt_oauth/config.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/chatgpt_oauth/oauth_flow.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/chatgpt_oauth/register_callbacks.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/chatgpt_oauth/test_plugin.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/chatgpt_oauth/utils.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/claude_code_oauth/README.md +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/claude_code_oauth/SETUP.md +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/claude_code_oauth/__init__.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/claude_code_oauth/config.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/claude_code_oauth/register_callbacks.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/claude_code_oauth/test_plugin.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/customizable_commands/__init__.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/customizable_commands/register_callbacks.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/example_custom_command/README.md +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/example_custom_command/register_callbacks.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/file_permission_handler/__init__.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/file_permission_handler/register_callbacks.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/oauth_puppy_html.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/shell_safety/__init__.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/plugins/shell_safety/command_cache.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/prompts/codex_system_prompt.md +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/pydantic_patches.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/reopenable_async_client.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/round_robin_model.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/session_storage.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/status_display.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/summarization_agent.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/terminal_utils.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/tools/__init__.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/tools/agent_tools.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/tools/browser/__init__.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/tools/browser/browser_control.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/tools/browser/browser_interactions.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/tools/browser/browser_locators.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/tools/browser/browser_navigation.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/tools/browser/browser_screenshot.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/tools/browser/browser_scripts.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/tools/browser/browser_workflows.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/tools/browser/camoufox_manager.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/tools/browser/vqa_agent.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/tools/common.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/tools/file_modifications.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/tools/file_operations.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/tools/tools_content.py +0 -0
- {code_puppy-0.0.315 → code_puppy-0.0.323}/code_puppy/version_checker.py +0 -0
|
@@ -4,11 +4,23 @@ import asyncio
|
|
|
4
4
|
import json
|
|
5
5
|
import math
|
|
6
6
|
import signal
|
|
7
|
+
import sys
|
|
7
8
|
import threading
|
|
8
9
|
import uuid
|
|
9
10
|
from abc import ABC, abstractmethod
|
|
10
11
|
from collections.abc import AsyncIterable
|
|
11
|
-
from typing import
|
|
12
|
+
from typing import (
|
|
13
|
+
Any,
|
|
14
|
+
Callable,
|
|
15
|
+
Dict,
|
|
16
|
+
List,
|
|
17
|
+
Optional,
|
|
18
|
+
Sequence,
|
|
19
|
+
Set,
|
|
20
|
+
Tuple,
|
|
21
|
+
Type,
|
|
22
|
+
Union,
|
|
23
|
+
)
|
|
12
24
|
|
|
13
25
|
import mcp
|
|
14
26
|
import pydantic
|
|
@@ -47,11 +59,10 @@ from code_puppy.config import (
|
|
|
47
59
|
get_protected_token_count,
|
|
48
60
|
get_use_dbos,
|
|
49
61
|
get_value,
|
|
50
|
-
load_mcp_server_configs,
|
|
51
62
|
)
|
|
52
63
|
from code_puppy.error_logging import log_error
|
|
53
64
|
from code_puppy.keymap import cancel_agent_uses_signal, get_cancel_agent_char_code
|
|
54
|
-
from code_puppy.mcp_ import
|
|
65
|
+
from code_puppy.mcp_ import get_mcp_manager
|
|
55
66
|
from code_puppy.messaging import (
|
|
56
67
|
emit_error,
|
|
57
68
|
emit_info,
|
|
@@ -90,6 +101,9 @@ class BaseAgent(ABC):
|
|
|
90
101
|
# Cache for MCP tool definitions (for token estimation)
|
|
91
102
|
# This is populated after the first successful run when MCP tools are retrieved
|
|
92
103
|
self._mcp_tool_definitions_cache: List[Dict[str, Any]] = []
|
|
104
|
+
# Shared console for streaming output - should be set by cli_runner
|
|
105
|
+
# to avoid conflicts between spinner's Live display and response streaming
|
|
106
|
+
self._console: Optional[Any] = None
|
|
93
107
|
|
|
94
108
|
@property
|
|
95
109
|
@abstractmethod
|
|
@@ -989,45 +1003,31 @@ class BaseAgent(ABC):
|
|
|
989
1003
|
return self._puppy_rules
|
|
990
1004
|
|
|
991
1005
|
def load_mcp_servers(self, extra_headers: Optional[Dict[str, str]] = None):
|
|
992
|
-
"""Load MCP servers through the manager and return pydantic-ai compatible servers.
|
|
1006
|
+
"""Load MCP servers through the manager and return pydantic-ai compatible servers.
|
|
1007
|
+
|
|
1008
|
+
Note: The manager automatically syncs from mcp_servers.json during initialization,
|
|
1009
|
+
so we don't need to sync here. Use reload_mcp_servers() to force a re-sync.
|
|
1010
|
+
"""
|
|
993
1011
|
|
|
994
1012
|
mcp_disabled = get_value("disable_mcp_servers")
|
|
995
1013
|
if mcp_disabled and str(mcp_disabled).lower() in ("1", "true", "yes", "on"):
|
|
996
1014
|
return []
|
|
997
1015
|
|
|
998
1016
|
manager = get_mcp_manager()
|
|
999
|
-
configs = load_mcp_server_configs()
|
|
1000
|
-
if not configs:
|
|
1001
|
-
existing_servers = manager.list_servers()
|
|
1002
|
-
if not existing_servers:
|
|
1003
|
-
return []
|
|
1004
|
-
else:
|
|
1005
|
-
for name, conf in configs.items():
|
|
1006
|
-
try:
|
|
1007
|
-
server_config = ServerConfig(
|
|
1008
|
-
id=conf.get("id", f"{name}_{hash(name)}"),
|
|
1009
|
-
name=name,
|
|
1010
|
-
type=conf.get("type", "sse"),
|
|
1011
|
-
enabled=conf.get("enabled", True),
|
|
1012
|
-
config=conf,
|
|
1013
|
-
)
|
|
1014
|
-
existing = manager.get_server_by_name(name)
|
|
1015
|
-
if not existing:
|
|
1016
|
-
manager.register_server(server_config)
|
|
1017
|
-
else:
|
|
1018
|
-
if existing.config != server_config.config:
|
|
1019
|
-
manager.update_server(existing.id, server_config)
|
|
1020
|
-
except Exception:
|
|
1021
|
-
continue
|
|
1022
|
-
|
|
1023
1017
|
return manager.get_servers_for_agent()
|
|
1024
1018
|
|
|
1025
1019
|
def reload_mcp_servers(self):
|
|
1026
|
-
"""Reload MCP servers and return updated servers.
|
|
1020
|
+
"""Reload MCP servers and return updated servers.
|
|
1021
|
+
|
|
1022
|
+
Forces a re-sync from mcp_servers.json to pick up any configuration changes.
|
|
1023
|
+
"""
|
|
1027
1024
|
# Clear the MCP tool cache when servers are reloaded
|
|
1028
1025
|
self._mcp_tool_definitions_cache = []
|
|
1029
|
-
|
|
1026
|
+
|
|
1027
|
+
# Force re-sync from mcp_servers.json
|
|
1030
1028
|
manager = get_mcp_manager()
|
|
1029
|
+
manager.sync_from_config()
|
|
1030
|
+
|
|
1031
1031
|
return manager.get_servers_for_agent()
|
|
1032
1032
|
|
|
1033
1033
|
def _load_model_with_fallback(
|
|
@@ -1242,6 +1242,74 @@ class BaseAgent(ABC):
|
|
|
1242
1242
|
self._mcp_servers = mcp_servers
|
|
1243
1243
|
return self._code_generation_agent
|
|
1244
1244
|
|
|
1245
|
+
def _create_agent_with_output_type(self, output_type: Type[Any]) -> PydanticAgent:
|
|
1246
|
+
"""Create a temporary agent configured with a custom output_type.
|
|
1247
|
+
|
|
1248
|
+
This is used when structured output is requested via run_with_mcp.
|
|
1249
|
+
The agent is created fresh with the same configuration as the main agent
|
|
1250
|
+
but with the specified output_type instead of str.
|
|
1251
|
+
|
|
1252
|
+
Args:
|
|
1253
|
+
output_type: The Pydantic model or type for structured output.
|
|
1254
|
+
|
|
1255
|
+
Returns:
|
|
1256
|
+
A configured PydanticAgent (or DBOSAgent wrapper) with the custom output_type.
|
|
1257
|
+
"""
|
|
1258
|
+
from code_puppy.model_utils import prepare_prompt_for_model
|
|
1259
|
+
from code_puppy.tools import register_tools_for_agent
|
|
1260
|
+
|
|
1261
|
+
model_name = self.get_model_name()
|
|
1262
|
+
models_config = ModelFactory.load_config()
|
|
1263
|
+
model, resolved_model_name = self._load_model_with_fallback(
|
|
1264
|
+
model_name, models_config, str(uuid.uuid4())
|
|
1265
|
+
)
|
|
1266
|
+
|
|
1267
|
+
instructions = self.get_system_prompt()
|
|
1268
|
+
puppy_rules = self.load_puppy_rules()
|
|
1269
|
+
if puppy_rules:
|
|
1270
|
+
instructions += f"\n{puppy_rules}"
|
|
1271
|
+
|
|
1272
|
+
mcp_servers = getattr(self, "_mcp_servers", []) or []
|
|
1273
|
+
model_settings = make_model_settings(resolved_model_name)
|
|
1274
|
+
|
|
1275
|
+
prepared = prepare_prompt_for_model(
|
|
1276
|
+
model_name, instructions, "", prepend_system_to_user=False
|
|
1277
|
+
)
|
|
1278
|
+
instructions = prepared.instructions
|
|
1279
|
+
|
|
1280
|
+
global _reload_count
|
|
1281
|
+
_reload_count += 1
|
|
1282
|
+
|
|
1283
|
+
if get_use_dbos():
|
|
1284
|
+
temp_agent = PydanticAgent(
|
|
1285
|
+
model=model,
|
|
1286
|
+
instructions=instructions,
|
|
1287
|
+
output_type=output_type,
|
|
1288
|
+
retries=3,
|
|
1289
|
+
toolsets=[],
|
|
1290
|
+
history_processors=[self.message_history_accumulator],
|
|
1291
|
+
model_settings=model_settings,
|
|
1292
|
+
)
|
|
1293
|
+
agent_tools = self.get_available_tools()
|
|
1294
|
+
register_tools_for_agent(temp_agent, agent_tools)
|
|
1295
|
+
dbos_agent = DBOSAgent(
|
|
1296
|
+
temp_agent, name=f"{self.name}-structured-{_reload_count}"
|
|
1297
|
+
)
|
|
1298
|
+
return dbos_agent
|
|
1299
|
+
else:
|
|
1300
|
+
temp_agent = PydanticAgent(
|
|
1301
|
+
model=model,
|
|
1302
|
+
instructions=instructions,
|
|
1303
|
+
output_type=output_type,
|
|
1304
|
+
retries=3,
|
|
1305
|
+
toolsets=mcp_servers,
|
|
1306
|
+
history_processors=[self.message_history_accumulator],
|
|
1307
|
+
model_settings=model_settings,
|
|
1308
|
+
)
|
|
1309
|
+
agent_tools = self.get_available_tools()
|
|
1310
|
+
register_tools_for_agent(temp_agent, agent_tools)
|
|
1311
|
+
return temp_agent
|
|
1312
|
+
|
|
1245
1313
|
# It's okay to decorate it with DBOS.step even if not using DBOS; the decorator is a no-op in that case.
|
|
1246
1314
|
@DBOS.step()
|
|
1247
1315
|
def message_history_accumulator(self, ctx: RunContext, messages: List[Any]):
|
|
@@ -1279,27 +1347,22 @@ class BaseAgent(ABC):
|
|
|
1279
1347
|
ctx: The run context.
|
|
1280
1348
|
events: Async iterable of streaming events (PartStartEvent, PartDeltaEvent, etc.).
|
|
1281
1349
|
"""
|
|
1282
|
-
import os
|
|
1283
|
-
import time as time_module
|
|
1284
|
-
|
|
1285
1350
|
from pydantic_ai import PartDeltaEvent, PartStartEvent
|
|
1286
1351
|
from pydantic_ai.messages import TextPartDelta, ThinkingPartDelta
|
|
1287
1352
|
from rich.console import Console
|
|
1288
|
-
from rich.live import Live
|
|
1289
1353
|
from rich.markdown import Markdown
|
|
1290
1354
|
from rich.markup import escape
|
|
1291
1355
|
|
|
1292
1356
|
from code_puppy.messaging.spinner import pause_all_spinners
|
|
1293
1357
|
|
|
1294
|
-
console
|
|
1295
|
-
|
|
1296
|
-
#
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
)
|
|
1358
|
+
# IMPORTANT: Use the shared console (set by cli_runner) to avoid conflicts
|
|
1359
|
+
# with the spinner's Live display. Multiple Console instances with separate
|
|
1360
|
+
# Live displays cause cursor positioning chaos and line duplication.
|
|
1361
|
+
if self._console is not None:
|
|
1362
|
+
console = self._console
|
|
1363
|
+
else:
|
|
1364
|
+
# Fallback if console not set (shouldn't happen in normal use)
|
|
1365
|
+
console = Console()
|
|
1303
1366
|
|
|
1304
1367
|
# Track which part indices we're currently streaming (for Text/Thinking parts)
|
|
1305
1368
|
streaming_parts: set[int] = set()
|
|
@@ -1308,11 +1371,9 @@ class BaseAgent(ABC):
|
|
|
1308
1371
|
) # Track which parts are thinking (for dim style)
|
|
1309
1372
|
text_parts: set[int] = set() # Track which parts are text
|
|
1310
1373
|
banner_printed: set[int] = set() # Track if banner was already printed
|
|
1311
|
-
text_buffer: dict[int, list[str]] = {} # Buffer text for markdown
|
|
1312
|
-
|
|
1374
|
+
text_buffer: dict[int, list[str]] = {} # Buffer text for final markdown render
|
|
1375
|
+
token_count: dict[int, int] = {} # Track token count per text part
|
|
1313
1376
|
did_stream_anything = False # Track if we streamed any content
|
|
1314
|
-
last_render_time: dict[int, float] = {} # Track last render time per part
|
|
1315
|
-
render_interval = 0.1 # Only re-render markdown every 100ms (throttle)
|
|
1316
1377
|
|
|
1317
1378
|
def _print_thinking_banner() -> None:
|
|
1318
1379
|
"""Print the THINKING banner with spinner pause and line clear."""
|
|
@@ -1377,9 +1438,11 @@ class BaseAgent(ABC):
|
|
|
1377
1438
|
streaming_parts.add(event.index)
|
|
1378
1439
|
text_parts.add(event.index)
|
|
1379
1440
|
text_buffer[event.index] = [] # Initialize buffer
|
|
1441
|
+
token_count[event.index] = 0 # Initialize token counter
|
|
1380
1442
|
# Buffer initial content if present
|
|
1381
1443
|
if part.content and part.content.strip():
|
|
1382
1444
|
text_buffer[event.index].append(part.content)
|
|
1445
|
+
token_count[event.index] += 1
|
|
1383
1446
|
|
|
1384
1447
|
# PartDeltaEvent - stream the content as it arrives
|
|
1385
1448
|
elif isinstance(event, PartDeltaEvent):
|
|
@@ -1387,43 +1450,23 @@ class BaseAgent(ABC):
|
|
|
1387
1450
|
delta = event.delta
|
|
1388
1451
|
if isinstance(delta, (TextPartDelta, ThinkingPartDelta)):
|
|
1389
1452
|
if delta.content_delta:
|
|
1390
|
-
# For text parts,
|
|
1453
|
+
# For text parts, show token counter then render at end
|
|
1391
1454
|
if event.index in text_parts:
|
|
1392
|
-
|
|
1455
|
+
import sys
|
|
1456
|
+
|
|
1457
|
+
# Print banner on first content
|
|
1393
1458
|
if event.index not in banner_printed:
|
|
1394
1459
|
_print_response_banner()
|
|
1395
1460
|
banner_printed.add(event.index)
|
|
1396
|
-
|
|
1397
|
-
if use_live_display:
|
|
1398
|
-
live = Live(
|
|
1399
|
-
Markdown(""),
|
|
1400
|
-
console=console,
|
|
1401
|
-
refresh_per_second=10,
|
|
1402
|
-
vertical_overflow="visible", # Allow scrolling for long content
|
|
1403
|
-
)
|
|
1404
|
-
live.start()
|
|
1405
|
-
live_displays[event.index] = live
|
|
1406
|
-
# Accumulate text and throttle markdown rendering
|
|
1407
|
-
# (Markdown parsing is O(n), doing it on every token = O(n²) death)
|
|
1461
|
+
# Accumulate text for final markdown render
|
|
1408
1462
|
text_buffer[event.index].append(delta.content_delta)
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
and now - last_render >= render_interval
|
|
1417
|
-
):
|
|
1418
|
-
content = "".join(text_buffer[event.index])
|
|
1419
|
-
if event.index in live_displays:
|
|
1420
|
-
try:
|
|
1421
|
-
live_displays[event.index].update(
|
|
1422
|
-
Markdown(content)
|
|
1423
|
-
)
|
|
1424
|
-
last_render_time[event.index] = now
|
|
1425
|
-
except Exception:
|
|
1426
|
-
pass
|
|
1463
|
+
token_count[event.index] += 1
|
|
1464
|
+
# Update token counter in place (single line)
|
|
1465
|
+
count = token_count[event.index]
|
|
1466
|
+
sys.stdout.write(
|
|
1467
|
+
f"\r\x1b[K ⏳ Receiving... {count} tokens"
|
|
1468
|
+
)
|
|
1469
|
+
sys.stdout.flush()
|
|
1427
1470
|
else:
|
|
1428
1471
|
# For thinking parts, stream immediately (dim)
|
|
1429
1472
|
if event.index not in banner_printed:
|
|
@@ -1435,36 +1478,24 @@ class BaseAgent(ABC):
|
|
|
1435
1478
|
# PartEndEvent - finish the streaming with a newline
|
|
1436
1479
|
elif isinstance(event, PartEndEvent):
|
|
1437
1480
|
if event.index in streaming_parts:
|
|
1438
|
-
# For text parts,
|
|
1481
|
+
# For text parts, clear counter line and render markdown
|
|
1439
1482
|
if event.index in text_parts:
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
)
|
|
1448
|
-
except Exception:
|
|
1449
|
-
pass
|
|
1450
|
-
if event.index in live_displays:
|
|
1451
|
-
try:
|
|
1452
|
-
live_displays[event.index].stop()
|
|
1453
|
-
except Exception:
|
|
1454
|
-
pass
|
|
1455
|
-
del live_displays[event.index]
|
|
1456
|
-
# When not using Live display, print the final content as markdown
|
|
1457
|
-
elif event.index in text_buffer:
|
|
1483
|
+
import sys
|
|
1484
|
+
|
|
1485
|
+
# Clear the token counter line
|
|
1486
|
+
sys.stdout.write("\r\x1b[K")
|
|
1487
|
+
sys.stdout.flush()
|
|
1488
|
+
# Render the final markdown nicely
|
|
1489
|
+
if event.index in text_buffer:
|
|
1458
1490
|
try:
|
|
1459
1491
|
final_content = "".join(text_buffer[event.index])
|
|
1460
1492
|
if final_content.strip():
|
|
1461
1493
|
console.print(Markdown(final_content))
|
|
1462
1494
|
except Exception:
|
|
1463
1495
|
pass
|
|
1464
|
-
if event.index in text_buffer:
|
|
1465
1496
|
del text_buffer[event.index]
|
|
1466
|
-
# Clean up
|
|
1467
|
-
|
|
1497
|
+
# Clean up token count
|
|
1498
|
+
token_count.pop(event.index, None)
|
|
1468
1499
|
# For thinking parts, just print newline
|
|
1469
1500
|
elif event.index in banner_printed:
|
|
1470
1501
|
console.print() # Final newline after streaming
|
|
@@ -1639,6 +1670,7 @@ class BaseAgent(ABC):
|
|
|
1639
1670
|
*,
|
|
1640
1671
|
attachments: Optional[Sequence[BinaryContent]] = None,
|
|
1641
1672
|
link_attachments: Optional[Sequence[Union[ImageUrl, DocumentUrl]]] = None,
|
|
1673
|
+
output_type: Optional[Type[Any]] = None,
|
|
1642
1674
|
**kwargs,
|
|
1643
1675
|
) -> Any:
|
|
1644
1676
|
"""Run the agent with MCP servers, attachments, and full cancellation support.
|
|
@@ -1647,10 +1679,13 @@ class BaseAgent(ABC):
|
|
|
1647
1679
|
prompt: Primary user prompt text (may be empty when attachments present).
|
|
1648
1680
|
attachments: Local binary payloads (e.g., dragged images) to include.
|
|
1649
1681
|
link_attachments: Remote assets (image/document URLs) to include.
|
|
1682
|
+
output_type: Optional Pydantic model or type for structured output.
|
|
1683
|
+
When provided, creates a temporary agent configured to return
|
|
1684
|
+
this type instead of the default string output.
|
|
1650
1685
|
**kwargs: Additional arguments forwarded to `pydantic_ai.Agent.run`.
|
|
1651
1686
|
|
|
1652
1687
|
Returns:
|
|
1653
|
-
The agent's response.
|
|
1688
|
+
The agent's response (typed according to output_type if specified).
|
|
1654
1689
|
|
|
1655
1690
|
Raises:
|
|
1656
1691
|
asyncio.CancelledError: When execution is cancelled by user.
|
|
@@ -1674,6 +1709,11 @@ class BaseAgent(ABC):
|
|
|
1674
1709
|
pydantic_agent = (
|
|
1675
1710
|
self._code_generation_agent or self.reload_code_generation_agent()
|
|
1676
1711
|
)
|
|
1712
|
+
|
|
1713
|
+
# If a custom output_type is specified, create a temporary agent with that type
|
|
1714
|
+
if output_type is not None:
|
|
1715
|
+
pydantic_agent = self._create_agent_with_output_type(output_type)
|
|
1716
|
+
|
|
1677
1717
|
# Handle claude-code and chatgpt-codex models: prepend system prompt to first user message
|
|
1678
1718
|
from code_puppy.model_utils import is_chatgpt_codex_model, is_claude_code_model
|
|
1679
1719
|
|
|
@@ -1871,29 +1911,72 @@ class BaseAgent(ABC):
|
|
|
1871
1911
|
# When using keyboard-based cancel, SIGINT should be a no-op
|
|
1872
1912
|
# (just show a hint to user about the configured cancel key)
|
|
1873
1913
|
from code_puppy.keymap import get_cancel_agent_display_name
|
|
1914
|
+
import sys
|
|
1874
1915
|
|
|
1875
1916
|
cancel_key = get_cancel_agent_display_name()
|
|
1876
|
-
|
|
1917
|
+
if sys.platform == "win32":
|
|
1918
|
+
# On Windows, we use keyboard listener, so SIGINT might still fire
|
|
1919
|
+
# but we handle cancellation via the key listener
|
|
1920
|
+
pass # Silent on Windows - the key listener handles it
|
|
1921
|
+
else:
|
|
1922
|
+
emit_info(f"Use {cancel_key} to cancel the agent task.")
|
|
1877
1923
|
|
|
1878
1924
|
original_handler = None
|
|
1879
1925
|
key_listener_stop_event = None
|
|
1880
1926
|
_key_listener_thread = None
|
|
1927
|
+
_windows_ctrl_handler = None # Store reference to prevent garbage collection
|
|
1881
1928
|
|
|
1882
1929
|
try:
|
|
1883
|
-
if
|
|
1884
|
-
# Use
|
|
1930
|
+
if sys.platform == "win32":
|
|
1931
|
+
# Windows: Use SetConsoleCtrlHandler for reliable Ctrl+C handling
|
|
1932
|
+
import ctypes
|
|
1933
|
+
|
|
1934
|
+
# Define the handler function type
|
|
1935
|
+
HANDLER_ROUTINE = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.c_ulong)
|
|
1936
|
+
|
|
1937
|
+
def windows_ctrl_handler(ctrl_type):
|
|
1938
|
+
"""Handle Windows console control events."""
|
|
1939
|
+
CTRL_C_EVENT = 0
|
|
1940
|
+
CTRL_BREAK_EVENT = 1
|
|
1941
|
+
|
|
1942
|
+
if ctrl_type in (CTRL_C_EVENT, CTRL_BREAK_EVENT):
|
|
1943
|
+
# Check if we're awaiting user input
|
|
1944
|
+
if is_awaiting_user_input():
|
|
1945
|
+
return False # Let default handler run
|
|
1946
|
+
|
|
1947
|
+
# Schedule agent cancellation
|
|
1948
|
+
schedule_agent_cancel()
|
|
1949
|
+
return True # We handled it, don't terminate
|
|
1950
|
+
|
|
1951
|
+
return False # Let other handlers process it
|
|
1952
|
+
|
|
1953
|
+
# Create the callback - must keep reference alive!
|
|
1954
|
+
_windows_ctrl_handler = HANDLER_ROUTINE(windows_ctrl_handler)
|
|
1955
|
+
|
|
1956
|
+
# Register the handler
|
|
1957
|
+
kernel32 = ctypes.windll.kernel32
|
|
1958
|
+
if not kernel32.SetConsoleCtrlHandler(_windows_ctrl_handler, True):
|
|
1959
|
+
emit_warning("Failed to set Windows Ctrl+C handler")
|
|
1960
|
+
|
|
1961
|
+
# Also spawn keyboard listener for Ctrl+X (shell cancel) and other keys
|
|
1962
|
+
key_listener_stop_event = threading.Event()
|
|
1963
|
+
_key_listener_thread = self._spawn_ctrl_x_key_listener(
|
|
1964
|
+
key_listener_stop_event,
|
|
1965
|
+
on_escape=lambda: None, # Ctrl+X handled by command_runner
|
|
1966
|
+
on_cancel_agent=None, # Ctrl+C handled by SetConsoleCtrlHandler above
|
|
1967
|
+
)
|
|
1968
|
+
elif cancel_agent_uses_signal():
|
|
1969
|
+
# Unix with Ctrl+C: Use SIGINT-based cancellation
|
|
1885
1970
|
original_handler = signal.signal(
|
|
1886
1971
|
signal.SIGINT, keyboard_interrupt_handler
|
|
1887
1972
|
)
|
|
1888
1973
|
else:
|
|
1889
|
-
# Use keyboard listener
|
|
1890
|
-
# Set a graceful SIGINT handler that shows a hint
|
|
1974
|
+
# Unix with different cancel key: Use keyboard listener
|
|
1891
1975
|
original_handler = signal.signal(signal.SIGINT, graceful_sigint_handler)
|
|
1892
|
-
# Spawn keyboard listener with the cancel agent callback
|
|
1893
1976
|
key_listener_stop_event = threading.Event()
|
|
1894
1977
|
_key_listener_thread = self._spawn_ctrl_x_key_listener(
|
|
1895
1978
|
key_listener_stop_event,
|
|
1896
|
-
on_escape=lambda: None,
|
|
1979
|
+
on_escape=lambda: None,
|
|
1897
1980
|
on_cancel_agent=schedule_agent_cancel,
|
|
1898
1981
|
)
|
|
1899
1982
|
|
|
@@ -1918,8 +2001,17 @@ class BaseAgent(ABC):
|
|
|
1918
2001
|
# Stop keyboard listener if it was started
|
|
1919
2002
|
if key_listener_stop_event is not None:
|
|
1920
2003
|
key_listener_stop_event.set()
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
2004
|
+
|
|
2005
|
+
# Unregister Windows Ctrl handler
|
|
2006
|
+
if sys.platform == "win32" and _windows_ctrl_handler is not None:
|
|
2007
|
+
try:
|
|
2008
|
+
import ctypes
|
|
2009
|
+
|
|
2010
|
+
kernel32 = ctypes.windll.kernel32
|
|
2011
|
+
kernel32.SetConsoleCtrlHandler(_windows_ctrl_handler, False)
|
|
2012
|
+
except Exception:
|
|
2013
|
+
pass # Best effort cleanup
|
|
2014
|
+
|
|
2015
|
+
# Restore original signal handler (Unix)
|
|
2016
|
+
if original_handler is not None:
|
|
1925
2017
|
signal.signal(signal.SIGINT, original_handler)
|
|
@@ -706,6 +706,12 @@ async def run_prompt_with_attachments(
|
|
|
706
706
|
attachments = [attachment.content for attachment in processed_prompt.attachments]
|
|
707
707
|
link_attachments = [link.url_part for link in processed_prompt.link_attachments]
|
|
708
708
|
|
|
709
|
+
# IMPORTANT: Set the shared console on the agent so that streaming output
|
|
710
|
+
# uses the same console as the spinner. This prevents Live display conflicts
|
|
711
|
+
# that cause line duplication during markdown streaming.
|
|
712
|
+
if spinner_console is not None:
|
|
713
|
+
agent._console = spinner_console
|
|
714
|
+
|
|
709
715
|
# Create the agent task first so we can track and cancel it
|
|
710
716
|
agent_task = asyncio.create_task(
|
|
711
717
|
agent.run_with_mcp(
|
|
@@ -784,5 +790,6 @@ def main_entry():
|
|
|
784
790
|
DBOS.destroy()
|
|
785
791
|
return 0
|
|
786
792
|
finally:
|
|
787
|
-
# Reset terminal on
|
|
793
|
+
# Reset terminal on all platforms for clean state
|
|
794
|
+
reset_windows_terminal_full() # Safe no-op on non-Windows
|
|
788
795
|
reset_unix_terminal()
|