code-puppy 0.0.169__py3-none-any.whl → 0.0.366__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 +7 -1
- code_puppy/agents/__init__.py +8 -8
- code_puppy/agents/agent_c_reviewer.py +155 -0
- code_puppy/agents/agent_code_puppy.py +9 -2
- 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 +48 -9
- code_puppy/agents/agent_golang_reviewer.py +151 -0
- code_puppy/agents/agent_javascript_reviewer.py +160 -0
- code_puppy/agents/agent_manager.py +146 -199
- code_puppy/agents/agent_pack_leader.py +383 -0
- code_puppy/agents/agent_planning.py +163 -0
- code_puppy/agents/agent_python_programmer.py +165 -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_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 +1713 -1
- code_puppy/agents/event_stream_handler.py +350 -0
- code_puppy/agents/json_agent.py +12 -1
- code_puppy/agents/pack/__init__.py +34 -0
- code_puppy/agents/pack/bloodhound.py +304 -0
- code_puppy/agents/pack/husky.py +321 -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 +446 -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 +74 -0
- code_puppy/api/routers/sessions.py +232 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +174 -4
- code_puppy/chatgpt_codex_client.py +283 -0
- code_puppy/claude_cache_client.py +586 -0
- code_puppy/cli_runner.py +916 -0
- code_puppy/command_line/add_model_menu.py +1079 -0
- code_puppy/command_line/agent_menu.py +395 -0
- code_puppy/command_line/attachments.py +395 -0
- code_puppy/command_line/autosave_menu.py +605 -0
- code_puppy/command_line/clipboard.py +527 -0
- code_puppy/command_line/colors_menu.py +520 -0
- code_puppy/command_line/command_handler.py +233 -627
- code_puppy/command_line/command_registry.py +150 -0
- code_puppy/command_line/config_commands.py +715 -0
- code_puppy/command_line/core_commands.py +792 -0
- code_puppy/command_line/diff_menu.py +863 -0
- code_puppy/command_line/load_context_completion.py +15 -22
- code_puppy/command_line/mcp/base.py +1 -4
- 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 +9 -4
- code_puppy/command_line/mcp/help_command.py +6 -5
- code_puppy/command_line/mcp/install_command.py +16 -27
- code_puppy/command_line/mcp/install_menu.py +685 -0
- code_puppy/command_line/mcp/list_command.py +3 -3
- code_puppy/command_line/mcp/logs_command.py +174 -65
- code_puppy/command_line/mcp/remove_command.py +2 -2
- code_puppy/command_line/mcp/restart_command.py +12 -4
- code_puppy/command_line/mcp/search_command.py +17 -11
- code_puppy/command_line/mcp/start_all_command.py +22 -13
- code_puppy/command_line/mcp/start_command.py +50 -31
- code_puppy/command_line/mcp/status_command.py +6 -7
- code_puppy/command_line/mcp/stop_all_command.py +11 -8
- code_puppy/command_line/mcp/stop_command.py +11 -10
- code_puppy/command_line/mcp/test_command.py +2 -2
- code_puppy/command_line/mcp/utils.py +1 -1
- code_puppy/command_line/mcp/wizard_utils.py +22 -18
- code_puppy/command_line/mcp_completion.py +174 -0
- code_puppy/command_line/model_picker_completion.py +89 -30
- code_puppy/command_line/model_settings_menu.py +884 -0
- code_puppy/command_line/motd.py +14 -8
- code_puppy/command_line/onboarding_slides.py +179 -0
- code_puppy/command_line/onboarding_wizard.py +340 -0
- code_puppy/command_line/pin_command_completion.py +329 -0
- code_puppy/command_line/prompt_toolkit_completion.py +626 -75
- code_puppy/command_line/session_commands.py +296 -0
- code_puppy/command_line/utils.py +54 -0
- code_puppy/config.py +1181 -51
- code_puppy/error_logging.py +118 -0
- code_puppy/gemini_code_assist.py +385 -0
- code_puppy/gemini_model.py +602 -0
- code_puppy/http_utils.py +220 -104
- code_puppy/keymap.py +128 -0
- code_puppy/main.py +5 -594
- code_puppy/{mcp → mcp_}/__init__.py +17 -0
- code_puppy/{mcp → mcp_}/async_lifecycle.py +35 -4
- code_puppy/{mcp → mcp_}/blocking_startup.py +70 -43
- code_puppy/{mcp → mcp_}/captured_stdio_server.py +2 -2
- code_puppy/{mcp → mcp_}/config_wizard.py +5 -5
- code_puppy/{mcp → mcp_}/dashboard.py +15 -6
- code_puppy/{mcp → mcp_}/examples/retry_example.py +4 -1
- code_puppy/{mcp → mcp_}/managed_server.py +66 -39
- code_puppy/{mcp → mcp_}/manager.py +146 -52
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/{mcp → mcp_}/registry.py +6 -6
- code_puppy/{mcp → mcp_}/server_registry_catalog.py +25 -8
- code_puppy/messaging/__init__.py +199 -2
- code_puppy/messaging/bus.py +610 -0
- code_puppy/messaging/commands.py +167 -0
- code_puppy/messaging/markdown_patches.py +57 -0
- code_puppy/messaging/message_queue.py +17 -48
- code_puppy/messaging/messages.py +500 -0
- code_puppy/messaging/queue_console.py +1 -24
- code_puppy/messaging/renderers.py +43 -146
- code_puppy/messaging/rich_renderer.py +1027 -0
- code_puppy/messaging/spinner/__init__.py +33 -5
- code_puppy/messaging/spinner/console_spinner.py +92 -52
- code_puppy/messaging/spinner/spinner_base.py +29 -0
- code_puppy/messaging/subagent_console.py +461 -0
- code_puppy/model_factory.py +686 -80
- code_puppy/model_utils.py +167 -0
- code_puppy/models.json +86 -104
- code_puppy/models_dev_api.json +1 -0
- code_puppy/models_dev_parser.py +592 -0
- code_puppy/plugins/__init__.py +164 -10
- 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 +704 -0
- code_puppy/plugins/antigravity_oauth/config.py +42 -0
- code_puppy/plugins/antigravity_oauth/constants.py +136 -0
- code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
- code_puppy/plugins/antigravity_oauth/storage.py +271 -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 +767 -0
- code_puppy/plugins/antigravity_oauth/utils.py +169 -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 +328 -0
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
- code_puppy/plugins/chatgpt_oauth/utils.py +489 -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 +6 -0
- code_puppy/plugins/claude_code_oauth/config.py +50 -0
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
- code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
- code_puppy/plugins/claude_code_oauth/utils.py +518 -0
- code_puppy/plugins/customizable_commands/__init__.py +0 -0
- code_puppy/plugins/customizable_commands/register_callbacks.py +169 -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 +523 -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/oauth_puppy_html.py +228 -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/prompts/antigravity_system_prompt.md +1 -0
- code_puppy/prompts/codex_system_prompt.md +310 -0
- code_puppy/pydantic_patches.py +131 -0
- code_puppy/reopenable_async_client.py +8 -8
- code_puppy/round_robin_model.py +10 -15
- code_puppy/session_storage.py +294 -0
- code_puppy/status_display.py +21 -4
- code_puppy/summarization_agent.py +52 -14
- code_puppy/terminal_utils.py +418 -0
- code_puppy/tools/__init__.py +139 -6
- code_puppy/tools/agent_tools.py +548 -49
- 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 +316 -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 +521 -0
- code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
- code_puppy/tools/browser/terminal_tools.py +525 -0
- code_puppy/tools/command_runner.py +941 -153
- code_puppy/tools/common.py +1146 -6
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/file_modifications.py +288 -89
- code_puppy/tools/file_operations.py +352 -266
- code_puppy/tools/subagent_context.py +158 -0
- code_puppy/uvx_detection.py +242 -0
- code_puppy/version_checker.py +30 -11
- code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
- code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
- {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/METADATA +184 -67
- code_puppy-0.0.366.dist-info/RECORD +217 -0
- {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
- {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +1 -0
- code_puppy/agent.py +0 -231
- code_puppy/agents/agent_orchestrator.json +0 -26
- code_puppy/agents/runtime_manager.py +0 -272
- code_puppy/command_line/mcp/add_command.py +0 -183
- code_puppy/command_line/meta_command_handler.py +0 -153
- code_puppy/message_history_processor.py +0 -490
- code_puppy/messaging/spinner/textual_spinner.py +0 -101
- code_puppy/state_management.py +0 -200
- code_puppy/tui/__init__.py +0 -10
- code_puppy/tui/app.py +0 -986
- code_puppy/tui/components/__init__.py +0 -21
- code_puppy/tui/components/chat_view.py +0 -550
- code_puppy/tui/components/command_history_modal.py +0 -218
- code_puppy/tui/components/copy_button.py +0 -139
- code_puppy/tui/components/custom_widgets.py +0 -63
- code_puppy/tui/components/human_input_modal.py +0 -175
- code_puppy/tui/components/input_area.py +0 -167
- code_puppy/tui/components/sidebar.py +0 -309
- code_puppy/tui/components/status_bar.py +0 -182
- code_puppy/tui/messages.py +0 -27
- code_puppy/tui/models/__init__.py +0 -8
- code_puppy/tui/models/chat_message.py +0 -25
- code_puppy/tui/models/command_history.py +0 -89
- code_puppy/tui/models/enums.py +0 -24
- code_puppy/tui/screens/__init__.py +0 -15
- code_puppy/tui/screens/help.py +0 -130
- code_puppy/tui/screens/mcp_install_wizard.py +0 -803
- code_puppy/tui/screens/settings.py +0 -290
- code_puppy/tui/screens/tools.py +0 -74
- code_puppy-0.0.169.data/data/code_puppy/models.json +0 -128
- code_puppy-0.0.169.dist-info/RECORD +0 -112
- /code_puppy/{mcp → mcp_}/circuit_breaker.py +0 -0
- /code_puppy/{mcp → mcp_}/error_isolation.py +0 -0
- /code_puppy/{mcp → mcp_}/health_monitor.py +0 -0
- /code_puppy/{mcp → mcp_}/retry_manager.py +0 -0
- /code_puppy/{mcp → mcp_}/status_tracker.py +0 -0
- /code_puppy/{mcp → mcp_}/system_tools.py +0 -0
- {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
"""MessageBus - Central coordinator for bidirectional Agent <-> UI communication.
|
|
2
|
+
|
|
3
|
+
The MessageBus manages two queues:
|
|
4
|
+
- outgoing: Messages flow from Agent → UI (AnyMessage)
|
|
5
|
+
- incoming: Commands flow from UI → Agent (AnyCommand)
|
|
6
|
+
|
|
7
|
+
It also handles request/response correlation for user interactions:
|
|
8
|
+
1. Agent calls request_input() which emits a UserInputRequest and waits
|
|
9
|
+
2. UI receives the request and displays a prompt
|
|
10
|
+
3. User provides input, UI calls provide_response() with UserInputResponse
|
|
11
|
+
4. MessageBus matches the response to the waiting request via prompt_id
|
|
12
|
+
5. Agent's request_input() returns with the user's value
|
|
13
|
+
|
|
14
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
15
|
+
│ MessageBus │
|
|
16
|
+
│ ┌─────────────┐ ┌─────────────┐ │
|
|
17
|
+
│ │ outgoing │ Messages (Agent→UI) │ incoming │ │
|
|
18
|
+
│ │ Queue │ ───────────────────> │ Queue │ │
|
|
19
|
+
│ │ [AnyMessage]│ │ [AnyCommand]│ │
|
|
20
|
+
│ └─────────────┘ └─────────────┘ │
|
|
21
|
+
│ ↑ │ │
|
|
22
|
+
│ │ ↓ │
|
|
23
|
+
│ emit() provide_response() │
|
|
24
|
+
│ emit_text() │
|
|
25
|
+
│ request_input() ─────────────────────────────────────────│
|
|
26
|
+
│ ↑ (waits for matching response) │
|
|
27
|
+
│ │ │
|
|
28
|
+
│ ┌──────┴──────┐ │
|
|
29
|
+
│ │ pending │ prompt_id → Future │
|
|
30
|
+
│ │ requests │ │
|
|
31
|
+
│ └─────────────┘ │
|
|
32
|
+
└─────────────────────────────────────────────────────────────┘
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
import asyncio
|
|
36
|
+
import queue
|
|
37
|
+
import threading
|
|
38
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
39
|
+
from uuid import uuid4
|
|
40
|
+
|
|
41
|
+
from .commands import (
|
|
42
|
+
AnyCommand,
|
|
43
|
+
ConfirmationResponse,
|
|
44
|
+
SelectionResponse,
|
|
45
|
+
UserInputResponse,
|
|
46
|
+
)
|
|
47
|
+
from .messages import (
|
|
48
|
+
AnyMessage,
|
|
49
|
+
ConfirmationRequest,
|
|
50
|
+
MessageCategory,
|
|
51
|
+
MessageLevel,
|
|
52
|
+
SelectionRequest,
|
|
53
|
+
TextMessage,
|
|
54
|
+
UserInputRequest,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class MessageBus:
|
|
59
|
+
"""Central coordinator for bidirectional Agent <-> UI communication.
|
|
60
|
+
|
|
61
|
+
Thread-safe message bus that works in both sync and async contexts.
|
|
62
|
+
Uses stdlib queue.Queue for thread-safe sync operation.
|
|
63
|
+
Manages outgoing messages, incoming commands, and request/response correlation.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(self, maxsize: int = 1000) -> None:
|
|
67
|
+
"""Initialize the MessageBus.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
maxsize: Maximum queue size before blocking/dropping.
|
|
71
|
+
"""
|
|
72
|
+
self._maxsize = maxsize
|
|
73
|
+
self._lock = threading.Lock()
|
|
74
|
+
|
|
75
|
+
# Use sync queues by default (works in any context)
|
|
76
|
+
self._outgoing: queue.Queue[AnyMessage] = queue.Queue(maxsize=maxsize)
|
|
77
|
+
self._incoming: queue.Queue[AnyCommand] = queue.Queue(maxsize=maxsize)
|
|
78
|
+
|
|
79
|
+
# Event loop reference for async request/response (optional)
|
|
80
|
+
self._event_loop: Optional[asyncio.AbstractEventLoop] = None
|
|
81
|
+
|
|
82
|
+
# Startup buffering
|
|
83
|
+
self._startup_buffer: List[AnyMessage] = []
|
|
84
|
+
self._has_active_renderer = False
|
|
85
|
+
|
|
86
|
+
# Request/Response correlation: prompt_id → Future (for async usage)
|
|
87
|
+
self._pending_requests: Dict[str, asyncio.Future[Any]] = {}
|
|
88
|
+
|
|
89
|
+
# Session context for multi-agent tracking
|
|
90
|
+
self._current_session_id: Optional[str] = None
|
|
91
|
+
|
|
92
|
+
# =========================================================================
|
|
93
|
+
# Outgoing Messages (Agent → UI)
|
|
94
|
+
# =========================================================================
|
|
95
|
+
|
|
96
|
+
def emit(self, message: AnyMessage) -> None:
|
|
97
|
+
"""Emit a message to the UI.
|
|
98
|
+
|
|
99
|
+
Thread-safe. Can be called from sync or async context.
|
|
100
|
+
If no renderer is active, messages are buffered for later.
|
|
101
|
+
Auto-tags message with current session_id if not already set.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
message: The message to emit.
|
|
105
|
+
"""
|
|
106
|
+
# Auto-tag message with current session if not already set
|
|
107
|
+
with self._lock:
|
|
108
|
+
if message.session_id is None and self._current_session_id is not None:
|
|
109
|
+
message.session_id = self._current_session_id
|
|
110
|
+
|
|
111
|
+
if not self._has_active_renderer:
|
|
112
|
+
self._startup_buffer.append(message)
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
# Direct put into thread-safe queue
|
|
116
|
+
try:
|
|
117
|
+
self._outgoing.put_nowait(message)
|
|
118
|
+
except queue.Full:
|
|
119
|
+
# Drop oldest and retry
|
|
120
|
+
try:
|
|
121
|
+
self._outgoing.get_nowait()
|
|
122
|
+
self._outgoing.put_nowait(message)
|
|
123
|
+
except queue.Empty:
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
def emit_text(
|
|
127
|
+
self,
|
|
128
|
+
level: MessageLevel,
|
|
129
|
+
text: str,
|
|
130
|
+
category: MessageCategory = MessageCategory.SYSTEM,
|
|
131
|
+
) -> None:
|
|
132
|
+
"""Emit a text message with the specified level.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
level: Severity level (DEBUG, INFO, WARNING, ERROR, SUCCESS).
|
|
136
|
+
text: Plain text content (no Rich markup!).
|
|
137
|
+
category: Message category for routing.
|
|
138
|
+
"""
|
|
139
|
+
message = TextMessage(level=level, text=text, category=category)
|
|
140
|
+
self.emit(message)
|
|
141
|
+
|
|
142
|
+
def emit_info(self, text: str) -> None:
|
|
143
|
+
"""Emit an INFO level text message."""
|
|
144
|
+
self.emit_text(MessageLevel.INFO, text)
|
|
145
|
+
|
|
146
|
+
def emit_warning(self, text: str) -> None:
|
|
147
|
+
"""Emit a WARNING level text message."""
|
|
148
|
+
self.emit_text(MessageLevel.WARNING, text)
|
|
149
|
+
|
|
150
|
+
def emit_error(self, text: str) -> None:
|
|
151
|
+
"""Emit an ERROR level text message."""
|
|
152
|
+
self.emit_text(MessageLevel.ERROR, text)
|
|
153
|
+
|
|
154
|
+
def emit_success(self, text: str) -> None:
|
|
155
|
+
"""Emit a SUCCESS level text message."""
|
|
156
|
+
self.emit_text(MessageLevel.SUCCESS, text)
|
|
157
|
+
|
|
158
|
+
def emit_debug(self, text: str) -> None:
|
|
159
|
+
"""Emit a DEBUG level text message."""
|
|
160
|
+
self.emit_text(MessageLevel.DEBUG, text)
|
|
161
|
+
|
|
162
|
+
def emit_shell_line(self, line: str, stream: str = "stdout") -> None:
|
|
163
|
+
"""Emit a shell output line with ANSI preservation.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
line: The output line (may contain ANSI codes).
|
|
167
|
+
stream: Which stream this came from ("stdout" or "stderr").
|
|
168
|
+
"""
|
|
169
|
+
from .messages import ShellLineMessage
|
|
170
|
+
|
|
171
|
+
message = ShellLineMessage(line=line, stream=stream) # type: ignore[arg-type]
|
|
172
|
+
self.emit(message)
|
|
173
|
+
|
|
174
|
+
# =========================================================================
|
|
175
|
+
# Session Context (Multi-Agent Tracking)
|
|
176
|
+
# =========================================================================
|
|
177
|
+
|
|
178
|
+
def set_session_context(self, session_id: Optional[str]) -> None:
|
|
179
|
+
"""Set the current session context for auto-tagging messages.
|
|
180
|
+
|
|
181
|
+
When set, all messages emitted via emit() will be automatically tagged
|
|
182
|
+
with this session_id unless they already have one set.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
session_id: The session ID to tag messages with, or None to clear.
|
|
186
|
+
"""
|
|
187
|
+
with self._lock:
|
|
188
|
+
self._current_session_id = session_id
|
|
189
|
+
|
|
190
|
+
def get_session_context(self) -> Optional[str]:
|
|
191
|
+
"""Get the current session context.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
The current session_id, or None if not set.
|
|
195
|
+
"""
|
|
196
|
+
with self._lock:
|
|
197
|
+
return self._current_session_id
|
|
198
|
+
|
|
199
|
+
# =========================================================================
|
|
200
|
+
# User Input Requests (Agent waits for UI response)
|
|
201
|
+
# =========================================================================
|
|
202
|
+
|
|
203
|
+
async def request_input(
|
|
204
|
+
self,
|
|
205
|
+
prompt_text: str,
|
|
206
|
+
default: Optional[str] = None,
|
|
207
|
+
input_type: str = "text",
|
|
208
|
+
) -> str:
|
|
209
|
+
"""Request text input from the user.
|
|
210
|
+
|
|
211
|
+
Emits a UserInputRequest and blocks until the UI provides a response.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
prompt_text: The prompt to display to the user.
|
|
215
|
+
default: Default value if user provides empty input.
|
|
216
|
+
input_type: "text" or "password".
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
The user's input string.
|
|
220
|
+
"""
|
|
221
|
+
prompt_id = str(uuid4())
|
|
222
|
+
|
|
223
|
+
# Create a Future to wait on
|
|
224
|
+
loop = asyncio.get_running_loop()
|
|
225
|
+
future: asyncio.Future[str] = loop.create_future()
|
|
226
|
+
|
|
227
|
+
with self._lock:
|
|
228
|
+
self._pending_requests[prompt_id] = future
|
|
229
|
+
|
|
230
|
+
# Emit the request
|
|
231
|
+
request = UserInputRequest(
|
|
232
|
+
prompt_id=prompt_id,
|
|
233
|
+
prompt_text=prompt_text,
|
|
234
|
+
default_value=default,
|
|
235
|
+
input_type=input_type, # type: ignore[arg-type]
|
|
236
|
+
)
|
|
237
|
+
self.emit(request)
|
|
238
|
+
|
|
239
|
+
try:
|
|
240
|
+
# Wait for response
|
|
241
|
+
result = await future
|
|
242
|
+
return result if result else (default or "")
|
|
243
|
+
finally:
|
|
244
|
+
# Clean up
|
|
245
|
+
with self._lock:
|
|
246
|
+
self._pending_requests.pop(prompt_id, None)
|
|
247
|
+
|
|
248
|
+
async def request_confirmation(
|
|
249
|
+
self,
|
|
250
|
+
title: str,
|
|
251
|
+
description: str,
|
|
252
|
+
options: Optional[List[str]] = None,
|
|
253
|
+
allow_feedback: bool = False,
|
|
254
|
+
) -> Tuple[bool, Optional[str]]:
|
|
255
|
+
"""Request confirmation from the user.
|
|
256
|
+
|
|
257
|
+
Emits a ConfirmationRequest and blocks until the UI provides a response.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
title: Title/headline for the confirmation.
|
|
261
|
+
description: Detailed description of what's being confirmed.
|
|
262
|
+
options: Options to choose from (default: ["Yes", "No"]).
|
|
263
|
+
allow_feedback: Whether to allow free-form feedback.
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
Tuple of (confirmed: bool, feedback: Optional[str]).
|
|
267
|
+
"""
|
|
268
|
+
prompt_id = str(uuid4())
|
|
269
|
+
|
|
270
|
+
loop = asyncio.get_running_loop()
|
|
271
|
+
future: asyncio.Future[Tuple[bool, Optional[str]]] = loop.create_future()
|
|
272
|
+
|
|
273
|
+
with self._lock:
|
|
274
|
+
self._pending_requests[prompt_id] = future
|
|
275
|
+
|
|
276
|
+
request = ConfirmationRequest(
|
|
277
|
+
prompt_id=prompt_id,
|
|
278
|
+
title=title,
|
|
279
|
+
description=description,
|
|
280
|
+
options=options or ["Yes", "No"],
|
|
281
|
+
allow_feedback=allow_feedback,
|
|
282
|
+
)
|
|
283
|
+
self.emit(request)
|
|
284
|
+
|
|
285
|
+
try:
|
|
286
|
+
return await future
|
|
287
|
+
finally:
|
|
288
|
+
with self._lock:
|
|
289
|
+
self._pending_requests.pop(prompt_id, None)
|
|
290
|
+
|
|
291
|
+
async def request_selection(
|
|
292
|
+
self,
|
|
293
|
+
prompt_text: str,
|
|
294
|
+
options: List[str],
|
|
295
|
+
allow_cancel: bool = True,
|
|
296
|
+
) -> Tuple[int, str]:
|
|
297
|
+
"""Request the user to select from a list of options.
|
|
298
|
+
|
|
299
|
+
Emits a SelectionRequest and blocks until the UI provides a response.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
prompt_text: The prompt to display.
|
|
303
|
+
options: List of options to choose from.
|
|
304
|
+
allow_cancel: Whether the user can cancel without selecting.
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
Tuple of (selected_index: int, selected_value: str).
|
|
308
|
+
Returns (-1, "") if cancelled.
|
|
309
|
+
"""
|
|
310
|
+
prompt_id = str(uuid4())
|
|
311
|
+
|
|
312
|
+
loop = asyncio.get_running_loop()
|
|
313
|
+
future: asyncio.Future[Tuple[int, str]] = loop.create_future()
|
|
314
|
+
|
|
315
|
+
with self._lock:
|
|
316
|
+
self._pending_requests[prompt_id] = future
|
|
317
|
+
|
|
318
|
+
request = SelectionRequest(
|
|
319
|
+
prompt_id=prompt_id,
|
|
320
|
+
prompt_text=prompt_text,
|
|
321
|
+
options=options,
|
|
322
|
+
allow_cancel=allow_cancel,
|
|
323
|
+
)
|
|
324
|
+
self.emit(request)
|
|
325
|
+
|
|
326
|
+
try:
|
|
327
|
+
return await future
|
|
328
|
+
finally:
|
|
329
|
+
with self._lock:
|
|
330
|
+
self._pending_requests.pop(prompt_id, None)
|
|
331
|
+
|
|
332
|
+
# =========================================================================
|
|
333
|
+
# Incoming Commands (UI → Agent)
|
|
334
|
+
# =========================================================================
|
|
335
|
+
|
|
336
|
+
def provide_response(self, command: AnyCommand) -> None:
|
|
337
|
+
"""Provide a response to a pending request.
|
|
338
|
+
|
|
339
|
+
Called by the UI when the user provides input, confirmation, or selection.
|
|
340
|
+
Matches the response to the waiting request via prompt_id.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
command: The response command (UserInputResponse, etc.).
|
|
344
|
+
"""
|
|
345
|
+
# Handle user interaction responses
|
|
346
|
+
if isinstance(command, UserInputResponse):
|
|
347
|
+
self._complete_request(command.prompt_id, command.value)
|
|
348
|
+
elif isinstance(command, ConfirmationResponse):
|
|
349
|
+
self._complete_request(
|
|
350
|
+
command.prompt_id, (command.confirmed, command.feedback)
|
|
351
|
+
)
|
|
352
|
+
elif isinstance(command, SelectionResponse):
|
|
353
|
+
self._complete_request(
|
|
354
|
+
command.prompt_id, (command.selected_index, command.selected_value)
|
|
355
|
+
)
|
|
356
|
+
else:
|
|
357
|
+
# For non-response commands (CancelAgentCommand, etc.),
|
|
358
|
+
# put them in the incoming queue for the agent to process
|
|
359
|
+
try:
|
|
360
|
+
self._incoming.put_nowait(command)
|
|
361
|
+
except queue.Full:
|
|
362
|
+
# Drop oldest and retry
|
|
363
|
+
try:
|
|
364
|
+
self._incoming.get_nowait()
|
|
365
|
+
self._incoming.put_nowait(command)
|
|
366
|
+
except queue.Empty:
|
|
367
|
+
pass
|
|
368
|
+
|
|
369
|
+
def _complete_request(self, prompt_id: str, result: object) -> None:
|
|
370
|
+
"""Complete a pending request with the given result."""
|
|
371
|
+
with self._lock:
|
|
372
|
+
future = self._pending_requests.get(prompt_id)
|
|
373
|
+
|
|
374
|
+
if future is not None and not future.done():
|
|
375
|
+
# Must set result from the event loop thread if we have one
|
|
376
|
+
if self._event_loop is not None:
|
|
377
|
+
try:
|
|
378
|
+
self._event_loop.call_soon_threadsafe(
|
|
379
|
+
self._set_future_result, future, result
|
|
380
|
+
)
|
|
381
|
+
except RuntimeError:
|
|
382
|
+
# Event loop closed - try direct set
|
|
383
|
+
self._set_future_result(future, result)
|
|
384
|
+
else:
|
|
385
|
+
# No event loop - try direct set
|
|
386
|
+
self._set_future_result(future, result)
|
|
387
|
+
|
|
388
|
+
def _set_future_result(self, future: asyncio.Future[Any], result: object) -> None:
|
|
389
|
+
"""Set a future's result if not already done."""
|
|
390
|
+
if not future.done():
|
|
391
|
+
future.set_result(result)
|
|
392
|
+
|
|
393
|
+
# =========================================================================
|
|
394
|
+
# Queue Access (for renderers/consumers)
|
|
395
|
+
# =========================================================================
|
|
396
|
+
|
|
397
|
+
async def get_message(self) -> AnyMessage:
|
|
398
|
+
"""Get the next outgoing message (async).
|
|
399
|
+
|
|
400
|
+
Called by the renderer to consume messages.
|
|
401
|
+
Blocks until a message is available.
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
The next message to display.
|
|
405
|
+
"""
|
|
406
|
+
# For async usage, wrap sync queue in asyncio-friendly way
|
|
407
|
+
while True:
|
|
408
|
+
try:
|
|
409
|
+
return self._outgoing.get_nowait()
|
|
410
|
+
except queue.Empty:
|
|
411
|
+
await asyncio.sleep(0.01)
|
|
412
|
+
|
|
413
|
+
def get_message_nowait(self) -> Optional[AnyMessage]:
|
|
414
|
+
"""Get the next outgoing message without blocking.
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
The next message, or None if queue is empty.
|
|
418
|
+
"""
|
|
419
|
+
try:
|
|
420
|
+
return self._outgoing.get_nowait()
|
|
421
|
+
except queue.Empty:
|
|
422
|
+
return None
|
|
423
|
+
|
|
424
|
+
async def get_command(self) -> AnyCommand:
|
|
425
|
+
"""Get the next incoming command (async).
|
|
426
|
+
|
|
427
|
+
Called by the agent to consume commands (e.g., CancelAgentCommand).
|
|
428
|
+
Blocks until a command is available.
|
|
429
|
+
|
|
430
|
+
Returns:
|
|
431
|
+
The next command to process.
|
|
432
|
+
"""
|
|
433
|
+
# For async usage, wrap sync queue in asyncio-friendly way
|
|
434
|
+
while True:
|
|
435
|
+
try:
|
|
436
|
+
return self._incoming.get_nowait()
|
|
437
|
+
except queue.Empty:
|
|
438
|
+
await asyncio.sleep(0.01)
|
|
439
|
+
|
|
440
|
+
# =========================================================================
|
|
441
|
+
# Startup Buffering
|
|
442
|
+
# =========================================================================
|
|
443
|
+
|
|
444
|
+
def get_buffered_messages(self) -> List[AnyMessage]:
|
|
445
|
+
"""Get all messages buffered before renderer attached.
|
|
446
|
+
|
|
447
|
+
Returns a copy of the buffer. Call clear_buffer() after processing.
|
|
448
|
+
|
|
449
|
+
Returns:
|
|
450
|
+
List of buffered messages.
|
|
451
|
+
"""
|
|
452
|
+
with self._lock:
|
|
453
|
+
return list(self._startup_buffer)
|
|
454
|
+
|
|
455
|
+
def clear_buffer(self) -> None:
|
|
456
|
+
"""Clear the startup buffer after processing."""
|
|
457
|
+
with self._lock:
|
|
458
|
+
self._startup_buffer.clear()
|
|
459
|
+
|
|
460
|
+
def mark_renderer_active(self) -> None:
|
|
461
|
+
"""Mark that a renderer is now active and consuming messages.
|
|
462
|
+
|
|
463
|
+
Call this when a renderer attaches. Messages will no longer be
|
|
464
|
+
buffered and will go directly to the outgoing queue.
|
|
465
|
+
"""
|
|
466
|
+
with self._lock:
|
|
467
|
+
self._has_active_renderer = True
|
|
468
|
+
|
|
469
|
+
def mark_renderer_inactive(self) -> None:
|
|
470
|
+
"""Mark that no renderer is currently active.
|
|
471
|
+
|
|
472
|
+
Messages will be buffered until a renderer attaches again.
|
|
473
|
+
"""
|
|
474
|
+
with self._lock:
|
|
475
|
+
self._has_active_renderer = False
|
|
476
|
+
|
|
477
|
+
@property
|
|
478
|
+
def has_active_renderer(self) -> bool:
|
|
479
|
+
"""Check if a renderer is currently active."""
|
|
480
|
+
with self._lock:
|
|
481
|
+
return self._has_active_renderer
|
|
482
|
+
|
|
483
|
+
# =========================================================================
|
|
484
|
+
# Queue Status
|
|
485
|
+
# =========================================================================
|
|
486
|
+
|
|
487
|
+
@property
|
|
488
|
+
def outgoing_qsize(self) -> int:
|
|
489
|
+
"""Number of messages waiting in the outgoing queue."""
|
|
490
|
+
return self._outgoing.qsize()
|
|
491
|
+
|
|
492
|
+
@property
|
|
493
|
+
def incoming_qsize(self) -> int:
|
|
494
|
+
"""Number of commands waiting in the incoming queue."""
|
|
495
|
+
return self._incoming.qsize()
|
|
496
|
+
|
|
497
|
+
@property
|
|
498
|
+
def pending_requests_count(self) -> int:
|
|
499
|
+
"""Number of requests waiting for responses."""
|
|
500
|
+
with self._lock:
|
|
501
|
+
return len(self._pending_requests)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
# =============================================================================
|
|
505
|
+
# Global Singleton
|
|
506
|
+
# =============================================================================
|
|
507
|
+
|
|
508
|
+
_global_bus: Optional[MessageBus] = None
|
|
509
|
+
_bus_lock = threading.Lock()
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def get_message_bus() -> MessageBus:
|
|
513
|
+
"""Get or create the global MessageBus singleton.
|
|
514
|
+
|
|
515
|
+
Thread-safe. Creates the bus on first call.
|
|
516
|
+
|
|
517
|
+
Returns:
|
|
518
|
+
The global MessageBus instance.
|
|
519
|
+
"""
|
|
520
|
+
global _global_bus
|
|
521
|
+
|
|
522
|
+
with _bus_lock:
|
|
523
|
+
if _global_bus is None:
|
|
524
|
+
_global_bus = MessageBus()
|
|
525
|
+
return _global_bus
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def reset_message_bus() -> None:
|
|
529
|
+
"""Reset the global MessageBus (for testing).
|
|
530
|
+
|
|
531
|
+
Warning: This will lose any pending messages/requests!
|
|
532
|
+
"""
|
|
533
|
+
global _global_bus
|
|
534
|
+
|
|
535
|
+
with _bus_lock:
|
|
536
|
+
_global_bus = None
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
# =============================================================================
|
|
540
|
+
# Convenience Functions
|
|
541
|
+
# =============================================================================
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def emit(message: AnyMessage) -> None:
|
|
545
|
+
"""Emit a message via the global bus."""
|
|
546
|
+
get_message_bus().emit(message)
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def emit_info(text: str) -> None:
|
|
550
|
+
"""Emit an INFO message via the global bus."""
|
|
551
|
+
get_message_bus().emit_info(text)
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def emit_warning(text: str) -> None:
|
|
555
|
+
"""Emit a WARNING message via the global bus."""
|
|
556
|
+
get_message_bus().emit_warning(text)
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def emit_error(text: str) -> None:
|
|
560
|
+
"""Emit an ERROR message via the global bus."""
|
|
561
|
+
get_message_bus().emit_error(text)
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def emit_success(text: str) -> None:
|
|
565
|
+
"""Emit a SUCCESS message via the global bus."""
|
|
566
|
+
get_message_bus().emit_success(text)
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def emit_debug(text: str) -> None:
|
|
570
|
+
"""Emit a DEBUG message via the global bus."""
|
|
571
|
+
get_message_bus().emit_debug(text)
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def emit_shell_line(line: str, stream: str = "stdout") -> None:
|
|
575
|
+
"""Emit a shell output line with ANSI preservation."""
|
|
576
|
+
get_message_bus().emit_shell_line(line, stream)
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def set_session_context(session_id: Optional[str]) -> None:
|
|
580
|
+
"""Set the session context on the global bus."""
|
|
581
|
+
get_message_bus().set_session_context(session_id)
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def get_session_context() -> Optional[str]:
|
|
585
|
+
"""Get the session context from the global bus."""
|
|
586
|
+
return get_message_bus().get_session_context()
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
# =============================================================================
|
|
590
|
+
# Export all public symbols
|
|
591
|
+
# =============================================================================
|
|
592
|
+
|
|
593
|
+
__all__ = [
|
|
594
|
+
# Main class
|
|
595
|
+
"MessageBus",
|
|
596
|
+
# Singleton access
|
|
597
|
+
"get_message_bus",
|
|
598
|
+
"reset_message_bus",
|
|
599
|
+
# Convenience functions
|
|
600
|
+
"emit",
|
|
601
|
+
"emit_info",
|
|
602
|
+
"emit_warning",
|
|
603
|
+
"emit_error",
|
|
604
|
+
"emit_success",
|
|
605
|
+
"emit_debug",
|
|
606
|
+
"emit_shell_line",
|
|
607
|
+
# Session context
|
|
608
|
+
"set_session_context",
|
|
609
|
+
"get_session_context",
|
|
610
|
+
]
|