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,1407 @@
1
+ import asyncio
2
+ import fnmatch
3
+ import hashlib
4
+ import os
5
+ import sys
6
+ import time
7
+ from collections.abc import Callable
8
+ from pathlib import Path
9
+
10
+ from prompt_toolkit import Application
11
+ from prompt_toolkit.formatted_text import HTML
12
+ from prompt_toolkit.key_binding import KeyBindings
13
+ from prompt_toolkit.layout import Layout, Window
14
+ from prompt_toolkit.layout.controls import FormattedTextControl
15
+ from rapidfuzz.distance import JaroWinkler
16
+ from rich.console import Console
17
+ from rich.panel import Panel
18
+ from rich.prompt import Prompt
19
+ from rich.text import Text
20
+
21
+ # Syntax highlighting imports for "syntax" diff mode
22
+ try:
23
+ from pygments import lex
24
+ from pygments.lexers import TextLexer, get_lexer_by_name
25
+ from pygments.token import Token
26
+
27
+ PYGMENTS_AVAILABLE = True
28
+ except ImportError:
29
+ PYGMENTS_AVAILABLE = False
30
+
31
+ # Import our queue-based console system
32
+ try:
33
+ from code_muse.messaging import (
34
+ emit_error,
35
+ emit_info,
36
+ emit_success,
37
+ emit_warning,
38
+ get_queue_console,
39
+ )
40
+
41
+ # Use queue console by default, but allow fallback
42
+ NO_COLOR = bool(int(os.environ.get("MUSE_NO_COLOR", "0")))
43
+ _rich_console = Console(no_color=NO_COLOR)
44
+ console = get_queue_console()
45
+ # Set the fallback console for compatibility
46
+ console.fallback_console = _rich_console
47
+ except ImportError:
48
+ # Fallback to regular Rich console if messaging system not available
49
+ NO_COLOR = bool(int(os.environ.get("MUSE_NO_COLOR", "0")))
50
+ console = Console(no_color=NO_COLOR)
51
+
52
+ # Provide fallback emit functions
53
+ def emit_error(msg: str) -> None:
54
+ console.print(f"[bold red]{msg}[/bold red]")
55
+
56
+ def emit_info(msg: str) -> None:
57
+ console.print(msg)
58
+
59
+ def emit_success(msg: str) -> None:
60
+ console.print(f"[bold green]{msg}[/bold green]")
61
+
62
+ def emit_warning(msg: str) -> None:
63
+ console.print(f"[bold yellow]{msg}[/bold yellow]")
64
+
65
+
66
+ def should_suppress_browser() -> bool:
67
+ """Check if browsers should be suppressed (headless mode).
68
+
69
+ Returns:
70
+ True if browsers should be suppressed, False if they can open normally
71
+
72
+ This respects multiple headless mode controls:
73
+ - HEADLESS=true environment variable (suppresses ALL browsers)
74
+ - BROWSER_HEADLESS=true environment variable (for browser automation)
75
+ - CI=true environment variable (continuous integration)
76
+ - PYTEST_CURRENT_TEST environment variable (running under pytest)
77
+ """
78
+ # Explicit headless mode
79
+ if os.getenv("HEADLESS", "").lower() == "true":
80
+ return True
81
+
82
+ # Browser-specific headless mode
83
+ if os.getenv("BROWSER_HEADLESS", "").lower() == "true":
84
+ return True
85
+
86
+ # Continuous integration environments
87
+ if os.getenv("CI", "").lower() == "true":
88
+ return True
89
+
90
+ # Default to allowing browsers
91
+ return "PYTEST_CURRENT_TEST" in os.environ
92
+
93
+
94
+ # -------------------
95
+ # Shared ignore patterns/helpers
96
+ # Split into directory vs file patterns so tools can choose appropriately
97
+ # - list_files should ignore only directories (still show binary files inside non-ignored dirs)
98
+ # - grep should ignore both directories and files (avoid grepping binaries)
99
+ # -------------------
100
+ DIR_IGNORE_PATTERNS = [
101
+ # Version control
102
+ "**/.git/**",
103
+ "**/.git",
104
+ ".git/**",
105
+ ".git",
106
+ "**/.svn/**",
107
+ "**/.hg/**",
108
+ "**/.bzr/**",
109
+ # Node.js / JavaScript / TypeScript
110
+ "**/node_modules/**",
111
+ "**/node_modules/**/*.js",
112
+ "node_modules/**",
113
+ "node_modules",
114
+ "**/npm-debug.log*",
115
+ "**/yarn-debug.log*",
116
+ "**/yarn-error.log*",
117
+ "**/pnpm-debug.log*",
118
+ "**/.npm/**",
119
+ "**/.yarn/**",
120
+ "**/.pnpm-store/**",
121
+ "**/coverage/**",
122
+ "**/.nyc_output/**",
123
+ "**/dist/**",
124
+ "**/dist",
125
+ "**/build/**",
126
+ "**/build",
127
+ "**/.next/**",
128
+ "**/.nuxt/**",
129
+ "**/out/**",
130
+ "**/.cache/**",
131
+ "**/.parcel-cache/**",
132
+ "**/.vite/**",
133
+ "**/storybook-static/**",
134
+ "**/*.tsbuildinfo/**",
135
+ # Python
136
+ "**/__pycache__/**",
137
+ "**/__pycache__",
138
+ "__pycache__/**",
139
+ "__pycache__",
140
+ "**/*.pyc",
141
+ "**/*.pyo",
142
+ "**/*.pyd",
143
+ "**/.pytest_cache/**",
144
+ "**/.mypy_cache/**",
145
+ "**/.coverage",
146
+ "**/htmlcov/**",
147
+ "**/.tox/**",
148
+ "**/.nox/**",
149
+ "**/site-packages/**",
150
+ "**/.venv/**",
151
+ "**/.venv",
152
+ "**/venv/**",
153
+ "**/venv",
154
+ "**/env/**",
155
+ "**/ENV/**",
156
+ "**/.env",
157
+ "**/pip-wheel-metadata/**",
158
+ "**/*.egg-info/**",
159
+ "**/dist/**",
160
+ "**/wheels/**",
161
+ "**/pytest-reports/**",
162
+ # Java (Maven, Gradle, SBT)
163
+ "**/target/**",
164
+ "**/target",
165
+ "**/build/**",
166
+ "**/build",
167
+ "**/.gradle/**",
168
+ "**/gradle-app.setting",
169
+ "**/*.class",
170
+ "**/*.jar",
171
+ "**/*.war",
172
+ "**/*.ear",
173
+ "**/*.nar",
174
+ "**/hs_err_pid*",
175
+ "**/.classpath",
176
+ "**/.project",
177
+ "**/.settings/**",
178
+ "**/bin/**",
179
+ "**/project/target/**",
180
+ "**/project/project/**",
181
+ # Go
182
+ "**/vendor/**",
183
+ "**/*.exe",
184
+ "**/*.exe~",
185
+ "**/*.dll",
186
+ "**/*.so",
187
+ "**/*.dylib",
188
+ "**/*.test",
189
+ "**/*.out",
190
+ "**/go.work",
191
+ "**/go.work.sum",
192
+ # Rust
193
+ "**/target/**",
194
+ "**/Cargo.lock",
195
+ "**/*.pdb",
196
+ # Ruby
197
+ "**/vendor/**",
198
+ "**/.bundle/**",
199
+ "**/Gemfile.lock",
200
+ "**/*.gem",
201
+ "**/.rvm/**",
202
+ "**/.rbenv/**",
203
+ "**/coverage/**",
204
+ "**/.yardoc/**",
205
+ "**/doc/**",
206
+ "**/rdoc/**",
207
+ "**/.sass-cache/**",
208
+ "**/.jekyll-cache/**",
209
+ "**/_site/**",
210
+ # PHP
211
+ "**/vendor/**",
212
+ "**/composer.lock",
213
+ "**/.phpunit.result.cache",
214
+ "**/storage/logs/**",
215
+ "**/storage/framework/cache/**",
216
+ "**/storage/framework/sessions/**",
217
+ "**/storage/framework/testing/**",
218
+ "**/storage/framework/views/**",
219
+ "**/bootstrap/cache/**",
220
+ # .NET / C#
221
+ "**/bin/**",
222
+ "**/obj/**",
223
+ "**/packages/**",
224
+ "**/*.cache",
225
+ "**/*.dll",
226
+ "**/*.exe",
227
+ "**/*.pdb",
228
+ "**/*.user",
229
+ "**/*.suo",
230
+ "**/.vs/**",
231
+ "**/TestResults/**",
232
+ "**/BenchmarkDotNet.Artifacts/**",
233
+ # C/C++
234
+ "**/*.o",
235
+ "**/*.obj",
236
+ "**/*.so",
237
+ "**/*.dll",
238
+ "**/*.a",
239
+ "**/*.lib",
240
+ "**/*.dylib",
241
+ "**/*.exe",
242
+ "**/CMakeFiles/**",
243
+ "**/CMakeCache.txt",
244
+ "**/cmake_install.cmake",
245
+ "**/Makefile",
246
+ "**/compile_commands.json",
247
+ "**/.deps/**",
248
+ "**/.libs/**",
249
+ "**/autom4te.cache/**",
250
+ # Perl
251
+ "**/blib/**",
252
+ "**/_build/**",
253
+ "**/Build",
254
+ "**/Build.bat",
255
+ "**/*.tmp",
256
+ "**/*.bak",
257
+ "**/*.old",
258
+ "**/Makefile.old",
259
+ "**/MANIFEST.bak",
260
+ "**/META.yml",
261
+ "**/META.json",
262
+ "**/MYMETA.*",
263
+ "**/.prove",
264
+ # Scala
265
+ "**/target/**",
266
+ "**/project/target/**",
267
+ "**/project/project/**",
268
+ "**/.bloop/**",
269
+ "**/.metals/**",
270
+ "**/.ammonite/**",
271
+ "**/*.class",
272
+ # Elixir
273
+ "**/_build/**",
274
+ "**/deps/**",
275
+ "**/*.beam",
276
+ "**/.fetch",
277
+ "**/erl_crash.dump",
278
+ "**/*.ez",
279
+ "**/doc/**",
280
+ "**/.elixir_ls/**",
281
+ # Swift
282
+ "**/.build/**",
283
+ "**/Packages/**",
284
+ "**/*.xcodeproj/**",
285
+ "**/*.xcworkspace/**",
286
+ "**/DerivedData/**",
287
+ "**/xcuserdata/**",
288
+ "**/*.dSYM/**",
289
+ # Kotlin
290
+ "**/build/**",
291
+ "**/.gradle/**",
292
+ "**/*.class",
293
+ "**/*.jar",
294
+ "**/*.kotlin_module",
295
+ # Clojure
296
+ "**/target/**",
297
+ "**/.lein-**",
298
+ "**/.nrepl-port",
299
+ "**/pom.xml.asc",
300
+ "**/*.jar",
301
+ "**/*.class",
302
+ # Dart/Flutter
303
+ "**/.dart_tool/**",
304
+ "**/build/**",
305
+ "**/.packages",
306
+ "**/pubspec.lock",
307
+ "**/*.g.dart",
308
+ "**/*.freezed.dart",
309
+ "**/*.gr.dart",
310
+ # Haskell
311
+ "**/dist/**",
312
+ "**/dist-newstyle/**",
313
+ "**/.stack-work/**",
314
+ "**/*.hi",
315
+ "**/*.o",
316
+ "**/*.prof",
317
+ "**/*.aux",
318
+ "**/*.hp",
319
+ "**/*.eventlog",
320
+ "**/*.tix",
321
+ # Erlang
322
+ "**/ebin/**",
323
+ "**/rel/**",
324
+ "**/deps/**",
325
+ "**/*.beam",
326
+ "**/*.boot",
327
+ "**/*.plt",
328
+ "**/erl_crash.dump",
329
+ # Common cache and temp directories
330
+ "**/.cache/**",
331
+ "**/cache/**",
332
+ "**/tmp/**",
333
+ "**/temp/**",
334
+ "**/.tmp/**",
335
+ "**/.temp/**",
336
+ "**/logs/**",
337
+ "**/*.log",
338
+ "**/*.log.*",
339
+ # IDE and editor files
340
+ "**/.idea/**",
341
+ "**/.idea",
342
+ "**/.vscode/**",
343
+ "**/.vscode",
344
+ "**/*.swp",
345
+ "**/*.swo",
346
+ "**/*~",
347
+ "**/.#*",
348
+ "**/#*#",
349
+ "**/.emacs.d/auto-save-list/**",
350
+ "**/.vim/**",
351
+ "**/.netrwhist",
352
+ "**/Session.vim",
353
+ "**/.sublime-project",
354
+ "**/.sublime-workspace",
355
+ # OS-specific files
356
+ "**/.DS_Store",
357
+ ".DS_Store",
358
+ "**/Thumbs.db",
359
+ "**/Desktop.ini",
360
+ "**/.directory",
361
+ "**/*.lnk",
362
+ # Common artifacts
363
+ "**/*.orig",
364
+ "**/*.rej",
365
+ "**/*.patch",
366
+ "**/*.diff",
367
+ "**/.*.orig",
368
+ "**/.*.rej",
369
+ # Backup files
370
+ "**/*~",
371
+ "**/*.bak",
372
+ "**/*.backup",
373
+ "**/*.old",
374
+ "**/*.save",
375
+ # Hidden files (but be careful with this one)
376
+ "**/.*", # Commented out as it might be too aggressive
377
+ # Directory-only section ends here
378
+ ]
379
+
380
+ FILE_IGNORE_PATTERNS = [
381
+ # Binary image formats
382
+ "**/*.png",
383
+ "**/*.jpg",
384
+ "**/*.jpeg",
385
+ "**/*.gif",
386
+ "**/*.bmp",
387
+ "**/*.tiff",
388
+ "**/*.tif",
389
+ "**/*.webp",
390
+ "**/*.ico",
391
+ "**/*.svg",
392
+ # Binary document formats
393
+ "**/*.pdf",
394
+ "**/*.doc",
395
+ "**/*.docx",
396
+ "**/*.xls",
397
+ "**/*.xlsx",
398
+ "**/*.ppt",
399
+ "**/*.pptx",
400
+ # Archive formats
401
+ "**/*.zip",
402
+ "**/*.tar",
403
+ "**/*.gz",
404
+ "**/*.bz2",
405
+ "**/*.xz",
406
+ "**/*.rar",
407
+ "**/*.7z",
408
+ # Media files
409
+ "**/*.mp3",
410
+ "**/*.mp4",
411
+ "**/*.avi",
412
+ "**/*.mov",
413
+ "**/*.wmv",
414
+ "**/*.flv",
415
+ "**/*.wav",
416
+ "**/*.ogg",
417
+ # Font files
418
+ "**/*.ttf",
419
+ "**/*.otf",
420
+ "**/*.woff",
421
+ "**/*.woff2",
422
+ "**/*.eot",
423
+ # Other binary formats
424
+ "**/*.bin",
425
+ "**/*.dat",
426
+ "**/*.db",
427
+ "**/*.sqlite",
428
+ "**/*.sqlite3",
429
+ ]
430
+
431
+ # Backwards compatibility for any imports still referring to IGNORE_PATTERNS
432
+ IGNORE_PATTERNS = DIR_IGNORE_PATTERNS + FILE_IGNORE_PATTERNS
433
+
434
+
435
+ def should_ignore_path(path: str) -> bool:
436
+ """Return True if *path* matches any pattern in IGNORE_PATTERNS."""
437
+ # Convert path to Path object for better pattern matching
438
+ path_obj = Path(path)
439
+
440
+ for pattern in IGNORE_PATTERNS:
441
+ # Try pathlib's match method which handles ** patterns properly
442
+ try:
443
+ if path_obj.match(pattern):
444
+ return True
445
+ except ValueError:
446
+ # If pathlib can't handle the pattern, fall back to fnmatch
447
+ if fnmatch.fnmatch(path, pattern):
448
+ return True
449
+
450
+ # Additional check: if pattern contains **, try matching against
451
+ # different parts of the path to handle edge cases
452
+ if "**" in pattern:
453
+ # Convert pattern to handle different path representations
454
+ simplified_pattern = pattern.replace("**/", "").replace("/**", "")
455
+
456
+ # Check if any part of the path matches the simplified pattern
457
+ path_parts = path_obj.parts
458
+ for i in range(len(path_parts)):
459
+ subpath = Path(*path_parts[i:])
460
+ if fnmatch.fnmatch(str(subpath), simplified_pattern):
461
+ return True
462
+ # Also check individual parts
463
+ if fnmatch.fnmatch(path_parts[i], simplified_pattern):
464
+ return True
465
+
466
+ return False
467
+
468
+
469
+ def should_ignore_dir_path(path: str) -> bool:
470
+ """Return True if path matches any directory ignore pattern (directories only)."""
471
+ path_obj = Path(path)
472
+ for pattern in DIR_IGNORE_PATTERNS:
473
+ try:
474
+ if path_obj.match(pattern):
475
+ return True
476
+ except ValueError:
477
+ if fnmatch.fnmatch(path, pattern):
478
+ return True
479
+ if "**" in pattern:
480
+ simplified = pattern.replace("**/", "").replace("/**", "")
481
+ parts = path_obj.parts
482
+ for i in range(len(parts)):
483
+ subpath = Path(*parts[i:])
484
+ if fnmatch.fnmatch(str(subpath), simplified):
485
+ return True
486
+ if fnmatch.fnmatch(parts[i], simplified):
487
+ return True
488
+ return False
489
+
490
+
491
+ # ============================================================================
492
+ # SYNTAX HIGHLIGHTING FOR DIFFS ("syntax" mode)
493
+ # ============================================================================
494
+
495
+ # Monokai color scheme - because we have taste 🎨
496
+ TOKEN_COLORS = (
497
+ {
498
+ Token.Keyword: "#f92672" if PYGMENTS_AVAILABLE else "magenta",
499
+ Token.Name.Builtin: "#66d9ef" if PYGMENTS_AVAILABLE else "cyan",
500
+ Token.Name.Function: "#a6e22e" if PYGMENTS_AVAILABLE else "green",
501
+ Token.String: "#e6db74" if PYGMENTS_AVAILABLE else "yellow",
502
+ Token.Number: "#ae81ff" if PYGMENTS_AVAILABLE else "magenta",
503
+ Token.Comment: "#75715e" if PYGMENTS_AVAILABLE else "bright_black",
504
+ Token.Operator: "#f92672" if PYGMENTS_AVAILABLE else "magenta",
505
+ }
506
+ if PYGMENTS_AVAILABLE
507
+ else {}
508
+ )
509
+
510
+ EXTENSION_TO_LEXER_NAME = {
511
+ ".py": "python",
512
+ ".js": "javascript",
513
+ ".jsx": "jsx",
514
+ ".ts": "typescript",
515
+ ".tsx": "tsx",
516
+ ".java": "java",
517
+ ".c": "c",
518
+ ".h": "c",
519
+ ".cpp": "cpp",
520
+ ".hpp": "cpp",
521
+ ".cc": "cpp",
522
+ ".cxx": "cpp",
523
+ ".cs": "csharp",
524
+ ".rs": "rust",
525
+ ".go": "go",
526
+ ".rb": "ruby",
527
+ ".php": "php",
528
+ ".html": "html",
529
+ ".htm": "html",
530
+ ".css": "css",
531
+ ".scss": "scss",
532
+ ".json": "json",
533
+ ".yaml": "yaml",
534
+ ".yml": "yaml",
535
+ ".md": "markdown",
536
+ ".sh": "bash",
537
+ ".bash": "bash",
538
+ ".sql": "sql",
539
+ ".txt": "text",
540
+ }
541
+
542
+
543
+ def _get_lexer_for_extension(extension: str):
544
+ """Get the appropriate Pygments lexer for a file extension.
545
+
546
+ Args:
547
+ extension: File extension (with or without leading dot)
548
+
549
+ Returns:
550
+ A Pygments lexer instance or None if Pygments not available
551
+ """
552
+ if not PYGMENTS_AVAILABLE:
553
+ return None
554
+
555
+ # Normalize extension to have leading dot and be lowercase
556
+ if not extension.startswith("."):
557
+ extension = f".{extension}"
558
+ extension = extension.lower()
559
+
560
+ lexer_name = EXTENSION_TO_LEXER_NAME.get(extension, "text")
561
+
562
+ try:
563
+ return get_lexer_by_name(lexer_name)
564
+ except Exception:
565
+ # Fallback to plain text if lexer not found
566
+ return TextLexer()
567
+
568
+
569
+ def _get_token_color(token_type) -> str:
570
+ """Get color for a token type from our Monokai scheme.
571
+
572
+ Args:
573
+ token_type: Pygments token type
574
+
575
+ Returns:
576
+ Hex color string or color name
577
+ """
578
+ if not PYGMENTS_AVAILABLE:
579
+ return "#cccccc"
580
+
581
+ for ttype, color in TOKEN_COLORS.items():
582
+ if token_type in ttype:
583
+ return color
584
+ return "#cccccc" # Default light-grey for unmatched tokens
585
+
586
+
587
+ def _highlight_code_line(code: str, bg_color: str | None, lexer) -> Text:
588
+ """Highlight a line of code with syntax highlighting and optional background color.
589
+
590
+ Args:
591
+ code: The code string to highlight
592
+ bg_color: Background color in hex format, or None for no background
593
+ lexer: Pygments lexer instance to use
594
+
595
+ Returns:
596
+ Rich Text object with styling applied
597
+ """
598
+ if not PYGMENTS_AVAILABLE or lexer is None:
599
+ # Fallback: just return text with optional background
600
+ if bg_color:
601
+ return Text(code, style=f"on {bg_color}")
602
+ return Text(code)
603
+
604
+ text = Text()
605
+
606
+ for token_type, value in lex(code, lexer):
607
+ # Strip trailing newlines that Pygments adds
608
+ # Pygments lexer always adds a \n at the end of the last token
609
+ value = value.rstrip("\n")
610
+
611
+ # Skip if the value is now empty (was only whitespace/newlines)
612
+ if not value:
613
+ continue
614
+
615
+ fg_color = _get_token_color(token_type)
616
+ # Apply foreground color and optional background
617
+ if bg_color:
618
+ text.append(value, style=f"{fg_color} on {bg_color}")
619
+ else:
620
+ text.append(value, style=fg_color)
621
+
622
+ return text
623
+
624
+
625
+ def _extract_file_extension_from_diff(diff_text: str) -> str:
626
+ """Extract file extension from diff headers.
627
+
628
+ Args:
629
+ diff_text: Unified diff text
630
+
631
+ Returns:
632
+ File extension (e.g., '.py') or '.txt' as fallback
633
+ """
634
+ import re
635
+
636
+ # Look for +++ b/filename.ext or --- a/filename.ext headers
637
+ pattern = r"^(?:\+\+\+|---) [ab]/.*?(\.[a-zA-Z0-9]+)$"
638
+
639
+ for line in diff_text.split("\n")[:10]: # Check first 10 lines
640
+ match = re.search(pattern, line)
641
+ if match:
642
+ return match.group(1)
643
+
644
+ return ".txt" # Fallback to plain text
645
+
646
+
647
+ # ============================================================================
648
+ # COLOR PAIR OPTIMIZATION (for "highlighted" mode)
649
+ # ============================================================================
650
+
651
+
652
+ def brighten_hex(hex_color: str, factor: float) -> str:
653
+ """
654
+ Darken a hex color by multiplying each RGB channel by `factor`.
655
+ factor=1.0 -> no change
656
+ factor=0.0 -> black
657
+ factor=0.18 -> good for diff backgrounds (recommended)
658
+ """
659
+ hex_color = hex_color.lstrip("#")
660
+ if len(hex_color) != 6:
661
+ raise ValueError(f"Expected #RRGGBB, got {hex_color!r}")
662
+
663
+ r = int(hex_color[0:2], 16)
664
+ g = int(hex_color[2:4], 16)
665
+ b = int(hex_color[4:6], 16)
666
+
667
+ r = max(0, min(255, int(r * (1 + factor))))
668
+ g = max(0, min(255, int(g * (1 + factor))))
669
+ b = max(0, min(255, int(b * (1 + factor))))
670
+
671
+ return f"#{r:02x}{g:02x}{b:02x}"
672
+
673
+
674
+ def _format_diff_with_syntax_highlighting(
675
+ diff_text: str,
676
+ addition_color: str | None = None,
677
+ deletion_color: str | None = None,
678
+ ) -> Text:
679
+ """Format diff with full syntax highlighting using Pygments.
680
+
681
+ This renders diffs with:
682
+ - Syntax highlighting for code tokens
683
+ - Colored backgrounds for context/added/removed lines
684
+ - Monokai color scheme
685
+ - Optional custom colors for additions/deletions
686
+
687
+ Args:
688
+ diff_text: Raw unified diff text
689
+ addition_color: Optional custom color for added lines (default: green)
690
+ deletion_color: Optional custom color for deleted lines (default: red)
691
+
692
+ Returns:
693
+ Rich Text object with syntax highlighting (can be passed to emit_info)
694
+ """
695
+ if not PYGMENTS_AVAILABLE:
696
+ return Text(diff_text)
697
+
698
+ # Extract file extension from diff headers
699
+ extension = _extract_file_extension_from_diff(diff_text)
700
+ lexer = _get_lexer_for_extension(extension)
701
+
702
+ # Generate background colors from foreground colors
703
+ add_fg = brighten_hex(addition_color, 0.6)
704
+ del_fg = brighten_hex(deletion_color, 0.6)
705
+
706
+ # Background colors for different line types
707
+ # Context lines have no background (None) for clean, minimal diffs
708
+ bg_colors = {
709
+ "removed": deletion_color,
710
+ "added": addition_color,
711
+ "context": None, # No background for unchanged lines
712
+ }
713
+
714
+ lines = diff_text.split("\n")
715
+ # Remove trailing empty line if it exists (from trailing \n in diff)
716
+ if lines and lines[-1] == "":
717
+ lines = lines[:-1]
718
+ result = Text()
719
+
720
+ for i, line in enumerate(lines):
721
+ if not line:
722
+ # Empty line - just add a newline if not the last line
723
+ if i < len(lines) - 1:
724
+ result.append("\n")
725
+ continue
726
+
727
+ # Skip diff headers - they're redundant noise since we show the filename in the banner
728
+ if line.startswith(("---", "+++", "@@", "diff ", "index ")):
729
+ continue
730
+ else:
731
+ # Determine line type and extract code content
732
+ if line.startswith("-"):
733
+ line_type = "removed"
734
+ code = line[1:] # Remove the '-' prefix
735
+ marker_style = f"bold {del_fg} on {bg_colors[line_type]}"
736
+ prefix = "- "
737
+ elif line.startswith("+"):
738
+ line_type = "added"
739
+ code = line[1:] # Remove the '+' prefix
740
+ marker_style = f"bold {add_fg} on {bg_colors[line_type]}"
741
+ prefix = "+ "
742
+ else:
743
+ line_type = "context"
744
+ code = line[1:] if line.startswith(" ") else line
745
+ # Context lines have no background - clean and minimal
746
+ marker_style = "" # No special styling for context markers
747
+ prefix = " "
748
+
749
+ # Add the marker prefix
750
+ if marker_style: # Only apply style if we have one
751
+ result.append(prefix, style=marker_style)
752
+ else:
753
+ result.append(prefix)
754
+
755
+ # Add syntax-highlighted code
756
+ highlighted = _highlight_code_line(code, bg_colors[line_type], lexer)
757
+ result.append_text(highlighted)
758
+
759
+ # Add newline after each line except the last
760
+ if i < len(lines) - 1:
761
+ result.append("\n")
762
+
763
+ return result
764
+
765
+
766
+ def format_diff_with_colors(diff_text: str) -> Text:
767
+ """Format diff text with beautiful syntax highlighting.
768
+
769
+ This is the canonical diff formatting function used across the codebase.
770
+ It applies user-configurable color coding with full syntax highlighting using Pygments.
771
+
772
+ The function respects user preferences from config:
773
+ - get_diff_addition_color(): Color for added lines (markers and backgrounds)
774
+ - get_diff_deletion_color(): Color for deleted lines (markers and backgrounds)
775
+
776
+ Args:
777
+ diff_text: Raw diff text to format
778
+
779
+ Returns:
780
+ Rich Text object with syntax highlighting
781
+ """
782
+ from code_muse.config import (
783
+ get_diff_addition_color,
784
+ get_diff_deletion_color,
785
+ )
786
+
787
+ if not diff_text or not diff_text.strip():
788
+ return Text("-- no diff available --", style="dim")
789
+
790
+ addition_base_color = get_diff_addition_color()
791
+ deletion_base_color = get_diff_deletion_color()
792
+
793
+ # Always use beautiful syntax highlighting!
794
+ if not PYGMENTS_AVAILABLE:
795
+ emit_warning("Pygments not available, diffs will look plain")
796
+ # Return plain text as fallback
797
+ return Text(diff_text)
798
+
799
+ # Return Text object with custom colors - emit_info handles this correctly
800
+ return _format_diff_with_syntax_highlighting(
801
+ diff_text,
802
+ addition_color=addition_base_color,
803
+ deletion_color=deletion_base_color,
804
+ )
805
+
806
+
807
+ async def arrow_select_async(
808
+ message: str,
809
+ choices: list[str],
810
+ preview_callback: Callable[[int | None, str]] = None,
811
+ ) -> str:
812
+ """Async version: Show an arrow-key navigable selector with optional preview.
813
+
814
+ Args:
815
+ message: The prompt message to display
816
+ choices: List of choice strings
817
+ preview_callback: Optional callback that takes the selected index and returns
818
+ preview text to display below the choices
819
+
820
+ Returns:
821
+ The selected choice string
822
+
823
+ Raises:
824
+ KeyboardInterrupt: If user cancels with Ctrl-C
825
+ """
826
+ import html
827
+
828
+ selected_index = [0] # Mutable container for selected index
829
+ result = [None] # Mutable container for result
830
+
831
+ def get_formatted_text():
832
+ """Generate the formatted text for display."""
833
+ # Escape XML special characters to prevent parsing errors
834
+ safe_message = html.escape(message)
835
+ lines = [f"<b>{safe_message}</b>", ""]
836
+ for i, choice in enumerate(choices):
837
+ safe_choice = html.escape(choice)
838
+ if i == selected_index[0]:
839
+ lines.append(f"<ansigreen>❯ {safe_choice}</ansigreen>")
840
+ else:
841
+ lines.append(f" {safe_choice}")
842
+ lines.append("")
843
+
844
+ # Add preview section if callback provided
845
+ if preview_callback is not None:
846
+ preview_text = preview_callback(selected_index[0])
847
+ if preview_text:
848
+ import textwrap
849
+
850
+ # Box width (excluding borders and padding)
851
+ box_width = 60
852
+ border_top = (
853
+ "<ansiyellow>┌─ Preview "
854
+ + "─" * (box_width - 10)
855
+ + "┐</ansiyellow>"
856
+ )
857
+ border_bottom = "<ansiyellow>└" + "─" * box_width + "┘</ansiyellow>"
858
+
859
+ lines.append(border_top)
860
+
861
+ # Wrap text to fit within box width (minus padding)
862
+ wrapped_lines = textwrap.wrap(preview_text, width=box_width - 2)
863
+
864
+ # If no wrapped lines (empty text), add empty line
865
+ if not wrapped_lines:
866
+ wrapped_lines = [""]
867
+
868
+ for wrapped_line in wrapped_lines:
869
+ safe_preview = html.escape(wrapped_line)
870
+ # Pad line to box width for consistent appearance
871
+ padded_line = safe_preview.ljust(box_width - 2)
872
+ lines.append(f"<dim>│ {padded_line} │</dim>")
873
+
874
+ lines.append(border_bottom)
875
+ lines.append("")
876
+
877
+ lines.append(
878
+ "<ansicyan>(Use ↑↓ or Ctrl+P/N to select, Enter to confirm)</ansicyan>"
879
+ )
880
+ return HTML("\n".join(lines))
881
+
882
+ # Key bindings
883
+ kb = KeyBindings()
884
+
885
+ @kb.add("up")
886
+ @kb.add("c-p") # Ctrl+P = previous (Emacs-style)
887
+ def move_up(event):
888
+ selected_index[0] = (selected_index[0] - 1) % len(choices)
889
+ event.app.invalidate() # Force redraw to update preview
890
+
891
+ @kb.add("down")
892
+ @kb.add("c-n") # Ctrl+N = next (Emacs-style)
893
+ def move_down(event):
894
+ selected_index[0] = (selected_index[0] + 1) % len(choices)
895
+ event.app.invalidate() # Force redraw to update preview
896
+
897
+ @kb.add("enter")
898
+ def accept(event):
899
+ result[0] = choices[selected_index[0]]
900
+ event.app.exit()
901
+
902
+ @kb.add("c-c") # Ctrl-C
903
+ def cancel(event):
904
+ result[0] = None
905
+ event.app.exit()
906
+
907
+ # Layout
908
+ control = FormattedTextControl(get_formatted_text)
909
+ layout = Layout(Window(content=control))
910
+
911
+ # Application
912
+ app = Application(
913
+ layout=layout,
914
+ key_bindings=kb,
915
+ full_screen=False,
916
+ )
917
+
918
+ # Flush output before prompt_toolkit takes control
919
+ sys.stdout.flush()
920
+ sys.stderr.flush()
921
+
922
+ # Run the app asynchronously
923
+ await app.run_async()
924
+
925
+ if result[0] is None:
926
+ raise KeyboardInterrupt()
927
+
928
+ return result[0]
929
+
930
+
931
+ def arrow_select(message: str, choices: list[str]) -> str:
932
+ """Show an arrow-key navigable selector (synchronous version).
933
+
934
+ Args:
935
+ message: The prompt message to display
936
+ choices: List of choice strings
937
+
938
+ Returns:
939
+ The selected choice string
940
+
941
+ Raises:
942
+ KeyboardInterrupt: If user cancels with Ctrl-C
943
+ """
944
+
945
+ selected_index = [0] # Mutable container for selected index
946
+ result = [None] # Mutable container for result
947
+
948
+ def get_formatted_text():
949
+ """Generate the formatted text for display."""
950
+ lines = [f"<b>{message}</b>", ""]
951
+ for i, choice in enumerate(choices):
952
+ if i == selected_index[0]:
953
+ lines.append(f"<ansigreen>❯ {choice}</ansigreen>")
954
+ else:
955
+ lines.append(f" {choice}")
956
+ lines.append("")
957
+ lines.append(
958
+ "<ansicyan>(Use ↑↓ or Ctrl+P/N to select, Enter to confirm)</ansicyan>"
959
+ )
960
+ return HTML("\n".join(lines))
961
+
962
+ # Key bindings
963
+ kb = KeyBindings()
964
+
965
+ @kb.add("up")
966
+ @kb.add("c-p") # Ctrl+P = previous (Emacs-style)
967
+ def move_up(event):
968
+ selected_index[0] = (selected_index[0] - 1) % len(choices)
969
+ event.app.invalidate() # Force redraw to update preview
970
+
971
+ @kb.add("down")
972
+ @kb.add("c-n") # Ctrl+N = next (Emacs-style)
973
+ def move_down(event):
974
+ selected_index[0] = (selected_index[0] + 1) % len(choices)
975
+ event.app.invalidate() # Force redraw to update preview
976
+
977
+ @kb.add("enter")
978
+ def accept(event):
979
+ result[0] = choices[selected_index[0]]
980
+ event.app.exit()
981
+
982
+ @kb.add("c-c") # Ctrl-C
983
+ def cancel(event):
984
+ result[0] = None
985
+ event.app.exit()
986
+
987
+ # Layout
988
+ control = FormattedTextControl(get_formatted_text)
989
+ layout = Layout(Window(content=control))
990
+
991
+ # Application
992
+ app = Application(
993
+ layout=layout,
994
+ key_bindings=kb,
995
+ full_screen=False,
996
+ )
997
+
998
+ # Flush output before prompt_toolkit takes control
999
+ sys.stdout.flush()
1000
+ sys.stderr.flush()
1001
+
1002
+ # Check if we're already in an async context
1003
+ try:
1004
+ asyncio.get_running_loop()
1005
+ # We're in an async context - can't use app.run()
1006
+ # Caller should use arrow_select_async instead
1007
+ raise RuntimeError(
1008
+ "arrow_select() called from async context. Use arrow_select_async() instead."
1009
+ )
1010
+ except RuntimeError as e:
1011
+ if "no running event loop" in str(e).lower():
1012
+ # No event loop, safe to use app.run()
1013
+ app.run()
1014
+ else:
1015
+ # Re-raise if it's our error message
1016
+ raise
1017
+
1018
+ if result[0] is None:
1019
+ raise KeyboardInterrupt()
1020
+
1021
+ return result[0]
1022
+
1023
+
1024
+ def get_user_approval(
1025
+ title: str,
1026
+ content: Text | str,
1027
+ preview: str | None = None,
1028
+ border_style: str = "dim white",
1029
+ agent_name: str | None = None,
1030
+ ) -> tuple[bool, str | None]:
1031
+ """Show a beautiful approval panel with arrow-key selector.
1032
+
1033
+ Args:
1034
+ title: Title for the panel (e.g., "File Operation", "Shell Command")
1035
+ content: Main content to display (Rich Text object or string)
1036
+ preview: Optional preview content (like a diff)
1037
+ border_style: Border color/style for the panel
1038
+ agent_name: Name of the assistant (defaults to config value)
1039
+
1040
+ Returns:
1041
+ Tuple of (confirmed: bool, user_feedback: str | None)
1042
+ - confirmed: True if approved, False if rejected
1043
+ - user_feedback: Optional feedback text if user provided it
1044
+ """
1045
+ import time
1046
+
1047
+ from code_muse.config import get_auto_approve
1048
+ from code_muse.tools.command_runner import set_awaiting_user_input
1049
+
1050
+ if get_auto_approve():
1051
+ return True, None
1052
+
1053
+ if agent_name is None:
1054
+ from code_muse.config import get_agent_name
1055
+
1056
+ agent_name = get_agent_name().title()
1057
+
1058
+ # Build panel content
1059
+ panel_content = Text(content) if isinstance(content, str) else content
1060
+
1061
+ # Add preview if provided
1062
+ if preview:
1063
+ panel_content.append("\n\n", style="")
1064
+ panel_content.append("Preview of changes:", style="bold underline")
1065
+ panel_content.append("\n", style="")
1066
+ formatted_preview = format_diff_with_colors(preview)
1067
+
1068
+ # Handle both string (text mode) and Text object (highlight mode)
1069
+ if isinstance(formatted_preview, Text):
1070
+ preview_text = formatted_preview
1071
+ else:
1072
+ preview_text = Text.from_markup(formatted_preview)
1073
+
1074
+ panel_content.append(preview_text)
1075
+
1076
+ # Mark that we showed a diff preview
1077
+ try:
1078
+ from code_muse.plugins.file_permission_handler.register_callbacks import (
1079
+ set_diff_already_shown,
1080
+ )
1081
+
1082
+ set_diff_already_shown(True)
1083
+ except ImportError:
1084
+ pass
1085
+
1086
+ # Create panel
1087
+ panel = Panel(
1088
+ panel_content,
1089
+ title=f"[bold white]{title}[/bold white]",
1090
+ border_style=border_style,
1091
+ padding=(1, 2),
1092
+ )
1093
+
1094
+ # Pause spinners BEFORE showing panel
1095
+ set_awaiting_user_input(True)
1096
+ # Also explicitly pause spinners to ensure they're fully stopped
1097
+ try:
1098
+ from code_muse.messaging.spinner import pause_all_spinners
1099
+
1100
+ pause_all_spinners()
1101
+ except Exception:
1102
+ pass
1103
+
1104
+ time.sleep(0.3) # Let spinners fully stop
1105
+
1106
+ # Display panel
1107
+ local_console = Console()
1108
+ emit_info("")
1109
+ local_console.print(panel)
1110
+ emit_info("")
1111
+
1112
+ # Flush and buffer before selector
1113
+ sys.stdout.flush()
1114
+ sys.stderr.flush()
1115
+ time.sleep(0.1)
1116
+
1117
+ user_feedback = None
1118
+ confirmed = False
1119
+
1120
+ try:
1121
+ # Final flush
1122
+ sys.stdout.flush()
1123
+
1124
+ # Show arrow-key selector
1125
+ choice = arrow_select(
1126
+ "💭 What would you like to do?",
1127
+ [
1128
+ "✓ Approve",
1129
+ "✗ Reject",
1130
+ f"💬 Reject with feedback (tell {agent_name} what to change)",
1131
+ ],
1132
+ )
1133
+
1134
+ if choice == "✓ Approve":
1135
+ confirmed = True
1136
+ elif choice == "✗ Reject":
1137
+ confirmed = False
1138
+ else:
1139
+ # User wants to provide feedback
1140
+ confirmed = False
1141
+ emit_info("")
1142
+ emit_info(f"Tell {agent_name} what to change:")
1143
+ user_feedback = Prompt.ask(
1144
+ "[bold green]➤[/bold green]",
1145
+ default="",
1146
+ ).strip()
1147
+
1148
+ if not user_feedback:
1149
+ user_feedback = None
1150
+
1151
+ except KeyboardInterrupt, EOFError:
1152
+ emit_error("Cancelled by user")
1153
+ confirmed = False
1154
+
1155
+ finally:
1156
+ set_awaiting_user_input(False)
1157
+
1158
+ # Force Rich console to reset display state to prevent artifacts
1159
+ try:
1160
+ # Clear Rich's internal display state to prevent artifacts
1161
+ local_console.file.write("\r") # Return to start of line
1162
+ local_console.file.write("\x1b[K") # Clear current line
1163
+ local_console.file.flush()
1164
+ except Exception:
1165
+ pass
1166
+
1167
+ # Ensure streams are flushed
1168
+ sys.stdout.flush()
1169
+ sys.stderr.flush()
1170
+
1171
+ # Show result BEFORE resuming spinners (no leftover distraction!)
1172
+ emit_info("")
1173
+ if not confirmed:
1174
+ if user_feedback:
1175
+ emit_error("Rejected with feedback!")
1176
+ emit_warning(f'Telling {agent_name}: "{user_feedback}"')
1177
+ else:
1178
+ emit_error("Rejected.")
1179
+ else:
1180
+ emit_success("Approved!")
1181
+
1182
+ # NOW resume spinners after showing the result
1183
+ try:
1184
+ from code_muse.messaging.spinner import resume_all_spinners
1185
+
1186
+ resume_all_spinners()
1187
+ except Exception:
1188
+ pass
1189
+
1190
+ return confirmed, user_feedback
1191
+
1192
+
1193
+ async def get_user_approval_async(
1194
+ title: str,
1195
+ content: Text | str,
1196
+ preview: str | None = None,
1197
+ border_style: str = "dim white",
1198
+ agent_name: str | None = None,
1199
+ ) -> tuple[bool, str | None]:
1200
+ """Async version of get_user_approval - show a beautiful approval panel with arrow-key selector.
1201
+
1202
+ Args:
1203
+ title: Title for the panel (e.g., "File Operation", "Shell Command")
1204
+ content: Main content to display (Rich Text object or string)
1205
+ preview: Optional preview content (like a diff)
1206
+ border_style: Border color/style for the panel
1207
+ agent_name: Name of the assistant (defaults to config value)
1208
+
1209
+ Returns:
1210
+ Tuple of (confirmed: bool, user_feedback: str | None)
1211
+ - confirmed: True if approved, False if rejected
1212
+ - user_feedback: Optional feedback text if user provided it
1213
+ """
1214
+
1215
+ from code_muse.config import get_auto_approve
1216
+ from code_muse.tools.command_runner import set_awaiting_user_input
1217
+
1218
+ if get_auto_approve():
1219
+ return True, None
1220
+
1221
+ if agent_name is None:
1222
+ from code_muse.config import get_agent_name
1223
+
1224
+ agent_name = get_agent_name().title()
1225
+
1226
+ # Build panel content
1227
+ panel_content = Text(content) if isinstance(content, str) else content
1228
+
1229
+ # Add preview if provided
1230
+ if preview:
1231
+ panel_content.append("\n\n", style="")
1232
+ panel_content.append("Preview of changes:", style="bold underline")
1233
+ panel_content.append("\n", style="")
1234
+ formatted_preview = format_diff_with_colors(preview)
1235
+
1236
+ # Handle both string (text mode) and Text object (highlight mode)
1237
+ if isinstance(formatted_preview, Text):
1238
+ preview_text = formatted_preview
1239
+ else:
1240
+ preview_text = Text.from_markup(formatted_preview)
1241
+
1242
+ panel_content.append(preview_text)
1243
+
1244
+ # Mark that we showed a diff preview
1245
+ try:
1246
+ from code_muse.plugins.file_permission_handler.register_callbacks import (
1247
+ set_diff_already_shown,
1248
+ )
1249
+
1250
+ set_diff_already_shown(True)
1251
+ except ImportError:
1252
+ pass
1253
+
1254
+ # Create panel
1255
+ panel = Panel(
1256
+ panel_content,
1257
+ title=f"[bold white]{title}[/bold white]",
1258
+ border_style=border_style,
1259
+ padding=(1, 2),
1260
+ )
1261
+
1262
+ # Pause spinners BEFORE showing panel
1263
+ set_awaiting_user_input(True)
1264
+ # Also explicitly pause spinners to ensure they're fully stopped
1265
+ try:
1266
+ from code_muse.messaging.spinner import pause_all_spinners
1267
+
1268
+ pause_all_spinners()
1269
+ except Exception:
1270
+ pass
1271
+
1272
+ await asyncio.sleep(0.3) # Let spinners fully stop
1273
+
1274
+ # Display panel
1275
+ local_console = Console()
1276
+ emit_info("")
1277
+ local_console.print(panel)
1278
+ emit_info("")
1279
+
1280
+ # Flush and buffer before selector
1281
+ sys.stdout.flush()
1282
+ sys.stderr.flush()
1283
+ await asyncio.sleep(0.1)
1284
+
1285
+ user_feedback = None
1286
+ confirmed = False
1287
+
1288
+ try:
1289
+ # Final flush
1290
+ sys.stdout.flush()
1291
+
1292
+ # Show arrow-key selector (ASYNC VERSION)
1293
+ choice = await arrow_select_async(
1294
+ "💭 What would you like to do?",
1295
+ [
1296
+ "✓ Approve",
1297
+ "✗ Reject",
1298
+ f"💬 Reject with feedback (tell {agent_name} what to change)",
1299
+ ],
1300
+ )
1301
+
1302
+ if choice == "✓ Approve":
1303
+ confirmed = True
1304
+ elif choice == "✗ Reject":
1305
+ confirmed = False
1306
+ else:
1307
+ # User wants to provide feedback
1308
+ confirmed = False
1309
+ emit_info("")
1310
+ emit_info(f"Tell {agent_name} what to change:")
1311
+ user_feedback = Prompt.ask(
1312
+ "[bold green]➤[/bold green]",
1313
+ default="",
1314
+ ).strip()
1315
+
1316
+ if not user_feedback:
1317
+ user_feedback = None
1318
+
1319
+ except KeyboardInterrupt, EOFError:
1320
+ emit_error("Cancelled by user")
1321
+ confirmed = False
1322
+
1323
+ finally:
1324
+ set_awaiting_user_input(False)
1325
+
1326
+ # Force Rich console to reset display state to prevent artifacts
1327
+ try:
1328
+ # Clear Rich's internal display state to prevent artifacts
1329
+ local_console.file.write("\r") # Return to start of line
1330
+ local_console.file.write("\x1b[K") # Clear current line
1331
+ local_console.file.flush()
1332
+ except Exception:
1333
+ pass
1334
+
1335
+ # Ensure streams are flushed
1336
+ sys.stdout.flush()
1337
+ sys.stderr.flush()
1338
+
1339
+ # Show result BEFORE resuming spinners (no leftover distraction!)
1340
+ emit_info("")
1341
+ if not confirmed:
1342
+ if user_feedback:
1343
+ emit_error("Rejected with feedback!")
1344
+ emit_warning(f'Telling {agent_name}: "{user_feedback}"')
1345
+ else:
1346
+ emit_error("Rejected.")
1347
+ else:
1348
+ emit_success("Approved!")
1349
+
1350
+ # NOW resume spinners after showing the result
1351
+ try:
1352
+ from code_muse.messaging.spinner import resume_all_spinners
1353
+
1354
+ resume_all_spinners()
1355
+ except Exception:
1356
+ pass
1357
+
1358
+ return confirmed, user_feedback
1359
+
1360
+
1361
+ def _find_best_window(
1362
+ haystack_lines: list[str],
1363
+ needle: str,
1364
+ ) -> tuple[tuple[int, int | None], float]:
1365
+ """
1366
+ Return (start, end) indices of the window with the highest
1367
+ Jaro-Winkler similarity to `needle`, along with that score.
1368
+ If nothing clears JW_THRESHOLD, return (None, score).
1369
+ """
1370
+ needle = needle.rstrip("\n")
1371
+ needle_lines = needle.splitlines()
1372
+ win_size = len(needle_lines)
1373
+ best_score = 0.0
1374
+ best_span: tuple[int, int | None] = None
1375
+ # Pre-join the needle once; join windows on the fly
1376
+ for i in range(len(haystack_lines) - win_size + 1):
1377
+ window = "\n".join(haystack_lines[i : i + win_size])
1378
+ score = JaroWinkler.normalized_similarity(window, needle)
1379
+ if score > best_score:
1380
+ best_score = score
1381
+ best_span = (i, i + win_size)
1382
+
1383
+ return best_span, best_score
1384
+
1385
+
1386
+ def generate_group_id(tool_name: str, extra_context: str | Path = "") -> str:
1387
+ """Generate a unique group_id for tool output grouping.
1388
+
1389
+ Args:
1390
+ tool_name: Name of the tool (e.g., 'list_files', 'edit_file')
1391
+ extra_context: Optional extra context to make group_id more unique
1392
+
1393
+ Returns:
1394
+ A string in format: tool_name_hash
1395
+ """
1396
+ # Create a unique identifier using timestamp, context, and a random component
1397
+ import random
1398
+
1399
+ timestamp = str(int(time.time() * 1000000)) # microseconds for more uniqueness
1400
+ random_component = random.randint(1000, 9999) # Add randomness
1401
+ context_string = f"{tool_name}_{timestamp}_{random_component}_{extra_context}"
1402
+
1403
+ # Generate a short hash
1404
+ hash_obj = hashlib.md5(context_string.encode())
1405
+ short_hash = hash_obj.hexdigest()[:8]
1406
+
1407
+ return f"{tool_name}_{short_hash}"