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