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,271 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Queue-based console that mimics Rich Console but sends messages to a queue.
|
|
3
|
+
|
|
4
|
+
This allows tools to use the same Rich console interface while having
|
|
5
|
+
their output captured and routed through our message queue system.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import traceback
|
|
9
|
+
from typing import Any, Optional
|
|
10
|
+
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.markdown import Markdown
|
|
13
|
+
from rich.table import Table
|
|
14
|
+
from rich.text import Text
|
|
15
|
+
|
|
16
|
+
from .message_queue import MessageQueue, MessageType, get_global_queue
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class QueueConsole:
|
|
20
|
+
"""
|
|
21
|
+
Console-like interface that sends messages to a queue instead of stdout.
|
|
22
|
+
|
|
23
|
+
This is designed to be a drop-in replacement for Rich Console that
|
|
24
|
+
routes messages through our queue system.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
queue: Optional[MessageQueue] = None,
|
|
30
|
+
fallback_console: Optional[Console] = None,
|
|
31
|
+
):
|
|
32
|
+
self.queue = queue or get_global_queue()
|
|
33
|
+
self.fallback_console = fallback_console or Console()
|
|
34
|
+
|
|
35
|
+
def print(
|
|
36
|
+
self,
|
|
37
|
+
*values: Any,
|
|
38
|
+
sep: str = " ",
|
|
39
|
+
end: str = "\n",
|
|
40
|
+
style: Optional[str] = None,
|
|
41
|
+
highlight: bool = True,
|
|
42
|
+
**kwargs,
|
|
43
|
+
):
|
|
44
|
+
"""Print values to the message queue."""
|
|
45
|
+
# Handle Rich objects properly
|
|
46
|
+
if len(values) == 1 and hasattr(values[0], "__rich_console__"):
|
|
47
|
+
# Single Rich object - pass it through directly
|
|
48
|
+
content = values[0]
|
|
49
|
+
message_type = self._infer_message_type_from_rich_object(content, style)
|
|
50
|
+
else:
|
|
51
|
+
# Convert to string, but handle Rich objects properly
|
|
52
|
+
processed_values = []
|
|
53
|
+
for v in values:
|
|
54
|
+
if hasattr(v, "__rich_console__"):
|
|
55
|
+
# For Rich objects, try to extract their text content
|
|
56
|
+
from io import StringIO
|
|
57
|
+
|
|
58
|
+
from rich.console import Console
|
|
59
|
+
|
|
60
|
+
string_io = StringIO()
|
|
61
|
+
# Use markup=True to properly process rich styling
|
|
62
|
+
# Use a reasonable width to prevent wrapping issues
|
|
63
|
+
temp_console = Console(
|
|
64
|
+
file=string_io, width=80, legacy_windows=False, markup=True
|
|
65
|
+
)
|
|
66
|
+
temp_console.print(v)
|
|
67
|
+
processed_values.append(string_io.getvalue().rstrip("\n"))
|
|
68
|
+
else:
|
|
69
|
+
processed_values.append(str(v))
|
|
70
|
+
|
|
71
|
+
content = sep.join(processed_values) + end
|
|
72
|
+
message_type = self._infer_message_type(content, style)
|
|
73
|
+
|
|
74
|
+
# Create Rich Text object if style is provided and content is string
|
|
75
|
+
if style and isinstance(content, str):
|
|
76
|
+
content = Text(content, style=style)
|
|
77
|
+
|
|
78
|
+
# Emit to queue
|
|
79
|
+
self.queue.emit_simple(
|
|
80
|
+
message_type, content, style=style, highlight=highlight, **kwargs
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def print_exception(
|
|
84
|
+
self,
|
|
85
|
+
*,
|
|
86
|
+
width: Optional[int] = None,
|
|
87
|
+
extra_lines: int = 3,
|
|
88
|
+
theme: Optional[str] = None,
|
|
89
|
+
word_wrap: bool = False,
|
|
90
|
+
show_locals: bool = False,
|
|
91
|
+
indent_guides: bool = True,
|
|
92
|
+
suppress: tuple = (),
|
|
93
|
+
max_frames: int = 100,
|
|
94
|
+
):
|
|
95
|
+
"""Print exception information to the queue."""
|
|
96
|
+
# Get the exception traceback
|
|
97
|
+
exc_text = traceback.format_exc()
|
|
98
|
+
|
|
99
|
+
# Emit as error message
|
|
100
|
+
self.queue.emit_simple(
|
|
101
|
+
MessageType.ERROR,
|
|
102
|
+
f"Exception:\n{exc_text}",
|
|
103
|
+
exception=True,
|
|
104
|
+
show_locals=show_locals,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def log(
|
|
108
|
+
self,
|
|
109
|
+
*values: Any,
|
|
110
|
+
sep: str = " ",
|
|
111
|
+
end: str = "\n",
|
|
112
|
+
style: Optional[str] = None,
|
|
113
|
+
justify: Optional[str] = None,
|
|
114
|
+
emoji: Optional[bool] = None,
|
|
115
|
+
markup: Optional[bool] = None,
|
|
116
|
+
highlight: Optional[bool] = None,
|
|
117
|
+
log_locals: bool = False,
|
|
118
|
+
):
|
|
119
|
+
"""Log a message (similar to print but with logging semantics)."""
|
|
120
|
+
content = sep.join(str(v) for v in values) + end
|
|
121
|
+
|
|
122
|
+
# Log messages are typically informational
|
|
123
|
+
message_type = MessageType.INFO
|
|
124
|
+
if style:
|
|
125
|
+
message_type = self._infer_message_type(content, style)
|
|
126
|
+
|
|
127
|
+
if style and isinstance(content, str):
|
|
128
|
+
content = Text(content, style=style)
|
|
129
|
+
|
|
130
|
+
self.queue.emit_simple(
|
|
131
|
+
message_type, content, log=True, style=style, log_locals=log_locals
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def _infer_message_type_from_rich_object(
|
|
135
|
+
self, content: Any, style: Optional[str] = None
|
|
136
|
+
) -> MessageType:
|
|
137
|
+
"""Infer message type from Rich object type and style."""
|
|
138
|
+
if style:
|
|
139
|
+
style_lower = style.lower()
|
|
140
|
+
if "red" in style_lower or "error" in style_lower:
|
|
141
|
+
return MessageType.ERROR
|
|
142
|
+
elif "yellow" in style_lower or "warning" in style_lower:
|
|
143
|
+
return MessageType.WARNING
|
|
144
|
+
elif "green" in style_lower or "success" in style_lower:
|
|
145
|
+
return MessageType.SUCCESS
|
|
146
|
+
elif "blue" in style_lower:
|
|
147
|
+
return MessageType.INFO
|
|
148
|
+
elif "purple" in style_lower or "magenta" in style_lower:
|
|
149
|
+
return MessageType.AGENT_REASONING
|
|
150
|
+
elif "dim" in style_lower:
|
|
151
|
+
return MessageType.SYSTEM
|
|
152
|
+
|
|
153
|
+
# Infer from object type
|
|
154
|
+
if isinstance(content, Markdown):
|
|
155
|
+
return MessageType.AGENT_REASONING
|
|
156
|
+
elif isinstance(content, Table):
|
|
157
|
+
return MessageType.TOOL_OUTPUT
|
|
158
|
+
elif hasattr(content, "lexer_name"): # Syntax object
|
|
159
|
+
return MessageType.TOOL_OUTPUT
|
|
160
|
+
|
|
161
|
+
return MessageType.INFO
|
|
162
|
+
|
|
163
|
+
def _infer_message_type(
|
|
164
|
+
self, content: str, style: Optional[str] = None
|
|
165
|
+
) -> MessageType:
|
|
166
|
+
"""Infer message type from content and style."""
|
|
167
|
+
if style:
|
|
168
|
+
style_lower = style.lower()
|
|
169
|
+
if "red" in style_lower or "error" in style_lower:
|
|
170
|
+
return MessageType.ERROR
|
|
171
|
+
elif "yellow" in style_lower or "warning" in style_lower:
|
|
172
|
+
return MessageType.WARNING
|
|
173
|
+
elif "green" in style_lower or "success" in style_lower:
|
|
174
|
+
return MessageType.SUCCESS
|
|
175
|
+
elif "blue" in style_lower:
|
|
176
|
+
return MessageType.INFO
|
|
177
|
+
elif "purple" in style_lower or "magenta" in style_lower:
|
|
178
|
+
return MessageType.AGENT_REASONING
|
|
179
|
+
elif "dim" in style_lower:
|
|
180
|
+
return MessageType.SYSTEM
|
|
181
|
+
|
|
182
|
+
# Infer from content patterns
|
|
183
|
+
content_lower = content.lower()
|
|
184
|
+
if any(word in content_lower for word in ["error", "failed", "exception"]):
|
|
185
|
+
return MessageType.ERROR
|
|
186
|
+
elif any(word in content_lower for word in ["warning", "warn"]):
|
|
187
|
+
return MessageType.WARNING
|
|
188
|
+
elif any(word in content_lower for word in ["success", "completed", "done"]):
|
|
189
|
+
return MessageType.SUCCESS
|
|
190
|
+
elif any(word in content_lower for word in ["tool", "command", "running"]):
|
|
191
|
+
return MessageType.TOOL_OUTPUT
|
|
192
|
+
|
|
193
|
+
return MessageType.INFO
|
|
194
|
+
|
|
195
|
+
# Additional methods to maintain Rich Console compatibility
|
|
196
|
+
def rule(self, title: str = "", *, align: str = "center", style: str = "rule.line"):
|
|
197
|
+
"""Print a horizontal rule."""
|
|
198
|
+
self.queue.emit_simple(
|
|
199
|
+
MessageType.SYSTEM,
|
|
200
|
+
f"─── {title} ───" if title else "─" * 40,
|
|
201
|
+
rule=True,
|
|
202
|
+
style=style,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
def status(self, status: str, *, spinner: str = "dots"):
|
|
206
|
+
"""Show a status message (simplified)."""
|
|
207
|
+
self.queue.emit_simple(
|
|
208
|
+
MessageType.INFO, f"⏳ {status}", status=True, spinner=spinner
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
def input(self, prompt: str = "") -> str:
|
|
212
|
+
"""Get user input without spinner interference.
|
|
213
|
+
|
|
214
|
+
This method coordinates with the TUI to pause any running spinners
|
|
215
|
+
and properly display the user input prompt.
|
|
216
|
+
"""
|
|
217
|
+
# Set the global flag that we're awaiting user input
|
|
218
|
+
from code_puppy.tools.command_runner import set_awaiting_user_input
|
|
219
|
+
|
|
220
|
+
set_awaiting_user_input(True)
|
|
221
|
+
|
|
222
|
+
# Emit the prompt as a system message so it shows in the TUI chat
|
|
223
|
+
if prompt:
|
|
224
|
+
self.queue.emit_simple(MessageType.SYSTEM, prompt, requires_user_input=True)
|
|
225
|
+
|
|
226
|
+
# Create a new, isolated console instance specifically for input
|
|
227
|
+
# This bypasses any spinner or queue system interference
|
|
228
|
+
input_console = Console(file=__import__("sys").stderr, force_terminal=True)
|
|
229
|
+
|
|
230
|
+
# Clear any spinner artifacts and position cursor properly
|
|
231
|
+
if prompt:
|
|
232
|
+
input_console.print(prompt, end="", style="bold cyan")
|
|
233
|
+
|
|
234
|
+
# Use regular input() which will read from stdin
|
|
235
|
+
# Since we printed the prompt to stderr, this should work cleanly
|
|
236
|
+
try:
|
|
237
|
+
user_response = input()
|
|
238
|
+
|
|
239
|
+
# Show the user's response in the chat as well
|
|
240
|
+
if user_response:
|
|
241
|
+
self.queue.emit_simple(
|
|
242
|
+
MessageType.INFO, f"User response: {user_response}"
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
return user_response
|
|
246
|
+
except (KeyboardInterrupt, EOFError):
|
|
247
|
+
# Handle interruption gracefully
|
|
248
|
+
input_console.print("\n[yellow]Input cancelled[/yellow]")
|
|
249
|
+
self.queue.emit_simple(MessageType.WARNING, "User input cancelled")
|
|
250
|
+
return ""
|
|
251
|
+
finally:
|
|
252
|
+
# Clear the global flag for awaiting user input
|
|
253
|
+
from code_puppy.tools.command_runner import set_awaiting_user_input
|
|
254
|
+
|
|
255
|
+
set_awaiting_user_input(False)
|
|
256
|
+
|
|
257
|
+
# File-like interface for compatibility
|
|
258
|
+
@property
|
|
259
|
+
def file(self):
|
|
260
|
+
"""Get the current file (for compatibility)."""
|
|
261
|
+
return self.fallback_console.file
|
|
262
|
+
|
|
263
|
+
@file.setter
|
|
264
|
+
def file(self, value):
|
|
265
|
+
"""Set the current file (for compatibility)."""
|
|
266
|
+
self.fallback_console.file = value
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def get_queue_console(queue: Optional[MessageQueue] = None) -> QueueConsole:
|
|
270
|
+
"""Get a QueueConsole instance."""
|
|
271
|
+
return QueueConsole(queue or get_global_queue())
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Renderer implementations for different UI modes.
|
|
3
|
+
|
|
4
|
+
These renderers consume messages from the queue and display them
|
|
5
|
+
appropriately for their respective interfaces.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import threading
|
|
10
|
+
from abc import ABC, abstractmethod
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.markdown import Markdown
|
|
15
|
+
from rich.markup import escape as escape_rich_markup
|
|
16
|
+
|
|
17
|
+
from .message_queue import MessageQueue, MessageType, UIMessage
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class MessageRenderer(ABC):
|
|
21
|
+
"""Base class for message renderers."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, queue: MessageQueue):
|
|
24
|
+
self.queue = queue
|
|
25
|
+
self._running = False
|
|
26
|
+
self._task = None
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
async def render_message(self, message: UIMessage):
|
|
30
|
+
"""Render a single message."""
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
async def start(self):
|
|
34
|
+
"""Start the renderer."""
|
|
35
|
+
if self._running:
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
self._running = True
|
|
39
|
+
# Mark the queue as having an active renderer
|
|
40
|
+
self.queue.mark_renderer_active()
|
|
41
|
+
self._task = asyncio.create_task(self._consume_messages())
|
|
42
|
+
|
|
43
|
+
async def stop(self):
|
|
44
|
+
"""Stop the renderer."""
|
|
45
|
+
self._running = False
|
|
46
|
+
# Mark the queue as having no active renderer
|
|
47
|
+
self.queue.mark_renderer_inactive()
|
|
48
|
+
if self._task:
|
|
49
|
+
self._task.cancel()
|
|
50
|
+
try:
|
|
51
|
+
await self._task
|
|
52
|
+
except asyncio.CancelledError:
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
async def _consume_messages(self):
|
|
56
|
+
"""Consume messages from the queue."""
|
|
57
|
+
while self._running:
|
|
58
|
+
try:
|
|
59
|
+
message = await asyncio.wait_for(self.queue.get_async(), timeout=0.1)
|
|
60
|
+
await self.render_message(message)
|
|
61
|
+
except asyncio.TimeoutError:
|
|
62
|
+
continue
|
|
63
|
+
except asyncio.CancelledError:
|
|
64
|
+
break
|
|
65
|
+
except Exception as e:
|
|
66
|
+
# Log error but continue processing
|
|
67
|
+
# Note: Using sys.stderr - can't use messaging in renderer
|
|
68
|
+
import sys
|
|
69
|
+
|
|
70
|
+
sys.stderr.write(f"Error rendering message: {e}\n")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class InteractiveRenderer(MessageRenderer):
|
|
74
|
+
"""Renderer for interactive CLI mode using Rich console.
|
|
75
|
+
|
|
76
|
+
Note: This async-based renderer is not currently used in the codebase.
|
|
77
|
+
Interactive mode currently uses SynchronousInteractiveRenderer instead.
|
|
78
|
+
A future refactoring might consolidate these renderers.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __init__(self, queue: MessageQueue, console: Optional[Console] = None):
|
|
82
|
+
super().__init__(queue)
|
|
83
|
+
self.console = console or Console()
|
|
84
|
+
|
|
85
|
+
async def render_message(self, message: UIMessage):
|
|
86
|
+
"""Render a message using Rich console."""
|
|
87
|
+
# Handle human input requests
|
|
88
|
+
if message.type == MessageType.HUMAN_INPUT_REQUEST:
|
|
89
|
+
await self._handle_human_input_request(message)
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
# Convert message type to appropriate Rich styling
|
|
93
|
+
if message.type == MessageType.ERROR:
|
|
94
|
+
style = "bold red"
|
|
95
|
+
elif message.type == MessageType.WARNING:
|
|
96
|
+
style = "yellow"
|
|
97
|
+
elif message.type == MessageType.SUCCESS:
|
|
98
|
+
style = "green"
|
|
99
|
+
elif message.type == MessageType.TOOL_OUTPUT:
|
|
100
|
+
style = "blue"
|
|
101
|
+
elif message.type == MessageType.AGENT_REASONING:
|
|
102
|
+
style = None
|
|
103
|
+
elif message.type == MessageType.PLANNED_NEXT_STEPS:
|
|
104
|
+
style = None
|
|
105
|
+
elif message.type == MessageType.AGENT_RESPONSE:
|
|
106
|
+
# Special handling for agent responses - they'll be rendered as markdown
|
|
107
|
+
style = None
|
|
108
|
+
elif message.type == MessageType.SYSTEM:
|
|
109
|
+
style = "dim"
|
|
110
|
+
else:
|
|
111
|
+
style = None
|
|
112
|
+
|
|
113
|
+
# Make version messages dim regardless of message type
|
|
114
|
+
if isinstance(message.content, str):
|
|
115
|
+
if (
|
|
116
|
+
"Current version:" in message.content
|
|
117
|
+
or "Latest version:" in message.content
|
|
118
|
+
):
|
|
119
|
+
style = "dim"
|
|
120
|
+
|
|
121
|
+
# Render the content
|
|
122
|
+
if isinstance(message.content, str):
|
|
123
|
+
if message.type == MessageType.AGENT_RESPONSE:
|
|
124
|
+
# Render agent responses as markdown
|
|
125
|
+
try:
|
|
126
|
+
markdown = Markdown(message.content)
|
|
127
|
+
self.console.print(markdown)
|
|
128
|
+
except Exception:
|
|
129
|
+
# Fallback to plain text if markdown parsing fails
|
|
130
|
+
safe_content = escape_rich_markup(message.content)
|
|
131
|
+
self.console.print(safe_content)
|
|
132
|
+
elif style:
|
|
133
|
+
# Escape Rich markup to prevent crashes from malformed tags
|
|
134
|
+
safe_content = escape_rich_markup(message.content)
|
|
135
|
+
self.console.print(safe_content, style=style)
|
|
136
|
+
else:
|
|
137
|
+
safe_content = escape_rich_markup(message.content)
|
|
138
|
+
self.console.print(safe_content)
|
|
139
|
+
else:
|
|
140
|
+
# For complex Rich objects (Tables, Markdown, Text, etc.)
|
|
141
|
+
self.console.print(message.content)
|
|
142
|
+
|
|
143
|
+
# Ensure output is immediately flushed to the terminal
|
|
144
|
+
# This fixes the issue where messages don't appear until user input
|
|
145
|
+
if hasattr(self.console.file, "flush"):
|
|
146
|
+
self.console.file.flush()
|
|
147
|
+
|
|
148
|
+
async def _handle_human_input_request(self, message: UIMessage):
|
|
149
|
+
"""Handle a human input request in async mode."""
|
|
150
|
+
# This renderer is not currently used in practice, but if it were:
|
|
151
|
+
# We would need async input handling here
|
|
152
|
+
# For now, just render as a system message
|
|
153
|
+
safe_content = escape_rich_markup(str(message.content))
|
|
154
|
+
self.console.print(f"[bold cyan]INPUT REQUESTED:[/bold cyan] {safe_content}")
|
|
155
|
+
if hasattr(self.console.file, "flush"):
|
|
156
|
+
self.console.file.flush()
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class SynchronousInteractiveRenderer:
|
|
160
|
+
"""
|
|
161
|
+
Synchronous renderer for interactive mode that doesn't require async.
|
|
162
|
+
|
|
163
|
+
This is useful for cases where we want immediate rendering without
|
|
164
|
+
the overhead of async message processing.
|
|
165
|
+
|
|
166
|
+
Note: As part of the messaging system refactoring, we're keeping this class for now
|
|
167
|
+
as it's essential for the interactive mode to function properly. Future refactoring
|
|
168
|
+
could replace this with a simpler implementation that leverages the unified message
|
|
169
|
+
queue system more effectively, or potentially convert interactive mode to use
|
|
170
|
+
async/await consistently and use InteractiveRenderer instead.
|
|
171
|
+
|
|
172
|
+
Current responsibilities:
|
|
173
|
+
- Consumes messages from the queue in a background thread
|
|
174
|
+
- Renders messages to the console in real-time without requiring async code
|
|
175
|
+
- Registers as a direct listener to the message queue for immediate processing
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
def __init__(self, queue: MessageQueue, console: Optional[Console] = None):
|
|
179
|
+
self.queue = queue
|
|
180
|
+
self.console = console or Console()
|
|
181
|
+
self._running = False
|
|
182
|
+
self._thread = None
|
|
183
|
+
|
|
184
|
+
def start(self):
|
|
185
|
+
"""Start the synchronous renderer in a background thread."""
|
|
186
|
+
if self._running:
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
self._running = True
|
|
190
|
+
# Mark the queue as having an active renderer
|
|
191
|
+
self.queue.mark_renderer_active()
|
|
192
|
+
# Add ourselves as a listener for immediate processing
|
|
193
|
+
self.queue.add_listener(self._render_message)
|
|
194
|
+
self._thread = threading.Thread(target=self._consume_messages, daemon=True)
|
|
195
|
+
self._thread.start()
|
|
196
|
+
|
|
197
|
+
def stop(self):
|
|
198
|
+
"""Stop the synchronous renderer."""
|
|
199
|
+
self._running = False
|
|
200
|
+
# Mark the queue as having no active renderer
|
|
201
|
+
self.queue.mark_renderer_inactive()
|
|
202
|
+
# Remove ourselves as a listener
|
|
203
|
+
self.queue.remove_listener(self._render_message)
|
|
204
|
+
if self._thread and self._thread.is_alive():
|
|
205
|
+
self._thread.join(timeout=1.0)
|
|
206
|
+
|
|
207
|
+
def _consume_messages(self):
|
|
208
|
+
"""Consume messages synchronously."""
|
|
209
|
+
while self._running:
|
|
210
|
+
message = self.queue.get_nowait()
|
|
211
|
+
if message:
|
|
212
|
+
self._render_message(message)
|
|
213
|
+
else:
|
|
214
|
+
# No messages, sleep briefly
|
|
215
|
+
import time
|
|
216
|
+
|
|
217
|
+
time.sleep(0.01)
|
|
218
|
+
|
|
219
|
+
def _render_message(self, message: UIMessage):
|
|
220
|
+
"""Render a message using Rich console."""
|
|
221
|
+
# Handle human input requests
|
|
222
|
+
if message.type == MessageType.HUMAN_INPUT_REQUEST:
|
|
223
|
+
self._handle_human_input_request(message)
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
# Convert message type to appropriate Rich styling
|
|
227
|
+
if message.type == MessageType.ERROR:
|
|
228
|
+
style = "bold red"
|
|
229
|
+
elif message.type == MessageType.WARNING:
|
|
230
|
+
style = "yellow"
|
|
231
|
+
elif message.type == MessageType.SUCCESS:
|
|
232
|
+
style = "green"
|
|
233
|
+
elif message.type == MessageType.TOOL_OUTPUT:
|
|
234
|
+
style = "blue"
|
|
235
|
+
elif message.type == MessageType.AGENT_REASONING:
|
|
236
|
+
style = None
|
|
237
|
+
elif message.type == MessageType.AGENT_RESPONSE:
|
|
238
|
+
# Special handling for agent responses - they'll be rendered as markdown
|
|
239
|
+
style = None
|
|
240
|
+
elif message.type == MessageType.SYSTEM:
|
|
241
|
+
style = "dim"
|
|
242
|
+
else:
|
|
243
|
+
style = None
|
|
244
|
+
|
|
245
|
+
# Make version messages dim regardless of message type
|
|
246
|
+
if isinstance(message.content, str):
|
|
247
|
+
if (
|
|
248
|
+
"Current version:" in message.content
|
|
249
|
+
or "Latest version:" in message.content
|
|
250
|
+
):
|
|
251
|
+
style = "dim"
|
|
252
|
+
|
|
253
|
+
# Render the content
|
|
254
|
+
if isinstance(message.content, str):
|
|
255
|
+
if message.type == MessageType.AGENT_RESPONSE:
|
|
256
|
+
# Render agent responses as markdown
|
|
257
|
+
try:
|
|
258
|
+
markdown = Markdown(message.content)
|
|
259
|
+
self.console.print(markdown)
|
|
260
|
+
except Exception:
|
|
261
|
+
# Fallback to plain text if markdown parsing fails
|
|
262
|
+
safe_content = escape_rich_markup(message.content)
|
|
263
|
+
self.console.print(safe_content)
|
|
264
|
+
elif style:
|
|
265
|
+
# Escape Rich markup to prevent crashes from malformed tags
|
|
266
|
+
# in shell output or other user-provided content
|
|
267
|
+
safe_content = escape_rich_markup(message.content)
|
|
268
|
+
self.console.print(safe_content, style=style)
|
|
269
|
+
else:
|
|
270
|
+
safe_content = escape_rich_markup(message.content)
|
|
271
|
+
self.console.print(safe_content)
|
|
272
|
+
else:
|
|
273
|
+
# For complex Rich objects (Tables, Markdown, Text, etc.)
|
|
274
|
+
self.console.print(message.content)
|
|
275
|
+
|
|
276
|
+
# Ensure output is immediately flushed to the terminal
|
|
277
|
+
# This fixes the issue where messages don't appear until user input
|
|
278
|
+
if hasattr(self.console.file, "flush"):
|
|
279
|
+
self.console.file.flush()
|
|
280
|
+
|
|
281
|
+
def _handle_human_input_request(self, message: UIMessage):
|
|
282
|
+
"""Handle a human input request in interactive mode."""
|
|
283
|
+
prompt_id = message.metadata.get("prompt_id") if message.metadata else None
|
|
284
|
+
if not prompt_id:
|
|
285
|
+
self.console.print(
|
|
286
|
+
"[bold red]Error: Invalid human input request[/bold red]"
|
|
287
|
+
)
|
|
288
|
+
return
|
|
289
|
+
|
|
290
|
+
# Display the prompt - escape to prevent markup injection
|
|
291
|
+
safe_content = escape_rich_markup(str(message.content))
|
|
292
|
+
self.console.print(f"[bold cyan]{safe_content}[/bold cyan]")
|
|
293
|
+
if hasattr(self.console.file, "flush"):
|
|
294
|
+
self.console.file.flush()
|
|
295
|
+
|
|
296
|
+
# Get user input
|
|
297
|
+
try:
|
|
298
|
+
# Use basic input for now - could be enhanced with prompt_toolkit later
|
|
299
|
+
response = input(">>> ")
|
|
300
|
+
|
|
301
|
+
# Provide the response back to the queue
|
|
302
|
+
from .message_queue import provide_prompt_response
|
|
303
|
+
|
|
304
|
+
provide_prompt_response(prompt_id, response)
|
|
305
|
+
|
|
306
|
+
except (EOFError, KeyboardInterrupt):
|
|
307
|
+
# Handle Ctrl+C or Ctrl+D
|
|
308
|
+
provide_prompt_response(prompt_id, "")
|
|
309
|
+
except Exception as e:
|
|
310
|
+
self.console.print(f"[bold red]Error getting input: {e}[/bold red]")
|
|
311
|
+
provide_prompt_response(prompt_id, "")
|