code-puppy 0.0.214__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.
Files changed (231) hide show
  1. code_puppy/__init__.py +7 -1
  2. code_puppy/agents/__init__.py +2 -0
  3. code_puppy/agents/agent_c_reviewer.py +59 -6
  4. code_puppy/agents/agent_code_puppy.py +7 -1
  5. code_puppy/agents/agent_code_reviewer.py +12 -2
  6. code_puppy/agents/agent_cpp_reviewer.py +73 -6
  7. code_puppy/agents/agent_creator_agent.py +45 -4
  8. code_puppy/agents/agent_golang_reviewer.py +92 -3
  9. code_puppy/agents/agent_javascript_reviewer.py +101 -8
  10. code_puppy/agents/agent_manager.py +81 -4
  11. code_puppy/agents/agent_pack_leader.py +383 -0
  12. code_puppy/agents/agent_planning.py +163 -0
  13. code_puppy/agents/agent_python_programmer.py +165 -0
  14. code_puppy/agents/agent_python_reviewer.py +28 -6
  15. code_puppy/agents/agent_qa_expert.py +98 -6
  16. code_puppy/agents/agent_qa_kitten.py +12 -7
  17. code_puppy/agents/agent_security_auditor.py +113 -3
  18. code_puppy/agents/agent_terminal_qa.py +323 -0
  19. code_puppy/agents/agent_typescript_reviewer.py +106 -7
  20. code_puppy/agents/base_agent.py +802 -176
  21. code_puppy/agents/event_stream_handler.py +350 -0
  22. code_puppy/agents/pack/__init__.py +34 -0
  23. code_puppy/agents/pack/bloodhound.py +304 -0
  24. code_puppy/agents/pack/husky.py +321 -0
  25. code_puppy/agents/pack/retriever.py +393 -0
  26. code_puppy/agents/pack/shepherd.py +348 -0
  27. code_puppy/agents/pack/terrier.py +287 -0
  28. code_puppy/agents/pack/watchdog.py +367 -0
  29. code_puppy/agents/prompt_reviewer.py +145 -0
  30. code_puppy/agents/subagent_stream_handler.py +276 -0
  31. code_puppy/api/__init__.py +13 -0
  32. code_puppy/api/app.py +169 -0
  33. code_puppy/api/main.py +21 -0
  34. code_puppy/api/pty_manager.py +446 -0
  35. code_puppy/api/routers/__init__.py +12 -0
  36. code_puppy/api/routers/agents.py +36 -0
  37. code_puppy/api/routers/commands.py +217 -0
  38. code_puppy/api/routers/config.py +74 -0
  39. code_puppy/api/routers/sessions.py +232 -0
  40. code_puppy/api/templates/terminal.html +361 -0
  41. code_puppy/api/websocket.py +154 -0
  42. code_puppy/callbacks.py +142 -4
  43. code_puppy/chatgpt_codex_client.py +283 -0
  44. code_puppy/claude_cache_client.py +586 -0
  45. code_puppy/cli_runner.py +916 -0
  46. code_puppy/command_line/add_model_menu.py +1079 -0
  47. code_puppy/command_line/agent_menu.py +395 -0
  48. code_puppy/command_line/attachments.py +10 -5
  49. code_puppy/command_line/autosave_menu.py +605 -0
  50. code_puppy/command_line/clipboard.py +527 -0
  51. code_puppy/command_line/colors_menu.py +520 -0
  52. code_puppy/command_line/command_handler.py +176 -738
  53. code_puppy/command_line/command_registry.py +150 -0
  54. code_puppy/command_line/config_commands.py +715 -0
  55. code_puppy/command_line/core_commands.py +792 -0
  56. code_puppy/command_line/diff_menu.py +863 -0
  57. code_puppy/command_line/load_context_completion.py +15 -22
  58. code_puppy/command_line/mcp/base.py +0 -3
  59. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  60. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  61. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  62. code_puppy/command_line/mcp/edit_command.py +148 -0
  63. code_puppy/command_line/mcp/handler.py +9 -4
  64. code_puppy/command_line/mcp/help_command.py +6 -5
  65. code_puppy/command_line/mcp/install_command.py +15 -26
  66. code_puppy/command_line/mcp/install_menu.py +685 -0
  67. code_puppy/command_line/mcp/list_command.py +2 -2
  68. code_puppy/command_line/mcp/logs_command.py +174 -65
  69. code_puppy/command_line/mcp/remove_command.py +2 -2
  70. code_puppy/command_line/mcp/restart_command.py +12 -4
  71. code_puppy/command_line/mcp/search_command.py +16 -10
  72. code_puppy/command_line/mcp/start_all_command.py +18 -6
  73. code_puppy/command_line/mcp/start_command.py +47 -25
  74. code_puppy/command_line/mcp/status_command.py +4 -5
  75. code_puppy/command_line/mcp/stop_all_command.py +7 -1
  76. code_puppy/command_line/mcp/stop_command.py +8 -4
  77. code_puppy/command_line/mcp/test_command.py +2 -2
  78. code_puppy/command_line/mcp/wizard_utils.py +20 -16
  79. code_puppy/command_line/mcp_completion.py +174 -0
  80. code_puppy/command_line/model_picker_completion.py +75 -25
  81. code_puppy/command_line/model_settings_menu.py +884 -0
  82. code_puppy/command_line/motd.py +14 -8
  83. code_puppy/command_line/onboarding_slides.py +179 -0
  84. code_puppy/command_line/onboarding_wizard.py +340 -0
  85. code_puppy/command_line/pin_command_completion.py +329 -0
  86. code_puppy/command_line/prompt_toolkit_completion.py +463 -63
  87. code_puppy/command_line/session_commands.py +296 -0
  88. code_puppy/command_line/utils.py +54 -0
  89. code_puppy/config.py +898 -112
  90. code_puppy/error_logging.py +118 -0
  91. code_puppy/gemini_code_assist.py +385 -0
  92. code_puppy/gemini_model.py +602 -0
  93. code_puppy/http_utils.py +210 -148
  94. code_puppy/keymap.py +128 -0
  95. code_puppy/main.py +5 -698
  96. code_puppy/mcp_/__init__.py +17 -0
  97. code_puppy/mcp_/async_lifecycle.py +35 -4
  98. code_puppy/mcp_/blocking_startup.py +70 -43
  99. code_puppy/mcp_/captured_stdio_server.py +2 -2
  100. code_puppy/mcp_/config_wizard.py +4 -4
  101. code_puppy/mcp_/dashboard.py +15 -6
  102. code_puppy/mcp_/managed_server.py +65 -38
  103. code_puppy/mcp_/manager.py +146 -52
  104. code_puppy/mcp_/mcp_logs.py +224 -0
  105. code_puppy/mcp_/registry.py +6 -6
  106. code_puppy/mcp_/server_registry_catalog.py +24 -5
  107. code_puppy/messaging/__init__.py +199 -2
  108. code_puppy/messaging/bus.py +610 -0
  109. code_puppy/messaging/commands.py +167 -0
  110. code_puppy/messaging/markdown_patches.py +57 -0
  111. code_puppy/messaging/message_queue.py +17 -48
  112. code_puppy/messaging/messages.py +500 -0
  113. code_puppy/messaging/queue_console.py +1 -24
  114. code_puppy/messaging/renderers.py +43 -146
  115. code_puppy/messaging/rich_renderer.py +1027 -0
  116. code_puppy/messaging/spinner/__init__.py +21 -5
  117. code_puppy/messaging/spinner/console_spinner.py +86 -51
  118. code_puppy/messaging/subagent_console.py +461 -0
  119. code_puppy/model_factory.py +634 -83
  120. code_puppy/model_utils.py +167 -0
  121. code_puppy/models.json +66 -68
  122. code_puppy/models_dev_api.json +1 -0
  123. code_puppy/models_dev_parser.py +592 -0
  124. code_puppy/plugins/__init__.py +164 -10
  125. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  126. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  127. code_puppy/plugins/antigravity_oauth/antigravity_model.py +704 -0
  128. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  129. code_puppy/plugins/antigravity_oauth/constants.py +136 -0
  130. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  131. code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
  132. code_puppy/plugins/antigravity_oauth/storage.py +271 -0
  133. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  134. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  135. code_puppy/plugins/antigravity_oauth/transport.py +767 -0
  136. code_puppy/plugins/antigravity_oauth/utils.py +169 -0
  137. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  138. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  139. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
  140. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
  141. code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
  142. code_puppy/plugins/chatgpt_oauth/utils.py +489 -0
  143. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  144. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  145. code_puppy/plugins/claude_code_oauth/__init__.py +6 -0
  146. code_puppy/plugins/claude_code_oauth/config.py +50 -0
  147. code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
  148. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  149. code_puppy/plugins/claude_code_oauth/utils.py +518 -0
  150. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  151. code_puppy/plugins/customizable_commands/register_callbacks.py +169 -0
  152. code_puppy/plugins/example_custom_command/README.md +280 -0
  153. code_puppy/plugins/example_custom_command/register_callbacks.py +2 -2
  154. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  155. code_puppy/plugins/file_permission_handler/register_callbacks.py +523 -0
  156. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  157. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  158. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  159. code_puppy/plugins/oauth_puppy_html.py +228 -0
  160. code_puppy/plugins/shell_safety/__init__.py +6 -0
  161. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  162. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  163. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  164. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  165. code_puppy/prompts/codex_system_prompt.md +310 -0
  166. code_puppy/pydantic_patches.py +131 -0
  167. code_puppy/reopenable_async_client.py +8 -8
  168. code_puppy/round_robin_model.py +9 -12
  169. code_puppy/session_storage.py +2 -1
  170. code_puppy/status_display.py +21 -4
  171. code_puppy/summarization_agent.py +41 -13
  172. code_puppy/terminal_utils.py +418 -0
  173. code_puppy/tools/__init__.py +37 -1
  174. code_puppy/tools/agent_tools.py +536 -52
  175. code_puppy/tools/browser/__init__.py +37 -0
  176. code_puppy/tools/browser/browser_control.py +19 -23
  177. code_puppy/tools/browser/browser_interactions.py +41 -48
  178. code_puppy/tools/browser/browser_locators.py +36 -38
  179. code_puppy/tools/browser/browser_manager.py +316 -0
  180. code_puppy/tools/browser/browser_navigation.py +16 -16
  181. code_puppy/tools/browser/browser_screenshot.py +79 -143
  182. code_puppy/tools/browser/browser_scripts.py +32 -42
  183. code_puppy/tools/browser/browser_workflows.py +44 -27
  184. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  185. code_puppy/tools/browser/terminal_command_tools.py +521 -0
  186. code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
  187. code_puppy/tools/browser/terminal_tools.py +525 -0
  188. code_puppy/tools/command_runner.py +930 -147
  189. code_puppy/tools/common.py +1113 -5
  190. code_puppy/tools/display.py +84 -0
  191. code_puppy/tools/file_modifications.py +288 -89
  192. code_puppy/tools/file_operations.py +226 -154
  193. code_puppy/tools/subagent_context.py +158 -0
  194. code_puppy/uvx_detection.py +242 -0
  195. code_puppy/version_checker.py +30 -11
  196. code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
  197. code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
  198. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/METADATA +149 -75
  199. code_puppy-0.0.366.dist-info/RECORD +217 -0
  200. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
  201. code_puppy/command_line/mcp/add_command.py +0 -183
  202. code_puppy/messaging/spinner/textual_spinner.py +0 -106
  203. code_puppy/tools/browser/camoufox_manager.py +0 -216
  204. code_puppy/tools/browser/vqa_agent.py +0 -70
  205. code_puppy/tui/__init__.py +0 -10
  206. code_puppy/tui/app.py +0 -1105
  207. code_puppy/tui/components/__init__.py +0 -21
  208. code_puppy/tui/components/chat_view.py +0 -551
  209. code_puppy/tui/components/command_history_modal.py +0 -218
  210. code_puppy/tui/components/copy_button.py +0 -139
  211. code_puppy/tui/components/custom_widgets.py +0 -63
  212. code_puppy/tui/components/human_input_modal.py +0 -175
  213. code_puppy/tui/components/input_area.py +0 -167
  214. code_puppy/tui/components/sidebar.py +0 -309
  215. code_puppy/tui/components/status_bar.py +0 -185
  216. code_puppy/tui/messages.py +0 -27
  217. code_puppy/tui/models/__init__.py +0 -8
  218. code_puppy/tui/models/chat_message.py +0 -25
  219. code_puppy/tui/models/command_history.py +0 -89
  220. code_puppy/tui/models/enums.py +0 -24
  221. code_puppy/tui/screens/__init__.py +0 -17
  222. code_puppy/tui/screens/autosave_picker.py +0 -175
  223. code_puppy/tui/screens/help.py +0 -130
  224. code_puppy/tui/screens/mcp_install_wizard.py +0 -803
  225. code_puppy/tui/screens/settings.py +0 -306
  226. code_puppy/tui/screens/tools.py +0 -74
  227. code_puppy/tui_state.py +0 -55
  228. code_puppy-0.0.214.data/data/code_puppy/models.json +0 -112
  229. code_puppy-0.0.214.dist-info/RECORD +0 -131
  230. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +0 -0
  231. {code_puppy-0.0.214.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
+ ]