code-muse 0.0.1__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 (394) hide show
  1. code_muse/__init__.py +26 -0
  2. code_muse/__main__.py +10 -0
  3. code_muse/agents/__init__.py +31 -0
  4. code_muse/agents/_builder.py +214 -0
  5. code_muse/agents/_compaction.py +506 -0
  6. code_muse/agents/_diagnostics.py +171 -0
  7. code_muse/agents/_history.py +382 -0
  8. code_muse/agents/_key_listeners.py +148 -0
  9. code_muse/agents/_non_streaming_render.py +148 -0
  10. code_muse/agents/_runtime.py +596 -0
  11. code_muse/agents/agent_creator_agent.py +603 -0
  12. code_muse/agents/agent_helios.py +47 -0
  13. code_muse/agents/agent_manager.py +740 -0
  14. code_muse/agents/agent_muse.py +78 -0
  15. code_muse/agents/agent_planning.py +44 -0
  16. code_muse/agents/agent_qa_melpomene.py +207 -0
  17. code_muse/agents/base_agent.py +194 -0
  18. code_muse/agents/event_stream_handler.py +361 -0
  19. code_muse/agents/json_agent.py +201 -0
  20. code_muse/agents/prompt_v3.py +521 -0
  21. code_muse/agents/subagent_stream_handler.py +273 -0
  22. code_muse/callbacks.py +941 -0
  23. code_muse/chatgpt_codex_client.py +333 -0
  24. code_muse/claude_cache_client.py +853 -0
  25. code_muse/cli_runner/__init__.py +319 -0
  26. code_muse/cli_runner/args.py +63 -0
  27. code_muse/cli_runner/loop.py +510 -0
  28. code_muse/cli_runner/resume.py +72 -0
  29. code_muse/cli_runner/runner.py +161 -0
  30. code_muse/command_line/__init__.py +1 -0
  31. code_muse/command_line/add_model_menu.py +1331 -0
  32. code_muse/command_line/agent_menu.py +674 -0
  33. code_muse/command_line/attachments.py +397 -0
  34. code_muse/command_line/autosave_menu.py +709 -0
  35. code_muse/command_line/clipboard.py +528 -0
  36. code_muse/command_line/colors_menu.py +530 -0
  37. code_muse/command_line/command_handler.py +262 -0
  38. code_muse/command_line/command_registry.py +150 -0
  39. code_muse/command_line/config_commands.py +711 -0
  40. code_muse/command_line/core_commands.py +740 -0
  41. code_muse/command_line/diff_menu.py +865 -0
  42. code_muse/command_line/file_path_completion.py +73 -0
  43. code_muse/command_line/load_context_completion.py +57 -0
  44. code_muse/command_line/model_picker_completion.py +512 -0
  45. code_muse/command_line/model_settings_menu.py +983 -0
  46. code_muse/command_line/onboarding_slides.py +162 -0
  47. code_muse/command_line/onboarding_wizard.py +337 -0
  48. code_muse/command_line/pagination.py +41 -0
  49. code_muse/command_line/pin_command_completion.py +329 -0
  50. code_muse/command_line/prompt_toolkit_completion.py +886 -0
  51. code_muse/command_line/session_commands.py +304 -0
  52. code_muse/command_line/shell_passthrough.py +145 -0
  53. code_muse/command_line/skills_completion.py +158 -0
  54. code_muse/command_line/types.py +18 -0
  55. code_muse/command_line/uc_menu.py +908 -0
  56. code_muse/command_line/utils.py +105 -0
  57. code_muse/command_line/wiggum_state.py +77 -0
  58. code_muse/config.py +1138 -0
  59. code_muse/config_agent.py +168 -0
  60. code_muse/config_appearance.py +241 -0
  61. code_muse/config_model.py +357 -0
  62. code_muse/config_security.py +73 -0
  63. code_muse/error_logging.py +132 -0
  64. code_muse/evals/__init__.py +35 -0
  65. code_muse/evals/eval_helpers.py +81 -0
  66. code_muse/evals/eval_runner.py +299 -0
  67. code_muse/evals/sample_evals/__init__.py +1 -0
  68. code_muse/evals/sample_evals/eval_frugal_reads.py +59 -0
  69. code_muse/evals/sample_evals/eval_memory_planning.py +31 -0
  70. code_muse/evals/sample_evals/eval_shell_efficiency.py +39 -0
  71. code_muse/evals/sample_evals/eval_tool_masking.py +33 -0
  72. code_muse/fs_scan_cache/__init__.py +31 -0
  73. code_muse/fs_scan_cache/invalidation_hooks.py +89 -0
  74. code_muse/fs_scan_cache/scan_cache_core.cpython-314-darwin.so +0 -0
  75. code_muse/fs_scan_cache/scan_cache_core.pyx +203 -0
  76. code_muse/fs_scan_cache/tool_integration.py +309 -0
  77. code_muse/fs_scan_cache/ttl_policy.py +44 -0
  78. code_muse/gemini_code_assist.py +383 -0
  79. code_muse/gemini_model.py +838 -0
  80. code_muse/hook_engine/README.md +105 -0
  81. code_muse/hook_engine/__init__.py +21 -0
  82. code_muse/hook_engine/aliases.py +153 -0
  83. code_muse/hook_engine/engine.py +221 -0
  84. code_muse/hook_engine/executor.py +347 -0
  85. code_muse/hook_engine/matcher.py +154 -0
  86. code_muse/hook_engine/models.py +245 -0
  87. code_muse/hook_engine/registry.py +114 -0
  88. code_muse/hook_engine/trust.py +268 -0
  89. code_muse/hook_engine/validator.py +144 -0
  90. code_muse/http_utils.py +360 -0
  91. code_muse/keymap.py +128 -0
  92. code_muse/list_filtering.py +26 -0
  93. code_muse/main.py +10 -0
  94. code_muse/messaging/__init__.py +259 -0
  95. code_muse/messaging/bus.py +621 -0
  96. code_muse/messaging/commands.py +166 -0
  97. code_muse/messaging/markdown_patches.py +57 -0
  98. code_muse/messaging/message_queue.py +397 -0
  99. code_muse/messaging/messages.py +591 -0
  100. code_muse/messaging/queue_console.py +269 -0
  101. code_muse/messaging/renderers.py +308 -0
  102. code_muse/messaging/rich_renderer.py +1158 -0
  103. code_muse/messaging/shimmer.py +154 -0
  104. code_muse/messaging/spinner/__init__.py +87 -0
  105. code_muse/messaging/spinner/console_spinner.py +250 -0
  106. code_muse/messaging/spinner/spinner_base.py +82 -0
  107. code_muse/messaging/subagent_console.py +458 -0
  108. code_muse/model_factory.py +1203 -0
  109. code_muse/model_switching.py +59 -0
  110. code_muse/model_utils.py +156 -0
  111. code_muse/models.json +66 -0
  112. code_muse/models_cache/__init__.py +26 -0
  113. code_muse/models_cache/blocking_lru_cache.py +98 -0
  114. code_muse/models_cache/cache_writer.py +86 -0
  115. code_muse/models_cache/sha256_hash.cpython-314-darwin.so +0 -0
  116. code_muse/models_cache/sha256_hash.pyx +34 -0
  117. code_muse/models_cache/startup_integration.py +75 -0
  118. code_muse/models_dev_api.json +1 -0
  119. code_muse/models_dev_parser.py +590 -0
  120. code_muse/motion.py +126 -0
  121. code_muse/plugins/__init__.py +471 -0
  122. code_muse/plugins/agent_skills/__init__.py +32 -0
  123. code_muse/plugins/agent_skills/config.py +176 -0
  124. code_muse/plugins/agent_skills/discovery.py +309 -0
  125. code_muse/plugins/agent_skills/downloader.py +389 -0
  126. code_muse/plugins/agent_skills/installer.py +19 -0
  127. code_muse/plugins/agent_skills/metadata.py +293 -0
  128. code_muse/plugins/agent_skills/prompt_builder.py +66 -0
  129. code_muse/plugins/agent_skills/register_callbacks.py +298 -0
  130. code_muse/plugins/agent_skills/remote_catalog.py +320 -0
  131. code_muse/plugins/agent_skills/skill_catalog.py +254 -0
  132. code_muse/plugins/agent_skills/skills_install_menu.py +690 -0
  133. code_muse/plugins/agent_skills/skills_menu.py +791 -0
  134. code_muse/plugins/autonomous_memory/__init__.py +39 -0
  135. code_muse/plugins/autonomous_memory/bm25_scorer.cpython-314-darwin.so +0 -0
  136. code_muse/plugins/autonomous_memory/bm25_scorer.cpython-314-x86_64-linux-gnu.so +0 -0
  137. code_muse/plugins/autonomous_memory/bm25_scorer.pyx +291 -0
  138. code_muse/plugins/autonomous_memory/consolidation.py +82 -0
  139. code_muse/plugins/autonomous_memory/extraction.py +382 -0
  140. code_muse/plugins/autonomous_memory/lease_lock.py +105 -0
  141. code_muse/plugins/autonomous_memory/memory_injection.py +59 -0
  142. code_muse/plugins/autonomous_memory/register_callbacks.py +268 -0
  143. code_muse/plugins/autonomous_memory/secret_scanner.py +62 -0
  144. code_muse/plugins/autonomous_memory/session_scanner.py +163 -0
  145. code_muse/plugins/aws_bedrock/__init__.py +14 -0
  146. code_muse/plugins/aws_bedrock/config.py +99 -0
  147. code_muse/plugins/aws_bedrock/register_callbacks.py +241 -0
  148. code_muse/plugins/aws_bedrock/utils.py +153 -0
  149. code_muse/plugins/azure_foundry/README.md +238 -0
  150. code_muse/plugins/azure_foundry/__init__.py +15 -0
  151. code_muse/plugins/azure_foundry/config.py +125 -0
  152. code_muse/plugins/azure_foundry/discovery.py +187 -0
  153. code_muse/plugins/azure_foundry/register_callbacks.py +495 -0
  154. code_muse/plugins/azure_foundry/token.py +180 -0
  155. code_muse/plugins/azure_foundry/utils.py +345 -0
  156. code_muse/plugins/build_filter/__init__.py +1 -0
  157. code_muse/plugins/build_filter/register_callbacks.py +201 -0
  158. code_muse/plugins/build_filter/strategies/__init__.py +1 -0
  159. code_muse/plugins/build_filter/strategies/build.py +397 -0
  160. code_muse/plugins/chatgpt_oauth/__init__.py +6 -0
  161. code_muse/plugins/chatgpt_oauth/config.py +52 -0
  162. code_muse/plugins/chatgpt_oauth/oauth_flow.py +338 -0
  163. code_muse/plugins/chatgpt_oauth/register_callbacks.py +172 -0
  164. code_muse/plugins/chatgpt_oauth/test_plugin.py +301 -0
  165. code_muse/plugins/chatgpt_oauth/utils.py +538 -0
  166. code_muse/plugins/checkpointing/__init__.py +29 -0
  167. code_muse/plugins/checkpointing/checkpoint_hook.py +51 -0
  168. code_muse/plugins/checkpointing/conversation_snapshots.py +117 -0
  169. code_muse/plugins/checkpointing/register_callbacks.py +51 -0
  170. code_muse/plugins/checkpointing/restore_command.py +263 -0
  171. code_muse/plugins/checkpointing/rewind_shortcut.py +88 -0
  172. code_muse/plugins/checkpointing/shadow_git.py +90 -0
  173. code_muse/plugins/claude_code_hooks/__init__.py +1 -0
  174. code_muse/plugins/claude_code_hooks/config.py +188 -0
  175. code_muse/plugins/claude_code_hooks/register_callbacks.py +208 -0
  176. code_muse/plugins/claude_code_oauth/README.md +167 -0
  177. code_muse/plugins/claude_code_oauth/SETUP.md +93 -0
  178. code_muse/plugins/claude_code_oauth/__init__.py +25 -0
  179. code_muse/plugins/claude_code_oauth/config.py +52 -0
  180. code_muse/plugins/claude_code_oauth/fast_mode.py +124 -0
  181. code_muse/plugins/claude_code_oauth/prompt_handler.py +63 -0
  182. code_muse/plugins/claude_code_oauth/register_callbacks.py +547 -0
  183. code_muse/plugins/claude_code_oauth/test_fast_mode.py +165 -0
  184. code_muse/plugins/claude_code_oauth/test_plugin.py +283 -0
  185. code_muse/plugins/claude_code_oauth/token_refresh_heartbeat.py +237 -0
  186. code_muse/plugins/claude_code_oauth/utils.py +664 -0
  187. code_muse/plugins/copilot_auth/__init__.py +11 -0
  188. code_muse/plugins/copilot_auth/config.py +91 -0
  189. code_muse/plugins/copilot_auth/reasoning_client.py +409 -0
  190. code_muse/plugins/copilot_auth/register_callbacks.py +461 -0
  191. code_muse/plugins/copilot_auth/utils.py +584 -0
  192. code_muse/plugins/custom_commands/__init__.py +14 -0
  193. code_muse/plugins/custom_commands/args_injection.py +82 -0
  194. code_muse/plugins/custom_commands/command_discovery.py +89 -0
  195. code_muse/plugins/custom_commands/command_toml_schema.py +71 -0
  196. code_muse/plugins/custom_commands/register_callbacks.py +176 -0
  197. code_muse/plugins/customizable_commands/__init__.py +0 -0
  198. code_muse/plugins/customizable_commands/register_callbacks.py +136 -0
  199. code_muse/plugins/destructive_command_guard/__init__.py +14 -0
  200. code_muse/plugins/destructive_command_guard/detector.py +375 -0
  201. code_muse/plugins/destructive_command_guard/register_callbacks.py +148 -0
  202. code_muse/plugins/example_custom_command/README.md +280 -0
  203. code_muse/plugins/example_custom_command/register_callbacks.py +51 -0
  204. code_muse/plugins/file_permission_handler/__init__.py +4 -0
  205. code_muse/plugins/file_permission_handler/register_callbacks.py +441 -0
  206. code_muse/plugins/filter_engine/__init__.py +30 -0
  207. code_muse/plugins/filter_engine/classifier.py +153 -0
  208. code_muse/plugins/filter_engine/content_detector.py +184 -0
  209. code_muse/plugins/filter_engine/dispatcher.py +244 -0
  210. code_muse/plugins/filter_engine/register_callbacks.py +188 -0
  211. code_muse/plugins/filter_engine/registry.py +279 -0
  212. code_muse/plugins/filter_engine/strategies/__init__.py +8 -0
  213. code_muse/plugins/filter_engine/strategies/ast_compressor.cpython-314-darwin.so +0 -0
  214. code_muse/plugins/filter_engine/strategies/ast_compressor.cpython-314-x86_64-linux-gnu.so +0 -0
  215. code_muse/plugins/filter_engine/strategies/ast_compressor.pyx +348 -0
  216. code_muse/plugins/filter_engine/strategies/ast_parser.py +167 -0
  217. code_muse/plugins/filter_engine/strategies/code.cpython-314-darwin.so +0 -0
  218. code_muse/plugins/filter_engine/strategies/code.cpython-314-x86_64-linux-gnu.so +0 -0
  219. code_muse/plugins/filter_engine/strategies/code.pyx +584 -0
  220. code_muse/plugins/filter_engine/strategies/git.cpython-314-darwin.so +0 -0
  221. code_muse/plugins/filter_engine/strategies/git.cpython-314-x86_64-linux-gnu.so +0 -0
  222. code_muse/plugins/filter_engine/strategies/git.pyx +438 -0
  223. code_muse/plugins/filter_engine/strategies/json_compressor.cpython-314-darwin.so +0 -0
  224. code_muse/plugins/filter_engine/strategies/json_compressor.pyx +253 -0
  225. code_muse/plugins/filter_engine/strategies/json_patterns.cpython-314-darwin.so +0 -0
  226. code_muse/plugins/filter_engine/strategies/json_patterns.pyx +178 -0
  227. code_muse/plugins/filter_engine/strategies/lint.cpython-314-darwin.so +0 -0
  228. code_muse/plugins/filter_engine/strategies/lint.cpython-314-x86_64-linux-gnu.so +0 -0
  229. code_muse/plugins/filter_engine/strategies/lint.pyx +626 -0
  230. code_muse/plugins/filter_engine/strategies/test.cpython-314-darwin.so +0 -0
  231. code_muse/plugins/filter_engine/strategies/test.cpython-314-x86_64-linux-gnu.so +0 -0
  232. code_muse/plugins/filter_engine/strategies/test.pyx +431 -0
  233. code_muse/plugins/filter_engine/verbosity.py +63 -0
  234. code_muse/plugins/force_push_guard/__init__.py +5 -0
  235. code_muse/plugins/force_push_guard/detector.py +96 -0
  236. code_muse/plugins/force_push_guard/register_callbacks.py +144 -0
  237. code_muse/plugins/force_push_guard/test_detector.py +143 -0
  238. code_muse/plugins/frontend_emitter/__init__.py +25 -0
  239. code_muse/plugins/frontend_emitter/emitter.py +121 -0
  240. code_muse/plugins/frontend_emitter/register_callbacks.py +259 -0
  241. code_muse/plugins/gac/__init__.py +4 -0
  242. code_muse/plugins/gac/git_ops.py +136 -0
  243. code_muse/plugins/gac/prompt.py +191 -0
  244. code_muse/plugins/gac/register_callbacks.py +82 -0
  245. code_muse/plugins/hook_creator/__init__.py +1 -0
  246. code_muse/plugins/hook_creator/register_callbacks.py +34 -0
  247. code_muse/plugins/hook_manager/__init__.py +1 -0
  248. code_muse/plugins/hook_manager/config.py +289 -0
  249. code_muse/plugins/hook_manager/hooks_menu.py +563 -0
  250. code_muse/plugins/hook_manager/register_callbacks.py +227 -0
  251. code_muse/plugins/hook_monitor/register_callbacks.py +36 -0
  252. code_muse/plugins/mindpack/__init__.py +0 -0
  253. code_muse/plugins/mindpack/factory.py +930 -0
  254. code_muse/plugins/mindpack/judge.py +573 -0
  255. code_muse/plugins/mindpack/memory.py +100 -0
  256. code_muse/plugins/mindpack/mindpack_menu.py +1552 -0
  257. code_muse/plugins/mindpack/orchestration.py +605 -0
  258. code_muse/plugins/mindpack/register_callbacks.py +175 -0
  259. code_muse/plugins/mindpack/schemas.py +358 -0
  260. code_muse/plugins/mindpack/tools.py +387 -0
  261. code_muse/plugins/oauth_muse_html.py +226 -0
  262. code_muse/plugins/ollama_setup/__init__.py +5 -0
  263. code_muse/plugins/ollama_setup/completer.py +36 -0
  264. code_muse/plugins/ollama_setup/register_callbacks.py +410 -0
  265. code_muse/plugins/plan_command/__init__.py +0 -0
  266. code_muse/plugins/plan_command/register_callbacks.py +206 -0
  267. code_muse/plugins/plan_mode/__init__.py +37 -0
  268. code_muse/plugins/plan_mode/mode_cycling.py +40 -0
  269. code_muse/plugins/plan_mode/plan_generation.py +68 -0
  270. code_muse/plugins/plan_mode/plan_hooks.py +74 -0
  271. code_muse/plugins/plan_mode/plan_mode_tools.py +138 -0
  272. code_muse/plugins/plan_mode/register_callbacks.py +121 -0
  273. code_muse/plugins/plugin_trust/register_callbacks.py +140 -0
  274. code_muse/plugins/policy_engine/__init__.py +46 -0
  275. code_muse/plugins/policy_engine/approval_flow_integration.py +59 -0
  276. code_muse/plugins/policy_engine/policy_evaluator.py +75 -0
  277. code_muse/plugins/policy_engine/policy_file_discovery.py +90 -0
  278. code_muse/plugins/policy_engine/policy_toml_schema.py +115 -0
  279. code_muse/plugins/policy_engine/register_callbacks.py +112 -0
  280. code_muse/plugins/pop_command/__init__.py +1 -0
  281. code_muse/plugins/pop_command/register_callbacks.py +189 -0
  282. code_muse/plugins/prompt_newline/__init__.py +13 -0
  283. code_muse/plugins/prompt_newline/config.py +19 -0
  284. code_muse/plugins/prompt_newline/register_callbacks.py +159 -0
  285. code_muse/plugins/safety_status/__init__.py +0 -0
  286. code_muse/plugins/safety_status/register_callbacks.py +113 -0
  287. code_muse/plugins/semantic_compression/__init__.py +6 -0
  288. code_muse/plugins/semantic_compression/compressor.py +295 -0
  289. code_muse/plugins/semantic_compression/config.py +123 -0
  290. code_muse/plugins/semantic_compression/register_callbacks.py +320 -0
  291. code_muse/plugins/shell_minimizer/__init__.py +50 -0
  292. code_muse/plugins/shell_minimizer/builtin_filters.toml +393 -0
  293. code_muse/plugins/shell_minimizer/pipeline.py +556 -0
  294. code_muse/plugins/shell_minimizer/primitives.py +482 -0
  295. code_muse/plugins/shell_minimizer/register_callbacks.py +276 -0
  296. code_muse/plugins/shell_safety/__init__.py +6 -0
  297. code_muse/plugins/shell_safety/agent_shell_safety.py +69 -0
  298. code_muse/plugins/shell_safety/command_cache.py +149 -0
  299. code_muse/plugins/shell_safety/register_callbacks.py +202 -0
  300. code_muse/plugins/synthetic_status/__init__.py +1 -0
  301. code_muse/plugins/synthetic_status/register_callbacks.py +128 -0
  302. code_muse/plugins/synthetic_status/status_api.py +145 -0
  303. code_muse/plugins/token_caching/__init__.py +21 -0
  304. code_muse/plugins/token_caching/cache_hit_tracking.py +128 -0
  305. code_muse/plugins/token_caching/cacheable_prefix_detection.py +28 -0
  306. code_muse/plugins/token_caching/register_callbacks.py +54 -0
  307. code_muse/plugins/token_caching/stats_display.py +35 -0
  308. code_muse/plugins/token_tracking/__init__.py +26 -0
  309. code_muse/plugins/token_tracking/database.py +381 -0
  310. code_muse/plugins/token_tracking/edit_analyzer.py +97 -0
  311. code_muse/plugins/token_tracking/record.py +55 -0
  312. code_muse/plugins/token_tracking/register_callbacks.py +277 -0
  313. code_muse/plugins/token_tracking/reports.py +329 -0
  314. code_muse/plugins/universal_constructor/__init__.py +13 -0
  315. code_muse/plugins/universal_constructor/models.py +136 -0
  316. code_muse/plugins/universal_constructor/register_callbacks.py +47 -0
  317. code_muse/plugins/universal_constructor/registry.py +390 -0
  318. code_muse/plugins/universal_constructor/runner.py +474 -0
  319. code_muse/plugins/universal_constructor/safety.py +440 -0
  320. code_muse/plugins/universal_constructor/sandbox.py +584 -0
  321. code_muse/provider_identity.py +105 -0
  322. code_muse/pydantic_patches.py +410 -0
  323. code_muse/reopenable_async_client.py +233 -0
  324. code_muse/round_robin_model.py +151 -0
  325. code_muse/secret_storage.py +74 -0
  326. code_muse/security/__init__.py +1 -0
  327. code_muse/security/redaction.cpython-314-darwin.so +0 -0
  328. code_muse/security/redaction.cpython-314-x86_64-linux-gnu.so +0 -0
  329. code_muse/security/redaction.pyx +135 -0
  330. code_muse/session_storage.py +565 -0
  331. code_muse/status_display.py +261 -0
  332. code_muse/stream_parser/__init__.py +76 -0
  333. code_muse/stream_parser/assistant_text_parser.py +90 -0
  334. code_muse/stream_parser/citation_parser.py +76 -0
  335. code_muse/stream_parser/inline_hidden_tag_parser.py +236 -0
  336. code_muse/stream_parser/proposed_plan_parser.py +158 -0
  337. code_muse/stream_parser/stream_text_chunk.py +23 -0
  338. code_muse/stream_parser/stream_text_parser.py +27 -0
  339. code_muse/stream_parser/tagged_line_parser.cpython-314-darwin.so +0 -0
  340. code_muse/stream_parser/tagged_line_parser.pyx +251 -0
  341. code_muse/stream_parser/utf8_stream_parser.cpython-314-darwin.so +0 -0
  342. code_muse/stream_parser/utf8_stream_parser.pyx +206 -0
  343. code_muse/summarization_agent.py +308 -0
  344. code_muse/terminal_utils.cpython-314-darwin.so +0 -0
  345. code_muse/terminal_utils.cpython-314-x86_64-linux-gnu.so +0 -0
  346. code_muse/terminal_utils.pyx +483 -0
  347. code_muse/tools/__init__.py +459 -0
  348. code_muse/tools/agent_tools.py +613 -0
  349. code_muse/tools/ask_user_question/__init__.py +26 -0
  350. code_muse/tools/ask_user_question/constants.py +73 -0
  351. code_muse/tools/ask_user_question/demo_tui.py +55 -0
  352. code_muse/tools/ask_user_question/handler.py +232 -0
  353. code_muse/tools/ask_user_question/models.py +302 -0
  354. code_muse/tools/ask_user_question/registration.py +37 -0
  355. code_muse/tools/ask_user_question/renderers.py +336 -0
  356. code_muse/tools/ask_user_question/terminal_ui.py +327 -0
  357. code_muse/tools/ask_user_question/theme.py +156 -0
  358. code_muse/tools/ask_user_question/tui_loop.py +422 -0
  359. code_muse/tools/background_jobs.py +99 -0
  360. code_muse/tools/browser/__init__.py +37 -0
  361. code_muse/tools/browser/browser_control.py +289 -0
  362. code_muse/tools/browser/browser_interactions.py +545 -0
  363. code_muse/tools/browser/browser_locators.py +640 -0
  364. code_muse/tools/browser/browser_manager.py +376 -0
  365. code_muse/tools/browser/browser_navigation.py +251 -0
  366. code_muse/tools/browser/browser_screenshot.py +180 -0
  367. code_muse/tools/browser/browser_scripts.py +462 -0
  368. code_muse/tools/browser/browser_workflows.py +222 -0
  369. code_muse/tools/chrome_cdp/__init__.py +1070 -0
  370. code_muse/tools/chrome_cdp/register_callbacks.py +61 -0
  371. code_muse/tools/command_runner.py +1401 -0
  372. code_muse/tools/common.py +1407 -0
  373. code_muse/tools/display.py +87 -0
  374. code_muse/tools/file_modifications.py +1099 -0
  375. code_muse/tools/file_operations.py +860 -0
  376. code_muse/tools/image_tools.py +185 -0
  377. code_muse/tools/meetin_proxy/__init__.py +243 -0
  378. code_muse/tools/meetin_proxy/capture_addon.py +82 -0
  379. code_muse/tools/meetin_proxy/proxy_manager.py +326 -0
  380. code_muse/tools/meetin_proxy/register_callbacks.py +45 -0
  381. code_muse/tools/path_policy.py +219 -0
  382. code_muse/tools/skills_tools.py +586 -0
  383. code_muse/tools/subagent_context.py +158 -0
  384. code_muse/tools/tools_content.py +50 -0
  385. code_muse/tools/universal_constructor.py +965 -0
  386. code_muse/uvx_detection.py +241 -0
  387. code_muse/version_checker.py +86 -0
  388. code_muse-0.0.1.data/data/code_muse/models.json +66 -0
  389. code_muse-0.0.1.data/data/code_muse/models_dev_api.json +1 -0
  390. code_muse-0.0.1.dist-info/METADATA +845 -0
  391. code_muse-0.0.1.dist-info/RECORD +394 -0
  392. code_muse-0.0.1.dist-info/WHEEL +4 -0
  393. code_muse-0.0.1.dist-info/entry_points.txt +2 -0
  394. code_muse-0.0.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,1158 @@
1
+ """Rich console renderer for structured messages.
2
+
3
+ This module implements the presentation layer for Muse's messaging system.
4
+ It consumes structured messages from the MessageBus and renders them using Rich.
5
+
6
+ The renderer is responsible for ALL presentation decisions - the messages contain
7
+ only structured data with no formatting hints.
8
+ """
9
+
10
+ # Note: Syntax import removed - file content not displayed, only header
11
+ from pathlib import Path
12
+ from typing import Protocol, runtime_checkable
13
+
14
+ from rich.console import Console
15
+ from rich.markdown import Markdown
16
+ from rich.markup import escape as escape_rich_markup
17
+ from rich.panel import Panel
18
+ from rich.rule import Rule
19
+ from rich.table import Table
20
+
21
+ from code_muse.config import get_subagent_verbose
22
+ from code_muse.tools.common import format_diff_with_colors
23
+ from code_muse.tools.subagent_context import is_subagent
24
+
25
+ from .bus import MessageBus
26
+ from .commands import (
27
+ ConfirmationResponse,
28
+ SelectionResponse,
29
+ UserInputResponse,
30
+ )
31
+ from .messages import (
32
+ AgentReasoningMessage,
33
+ AgentResponseMessage,
34
+ AnyMessage,
35
+ ConfirmationRequest,
36
+ DiffMessage,
37
+ DividerMessage,
38
+ FileContentMessage,
39
+ FileListingMessage,
40
+ GrepResultMessage,
41
+ MessageLevel,
42
+ SelectionRequest,
43
+ ShellLineMessage,
44
+ ShellOutputMessage,
45
+ ShellStartMessage,
46
+ SkillActivateMessage,
47
+ SkillListMessage,
48
+ SpinnerControl,
49
+ StatusPanelMessage,
50
+ SubAgentInvocationMessage,
51
+ SubAgentResponseMessage,
52
+ TextMessage,
53
+ UniversalConstructorMessage,
54
+ UserInputRequest,
55
+ VersionCheckMessage,
56
+ )
57
+
58
+ # Note: Text and Tree were removed - no longer used in this implementation
59
+
60
+
61
+ # =============================================================================
62
+ # Renderer Protocol
63
+ # =============================================================================
64
+
65
+
66
+ @runtime_checkable
67
+ class RendererProtocol(Protocol):
68
+ """Protocol defining the interface for message renderers."""
69
+
70
+ async def render(self, message: AnyMessage) -> None:
71
+ """Render a single message."""
72
+ ...
73
+
74
+ async def start(self) -> None:
75
+ """Start the renderer (begin consuming messages)."""
76
+ ...
77
+
78
+ async def stop(self) -> None:
79
+ """Stop the renderer."""
80
+ ...
81
+
82
+
83
+ # =============================================================================
84
+ # Default Styles
85
+ # =============================================================================
86
+
87
+ DEFAULT_STYLES: dict[MessageLevel, str] = {
88
+ MessageLevel.ERROR: "bold red",
89
+ MessageLevel.WARNING: "yellow",
90
+ MessageLevel.SUCCESS: "green",
91
+ MessageLevel.INFO: "white",
92
+ MessageLevel.DEBUG: "dim",
93
+ }
94
+
95
+ DIFF_STYLES = {
96
+ "add": "green",
97
+ "remove": "red",
98
+ "context": "dim",
99
+ }
100
+
101
+
102
+ # =============================================================================
103
+ # Rich Console Renderer
104
+ # =============================================================================
105
+
106
+
107
+ class RichConsoleRenderer:
108
+ """Rich console implementation of the renderer protocol.
109
+
110
+ This renderer consumes messages from a MessageBus and renders them using Rich.
111
+ It uses a background thread for synchronous compatibility with the main loop.
112
+ """
113
+
114
+ def __init__(
115
+ self,
116
+ bus: MessageBus,
117
+ console: Console | None = None,
118
+ styles: dict[MessageLevel, str | None] = None,
119
+ ) -> None:
120
+ """Initialize the renderer.
121
+
122
+ Args:
123
+ bus: The MessageBus to consume messages from.
124
+ console: Rich Console instance (creates default if None).
125
+ styles: Custom style mappings (uses DEFAULT_STYLES if None).
126
+ """
127
+ import threading
128
+
129
+ self._bus = bus
130
+ self._console = console or Console()
131
+ self._styles = styles or DEFAULT_STYLES.copy()
132
+ self._running = False
133
+ self._thread: threading.Thread | None = None
134
+ self._spinners: dict[str, object] = {} # spinner_id -> status context
135
+
136
+ @property
137
+ def console(self) -> Console:
138
+ """Get the Rich console."""
139
+ return self._console
140
+
141
+ def _get_banner_color(self, banner_name: str) -> str:
142
+ """Get the configured color for a banner.
143
+
144
+ Args:
145
+ banner_name: The banner identifier (e.g., 'thinking', 'shell_command')
146
+
147
+ Returns:
148
+ Rich color name for the banner background
149
+ """
150
+ from code_muse.config import get_banner_color
151
+
152
+ return get_banner_color(banner_name)
153
+
154
+ def _format_banner(self, banner_name: str, text: str) -> str:
155
+ """Format a banner with its configured color.
156
+
157
+ Args:
158
+ banner_name: The banner identifier
159
+ text: The banner text
160
+
161
+ Returns:
162
+ Rich markup string for the banner
163
+ """
164
+ color = self._get_banner_color(banner_name)
165
+ # TODO: PEP 750 t-string — use templatelib when stable
166
+ return f"[bold white on {color}] {text} [/bold white on {color}]"
167
+
168
+ def _should_suppress_subagent_output(self) -> bool:
169
+ """Check if sub-agent output should be suppressed.
170
+
171
+ Returns:
172
+ True if we're in a sub-agent context and verbose mode is disabled
173
+ """
174
+ return is_subagent() and not get_subagent_verbose()
175
+
176
+ # =========================================================================
177
+ # Lifecycle (Synchronous - for compatibility with main.py)
178
+ # =========================================================================
179
+
180
+ def start(self) -> None:
181
+ """Start the renderer in a background thread.
182
+
183
+ This is synchronous to match the old SynchronousInteractiveRenderer API.
184
+ """
185
+ import threading
186
+
187
+ if self._running:
188
+ return
189
+
190
+ self._running = True
191
+ self._bus.mark_renderer_active()
192
+
193
+ # Start background thread for message consumption
194
+ self._thread = threading.Thread(target=self._consume_loop_sync, daemon=True)
195
+ self._thread.start()
196
+
197
+ def stop(self) -> None:
198
+ """Stop the renderer.
199
+
200
+ This is synchronous to match the old SynchronousInteractiveRenderer API.
201
+ """
202
+ self._running = False
203
+ self._bus.mark_renderer_inactive()
204
+
205
+ if self._thread and self._thread.is_alive():
206
+ self._thread.join(timeout=1.0)
207
+ self._thread = None
208
+
209
+ def _consume_loop_sync(self) -> None:
210
+ """Synchronous message consumption loop running in background thread."""
211
+ import time
212
+
213
+ # First, process any buffered messages
214
+ for msg in self._bus.get_buffered_messages():
215
+ self._render_sync(msg)
216
+ self._bus.clear_buffer()
217
+
218
+ # Then consume new messages
219
+ while self._running:
220
+ message = self._bus.get_message_nowait()
221
+ if message:
222
+ self._render_sync(message)
223
+ else:
224
+ time.sleep(0.01)
225
+
226
+ def _render_sync(self, message: AnyMessage) -> None:
227
+ """Render a message synchronously with error handling."""
228
+ try:
229
+ self._do_render(message)
230
+ except Exception as e:
231
+ # Don't let rendering errors crash the loop
232
+ # Escape the error message to prevent nested markup errors
233
+ safe_error = escape_rich_markup(str(e))
234
+ self._console.print(f"[dim red]Render error: {safe_error}[/dim red]")
235
+
236
+ # =========================================================================
237
+ # Async Lifecycle (for future async-first usage)
238
+ # =========================================================================
239
+
240
+ async def start_async(self) -> None:
241
+ """Start the renderer asynchronously."""
242
+ if self._running:
243
+ return
244
+
245
+ self._running = True
246
+ self._bus.mark_renderer_active()
247
+
248
+ # Process any buffered messages first
249
+ for msg in self._bus.get_buffered_messages():
250
+ self._render_sync(msg)
251
+ self._bus.clear_buffer()
252
+
253
+ async def stop_async(self) -> None:
254
+ """Stop the renderer asynchronously."""
255
+ self._running = False
256
+ self._bus.mark_renderer_inactive()
257
+
258
+ # =========================================================================
259
+ # Main Dispatch
260
+ # =========================================================================
261
+
262
+ def _do_render(self, message: AnyMessage) -> None:
263
+ """Synchronously render a message by dispatching to the appropriate handler.
264
+
265
+ Note: User input requests are skipped in sync mode as they require async.
266
+ """
267
+ # Dispatch based on message type
268
+ if isinstance(message, TextMessage):
269
+ self._render_text(message)
270
+ elif isinstance(message, FileListingMessage):
271
+ self._render_file_listing(message)
272
+ elif isinstance(message, FileContentMessage):
273
+ self._render_file_content(message)
274
+ elif isinstance(message, GrepResultMessage):
275
+ self._render_grep_result(message)
276
+ elif isinstance(message, DiffMessage):
277
+ self._render_diff(message)
278
+ elif isinstance(message, ShellStartMessage):
279
+ self._render_shell_start(message)
280
+ elif isinstance(message, ShellLineMessage):
281
+ self._render_shell_line(message)
282
+ elif isinstance(message, ShellOutputMessage):
283
+ self._render_shell_output(message)
284
+ elif isinstance(message, AgentReasoningMessage):
285
+ self._render_agent_reasoning(message)
286
+ elif isinstance(message, AgentResponseMessage):
287
+ # Skip rendering - we now stream agent responses via event_stream_handler
288
+ pass
289
+ elif isinstance(message, SubAgentInvocationMessage):
290
+ self._render_subagent_invocation(message)
291
+ elif isinstance(message, SubAgentResponseMessage):
292
+ # Skip rendering - we now display sub-agent responses via display_non_streamed_result
293
+ pass
294
+ elif isinstance(message, UniversalConstructorMessage):
295
+ self._render_universal_constructor(message)
296
+ elif isinstance(message, UserInputRequest):
297
+ # Can't handle async user input in sync context - skip
298
+ self._console.print("[dim]User input requested (requires async)[/dim]")
299
+ elif isinstance(message, ConfirmationRequest):
300
+ # Can't handle async confirmation in sync context - skip
301
+ self._console.print("[dim]Confirmation requested (requires async)[/dim]")
302
+ elif isinstance(message, SelectionRequest):
303
+ # Can't handle async selection in sync context - skip
304
+ self._console.print("[dim]Selection requested (requires async)[/dim]")
305
+ elif isinstance(message, SpinnerControl):
306
+ self._render_spinner_control(message)
307
+ elif isinstance(message, DividerMessage):
308
+ self._render_divider(message)
309
+ elif isinstance(message, StatusPanelMessage):
310
+ self._render_status_panel(message)
311
+ elif isinstance(message, VersionCheckMessage):
312
+ self._render_version_check(message)
313
+ elif isinstance(message, SkillListMessage):
314
+ self._render_skill_list(message)
315
+ elif isinstance(message, SkillActivateMessage):
316
+ self._render_skill_activate(message)
317
+ else:
318
+ # Unknown message type - render as debug
319
+ self._console.print(f"[dim]Unknown message: {type(message).__name__}[/dim]")
320
+
321
+ async def render(self, message: AnyMessage) -> None:
322
+ """Render a message asynchronously (supports user input requests)."""
323
+ # Handle async-only message types
324
+ if isinstance(message, UserInputRequest):
325
+ await self._render_user_input_request(message)
326
+ elif isinstance(message, ConfirmationRequest):
327
+ await self._render_confirmation_request(message)
328
+ elif isinstance(message, SelectionRequest):
329
+ await self._render_selection_request(message)
330
+ else:
331
+ # Use sync render for everything else
332
+ self._do_render(message)
333
+
334
+ # =========================================================================
335
+ # Text Messages
336
+ # =========================================================================
337
+
338
+ def _render_text(self, msg: TextMessage) -> None:
339
+ """Render a text message with appropriate styling.
340
+
341
+ Text is escaped to prevent Rich markup injection which could crash
342
+ the renderer if malformed tags are present in shell output or other
343
+ user-provided content.
344
+ """
345
+ style = self._styles.get(msg.level, "white")
346
+
347
+ # Make version messages dim
348
+ if "Current version:" in msg.text or "Latest version:" in msg.text:
349
+ style = "dim"
350
+
351
+ prefix = self._get_level_prefix(msg.level)
352
+ # Escape Rich markup to prevent crashes from malformed tags
353
+ safe_text = escape_rich_markup(msg.text)
354
+ self._console.print(f"{prefix}{safe_text}", style=style)
355
+
356
+ def _get_level_prefix(self, level: MessageLevel) -> str:
357
+ """Get a prefix icon for the message level."""
358
+ prefixes = {
359
+ MessageLevel.ERROR: "✗ ",
360
+ MessageLevel.WARNING: "⚠ ",
361
+ MessageLevel.SUCCESS: "✓ ",
362
+ MessageLevel.INFO: "ℹ ",
363
+ MessageLevel.DEBUG: "• ",
364
+ }
365
+ return prefixes.get(level, "")
366
+
367
+ # =========================================================================
368
+ # File Operations
369
+ # =========================================================================
370
+
371
+ def _render_file_listing(self, msg: FileListingMessage) -> None:
372
+ """Render a compact directory listing with directory summaries.
373
+
374
+ Instead of listing every file, we group by directory and show:
375
+ - Directory name
376
+ - Number of files
377
+ - Total size
378
+ - Number of subdirectories
379
+ """
380
+ # Skip for sub-agents unless verbose mode
381
+ if self._should_suppress_subagent_output():
382
+ return
383
+
384
+ from collections import defaultdict
385
+ from pathlib import Path
386
+
387
+ # Header on single line
388
+ rec_flag = f"(recursive={msg.recursive})"
389
+ banner = self._format_banner("directory_listing", "DIRECTORY LISTING")
390
+ self._console.print(
391
+ # TODO: PEP 750 t-string — use templatelib when stable
392
+ f"\n{banner} "
393
+ f"📂 [bold cyan]{msg.directory}[/bold cyan] [dim]{rec_flag}[/dim]\n"
394
+ )
395
+
396
+ # Build a tree structure: {parent_path: {files: [], dirs: set(), size: int}}
397
+ # Each key is a directory path, value contains direct children stats
398
+ dir_stats: dict = defaultdict(
399
+ lambda: {"files": [], "subdirs": set(), "total_size": 0}
400
+ )
401
+
402
+ # Root directory is represented as ""
403
+ root_key = ""
404
+
405
+ for entry in msg.files:
406
+ path = entry.path
407
+ parent = (
408
+ str(Path(path).parent) if Path(path).parent != Path(".") else root_key
409
+ )
410
+
411
+ if entry.type == "dir":
412
+ # Register this dir as a subdir of its parent
413
+ dir_stats[parent]["subdirs"].add(path)
414
+ # Ensure the dir itself exists in stats (even if empty)
415
+ _ = dir_stats[path]
416
+ else:
417
+ # It's a file - add to parent's stats
418
+ dir_stats[parent]["files"].append(entry)
419
+ dir_stats[parent]["total_size"] += entry.size
420
+
421
+ def render_dir_tree(dir_path: str, depth: int = 0) -> None:
422
+ """Recursively render directory with compact summary."""
423
+ stats = dir_stats.get(
424
+ dir_path, {"files": [], "subdirs": set(), "total_size": 0}
425
+ )
426
+ files = stats["files"]
427
+ subdirs = sorted(stats["subdirs"])
428
+
429
+ # Calculate total size including subdirectories (recursive)
430
+ def get_recursive_size(d: str) -> int:
431
+ s = dir_stats.get(d, {"files": [], "subdirs": set(), "total_size": 0})
432
+ size = s["total_size"]
433
+ for sub in s["subdirs"]:
434
+ size += get_recursive_size(sub)
435
+ return size
436
+
437
+ def get_recursive_file_count(d: str) -> int:
438
+ s = dir_stats.get(d, {"files": [], "subdirs": set(), "total_size": 0})
439
+ count = len(s["files"])
440
+ for sub in s["subdirs"]:
441
+ count += get_recursive_file_count(sub)
442
+ return count
443
+
444
+ indent = " " * depth
445
+
446
+ # For root level, just show contents
447
+ if dir_path == root_key:
448
+ # Show files at root level (depth 0)
449
+ for f in sorted(files, key=lambda x: x.path):
450
+ icon = self._get_file_icon(f.path)
451
+ name = Path(f.path).name
452
+ size_str = (
453
+ f" [dim]({self._format_size(f.size)})[/dim]"
454
+ if f.size > 0
455
+ else ""
456
+ )
457
+ self._console.print(
458
+ f"{indent}{icon} [green]{name}[/green]{size_str}"
459
+ )
460
+
461
+ # Show subdirs at root level
462
+ for subdir in subdirs:
463
+ render_dir_tree(subdir, depth)
464
+ else:
465
+ # Show directory with summary
466
+ dir_name = Path(dir_path).name
467
+ rec_size = get_recursive_size(dir_path)
468
+ rec_file_count = get_recursive_file_count(dir_path)
469
+ subdir_count = len(subdirs)
470
+
471
+ # Build summary parts
472
+ parts = []
473
+ if rec_file_count > 0:
474
+ parts.append(
475
+ f"{rec_file_count} file{'s' if rec_file_count != 1 else ''}"
476
+ )
477
+ if subdir_count > 0:
478
+ parts.append(
479
+ f"{subdir_count} subdir{'s' if subdir_count != 1 else ''}"
480
+ )
481
+ if rec_size > 0:
482
+ parts.append(self._format_size(rec_size))
483
+
484
+ summary = f" [dim]({', '.join(parts)})[/dim]" if parts else ""
485
+ self._console.print(
486
+ f"{indent}📁 [bold blue]{dir_name}/[/bold blue]{summary}"
487
+ )
488
+
489
+ # Recursively show subdirectories
490
+ for subdir in subdirs:
491
+ render_dir_tree(subdir, depth + 1)
492
+
493
+ # Render the tree starting from root
494
+ render_dir_tree(root_key, 0)
495
+
496
+ # Summary
497
+ self._console.print("\n[bold cyan]Summary:[/bold cyan]")
498
+ self._console.print(
499
+ f"📁 [blue]{msg.dir_count} directories[/blue], "
500
+ f"📄 [green]{msg.file_count} files[/green] "
501
+ f"[dim]({self._format_size(msg.total_size)} total)[/dim]"
502
+ )
503
+
504
+ def _render_file_content(self, msg: FileContentMessage) -> None:
505
+ """Render a file read - just show the header, not the content.
506
+
507
+ The file content is for the LLM only, not for display in the UI.
508
+ """
509
+ # Skip for sub-agents unless verbose mode
510
+ if self._should_suppress_subagent_output():
511
+ return
512
+
513
+ # Build line info
514
+ line_info = ""
515
+ if msg.start_line is not None and msg.num_lines is not None:
516
+ end_line = msg.start_line + msg.num_lines - 1
517
+ line_info = f" [dim](lines {msg.start_line}-{end_line})[/dim]"
518
+
519
+ # Just print the header - content is for LLM only
520
+ banner = self._format_banner("read_file", "READ FILE")
521
+ self._console.print(
522
+ f"\n{banner} 📂 [bold cyan]{msg.path}[/bold cyan]{line_info}"
523
+ )
524
+
525
+ def _render_grep_result(self, msg: GrepResultMessage) -> None:
526
+ """Render grep results grouped by file matching old format."""
527
+ # Skip for sub-agents unless verbose mode
528
+ if self._should_suppress_subagent_output():
529
+ return
530
+
531
+ import re
532
+
533
+ # Header
534
+ banner = self._format_banner("grep", "GREP")
535
+ self._console.print(
536
+ f"\n{banner} 📂 [dim]{msg.directory} for '{msg.search_term}'[/dim]"
537
+ )
538
+
539
+ if not msg.matches:
540
+ self._console.print(
541
+ f"[dim]No matches found for '{msg.search_term}' "
542
+ f"in {msg.directory}[/dim]"
543
+ )
544
+ return
545
+
546
+ # Group by file
547
+ by_file: dict[str, list] = {}
548
+ for match in msg.matches:
549
+ by_file.setdefault(match.file_path, []).append(match)
550
+
551
+ # Show verbose or concise based on message flag
552
+ if msg.verbose:
553
+ # Verbose mode: Show full output with line numbers and content
554
+ for file_path in sorted(by_file.keys()):
555
+ file_matches = by_file[file_path]
556
+ match_word = "match" if len(file_matches) == 1 else "matches"
557
+ self._console.print(
558
+ f"\n[dim]📄 {file_path} ({len(file_matches)} {match_word})[/dim]"
559
+ )
560
+
561
+ # Show each match with line number and content
562
+ for match in file_matches:
563
+ line = match.line_content
564
+ # Extract the actual search term (not ripgrep flags)
565
+ parts = msg.search_term.split()
566
+ search_term = msg.search_term # fallback
567
+ for part in parts:
568
+ if not part.startswith("-"):
569
+ search_term = part
570
+ break
571
+
572
+ # Case-insensitive highlighting
573
+ if search_term and not search_term.startswith("-"):
574
+ highlighted_line = re.sub(
575
+ f"({re.escape(search_term)})",
576
+ r"[bold yellow]\1[/bold yellow]",
577
+ line,
578
+ flags=re.IGNORECASE,
579
+ )
580
+ else:
581
+ highlighted_line = line
582
+
583
+ ln = match.line_number
584
+ self._console.print(f" [dim]{ln:4d}[/dim] │ {highlighted_line}")
585
+ else:
586
+ # Concise mode (default): Show only file summaries
587
+ self._console.print("")
588
+ for file_path in sorted(by_file.keys()):
589
+ file_matches = by_file[file_path]
590
+ match_word = "match" if len(file_matches) == 1 else "matches"
591
+ self._console.print(
592
+ f"[dim]📄 {file_path} ({len(file_matches)} {match_word})[/dim]"
593
+ )
594
+
595
+ # Summary - subtle
596
+ match_word = "match" if msg.total_matches == 1 else "matches"
597
+ file_word = "file" if len(by_file) == 1 else "files"
598
+ num_files = len(by_file)
599
+ self._console.print(
600
+ f"[dim]Found {msg.total_matches} {match_word} "
601
+ f"across {num_files} {file_word}[/dim]"
602
+ )
603
+
604
+ # Trailing newline for spinner separation
605
+ self._console.print()
606
+
607
+ # =========================================================================
608
+ # Diff
609
+ # =========================================================================
610
+
611
+ def _render_diff(self, msg: DiffMessage) -> None:
612
+ """Render a diff with beautiful syntax highlighting."""
613
+ # Skip for sub-agents unless verbose mode
614
+ if self._should_suppress_subagent_output():
615
+ return
616
+
617
+ # Operation-specific styling
618
+ op_icons = {"create": "✨", "modify": "✏️", "delete": "🗑️"}
619
+ op_colors = {"create": "green", "modify": "yellow", "delete": "red"}
620
+ icon = op_icons.get(msg.operation, "📄")
621
+ op_color = op_colors.get(msg.operation, "white")
622
+
623
+ # Choose banner based on operation type
624
+ if msg.operation == "create":
625
+ banner = self._format_banner("create_file", "CREATE FILE")
626
+ elif msg.operation == "delete":
627
+ banner = self._format_banner("delete_file", "DELETE FILE")
628
+ else:
629
+ banner = self._format_banner("replace_in_file", "EDIT FILE")
630
+ self._console.print(
631
+ f"\n{banner} "
632
+ f"{icon} [{op_color}]{msg.operation.upper()}[/{op_color}] "
633
+ f"[bold cyan]{msg.path}[/bold cyan]"
634
+ )
635
+
636
+ if not msg.diff_lines:
637
+ return
638
+
639
+ # Reconstruct unified diff text from diff_lines for format_diff_with_colors
640
+ diff_text_lines = []
641
+ for line in msg.diff_lines:
642
+ if line.type == "add":
643
+ diff_text_lines.append(f"+{line.content}")
644
+ elif line.type == "remove":
645
+ diff_text_lines.append(f"-{line.content}")
646
+ else: # context
647
+ # Don't add space prefix to diff headers - they need to be preserved
648
+ # exactly for syntax highlighting to detect the file extension
649
+ if line.content.startswith(("---", "+++", "@@", "diff ", "index ")):
650
+ diff_text_lines.append(line.content)
651
+ else:
652
+ diff_text_lines.append(f" {line.content}")
653
+
654
+ diff_text = "\n".join(diff_text_lines)
655
+
656
+ # Use the beautiful syntax-highlighted diff formatter
657
+ formatted_diff = format_diff_with_colors(diff_text)
658
+ self._console.print(formatted_diff)
659
+
660
+ # =========================================================================
661
+ # Shell Output
662
+ # =========================================================================
663
+
664
+ def _render_shell_start(self, msg: ShellStartMessage) -> None:
665
+ """Render shell command start notification."""
666
+ # Skip for sub-agents unless verbose mode
667
+ if self._should_suppress_subagent_output():
668
+ return
669
+
670
+ # Escape command to prevent Rich markup injection
671
+ safe_command = escape_rich_markup(msg.command)
672
+ # Header showing command is starting
673
+ banner = self._format_banner("shell_command", "SHELL COMMAND")
674
+
675
+ # Add background indicator if running in background mode
676
+ if msg.background:
677
+ self._console.print(
678
+ f"\n{banner} 🚀 [dim]$ {safe_command}[/dim] [bold magenta][BACKGROUND 🌙][/bold magenta]"
679
+ )
680
+ else:
681
+ self._console.print(f"\n{banner} 🚀 [dim]$ {safe_command}[/dim]")
682
+
683
+ # Show working directory if specified
684
+ if msg.cwd:
685
+ safe_cwd = escape_rich_markup(msg.cwd)
686
+ self._console.print(f"[dim]📂 Working directory: {safe_cwd}[/dim]")
687
+
688
+ # Show timeout or background status
689
+ if msg.background:
690
+ self._console.print("[dim]⏱ Runs detached (no timeout)[/dim]")
691
+ else:
692
+ self._console.print(f"[dim]⏱ Timeout: {msg.timeout}s[/dim]")
693
+
694
+ def _render_shell_line(self, msg: ShellLineMessage) -> None:
695
+ """Render shell output line preserving ANSI codes and carriage returns."""
696
+ import sys
697
+
698
+ from rich.text import Text
699
+
700
+ # Check if line contains carriage return (progress bar style output)
701
+ if "\r" in msg.line:
702
+ # Bypass Rich entirely - write directly to stdout so terminal interprets \r
703
+ # Apply dim styling manually via ANSI codes
704
+ sys.stdout.write(f"\033[2m{msg.line}\033[0m")
705
+ sys.stdout.flush()
706
+ else:
707
+ # Normal line: use Rich for nice formatting
708
+ text = Text.from_ansi(msg.line)
709
+ self._console.print(text, style="dim")
710
+
711
+ def _render_shell_output(self, msg: ShellOutputMessage) -> None:
712
+ """Render shell command output - just a trailing newline for spinner separation.
713
+
714
+ Shell command results are already returned to the LLM via tool responses,
715
+ so we don't need to clutter the UI with redundant output.
716
+ """
717
+ # Just print trailing newline for spinner separation
718
+ self._console.print()
719
+
720
+ # =========================================================================
721
+ # Agent Messages
722
+ # =========================================================================
723
+
724
+ def _render_agent_reasoning(self, msg: AgentReasoningMessage) -> None:
725
+ """Render agent reasoning matching old format."""
726
+ # Header matching old format
727
+ banner = self._format_banner("agent_reasoning", "AGENT REASONING")
728
+ self._console.print(f"\n{banner}")
729
+
730
+ # Current reasoning
731
+ self._console.print("[bold cyan]Current reasoning:[/bold cyan]")
732
+ # Render reasoning as markdown
733
+ md = Markdown(msg.reasoning)
734
+ self._console.print(md)
735
+
736
+ # Next steps (if any)
737
+ if msg.next_steps and msg.next_steps.strip():
738
+ self._console.print("\n[bold cyan]Planned next steps:[/bold cyan]")
739
+ md_steps = Markdown(msg.next_steps)
740
+ self._console.print(md_steps)
741
+
742
+ # Trailing newline for spinner separation
743
+ self._console.print()
744
+
745
+ def _render_agent_response(self, msg: AgentResponseMessage) -> None:
746
+ """Render agent response with header and markdown formatting."""
747
+ # Header
748
+ banner = self._format_banner("agent_response", "AGENT RESPONSE")
749
+ self._console.print(f"\n{banner}\n")
750
+
751
+ # Content (markdown or plain)
752
+ if msg.is_markdown:
753
+ md = Markdown(msg.content)
754
+ self._console.print(md)
755
+ else:
756
+ self._console.print(msg.content)
757
+
758
+ def _render_subagent_invocation(self, msg: SubAgentInvocationMessage) -> None:
759
+ """Render sub-agent invocation header with nice formatting."""
760
+ # Skip for sub-agents unless verbose mode (avoid nested invocation banners)
761
+ if self._should_suppress_subagent_output():
762
+ return
763
+
764
+ # Header with agent name and session
765
+ session_type = (
766
+ "New session"
767
+ if msg.is_new_session
768
+ else f"Continuing ({msg.message_count} messages)"
769
+ )
770
+ banner = self._format_banner("invoke_agent", "🤖 INVOKE AGENT")
771
+ self._console.print(
772
+ f"\n{banner} "
773
+ f"[bold cyan]{msg.agent_name}[/bold cyan] "
774
+ f"[dim]({session_type})[/dim]"
775
+ )
776
+
777
+ # Session ID
778
+ self._console.print(f"[dim]Session:[/dim] [bold]{msg.session_id}[/bold]")
779
+
780
+ # Prompt (truncated if too long, rendered as markdown)
781
+ prompt_display = (
782
+ msg.prompt[:200] + "..." if len(msg.prompt) > 200 else msg.prompt
783
+ )
784
+ self._console.print("[dim]Prompt:[/dim]")
785
+ md_prompt = Markdown(prompt_display)
786
+ self._console.print(md_prompt)
787
+
788
+ def _render_subagent_response(self, msg: SubAgentResponseMessage) -> None:
789
+ """Render sub-agent response with markdown formatting."""
790
+ # Response header
791
+ banner = self._format_banner("subagent_response", "✓ AGENT RESPONSE")
792
+ self._console.print(f"\n{banner} [bold cyan]{msg.agent_name}[/bold cyan]")
793
+
794
+ # Render response as markdown
795
+ md = Markdown(msg.response)
796
+ self._console.print(md)
797
+
798
+ # Footer with session info
799
+ self._console.print(
800
+ f"\n[dim]Session [bold]{msg.session_id}[/bold] saved "
801
+ f"({msg.message_count} messages)[/dim]"
802
+ )
803
+
804
+ def _render_universal_constructor(self, msg: UniversalConstructorMessage) -> None:
805
+ """Render universal_constructor tool output with banner."""
806
+ # Skip for sub-agents unless verbose mode
807
+ if self._should_suppress_subagent_output():
808
+ return
809
+
810
+ # Format banner
811
+ banner = self._format_banner("universal_constructor", "UNIVERSAL CONSTRUCTOR")
812
+
813
+ # Build the header line with action and optional tool name
814
+ # Escape user-controlled strings to prevent Rich markup injection
815
+ header_parts = [f"\n{banner} 🔧 [bold cyan]{msg.action.upper()}[/bold cyan]"]
816
+ if msg.tool_name:
817
+ safe_tool_name = escape_rich_markup(msg.tool_name)
818
+ header_parts.append(f" [dim]tool=[/dim][bold]{safe_tool_name}[/bold]")
819
+ self._console.print("".join(header_parts))
820
+
821
+ # Status indicator
822
+ safe_summary = escape_rich_markup(msg.summary) if msg.summary else ""
823
+ if msg.success:
824
+ self._console.print(f"[green]✓[/green] {safe_summary}")
825
+ else:
826
+ self._console.print(f"[red]✗[/red] {safe_summary}")
827
+
828
+ # Show details if present
829
+ if msg.details:
830
+ safe_details = escape_rich_markup(msg.details)
831
+ self._console.print(f"[dim]{safe_details}[/dim]")
832
+
833
+ # Trailing newline for spinner separation
834
+ self._console.print()
835
+
836
+ # =========================================================================
837
+ # User Interaction
838
+ # =========================================================================
839
+
840
+ async def _render_user_input_request(self, msg: UserInputRequest) -> None:
841
+ """Render input prompt and send response back to bus."""
842
+ prompt = msg.prompt_text
843
+ if msg.default_value:
844
+ prompt += f" [{msg.default_value}]"
845
+ prompt += ": "
846
+
847
+ # Get input (password hides input)
848
+ if msg.input_type == "password":
849
+ value = self._console.input(prompt, password=True)
850
+ else:
851
+ value = self._console.input(f"[cyan]{prompt}[/cyan]")
852
+
853
+ # Use default if empty
854
+ if not value and msg.default_value:
855
+ value = msg.default_value
856
+
857
+ # Send response back
858
+ response = UserInputResponse(prompt_id=msg.prompt_id, value=value)
859
+ self._bus.provide_response(response)
860
+
861
+ async def _render_confirmation_request(self, msg: ConfirmationRequest) -> None:
862
+ """Render confirmation dialog and send response back."""
863
+ # Show title and description - escape to prevent markup injection
864
+ safe_title = escape_rich_markup(msg.title)
865
+ safe_description = escape_rich_markup(msg.description)
866
+ self._console.print(f"\n[bold yellow]{safe_title}[/bold yellow]")
867
+ self._console.print(safe_description)
868
+
869
+ # Show options
870
+ options_str = "/".join(msg.options)
871
+ prompt = f"[{options_str}]"
872
+
873
+ while True:
874
+ choice = self._console.input(f"[cyan]{prompt}[/cyan] ").strip().lower()
875
+
876
+ # Check for match
877
+ for i, opt in enumerate(msg.options):
878
+ if choice == opt.lower() or choice == opt[0].lower():
879
+ confirmed = i == 0 # First option is "confirm"
880
+
881
+ # Get feedback if allowed
882
+ feedback = None
883
+ if msg.allow_feedback:
884
+ feedback = self._console.input(
885
+ "[dim]Feedback (optional): [/dim]"
886
+ )
887
+ feedback = feedback if feedback else None
888
+
889
+ response = ConfirmationResponse(
890
+ prompt_id=msg.prompt_id,
891
+ confirmed=confirmed,
892
+ feedback=feedback,
893
+ )
894
+ self._bus.provide_response(response)
895
+ return
896
+
897
+ self._console.print(f"[red]Please enter one of: {options_str}[/red]")
898
+
899
+ async def _render_selection_request(self, msg: SelectionRequest) -> None:
900
+ """Render selection menu and send response back."""
901
+ safe_prompt = escape_rich_markup(msg.prompt_text)
902
+ self._console.print(f"\n[bold]{safe_prompt}[/bold]")
903
+
904
+ # Show numbered options - escape to prevent markup injection
905
+ for i, opt in enumerate(msg.options):
906
+ safe_opt = escape_rich_markup(opt)
907
+ self._console.print(f" [cyan]{i + 1}[/cyan]. {safe_opt}")
908
+
909
+ if msg.allow_cancel:
910
+ self._console.print(" [dim]0. Cancel[/dim]")
911
+
912
+ while True:
913
+ choice = self._console.input("[cyan]Enter number: [/cyan]").strip()
914
+
915
+ try:
916
+ idx = int(choice)
917
+ if msg.allow_cancel and idx == 0:
918
+ response = SelectionResponse(
919
+ prompt_id=msg.prompt_id,
920
+ selected_index=-1,
921
+ selected_value="",
922
+ )
923
+ self._bus.provide_response(response)
924
+ return
925
+
926
+ if 1 <= idx <= len(msg.options):
927
+ response = SelectionResponse(
928
+ prompt_id=msg.prompt_id,
929
+ selected_index=idx - 1,
930
+ selected_value=msg.options[idx - 1],
931
+ )
932
+ self._bus.provide_response(response)
933
+ return
934
+ except ValueError:
935
+ pass
936
+
937
+ self._console.print(f"[red]Please enter 1-{len(msg.options)}[/red]")
938
+
939
+ # =========================================================================
940
+ # Control Messages
941
+ # =========================================================================
942
+
943
+ def _render_spinner_control(self, msg: SpinnerControl) -> None:
944
+ """Handle spinner control messages."""
945
+ # Note: Rich's spinner/status is typically used as a context manager.
946
+ # For full spinner support, we'd need a more complex implementation.
947
+ # For now, we just print the status text.
948
+ if msg.action == "start" and msg.text or msg.action == "update" and msg.text:
949
+ self._console.print(f"[dim]⠋ {msg.text}[/dim]")
950
+ elif msg.action == "stop":
951
+ pass # Spinner stopped
952
+
953
+ def _render_divider(self, msg: DividerMessage) -> None:
954
+ """Render a horizontal divider."""
955
+ chars = {"light": "─", "heavy": "━", "double": "═"}
956
+ char = chars.get(msg.style, "─")
957
+ rule = Rule(style="dim", characters=char)
958
+ self._console.print(rule)
959
+
960
+ # =========================================================================
961
+ # Status Messages
962
+ # =========================================================================
963
+
964
+ def _render_status_panel(self, msg: StatusPanelMessage) -> None:
965
+ """Render a status panel with key-value fields."""
966
+ table = Table(show_header=False, box=None, padding=(0, 1))
967
+ table.add_column("Key", style="bold cyan")
968
+ table.add_column("Value")
969
+
970
+ for key, value in msg.fields.items():
971
+ table.add_row(key, value)
972
+
973
+ panel = Panel(table, title=f"[bold]{msg.title}[/bold]", border_style="blue")
974
+ self._console.print(panel)
975
+
976
+ def _render_version_check(self, msg: VersionCheckMessage) -> None:
977
+ """Render version check information."""
978
+ if msg.update_available:
979
+ cur = msg.current_version
980
+ latest = msg.latest_version
981
+ self._console.print(f"[dim]⬆ Update available: {cur} → {latest}[/dim]")
982
+ else:
983
+ self._console.print(
984
+ f"[dim]✓ You're on the latest version ({msg.current_version})[/dim]"
985
+ )
986
+
987
+ # =========================================================================
988
+ # Helpers
989
+ # =========================================================================
990
+
991
+ def _format_size(self, size_bytes: int) -> str:
992
+ """Format byte size to human readable matching old format."""
993
+ if size_bytes < 1024:
994
+ return f"{size_bytes} B"
995
+ elif size_bytes < 1024 * 1024:
996
+ return f"{size_bytes / 1024:.1f} KB"
997
+ elif size_bytes < 1024 * 1024 * 1024:
998
+ return f"{size_bytes / (1024 * 1024):.1f} MB"
999
+ else:
1000
+ return f"{size_bytes / (1024 * 1024 * 1024):.1f} GB"
1001
+
1002
+ def _get_file_icon(self, file_path: str) -> str:
1003
+ """Get an emoji icon for a file based on its extension."""
1004
+ ext = Path(file_path).suffix.lower()
1005
+ icons = {
1006
+ # Python
1007
+ ".py": "🐍",
1008
+ ".pyw": "🐍",
1009
+ # JavaScript/TypeScript
1010
+ ".js": "📜",
1011
+ ".jsx": "📜",
1012
+ ".ts": "📜",
1013
+ ".tsx": "📜",
1014
+ # Web
1015
+ ".html": "🌐",
1016
+ ".htm": "🌐",
1017
+ ".xml": "🌐",
1018
+ ".css": "🎨",
1019
+ ".scss": "🎨",
1020
+ ".sass": "🎨",
1021
+ # Documentation
1022
+ ".md": "📝",
1023
+ ".markdown": "📝",
1024
+ ".rst": "📝",
1025
+ ".txt": "📝",
1026
+ # Config
1027
+ ".json": "⚙️",
1028
+ ".yaml": "⚙️",
1029
+ ".yml": "⚙️",
1030
+ ".toml": "⚙️",
1031
+ ".ini": "⚙️",
1032
+ # Images
1033
+ ".jpg": "🖼️",
1034
+ ".jpeg": "🖼️",
1035
+ ".png": "🖼️",
1036
+ ".gif": "🖼️",
1037
+ ".svg": "🖼️",
1038
+ ".webp": "🖼️",
1039
+ # Audio
1040
+ ".mp3": "🎵",
1041
+ ".wav": "🎵",
1042
+ ".ogg": "🎵",
1043
+ ".flac": "🎵",
1044
+ # Video
1045
+ ".mp4": "🎬",
1046
+ ".avi": "🎬",
1047
+ ".mov": "🎬",
1048
+ ".webm": "🎬",
1049
+ # Documents
1050
+ ".pdf": "📄",
1051
+ ".doc": "📄",
1052
+ ".docx": "📄",
1053
+ ".xls": "📄",
1054
+ ".xlsx": "📄",
1055
+ ".ppt": "📄",
1056
+ ".pptx": "📄",
1057
+ # Archives
1058
+ ".zip": "📦",
1059
+ ".tar": "📦",
1060
+ ".gz": "📦",
1061
+ ".rar": "📦",
1062
+ ".7z": "📦",
1063
+ # Executables
1064
+ ".exe": "⚡",
1065
+ ".dll": "⚡",
1066
+ ".so": "⚡",
1067
+ ".dylib": "⚡",
1068
+ }
1069
+ return icons.get(ext, "📄")
1070
+
1071
+ # =========================================================================
1072
+ # Skills
1073
+ # =========================================================================
1074
+
1075
+ def _render_skill_list(self, msg: SkillListMessage) -> None:
1076
+ """Render a list of available skills."""
1077
+ # Skip for sub-agents unless verbose mode
1078
+ if self._should_suppress_subagent_output():
1079
+ return
1080
+
1081
+ # Banner
1082
+ banner = self._format_banner("agent_response", "LIST SKILLS")
1083
+ query_info = f" matching [cyan]'{msg.query}'[/cyan]" if msg.query else ""
1084
+ self._console.print(
1085
+ f"\n{banner} 🛠️ Found [bold]{msg.total_count}[/bold] skill(s){query_info}\n"
1086
+ )
1087
+
1088
+ if not msg.skills:
1089
+ self._console.print("[dim] No skills found.[/dim]")
1090
+ self._console.print(
1091
+ "[dim] Install skills in ~/.muse/skills/[/dim]\n"
1092
+ )
1093
+ return
1094
+
1095
+ # Create a table for skills
1096
+ table = Table(show_header=True, header_style="bold", box=None, padding=(0, 2))
1097
+ table.add_column("Status", style="dim", width=8)
1098
+ table.add_column("Name", style="cyan")
1099
+ table.add_column("Description", style="dim")
1100
+ table.add_column("Tags", style="yellow dim")
1101
+
1102
+ for skill in msg.skills:
1103
+ status = "[green]✓[/green]" if skill.enabled else "[red]✗[/red]"
1104
+ tags = ", ".join(skill.tags[:3]) if skill.tags else "-"
1105
+ # Truncate description if too long
1106
+ desc = skill.description
1107
+ if len(desc) > 50:
1108
+ desc = desc[:47] + "..."
1109
+ table.add_row(status, skill.name, desc, tags)
1110
+
1111
+ self._console.print(table)
1112
+ self._console.print()
1113
+
1114
+ def _render_skill_activate(self, msg: SkillActivateMessage) -> None:
1115
+ """Render skill activation result."""
1116
+ # Skip for sub-agents unless verbose mode
1117
+ if self._should_suppress_subagent_output():
1118
+ return
1119
+
1120
+ # Banner
1121
+ banner = self._format_banner("agent_response", "ACTIVATE SKILL")
1122
+ status = "[green]✓[/green]" if msg.success else "[red]✗[/red]"
1123
+ self._console.print(
1124
+ f"\n{banner} {status} [bold cyan]{msg.skill_name}[/bold cyan]\n"
1125
+ )
1126
+
1127
+ if msg.success:
1128
+ # Show path
1129
+ self._console.print(f" [dim]Path:[/dim] {msg.skill_path}")
1130
+
1131
+ # Show resource count
1132
+ if msg.resource_count > 0:
1133
+ self._console.print(
1134
+ f" [dim]Resources:[/dim] {msg.resource_count} bundled file(s)"
1135
+ )
1136
+
1137
+ # Show preview
1138
+ if msg.content_preview:
1139
+ preview = msg.content_preview.replace("\n", " ")[:100]
1140
+ if len(msg.content_preview) > 100:
1141
+ preview += "..."
1142
+ self._console.print(f" [dim]Preview:[/dim] {preview}")
1143
+ else:
1144
+ self._console.print(" [red]Activation failed[/red]")
1145
+
1146
+ self._console.print()
1147
+
1148
+
1149
+ # =============================================================================
1150
+ # Export all public symbols
1151
+ # =============================================================================
1152
+
1153
+ __all__ = [
1154
+ "RendererProtocol",
1155
+ "RichConsoleRenderer",
1156
+ "DEFAULT_STYLES",
1157
+ "DIFF_STYLES",
1158
+ ]