codepp 0.0.437__py3-none-any.whl
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/__init__.py +10 -0
- code_puppy/__main__.py +10 -0
- code_puppy/agents/__init__.py +31 -0
- code_puppy/agents/agent_c_reviewer.py +155 -0
- code_puppy/agents/agent_code_puppy.py +117 -0
- code_puppy/agents/agent_code_reviewer.py +90 -0
- code_puppy/agents/agent_cpp_reviewer.py +132 -0
- code_puppy/agents/agent_creator_agent.py +638 -0
- code_puppy/agents/agent_golang_reviewer.py +151 -0
- code_puppy/agents/agent_helios.py +124 -0
- code_puppy/agents/agent_javascript_reviewer.py +160 -0
- code_puppy/agents/agent_manager.py +742 -0
- code_puppy/agents/agent_pack_leader.py +385 -0
- code_puppy/agents/agent_planning.py +165 -0
- code_puppy/agents/agent_python_programmer.py +169 -0
- code_puppy/agents/agent_python_reviewer.py +90 -0
- code_puppy/agents/agent_qa_expert.py +163 -0
- code_puppy/agents/agent_qa_kitten.py +208 -0
- code_puppy/agents/agent_scheduler.py +121 -0
- code_puppy/agents/agent_security_auditor.py +181 -0
- code_puppy/agents/agent_terminal_qa.py +323 -0
- code_puppy/agents/agent_typescript_reviewer.py +166 -0
- code_puppy/agents/base_agent.py +2156 -0
- code_puppy/agents/event_stream_handler.py +348 -0
- code_puppy/agents/json_agent.py +202 -0
- code_puppy/agents/pack/__init__.py +34 -0
- code_puppy/agents/pack/bloodhound.py +304 -0
- code_puppy/agents/pack/husky.py +327 -0
- code_puppy/agents/pack/retriever.py +393 -0
- code_puppy/agents/pack/shepherd.py +348 -0
- code_puppy/agents/pack/terrier.py +287 -0
- code_puppy/agents/pack/watchdog.py +367 -0
- code_puppy/agents/prompt_reviewer.py +145 -0
- code_puppy/agents/subagent_stream_handler.py +276 -0
- code_puppy/api/__init__.py +13 -0
- code_puppy/api/app.py +169 -0
- code_puppy/api/main.py +21 -0
- code_puppy/api/pty_manager.py +453 -0
- code_puppy/api/routers/__init__.py +12 -0
- code_puppy/api/routers/agents.py +36 -0
- code_puppy/api/routers/commands.py +217 -0
- code_puppy/api/routers/config.py +75 -0
- code_puppy/api/routers/sessions.py +234 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +692 -0
- code_puppy/chatgpt_codex_client.py +338 -0
- code_puppy/claude_cache_client.py +672 -0
- code_puppy/cli_runner.py +1073 -0
- code_puppy/command_line/__init__.py +1 -0
- code_puppy/command_line/add_model_menu.py +1092 -0
- code_puppy/command_line/agent_menu.py +662 -0
- code_puppy/command_line/attachments.py +395 -0
- code_puppy/command_line/autosave_menu.py +704 -0
- code_puppy/command_line/clipboard.py +527 -0
- code_puppy/command_line/colors_menu.py +532 -0
- code_puppy/command_line/command_handler.py +293 -0
- code_puppy/command_line/command_registry.py +150 -0
- code_puppy/command_line/config_commands.py +719 -0
- code_puppy/command_line/core_commands.py +867 -0
- code_puppy/command_line/diff_menu.py +865 -0
- code_puppy/command_line/file_path_completion.py +73 -0
- code_puppy/command_line/load_context_completion.py +52 -0
- code_puppy/command_line/mcp/__init__.py +10 -0
- code_puppy/command_line/mcp/base.py +32 -0
- code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
- code_puppy/command_line/mcp/custom_server_form.py +688 -0
- code_puppy/command_line/mcp/custom_server_installer.py +195 -0
- code_puppy/command_line/mcp/edit_command.py +148 -0
- code_puppy/command_line/mcp/handler.py +138 -0
- code_puppy/command_line/mcp/help_command.py +147 -0
- code_puppy/command_line/mcp/install_command.py +214 -0
- code_puppy/command_line/mcp/install_menu.py +705 -0
- code_puppy/command_line/mcp/list_command.py +94 -0
- code_puppy/command_line/mcp/logs_command.py +235 -0
- code_puppy/command_line/mcp/remove_command.py +82 -0
- code_puppy/command_line/mcp/restart_command.py +100 -0
- code_puppy/command_line/mcp/search_command.py +123 -0
- code_puppy/command_line/mcp/start_all_command.py +135 -0
- code_puppy/command_line/mcp/start_command.py +117 -0
- code_puppy/command_line/mcp/status_command.py +184 -0
- code_puppy/command_line/mcp/stop_all_command.py +112 -0
- code_puppy/command_line/mcp/stop_command.py +80 -0
- code_puppy/command_line/mcp/test_command.py +107 -0
- code_puppy/command_line/mcp/utils.py +129 -0
- code_puppy/command_line/mcp/wizard_utils.py +334 -0
- code_puppy/command_line/mcp_completion.py +174 -0
- code_puppy/command_line/model_picker_completion.py +197 -0
- code_puppy/command_line/model_settings_menu.py +932 -0
- code_puppy/command_line/motd.py +96 -0
- code_puppy/command_line/onboarding_slides.py +179 -0
- code_puppy/command_line/onboarding_wizard.py +342 -0
- code_puppy/command_line/pin_command_completion.py +329 -0
- code_puppy/command_line/prompt_toolkit_completion.py +846 -0
- code_puppy/command_line/session_commands.py +302 -0
- code_puppy/command_line/shell_passthrough.py +145 -0
- code_puppy/command_line/skills_completion.py +160 -0
- code_puppy/command_line/uc_menu.py +893 -0
- code_puppy/command_line/utils.py +93 -0
- code_puppy/command_line/wiggum_state.py +78 -0
- code_puppy/config.py +1770 -0
- code_puppy/error_logging.py +134 -0
- code_puppy/gemini_code_assist.py +385 -0
- code_puppy/gemini_model.py +754 -0
- code_puppy/hook_engine/README.md +105 -0
- code_puppy/hook_engine/__init__.py +21 -0
- code_puppy/hook_engine/aliases.py +155 -0
- code_puppy/hook_engine/engine.py +221 -0
- code_puppy/hook_engine/executor.py +296 -0
- code_puppy/hook_engine/matcher.py +156 -0
- code_puppy/hook_engine/models.py +240 -0
- code_puppy/hook_engine/registry.py +106 -0
- code_puppy/hook_engine/validator.py +144 -0
- code_puppy/http_utils.py +361 -0
- code_puppy/keymap.py +128 -0
- code_puppy/main.py +10 -0
- code_puppy/mcp_/__init__.py +66 -0
- code_puppy/mcp_/async_lifecycle.py +286 -0
- code_puppy/mcp_/blocking_startup.py +469 -0
- code_puppy/mcp_/captured_stdio_server.py +275 -0
- code_puppy/mcp_/circuit_breaker.py +290 -0
- code_puppy/mcp_/config_wizard.py +507 -0
- code_puppy/mcp_/dashboard.py +308 -0
- code_puppy/mcp_/error_isolation.py +407 -0
- code_puppy/mcp_/examples/retry_example.py +226 -0
- code_puppy/mcp_/health_monitor.py +589 -0
- code_puppy/mcp_/managed_server.py +428 -0
- code_puppy/mcp_/manager.py +807 -0
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/mcp_/registry.py +451 -0
- code_puppy/mcp_/retry_manager.py +337 -0
- code_puppy/mcp_/server_registry_catalog.py +1126 -0
- code_puppy/mcp_/status_tracker.py +355 -0
- code_puppy/mcp_/system_tools.py +209 -0
- code_puppy/mcp_prompts/__init__.py +1 -0
- code_puppy/mcp_prompts/hook_creator.py +103 -0
- code_puppy/messaging/__init__.py +255 -0
- code_puppy/messaging/bus.py +613 -0
- code_puppy/messaging/commands.py +167 -0
- code_puppy/messaging/markdown_patches.py +57 -0
- code_puppy/messaging/message_queue.py +361 -0
- code_puppy/messaging/messages.py +569 -0
- code_puppy/messaging/queue_console.py +271 -0
- code_puppy/messaging/renderers.py +311 -0
- code_puppy/messaging/rich_renderer.py +1158 -0
- code_puppy/messaging/spinner/__init__.py +83 -0
- code_puppy/messaging/spinner/console_spinner.py +240 -0
- code_puppy/messaging/spinner/spinner_base.py +95 -0
- code_puppy/messaging/subagent_console.py +460 -0
- code_puppy/model_factory.py +848 -0
- code_puppy/model_switching.py +63 -0
- code_puppy/model_utils.py +168 -0
- code_puppy/models.json +174 -0
- code_puppy/models_dev_api.json +1 -0
- code_puppy/models_dev_parser.py +592 -0
- code_puppy/plugins/__init__.py +186 -0
- code_puppy/plugins/agent_skills/__init__.py +22 -0
- code_puppy/plugins/agent_skills/config.py +175 -0
- code_puppy/plugins/agent_skills/discovery.py +136 -0
- code_puppy/plugins/agent_skills/downloader.py +392 -0
- code_puppy/plugins/agent_skills/installer.py +22 -0
- code_puppy/plugins/agent_skills/metadata.py +219 -0
- code_puppy/plugins/agent_skills/prompt_builder.py +60 -0
- code_puppy/plugins/agent_skills/register_callbacks.py +241 -0
- code_puppy/plugins/agent_skills/remote_catalog.py +322 -0
- code_puppy/plugins/agent_skills/skill_catalog.py +257 -0
- code_puppy/plugins/agent_skills/skills_install_menu.py +664 -0
- code_puppy/plugins/agent_skills/skills_menu.py +781 -0
- code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
- code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
- code_puppy/plugins/antigravity_oauth/antigravity_model.py +706 -0
- code_puppy/plugins/antigravity_oauth/config.py +42 -0
- code_puppy/plugins/antigravity_oauth/constants.py +133 -0
- code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +518 -0
- code_puppy/plugins/antigravity_oauth/storage.py +288 -0
- code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
- code_puppy/plugins/antigravity_oauth/token.py +167 -0
- code_puppy/plugins/antigravity_oauth/transport.py +863 -0
- code_puppy/plugins/antigravity_oauth/utils.py +168 -0
- code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
- code_puppy/plugins/chatgpt_oauth/config.py +52 -0
- code_puppy/plugins/chatgpt_oauth/oauth_flow.py +329 -0
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +176 -0
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +301 -0
- code_puppy/plugins/chatgpt_oauth/utils.py +523 -0
- code_puppy/plugins/claude_code_hooks/__init__.py +1 -0
- code_puppy/plugins/claude_code_hooks/config.py +137 -0
- code_puppy/plugins/claude_code_hooks/register_callbacks.py +175 -0
- code_puppy/plugins/claude_code_oauth/README.md +167 -0
- code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
- code_puppy/plugins/claude_code_oauth/__init__.py +25 -0
- code_puppy/plugins/claude_code_oauth/config.py +52 -0
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +453 -0
- code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
- code_puppy/plugins/claude_code_oauth/token_refresh_heartbeat.py +241 -0
- code_puppy/plugins/claude_code_oauth/utils.py +640 -0
- code_puppy/plugins/customizable_commands/__init__.py +0 -0
- code_puppy/plugins/customizable_commands/register_callbacks.py +152 -0
- code_puppy/plugins/example_custom_command/README.md +280 -0
- code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
- code_puppy/plugins/file_permission_handler/__init__.py +4 -0
- code_puppy/plugins/file_permission_handler/register_callbacks.py +470 -0
- code_puppy/plugins/frontend_emitter/__init__.py +25 -0
- code_puppy/plugins/frontend_emitter/emitter.py +121 -0
- code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
- code_puppy/plugins/hook_creator/__init__.py +1 -0
- code_puppy/plugins/hook_creator/register_callbacks.py +33 -0
- code_puppy/plugins/hook_manager/__init__.py +1 -0
- code_puppy/plugins/hook_manager/config.py +290 -0
- code_puppy/plugins/hook_manager/hooks_menu.py +564 -0
- code_puppy/plugins/hook_manager/register_callbacks.py +227 -0
- code_puppy/plugins/oauth_puppy_html.py +228 -0
- code_puppy/plugins/scheduler/__init__.py +1 -0
- code_puppy/plugins/scheduler/register_callbacks.py +88 -0
- code_puppy/plugins/scheduler/scheduler_menu.py +522 -0
- code_puppy/plugins/scheduler/scheduler_wizard.py +341 -0
- code_puppy/plugins/shell_safety/__init__.py +6 -0
- code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
- code_puppy/plugins/shell_safety/command_cache.py +156 -0
- code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
- code_puppy/plugins/synthetic_status/__init__.py +1 -0
- code_puppy/plugins/synthetic_status/register_callbacks.py +132 -0
- code_puppy/plugins/synthetic_status/status_api.py +147 -0
- code_puppy/plugins/universal_constructor/__init__.py +13 -0
- code_puppy/plugins/universal_constructor/models.py +138 -0
- code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
- code_puppy/plugins/universal_constructor/registry.py +302 -0
- code_puppy/plugins/universal_constructor/sandbox.py +584 -0
- code_puppy/prompts/antigravity_system_prompt.md +1 -0
- code_puppy/pydantic_patches.py +356 -0
- code_puppy/reopenable_async_client.py +232 -0
- code_puppy/round_robin_model.py +150 -0
- code_puppy/scheduler/__init__.py +41 -0
- code_puppy/scheduler/__main__.py +9 -0
- code_puppy/scheduler/cli.py +118 -0
- code_puppy/scheduler/config.py +126 -0
- code_puppy/scheduler/daemon.py +280 -0
- code_puppy/scheduler/executor.py +155 -0
- code_puppy/scheduler/platform.py +19 -0
- code_puppy/scheduler/platform_unix.py +22 -0
- code_puppy/scheduler/platform_win.py +32 -0
- code_puppy/session_storage.py +338 -0
- code_puppy/status_display.py +257 -0
- code_puppy/summarization_agent.py +176 -0
- code_puppy/terminal_utils.py +418 -0
- code_puppy/tools/__init__.py +501 -0
- code_puppy/tools/agent_tools.py +603 -0
- code_puppy/tools/ask_user_question/__init__.py +26 -0
- code_puppy/tools/ask_user_question/constants.py +73 -0
- code_puppy/tools/ask_user_question/demo_tui.py +55 -0
- code_puppy/tools/ask_user_question/handler.py +232 -0
- code_puppy/tools/ask_user_question/models.py +304 -0
- code_puppy/tools/ask_user_question/registration.py +26 -0
- code_puppy/tools/ask_user_question/renderers.py +309 -0
- code_puppy/tools/ask_user_question/terminal_ui.py +329 -0
- code_puppy/tools/ask_user_question/theme.py +155 -0
- code_puppy/tools/ask_user_question/tui_loop.py +423 -0
- code_puppy/tools/browser/__init__.py +37 -0
- code_puppy/tools/browser/browser_control.py +289 -0
- code_puppy/tools/browser/browser_interactions.py +545 -0
- code_puppy/tools/browser/browser_locators.py +640 -0
- code_puppy/tools/browser/browser_manager.py +378 -0
- code_puppy/tools/browser/browser_navigation.py +251 -0
- code_puppy/tools/browser/browser_screenshot.py +179 -0
- code_puppy/tools/browser/browser_scripts.py +462 -0
- code_puppy/tools/browser/browser_workflows.py +221 -0
- code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
- code_puppy/tools/browser/terminal_command_tools.py +534 -0
- code_puppy/tools/browser/terminal_screenshot_tools.py +552 -0
- code_puppy/tools/browser/terminal_tools.py +525 -0
- code_puppy/tools/command_runner.py +1346 -0
- code_puppy/tools/common.py +1409 -0
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/file_modifications.py +886 -0
- code_puppy/tools/file_operations.py +802 -0
- code_puppy/tools/scheduler_tools.py +412 -0
- code_puppy/tools/skills_tools.py +244 -0
- code_puppy/tools/subagent_context.py +158 -0
- code_puppy/tools/tools_content.py +51 -0
- code_puppy/tools/universal_constructor.py +889 -0
- code_puppy/uvx_detection.py +242 -0
- code_puppy/version_checker.py +82 -0
- codepp-0.0.437.dist-info/METADATA +766 -0
- codepp-0.0.437.dist-info/RECORD +288 -0
- codepp-0.0.437.dist-info/WHEEL +4 -0
- codepp-0.0.437.dist-info/entry_points.txt +3 -0
- codepp-0.0.437.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Command models for User → Agent communication in Code Puppy's messaging system.
|
|
2
|
+
|
|
3
|
+
This module defines Pydantic models for commands that flow FROM the UI TO the Agent.
|
|
4
|
+
This is the opposite direction of messages.py (which flows Agent → UI).
|
|
5
|
+
|
|
6
|
+
Commands are used for:
|
|
7
|
+
- Controlling agent execution (cancel, interrupt)
|
|
8
|
+
- Responding to agent requests for user input
|
|
9
|
+
- Providing confirmations and selections
|
|
10
|
+
|
|
11
|
+
The UI layer creates these commands and sends them to the agent/runtime.
|
|
12
|
+
The agent processes them and may emit messages in response.
|
|
13
|
+
|
|
14
|
+
┌─────────┐ Commands ┌─────────┐
|
|
15
|
+
│ UI │ ────────────> │ Agent │
|
|
16
|
+
│ (User) │ │ │
|
|
17
|
+
│ │ <──────────── │ │
|
|
18
|
+
└─────────┘ Messages └─────────┘
|
|
19
|
+
|
|
20
|
+
NO Rich markup or formatting should be embedded in any string fields.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from datetime import datetime, timezone
|
|
24
|
+
from typing import Optional, Union
|
|
25
|
+
from uuid import uuid4
|
|
26
|
+
|
|
27
|
+
from pydantic import BaseModel, Field
|
|
28
|
+
|
|
29
|
+
# =============================================================================
|
|
30
|
+
# Base Command
|
|
31
|
+
# =============================================================================
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class BaseCommand(BaseModel):
|
|
35
|
+
"""Base class for all commands with auto-generated id and timestamp."""
|
|
36
|
+
|
|
37
|
+
id: str = Field(
|
|
38
|
+
default_factory=lambda: str(uuid4()),
|
|
39
|
+
description="Unique identifier for this command instance",
|
|
40
|
+
)
|
|
41
|
+
timestamp: datetime = Field(
|
|
42
|
+
default_factory=lambda: datetime.now(timezone.utc),
|
|
43
|
+
description="When this command was created (UTC)",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
model_config = {"frozen": False, "extra": "forbid"}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# =============================================================================
|
|
50
|
+
# Agent Control Commands
|
|
51
|
+
# =============================================================================
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class CancelAgentCommand(BaseCommand):
|
|
55
|
+
"""Signals the agent to stop current execution gracefully.
|
|
56
|
+
|
|
57
|
+
The agent should finish any in-progress atomic operation, clean up,
|
|
58
|
+
and return control to the user. This is a soft cancellation.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
reason: Optional[str] = Field(
|
|
62
|
+
default=None,
|
|
63
|
+
description="Optional reason for cancellation (for logging/debugging)",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class InterruptShellCommand(BaseCommand):
|
|
68
|
+
"""Signals to interrupt a currently running shell command.
|
|
69
|
+
|
|
70
|
+
This is equivalent to pressing Ctrl+C in a terminal. The shell process
|
|
71
|
+
should receive SIGINT and terminate. Use this when a command is taking
|
|
72
|
+
too long or producing unwanted output.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
command_id: Optional[str] = Field(
|
|
76
|
+
default=None,
|
|
77
|
+
description="ID of the specific shell command to interrupt (None = current)",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# =============================================================================
|
|
82
|
+
# User Interaction Responses
|
|
83
|
+
# =============================================================================
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class UserInputResponse(BaseCommand):
|
|
87
|
+
"""Response to a UserInputRequest from the agent.
|
|
88
|
+
|
|
89
|
+
The prompt_id must match the prompt_id from the original UserInputRequest
|
|
90
|
+
so the agent can correlate the response with the request.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
prompt_id: str = Field(
|
|
94
|
+
description="ID of the prompt this responds to (must match request)"
|
|
95
|
+
)
|
|
96
|
+
value: str = Field(description="The user's input value")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class ConfirmationResponse(BaseCommand):
|
|
100
|
+
"""Response to a ConfirmationRequest from the agent.
|
|
101
|
+
|
|
102
|
+
The user can confirm or deny, and optionally provide feedback text
|
|
103
|
+
if the original request had allow_feedback=True.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
prompt_id: str = Field(
|
|
107
|
+
description="ID of the prompt this responds to (must match request)"
|
|
108
|
+
)
|
|
109
|
+
confirmed: bool = Field(
|
|
110
|
+
description="Whether the user confirmed (True) or denied (False)"
|
|
111
|
+
)
|
|
112
|
+
feedback: Optional[str] = Field(
|
|
113
|
+
default=None,
|
|
114
|
+
description="Optional feedback text from the user",
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class SelectionResponse(BaseCommand):
|
|
119
|
+
"""Response to a SelectionRequest from the agent.
|
|
120
|
+
|
|
121
|
+
Contains both the index and the value for convenience and validation.
|
|
122
|
+
The agent can verify that selected_value matches options[selected_index].
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
prompt_id: str = Field(
|
|
126
|
+
description="ID of the prompt this responds to (must match request)"
|
|
127
|
+
)
|
|
128
|
+
selected_index: int = Field(
|
|
129
|
+
ge=0,
|
|
130
|
+
description="Zero-based index of the selected option",
|
|
131
|
+
)
|
|
132
|
+
selected_value: str = Field(description="The value of the selected option")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# =============================================================================
|
|
136
|
+
# Union Type for Type Checking
|
|
137
|
+
# =============================================================================
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# All concrete command types (excludes BaseCommand itself)
|
|
141
|
+
AnyCommand = Union[
|
|
142
|
+
CancelAgentCommand,
|
|
143
|
+
InterruptShellCommand,
|
|
144
|
+
UserInputResponse,
|
|
145
|
+
ConfirmationResponse,
|
|
146
|
+
SelectionResponse,
|
|
147
|
+
]
|
|
148
|
+
"""Union of all command types for type checking."""
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# =============================================================================
|
|
152
|
+
# Export all public symbols
|
|
153
|
+
# =============================================================================
|
|
154
|
+
|
|
155
|
+
__all__ = [
|
|
156
|
+
# Base
|
|
157
|
+
"BaseCommand",
|
|
158
|
+
# Agent control
|
|
159
|
+
"CancelAgentCommand",
|
|
160
|
+
"InterruptShellCommand",
|
|
161
|
+
# User interaction responses
|
|
162
|
+
"UserInputResponse",
|
|
163
|
+
"ConfirmationResponse",
|
|
164
|
+
"SelectionResponse",
|
|
165
|
+
# Union type
|
|
166
|
+
"AnyCommand",
|
|
167
|
+
]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Patches for Rich's Markdown rendering.
|
|
2
|
+
|
|
3
|
+
This module provides customizations to Rich's default Markdown rendering,
|
|
4
|
+
particularly for header justification which is hardcoded to center in Rich.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from rich import box
|
|
8
|
+
from rich.markdown import Heading, Markdown
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.text import Text
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LeftJustifiedHeading(Heading):
|
|
14
|
+
"""A heading that left-justifies text instead of centering.
|
|
15
|
+
|
|
16
|
+
Rich's default Heading class hardcodes `text.justify = 'center'`,
|
|
17
|
+
which can look odd in a CLI context. This subclass overrides that
|
|
18
|
+
to use left justification instead.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __rich_console__(self, console, options):
|
|
22
|
+
"""Render the heading with left justification."""
|
|
23
|
+
text = self.text
|
|
24
|
+
text.justify = "left" # Override Rich's default 'center'
|
|
25
|
+
|
|
26
|
+
if self.tag == "h1":
|
|
27
|
+
# Draw a border around h1s (same as Rich default)
|
|
28
|
+
yield Panel(
|
|
29
|
+
text,
|
|
30
|
+
box=box.HEAVY,
|
|
31
|
+
style="markdown.h1.border",
|
|
32
|
+
)
|
|
33
|
+
else:
|
|
34
|
+
# Styled text for h2 and beyond (same as Rich default)
|
|
35
|
+
if self.tag == "h2":
|
|
36
|
+
yield Text("")
|
|
37
|
+
yield text
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
_patched = False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def patch_markdown_headings():
|
|
44
|
+
"""Patch Rich's Markdown to use left-justified headings.
|
|
45
|
+
|
|
46
|
+
This function is idempotent - calling it multiple times has no effect
|
|
47
|
+
after the first call.
|
|
48
|
+
"""
|
|
49
|
+
global _patched
|
|
50
|
+
if _patched:
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
Markdown.elements["heading_open"] = LeftJustifiedHeading
|
|
54
|
+
_patched = True
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
__all__ = ["patch_markdown_headings", "LeftJustifiedHeading"]
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Message queue system for decoupling Rich console output from renderers.
|
|
3
|
+
|
|
4
|
+
This allows interactive mode to consume messages and render them appropriately.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import logging
|
|
9
|
+
import queue
|
|
10
|
+
import threading
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
from enum import Enum
|
|
14
|
+
from typing import Any, Dict, Optional, Union
|
|
15
|
+
|
|
16
|
+
from rich.text import Text
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MessageType(Enum):
|
|
22
|
+
"""Types of messages that can be sent through the queue."""
|
|
23
|
+
|
|
24
|
+
# Basic content types
|
|
25
|
+
INFO = "info"
|
|
26
|
+
SUCCESS = "success"
|
|
27
|
+
WARNING = "warning"
|
|
28
|
+
ERROR = "error"
|
|
29
|
+
DIVIDER = "divider"
|
|
30
|
+
|
|
31
|
+
# Tool-specific types
|
|
32
|
+
TOOL_OUTPUT = "tool_output"
|
|
33
|
+
COMMAND_OUTPUT = "command_output"
|
|
34
|
+
FILE_OPERATION = "file_operation"
|
|
35
|
+
|
|
36
|
+
# Agent-specific types
|
|
37
|
+
AGENT_REASONING = "agent_reasoning"
|
|
38
|
+
PLANNED_NEXT_STEPS = "planned_next_steps"
|
|
39
|
+
AGENT_RESPONSE = "agent_response"
|
|
40
|
+
AGENT_STATUS = "agent_status"
|
|
41
|
+
|
|
42
|
+
# Human interaction types
|
|
43
|
+
HUMAN_INPUT_REQUEST = "human_input_request"
|
|
44
|
+
|
|
45
|
+
# System types
|
|
46
|
+
SYSTEM = "system"
|
|
47
|
+
DEBUG = "debug"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class UIMessage:
|
|
52
|
+
"""A message to be displayed in the UI."""
|
|
53
|
+
|
|
54
|
+
type: MessageType
|
|
55
|
+
content: Union[str, Text, Any] # Can be Rich Text, Table, Markdown, etc.
|
|
56
|
+
timestamp: datetime = None
|
|
57
|
+
metadata: Dict[str, Any] = None
|
|
58
|
+
|
|
59
|
+
def __post_init__(self):
|
|
60
|
+
if self.timestamp is None:
|
|
61
|
+
self.timestamp = datetime.now(timezone.utc)
|
|
62
|
+
if self.metadata is None:
|
|
63
|
+
self.metadata = {}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class MessageQueue:
|
|
67
|
+
"""Thread-safe message queue for UI messages."""
|
|
68
|
+
|
|
69
|
+
def __init__(self, maxsize: int = 1000):
|
|
70
|
+
self._queue = queue.Queue(maxsize=maxsize)
|
|
71
|
+
self._async_queue = None # Will be created when needed
|
|
72
|
+
self._async_queue_maxsize = maxsize
|
|
73
|
+
self._listeners = []
|
|
74
|
+
self._running = False
|
|
75
|
+
self._thread = None
|
|
76
|
+
self._startup_buffer = [] # Buffer messages before any renderer starts
|
|
77
|
+
self._has_active_renderer = False
|
|
78
|
+
self._event_loop = None # Store reference to the event loop
|
|
79
|
+
self._prompt_responses = {} # Store responses to human input requests
|
|
80
|
+
self._prompt_events = {} # threading.Event per prompt_id
|
|
81
|
+
self._prompt_id_counter = 0 # Counter for unique prompt IDs
|
|
82
|
+
|
|
83
|
+
def start(self):
|
|
84
|
+
"""Start the queue processing."""
|
|
85
|
+
if self._running:
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
self._running = True
|
|
89
|
+
self._thread = threading.Thread(target=self._process_messages, daemon=True)
|
|
90
|
+
self._thread.start()
|
|
91
|
+
|
|
92
|
+
def get_buffered_messages(self):
|
|
93
|
+
"""Get all currently buffered messages without waiting."""
|
|
94
|
+
# First get any startup buffered messages
|
|
95
|
+
messages = list(self._startup_buffer)
|
|
96
|
+
|
|
97
|
+
# Then get any queued messages
|
|
98
|
+
while True:
|
|
99
|
+
try:
|
|
100
|
+
message = self._queue.get_nowait()
|
|
101
|
+
messages.append(message)
|
|
102
|
+
except queue.Empty:
|
|
103
|
+
break
|
|
104
|
+
return messages
|
|
105
|
+
|
|
106
|
+
def clear_startup_buffer(self):
|
|
107
|
+
"""Clear the startup buffer after processing."""
|
|
108
|
+
self._startup_buffer.clear()
|
|
109
|
+
|
|
110
|
+
def stop(self):
|
|
111
|
+
"""Stop the queue processing."""
|
|
112
|
+
self._running = False
|
|
113
|
+
if self._thread and self._thread.is_alive():
|
|
114
|
+
self._thread.join(timeout=1.0)
|
|
115
|
+
|
|
116
|
+
def emit(self, message: UIMessage):
|
|
117
|
+
"""Emit a message to the queue."""
|
|
118
|
+
# If no renderer is active yet, buffer the message for startup
|
|
119
|
+
if not self._has_active_renderer:
|
|
120
|
+
self._startup_buffer.append(message)
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
self._queue.put_nowait(message)
|
|
125
|
+
except queue.Full:
|
|
126
|
+
# Drop oldest message to make room
|
|
127
|
+
try:
|
|
128
|
+
self._queue.get_nowait()
|
|
129
|
+
self._queue.put_nowait(message)
|
|
130
|
+
except queue.Empty:
|
|
131
|
+
pass
|
|
132
|
+
|
|
133
|
+
def emit_simple(self, message_type: MessageType, content: Any, **metadata):
|
|
134
|
+
"""Emit a simple message with just type and content."""
|
|
135
|
+
msg = UIMessage(type=message_type, content=content, metadata=metadata)
|
|
136
|
+
self.emit(msg)
|
|
137
|
+
|
|
138
|
+
def get_nowait(self) -> Optional[UIMessage]:
|
|
139
|
+
"""Get a message without blocking."""
|
|
140
|
+
try:
|
|
141
|
+
return self._queue.get_nowait()
|
|
142
|
+
except queue.Empty:
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
async def get_async(self) -> UIMessage:
|
|
146
|
+
"""Get a message asynchronously."""
|
|
147
|
+
# Lazy initialization of async queue and store event loop reference
|
|
148
|
+
if self._async_queue is None:
|
|
149
|
+
self._async_queue = asyncio.Queue(maxsize=self._async_queue_maxsize)
|
|
150
|
+
self._event_loop = asyncio.get_running_loop()
|
|
151
|
+
return await self._async_queue.get()
|
|
152
|
+
|
|
153
|
+
def _process_messages(self):
|
|
154
|
+
"""Process messages from sync to async queue."""
|
|
155
|
+
while self._running:
|
|
156
|
+
try:
|
|
157
|
+
message = self._queue.get(timeout=0.1)
|
|
158
|
+
|
|
159
|
+
# Try to put in async queue if we have an event loop reference
|
|
160
|
+
if self._event_loop is not None and self._async_queue is not None:
|
|
161
|
+
# Use thread-safe call to put message in async queue
|
|
162
|
+
# Create a bound method to avoid closure issues
|
|
163
|
+
try:
|
|
164
|
+
self._event_loop.call_soon_threadsafe(
|
|
165
|
+
self._async_queue.put_nowait, message
|
|
166
|
+
)
|
|
167
|
+
except Exception as e:
|
|
168
|
+
logger.debug("Failed to enqueue message to async queue: %s", e)
|
|
169
|
+
|
|
170
|
+
# Notify listeners immediately for sync processing
|
|
171
|
+
for listener in self._listeners:
|
|
172
|
+
try:
|
|
173
|
+
listener(message)
|
|
174
|
+
except Exception as e:
|
|
175
|
+
logger.debug("Listener error in message queue: %s", e)
|
|
176
|
+
|
|
177
|
+
except queue.Empty:
|
|
178
|
+
continue
|
|
179
|
+
|
|
180
|
+
def add_listener(self, callback):
|
|
181
|
+
"""Add a listener for messages (for direct sync consumption)."""
|
|
182
|
+
self._listeners.append(callback)
|
|
183
|
+
# Mark that we have an active renderer
|
|
184
|
+
self._has_active_renderer = True
|
|
185
|
+
|
|
186
|
+
def remove_listener(self, callback):
|
|
187
|
+
"""Remove a listener."""
|
|
188
|
+
if callback in self._listeners:
|
|
189
|
+
self._listeners.remove(callback)
|
|
190
|
+
# If no more listeners, mark as no active renderer
|
|
191
|
+
if not self._listeners:
|
|
192
|
+
self._has_active_renderer = False
|
|
193
|
+
|
|
194
|
+
def mark_renderer_active(self):
|
|
195
|
+
"""Mark that a renderer is now active and consuming messages."""
|
|
196
|
+
self._has_active_renderer = True
|
|
197
|
+
|
|
198
|
+
def mark_renderer_inactive(self):
|
|
199
|
+
"""Mark that no renderer is currently active."""
|
|
200
|
+
self._has_active_renderer = False
|
|
201
|
+
|
|
202
|
+
def create_prompt_request(self, prompt_text: str) -> str:
|
|
203
|
+
"""Create a human input request and return its unique ID."""
|
|
204
|
+
self._prompt_id_counter += 1
|
|
205
|
+
prompt_id = f"prompt_{self._prompt_id_counter}"
|
|
206
|
+
|
|
207
|
+
# Create event for this prompt
|
|
208
|
+
self._prompt_events[prompt_id] = threading.Event()
|
|
209
|
+
|
|
210
|
+
# Emit the human input request message
|
|
211
|
+
message = UIMessage(
|
|
212
|
+
type=MessageType.HUMAN_INPUT_REQUEST,
|
|
213
|
+
content=prompt_text,
|
|
214
|
+
metadata={"prompt_id": prompt_id},
|
|
215
|
+
)
|
|
216
|
+
self.emit(message)
|
|
217
|
+
|
|
218
|
+
return prompt_id
|
|
219
|
+
|
|
220
|
+
def wait_for_prompt_response(self, prompt_id: str, timeout: float = None) -> str:
|
|
221
|
+
"""Wait for a response to a human input request."""
|
|
222
|
+
# If response is already available, return immediately
|
|
223
|
+
if prompt_id in self._prompt_responses:
|
|
224
|
+
self._prompt_events.pop(prompt_id, None)
|
|
225
|
+
return self._prompt_responses.pop(prompt_id)
|
|
226
|
+
|
|
227
|
+
event = self._prompt_events.get(prompt_id)
|
|
228
|
+
if event is None:
|
|
229
|
+
# Fallback: create event if not already present
|
|
230
|
+
event = threading.Event()
|
|
231
|
+
self._prompt_events[prompt_id] = event
|
|
232
|
+
|
|
233
|
+
signaled = event.wait(timeout=timeout)
|
|
234
|
+
|
|
235
|
+
# Clean up the event
|
|
236
|
+
self._prompt_events.pop(prompt_id, None)
|
|
237
|
+
|
|
238
|
+
if not signaled:
|
|
239
|
+
raise TimeoutError(f"No response for prompt {prompt_id} within {timeout}s")
|
|
240
|
+
|
|
241
|
+
return self._prompt_responses.pop(prompt_id)
|
|
242
|
+
|
|
243
|
+
def provide_prompt_response(self, prompt_id: str, response: str):
|
|
244
|
+
"""Provide a response to a human input request."""
|
|
245
|
+
self._prompt_responses[prompt_id] = response
|
|
246
|
+
event = self._prompt_events.get(prompt_id)
|
|
247
|
+
if event is not None:
|
|
248
|
+
event.set()
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
# Global message queue instance
|
|
252
|
+
_global_queue: Optional[MessageQueue] = None
|
|
253
|
+
_queue_lock = threading.Lock()
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def get_global_queue() -> MessageQueue:
|
|
257
|
+
"""Get or create the global message queue."""
|
|
258
|
+
global _global_queue
|
|
259
|
+
|
|
260
|
+
with _queue_lock:
|
|
261
|
+
if _global_queue is None:
|
|
262
|
+
_global_queue = MessageQueue()
|
|
263
|
+
_global_queue.start()
|
|
264
|
+
|
|
265
|
+
return _global_queue
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def get_buffered_startup_messages():
|
|
269
|
+
"""Get any messages that were buffered before renderers started."""
|
|
270
|
+
queue = get_global_queue()
|
|
271
|
+
# Only return startup buffer messages, don't clear them yet
|
|
272
|
+
messages = list(queue._startup_buffer)
|
|
273
|
+
return messages
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def emit_message(message_type: MessageType, content: Any, **metadata):
|
|
277
|
+
"""Convenience function to emit a message to the global queue."""
|
|
278
|
+
queue = get_global_queue()
|
|
279
|
+
queue.emit_simple(message_type, content, **metadata)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def emit_info(content: Any, **metadata):
|
|
283
|
+
"""Emit an info message."""
|
|
284
|
+
emit_message(MessageType.INFO, content, **metadata)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def emit_success(content: Any, **metadata):
|
|
288
|
+
"""Emit a success message."""
|
|
289
|
+
emit_message(MessageType.SUCCESS, content, **metadata)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def emit_warning(content: Any, **metadata):
|
|
293
|
+
"""Emit a warning message."""
|
|
294
|
+
emit_message(MessageType.WARNING, content, **metadata)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def emit_error(content: Any, **metadata):
|
|
298
|
+
"""Emit an error message."""
|
|
299
|
+
emit_message(MessageType.ERROR, content, **metadata)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def emit_tool_output(content: Any, tool_name: str = None, **metadata):
|
|
303
|
+
"""Emit tool output."""
|
|
304
|
+
if tool_name:
|
|
305
|
+
metadata["tool_name"] = tool_name
|
|
306
|
+
emit_message(MessageType.TOOL_OUTPUT, content, **metadata)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def emit_command_output(content: Any, command: str = None, **metadata):
|
|
310
|
+
"""Emit command output."""
|
|
311
|
+
if command:
|
|
312
|
+
metadata["command"] = command
|
|
313
|
+
emit_message(MessageType.COMMAND_OUTPUT, content, **metadata)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def emit_agent_reasoning(content: Any, **metadata):
|
|
317
|
+
"""Emit agent reasoning."""
|
|
318
|
+
emit_message(MessageType.AGENT_REASONING, content, **metadata)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def emit_planned_next_steps(content: Any, **metadata):
|
|
322
|
+
"""Emit planned_next_steps"""
|
|
323
|
+
emit_message(MessageType.PLANNED_NEXT_STEPS, content, **metadata)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def emit_agent_response(content: Any, **metadata):
|
|
327
|
+
"""Emit agent_response"""
|
|
328
|
+
emit_message(MessageType.AGENT_RESPONSE, content, **metadata)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def emit_system_message(content: Any, **metadata):
|
|
332
|
+
"""Emit a system message."""
|
|
333
|
+
emit_message(MessageType.SYSTEM, content, **metadata)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def emit_divider(content: str = "─" * 100 + "\n", **metadata):
|
|
337
|
+
"""Emit a divider line"""
|
|
338
|
+
# TUI mode has been removed, always emit dividers
|
|
339
|
+
emit_message(MessageType.DIVIDER, content, **metadata)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def emit_prompt(prompt_text: str, timeout: float = None) -> str:
|
|
343
|
+
"""Emit a human input request and wait for response.
|
|
344
|
+
|
|
345
|
+
Uses safe_input for cross-platform compatibility, especially on Windows
|
|
346
|
+
where raw input() can fail after prompt_toolkit Applications.
|
|
347
|
+
"""
|
|
348
|
+
from code_puppy.command_line.utils import safe_input
|
|
349
|
+
from code_puppy.messaging import emit_info
|
|
350
|
+
|
|
351
|
+
emit_info(prompt_text)
|
|
352
|
+
|
|
353
|
+
# Use safe_input which resets Windows console state before reading
|
|
354
|
+
response = safe_input(">>> ")
|
|
355
|
+
return response
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def provide_prompt_response(prompt_id: str, response: str):
|
|
359
|
+
"""Provide a response to a human input request."""
|
|
360
|
+
queue = get_global_queue()
|
|
361
|
+
queue.provide_prompt_response(prompt_id, response)
|