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,167 @@
1
+ """Command models for User → Agent communication in Code Puppy's messaging system.
2
+
3
+ This module defines Pydantic models for commands that flow FROM the UI TO the Agent.
4
+ This is the opposite direction of messages.py (which flows Agent → UI).
5
+
6
+ Commands are used for:
7
+ - Controlling agent execution (cancel, interrupt)
8
+ - Responding to agent requests for user input
9
+ - Providing confirmations and selections
10
+
11
+ The UI layer creates these commands and sends them to the agent/runtime.
12
+ The agent processes them and may emit messages in response.
13
+
14
+ ┌─────────┐ Commands ┌─────────┐
15
+ │ UI │ ────────────> │ Agent │
16
+ │ (User) │ │ │
17
+ │ │ <──────────── │ │
18
+ └─────────┘ Messages └─────────┘
19
+
20
+ NO Rich markup or formatting should be embedded in any string fields.
21
+ """
22
+
23
+ from datetime import datetime, timezone
24
+ from typing import Optional, Union
25
+ from uuid import uuid4
26
+
27
+ from pydantic import BaseModel, Field
28
+
29
+ # =============================================================================
30
+ # Base Command
31
+ # =============================================================================
32
+
33
+
34
+ class BaseCommand(BaseModel):
35
+ """Base class for all commands with auto-generated id and timestamp."""
36
+
37
+ id: str = Field(
38
+ default_factory=lambda: str(uuid4()),
39
+ description="Unique identifier for this command instance",
40
+ )
41
+ timestamp: datetime = Field(
42
+ default_factory=lambda: datetime.now(timezone.utc),
43
+ description="When this command was created (UTC)",
44
+ )
45
+
46
+ model_config = {"frozen": False, "extra": "forbid"}
47
+
48
+
49
+ # =============================================================================
50
+ # Agent Control Commands
51
+ # =============================================================================
52
+
53
+
54
+ class CancelAgentCommand(BaseCommand):
55
+ """Signals the agent to stop current execution gracefully.
56
+
57
+ The agent should finish any in-progress atomic operation, clean up,
58
+ and return control to the user. This is a soft cancellation.
59
+ """
60
+
61
+ reason: Optional[str] = Field(
62
+ default=None,
63
+ description="Optional reason for cancellation (for logging/debugging)",
64
+ )
65
+
66
+
67
+ class InterruptShellCommand(BaseCommand):
68
+ """Signals to interrupt a currently running shell command.
69
+
70
+ This is equivalent to pressing Ctrl+C in a terminal. The shell process
71
+ should receive SIGINT and terminate. Use this when a command is taking
72
+ too long or producing unwanted output.
73
+ """
74
+
75
+ command_id: Optional[str] = Field(
76
+ default=None,
77
+ description="ID of the specific shell command to interrupt (None = current)",
78
+ )
79
+
80
+
81
+ # =============================================================================
82
+ # User Interaction Responses
83
+ # =============================================================================
84
+
85
+
86
+ class UserInputResponse(BaseCommand):
87
+ """Response to a UserInputRequest from the agent.
88
+
89
+ The prompt_id must match the prompt_id from the original UserInputRequest
90
+ so the agent can correlate the response with the request.
91
+ """
92
+
93
+ prompt_id: str = Field(
94
+ description="ID of the prompt this responds to (must match request)"
95
+ )
96
+ value: str = Field(description="The user's input value")
97
+
98
+
99
+ class ConfirmationResponse(BaseCommand):
100
+ """Response to a ConfirmationRequest from the agent.
101
+
102
+ The user can confirm or deny, and optionally provide feedback text
103
+ if the original request had allow_feedback=True.
104
+ """
105
+
106
+ prompt_id: str = Field(
107
+ description="ID of the prompt this responds to (must match request)"
108
+ )
109
+ confirmed: bool = Field(
110
+ description="Whether the user confirmed (True) or denied (False)"
111
+ )
112
+ feedback: Optional[str] = Field(
113
+ default=None,
114
+ description="Optional feedback text from the user",
115
+ )
116
+
117
+
118
+ class SelectionResponse(BaseCommand):
119
+ """Response to a SelectionRequest from the agent.
120
+
121
+ Contains both the index and the value for convenience and validation.
122
+ The agent can verify that selected_value matches options[selected_index].
123
+ """
124
+
125
+ prompt_id: str = Field(
126
+ description="ID of the prompt this responds to (must match request)"
127
+ )
128
+ selected_index: int = Field(
129
+ ge=0,
130
+ description="Zero-based index of the selected option",
131
+ )
132
+ selected_value: str = Field(description="The value of the selected option")
133
+
134
+
135
+ # =============================================================================
136
+ # Union Type for Type Checking
137
+ # =============================================================================
138
+
139
+
140
+ # All concrete command types (excludes BaseCommand itself)
141
+ AnyCommand = Union[
142
+ CancelAgentCommand,
143
+ InterruptShellCommand,
144
+ UserInputResponse,
145
+ ConfirmationResponse,
146
+ SelectionResponse,
147
+ ]
148
+ """Union of all command types for type checking."""
149
+
150
+
151
+ # =============================================================================
152
+ # Export all public symbols
153
+ # =============================================================================
154
+
155
+ __all__ = [
156
+ # Base
157
+ "BaseCommand",
158
+ # Agent control
159
+ "CancelAgentCommand",
160
+ "InterruptShellCommand",
161
+ # User interaction responses
162
+ "UserInputResponse",
163
+ "ConfirmationResponse",
164
+ "SelectionResponse",
165
+ # Union type
166
+ "AnyCommand",
167
+ ]
@@ -0,0 +1,57 @@
1
+ """Patches for Rich's Markdown rendering.
2
+
3
+ This module provides customizations to Rich's default Markdown rendering,
4
+ particularly for header justification which is hardcoded to center in Rich.
5
+ """
6
+
7
+ from rich import box
8
+ from rich.markdown import Heading, Markdown
9
+ from rich.panel import Panel
10
+ from rich.text import Text
11
+
12
+
13
+ class LeftJustifiedHeading(Heading):
14
+ """A heading that left-justifies text instead of centering.
15
+
16
+ Rich's default Heading class hardcodes `text.justify = 'center'`,
17
+ which can look odd in a CLI context. This subclass overrides that
18
+ to use left justification instead.
19
+ """
20
+
21
+ def __rich_console__(self, console, options):
22
+ """Render the heading with left justification."""
23
+ text = self.text
24
+ text.justify = "left" # Override Rich's default 'center'
25
+
26
+ if self.tag == "h1":
27
+ # Draw a border around h1s (same as Rich default)
28
+ yield Panel(
29
+ text,
30
+ box=box.HEAVY,
31
+ style="markdown.h1.border",
32
+ )
33
+ else:
34
+ # Styled text for h2 and beyond (same as Rich default)
35
+ if self.tag == "h2":
36
+ yield Text("")
37
+ yield text
38
+
39
+
40
+ _patched = False
41
+
42
+
43
+ def patch_markdown_headings():
44
+ """Patch Rich's Markdown to use left-justified headings.
45
+
46
+ This function is idempotent - calling it multiple times has no effect
47
+ after the first call.
48
+ """
49
+ global _patched
50
+ if _patched:
51
+ return
52
+
53
+ Markdown.elements["heading_open"] = LeftJustifiedHeading
54
+ _patched = True
55
+
56
+
57
+ __all__ = ["patch_markdown_headings", "LeftJustifiedHeading"]
@@ -0,0 +1,361 @@
1
+ """
2
+ Message queue system for decoupling Rich console output from renderers.
3
+
4
+ This allows interactive mode to consume messages and render them appropriately.
5
+ """
6
+
7
+ import asyncio
8
+ import logging
9
+ import queue
10
+ import threading
11
+ from dataclasses import dataclass
12
+ from datetime import datetime, timezone
13
+ from enum import Enum
14
+ from typing import Any, Dict, Optional, Union
15
+
16
+ from rich.text import Text
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class MessageType(Enum):
22
+ """Types of messages that can be sent through the queue."""
23
+
24
+ # Basic content types
25
+ INFO = "info"
26
+ SUCCESS = "success"
27
+ WARNING = "warning"
28
+ ERROR = "error"
29
+ DIVIDER = "divider"
30
+
31
+ # Tool-specific types
32
+ TOOL_OUTPUT = "tool_output"
33
+ COMMAND_OUTPUT = "command_output"
34
+ FILE_OPERATION = "file_operation"
35
+
36
+ # Agent-specific types
37
+ AGENT_REASONING = "agent_reasoning"
38
+ PLANNED_NEXT_STEPS = "planned_next_steps"
39
+ AGENT_RESPONSE = "agent_response"
40
+ AGENT_STATUS = "agent_status"
41
+
42
+ # Human interaction types
43
+ HUMAN_INPUT_REQUEST = "human_input_request"
44
+
45
+ # System types
46
+ SYSTEM = "system"
47
+ DEBUG = "debug"
48
+
49
+
50
+ @dataclass
51
+ class UIMessage:
52
+ """A message to be displayed in the UI."""
53
+
54
+ type: MessageType
55
+ content: Union[str, Text, Any] # Can be Rich Text, Table, Markdown, etc.
56
+ timestamp: datetime = None
57
+ metadata: Dict[str, Any] = None
58
+
59
+ def __post_init__(self):
60
+ if self.timestamp is None:
61
+ self.timestamp = datetime.now(timezone.utc)
62
+ if self.metadata is None:
63
+ self.metadata = {}
64
+
65
+
66
+ class MessageQueue:
67
+ """Thread-safe message queue for UI messages."""
68
+
69
+ def __init__(self, maxsize: int = 1000):
70
+ self._queue = queue.Queue(maxsize=maxsize)
71
+ self._async_queue = None # Will be created when needed
72
+ self._async_queue_maxsize = maxsize
73
+ self._listeners = []
74
+ self._running = False
75
+ self._thread = None
76
+ self._startup_buffer = [] # Buffer messages before any renderer starts
77
+ self._has_active_renderer = False
78
+ self._event_loop = None # Store reference to the event loop
79
+ self._prompt_responses = {} # Store responses to human input requests
80
+ self._prompt_events = {} # threading.Event per prompt_id
81
+ self._prompt_id_counter = 0 # Counter for unique prompt IDs
82
+
83
+ def start(self):
84
+ """Start the queue processing."""
85
+ if self._running:
86
+ return
87
+
88
+ self._running = True
89
+ self._thread = threading.Thread(target=self._process_messages, daemon=True)
90
+ self._thread.start()
91
+
92
+ def get_buffered_messages(self):
93
+ """Get all currently buffered messages without waiting."""
94
+ # First get any startup buffered messages
95
+ messages = list(self._startup_buffer)
96
+
97
+ # Then get any queued messages
98
+ while True:
99
+ try:
100
+ message = self._queue.get_nowait()
101
+ messages.append(message)
102
+ except queue.Empty:
103
+ break
104
+ return messages
105
+
106
+ def clear_startup_buffer(self):
107
+ """Clear the startup buffer after processing."""
108
+ self._startup_buffer.clear()
109
+
110
+ def stop(self):
111
+ """Stop the queue processing."""
112
+ self._running = False
113
+ if self._thread and self._thread.is_alive():
114
+ self._thread.join(timeout=1.0)
115
+
116
+ def emit(self, message: UIMessage):
117
+ """Emit a message to the queue."""
118
+ # If no renderer is active yet, buffer the message for startup
119
+ if not self._has_active_renderer:
120
+ self._startup_buffer.append(message)
121
+ return
122
+
123
+ try:
124
+ self._queue.put_nowait(message)
125
+ except queue.Full:
126
+ # Drop oldest message to make room
127
+ try:
128
+ self._queue.get_nowait()
129
+ self._queue.put_nowait(message)
130
+ except queue.Empty:
131
+ pass
132
+
133
+ def emit_simple(self, message_type: MessageType, content: Any, **metadata):
134
+ """Emit a simple message with just type and content."""
135
+ msg = UIMessage(type=message_type, content=content, metadata=metadata)
136
+ self.emit(msg)
137
+
138
+ def get_nowait(self) -> Optional[UIMessage]:
139
+ """Get a message without blocking."""
140
+ try:
141
+ return self._queue.get_nowait()
142
+ except queue.Empty:
143
+ return None
144
+
145
+ async def get_async(self) -> UIMessage:
146
+ """Get a message asynchronously."""
147
+ # Lazy initialization of async queue and store event loop reference
148
+ if self._async_queue is None:
149
+ self._async_queue = asyncio.Queue(maxsize=self._async_queue_maxsize)
150
+ self._event_loop = asyncio.get_running_loop()
151
+ return await self._async_queue.get()
152
+
153
+ def _process_messages(self):
154
+ """Process messages from sync to async queue."""
155
+ while self._running:
156
+ try:
157
+ message = self._queue.get(timeout=0.1)
158
+
159
+ # Try to put in async queue if we have an event loop reference
160
+ if self._event_loop is not None and self._async_queue is not None:
161
+ # Use thread-safe call to put message in async queue
162
+ # Create a bound method to avoid closure issues
163
+ try:
164
+ self._event_loop.call_soon_threadsafe(
165
+ self._async_queue.put_nowait, message
166
+ )
167
+ except Exception as e:
168
+ logger.debug("Failed to enqueue message to async queue: %s", e)
169
+
170
+ # Notify listeners immediately for sync processing
171
+ for listener in self._listeners:
172
+ try:
173
+ listener(message)
174
+ except Exception as e:
175
+ logger.debug("Listener error in message queue: %s", e)
176
+
177
+ except queue.Empty:
178
+ continue
179
+
180
+ def add_listener(self, callback):
181
+ """Add a listener for messages (for direct sync consumption)."""
182
+ self._listeners.append(callback)
183
+ # Mark that we have an active renderer
184
+ self._has_active_renderer = True
185
+
186
+ def remove_listener(self, callback):
187
+ """Remove a listener."""
188
+ if callback in self._listeners:
189
+ self._listeners.remove(callback)
190
+ # If no more listeners, mark as no active renderer
191
+ if not self._listeners:
192
+ self._has_active_renderer = False
193
+
194
+ def mark_renderer_active(self):
195
+ """Mark that a renderer is now active and consuming messages."""
196
+ self._has_active_renderer = True
197
+
198
+ def mark_renderer_inactive(self):
199
+ """Mark that no renderer is currently active."""
200
+ self._has_active_renderer = False
201
+
202
+ def create_prompt_request(self, prompt_text: str) -> str:
203
+ """Create a human input request and return its unique ID."""
204
+ self._prompt_id_counter += 1
205
+ prompt_id = f"prompt_{self._prompt_id_counter}"
206
+
207
+ # Create event for this prompt
208
+ self._prompt_events[prompt_id] = threading.Event()
209
+
210
+ # Emit the human input request message
211
+ message = UIMessage(
212
+ type=MessageType.HUMAN_INPUT_REQUEST,
213
+ content=prompt_text,
214
+ metadata={"prompt_id": prompt_id},
215
+ )
216
+ self.emit(message)
217
+
218
+ return prompt_id
219
+
220
+ def wait_for_prompt_response(self, prompt_id: str, timeout: float = None) -> str:
221
+ """Wait for a response to a human input request."""
222
+ # If response is already available, return immediately
223
+ if prompt_id in self._prompt_responses:
224
+ self._prompt_events.pop(prompt_id, None)
225
+ return self._prompt_responses.pop(prompt_id)
226
+
227
+ event = self._prompt_events.get(prompt_id)
228
+ if event is None:
229
+ # Fallback: create event if not already present
230
+ event = threading.Event()
231
+ self._prompt_events[prompt_id] = event
232
+
233
+ signaled = event.wait(timeout=timeout)
234
+
235
+ # Clean up the event
236
+ self._prompt_events.pop(prompt_id, None)
237
+
238
+ if not signaled:
239
+ raise TimeoutError(f"No response for prompt {prompt_id} within {timeout}s")
240
+
241
+ return self._prompt_responses.pop(prompt_id)
242
+
243
+ def provide_prompt_response(self, prompt_id: str, response: str):
244
+ """Provide a response to a human input request."""
245
+ self._prompt_responses[prompt_id] = response
246
+ event = self._prompt_events.get(prompt_id)
247
+ if event is not None:
248
+ event.set()
249
+
250
+
251
+ # Global message queue instance
252
+ _global_queue: Optional[MessageQueue] = None
253
+ _queue_lock = threading.Lock()
254
+
255
+
256
+ def get_global_queue() -> MessageQueue:
257
+ """Get or create the global message queue."""
258
+ global _global_queue
259
+
260
+ with _queue_lock:
261
+ if _global_queue is None:
262
+ _global_queue = MessageQueue()
263
+ _global_queue.start()
264
+
265
+ return _global_queue
266
+
267
+
268
+ def get_buffered_startup_messages():
269
+ """Get any messages that were buffered before renderers started."""
270
+ queue = get_global_queue()
271
+ # Only return startup buffer messages, don't clear them yet
272
+ messages = list(queue._startup_buffer)
273
+ return messages
274
+
275
+
276
+ def emit_message(message_type: MessageType, content: Any, **metadata):
277
+ """Convenience function to emit a message to the global queue."""
278
+ queue = get_global_queue()
279
+ queue.emit_simple(message_type, content, **metadata)
280
+
281
+
282
+ def emit_info(content: Any, **metadata):
283
+ """Emit an info message."""
284
+ emit_message(MessageType.INFO, content, **metadata)
285
+
286
+
287
+ def emit_success(content: Any, **metadata):
288
+ """Emit a success message."""
289
+ emit_message(MessageType.SUCCESS, content, **metadata)
290
+
291
+
292
+ def emit_warning(content: Any, **metadata):
293
+ """Emit a warning message."""
294
+ emit_message(MessageType.WARNING, content, **metadata)
295
+
296
+
297
+ def emit_error(content: Any, **metadata):
298
+ """Emit an error message."""
299
+ emit_message(MessageType.ERROR, content, **metadata)
300
+
301
+
302
+ def emit_tool_output(content: Any, tool_name: str = None, **metadata):
303
+ """Emit tool output."""
304
+ if tool_name:
305
+ metadata["tool_name"] = tool_name
306
+ emit_message(MessageType.TOOL_OUTPUT, content, **metadata)
307
+
308
+
309
+ def emit_command_output(content: Any, command: str = None, **metadata):
310
+ """Emit command output."""
311
+ if command:
312
+ metadata["command"] = command
313
+ emit_message(MessageType.COMMAND_OUTPUT, content, **metadata)
314
+
315
+
316
+ def emit_agent_reasoning(content: Any, **metadata):
317
+ """Emit agent reasoning."""
318
+ emit_message(MessageType.AGENT_REASONING, content, **metadata)
319
+
320
+
321
+ def emit_planned_next_steps(content: Any, **metadata):
322
+ """Emit planned_next_steps"""
323
+ emit_message(MessageType.PLANNED_NEXT_STEPS, content, **metadata)
324
+
325
+
326
+ def emit_agent_response(content: Any, **metadata):
327
+ """Emit agent_response"""
328
+ emit_message(MessageType.AGENT_RESPONSE, content, **metadata)
329
+
330
+
331
+ def emit_system_message(content: Any, **metadata):
332
+ """Emit a system message."""
333
+ emit_message(MessageType.SYSTEM, content, **metadata)
334
+
335
+
336
+ def emit_divider(content: str = "─" * 100 + "\n", **metadata):
337
+ """Emit a divider line"""
338
+ # TUI mode has been removed, always emit dividers
339
+ emit_message(MessageType.DIVIDER, content, **metadata)
340
+
341
+
342
+ def emit_prompt(prompt_text: str, timeout: float = None) -> str:
343
+ """Emit a human input request and wait for response.
344
+
345
+ Uses safe_input for cross-platform compatibility, especially on Windows
346
+ where raw input() can fail after prompt_toolkit Applications.
347
+ """
348
+ from code_puppy.command_line.utils import safe_input
349
+ from code_puppy.messaging import emit_info
350
+
351
+ emit_info(prompt_text)
352
+
353
+ # Use safe_input which resets Windows console state before reading
354
+ response = safe_input(">>> ")
355
+ return response
356
+
357
+
358
+ def provide_prompt_response(prompt_id: str, response: str):
359
+ """Provide a response to a human input request."""
360
+ queue = get_global_queue()
361
+ queue.provide_prompt_response(prompt_id, response)