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,886 @@
1
+ # ANSI color codes are no longer necessary because prompt_toolkit handles
2
+ # styling via the `Style` class. We keep them here commented-out in case
3
+ # someone needs raw ANSI later, but they are unused in the current code.
4
+ # RESET = '\033[0m'
5
+ # GREEN = '\033[1;32m'
6
+ # CYAN = '\033[1;36m'
7
+ # YELLOW = '\033[1;33m'
8
+ # BOLD = '\033[1m'
9
+ import asyncio
10
+ import os
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ from prompt_toolkit import PromptSession
15
+ from prompt_toolkit.completion import Completer, Completion, merge_completers
16
+ from prompt_toolkit.filters import is_searching
17
+ from prompt_toolkit.formatted_text import FormattedText
18
+ from prompt_toolkit.history import FileHistory
19
+ from prompt_toolkit.key_binding import KeyBindings
20
+ from prompt_toolkit.keys import Keys
21
+ from prompt_toolkit.layout.processors import Processor, Transformation
22
+ from prompt_toolkit.styles import Style
23
+
24
+ from code_muse.command_line.attachments import (
25
+ DEFAULT_ACCEPTED_DOCUMENT_EXTENSIONS,
26
+ DEFAULT_ACCEPTED_IMAGE_EXTENSIONS,
27
+ _detect_path_tokens,
28
+ _tokenise,
29
+ )
30
+ from code_muse.command_line.clipboard import (
31
+ capture_clipboard_image_to_pending,
32
+ has_image_in_clipboard,
33
+ )
34
+ from code_muse.command_line.command_registry import get_unique_commands
35
+ from code_muse.command_line.file_path_completion import FilePathCompleter
36
+ from code_muse.command_line.load_context_completion import LoadContextCompleter
37
+ from code_muse.command_line.model_picker_completion import (
38
+ ModelNameCompleter,
39
+ get_active_model,
40
+ )
41
+ from code_muse.command_line.pin_command_completion import PinCompleter, UnpinCompleter
42
+ from code_muse.command_line.skills_completion import SkillsCompleter
43
+ from code_muse.command_line.utils import list_directory
44
+ from code_muse.config import (
45
+ COMMAND_HISTORY_FILE,
46
+ get_config_keys,
47
+ get_value,
48
+ )
49
+
50
+
51
+ def _sanitize_for_encoding(text: str) -> str:
52
+ """Remove or replace characters that can't be safely encoded.
53
+
54
+ This handles:
55
+ - Lone surrogate characters (U+D800-U+DFFF) which are invalid in UTF-8
56
+ - Other problematic Unicode sequences from Windows copy-paste
57
+
58
+ Args:
59
+ text: The string to sanitize
60
+
61
+ Returns:
62
+ A cleaned string safe for UTF-8 encoding
63
+ """
64
+ # First, try to encode as UTF-8 to catch any problematic characters
65
+ try:
66
+ text.encode("utf-8")
67
+ return text # String is already valid UTF-8
68
+ except UnicodeEncodeError:
69
+ pass
70
+
71
+ # Replace surrogates and other problematic characters
72
+ # Use 'surrogatepass' to encode surrogates, then decode with 'replace' to clean them
73
+ try:
74
+ # Encode allowing surrogates, then decode replacing invalid sequences
75
+ cleaned = text.encode("utf-8", errors="surrogatepass").decode(
76
+ "utf-8", errors="replace"
77
+ )
78
+ return cleaned
79
+ except UnicodeEncodeError, UnicodeDecodeError:
80
+ # Last resort: filter out all non-BMP and surrogate characters
81
+ return "".join(
82
+ char
83
+ for char in text
84
+ if ord(char) < 0xD800 or (ord(char) > 0xDFFF and ord(char) < 0x10000)
85
+ )
86
+
87
+
88
+ class SafeFileHistory(FileHistory):
89
+ """A FileHistory that handles encoding errors gracefully on Windows.
90
+
91
+ Windows terminals and copy-paste operations can introduce invalid
92
+ Unicode surrogate characters that cause UTF-8 encoding to fail.
93
+ This class sanitizes history entries before writing them to disk.
94
+ """
95
+
96
+ def store_string(self, string: str) -> None:
97
+ """Store a string in the history, sanitizing it first."""
98
+ sanitized = _sanitize_for_encoding(string)
99
+ try:
100
+ super().store_string(sanitized)
101
+ except (UnicodeEncodeError, UnicodeDecodeError, OSError) as e:
102
+ # If we still can't write, log the error but don't crash
103
+ # This can happen with particularly malformed input
104
+ # Note: Using sys.stderr here intentionally - this is a low-level
105
+ # warning that shouldn't use the messaging system
106
+ sys.stderr.write(f"Warning: Could not save to command history: {e}\n")
107
+
108
+
109
+ class SetCompleter(Completer):
110
+ def __init__(self, trigger: str = "/set"):
111
+ self.trigger = trigger
112
+
113
+ def get_completions(self, document, complete_event):
114
+ cursor_position = document.cursor_position
115
+ text_before_cursor = document.text_before_cursor
116
+ stripped_text_for_trigger_check = text_before_cursor.lstrip()
117
+
118
+ # If user types just /set (no space), suggest adding a space
119
+ if stripped_text_for_trigger_check == self.trigger:
120
+ from prompt_toolkit.formatted_text import FormattedText
121
+
122
+ yield Completion(
123
+ self.trigger + " ",
124
+ start_position=-len(self.trigger),
125
+ display=self.trigger + " ",
126
+ display_meta=FormattedText(
127
+ [("class:set-completer-meta", "set config key")]
128
+ ),
129
+ )
130
+ return
131
+
132
+ # Require a space after /set before showing completions
133
+ if not stripped_text_for_trigger_check.startswith(self.trigger + " "):
134
+ return
135
+
136
+ # Determine the part of the text that is relevant for this completer
137
+ # This handles cases like " /set foo" where the trigger isn't at the start of the string
138
+ actual_trigger_pos = text_before_cursor.find(self.trigger)
139
+
140
+ # Extract the input after /set and space (up to cursor)
141
+ trigger_end = actual_trigger_pos + len(self.trigger) + 1 # +1 for the space
142
+ text_after_trigger = text_before_cursor[trigger_end:cursor_position].lstrip()
143
+ start_position = -(len(text_after_trigger))
144
+
145
+ # --- SPECIAL HANDLING FOR 'model' KEY ---
146
+ if text_after_trigger == "model":
147
+ # Don't return any completions -- let ModelNameCompleter handle it
148
+ return
149
+
150
+ # Get config keys and sort them alphabetically for consistent display
151
+ config_keys = sorted(get_config_keys())
152
+
153
+ for key in config_keys:
154
+ if key == "model" or key == "muse_token":
155
+ continue # exclude 'model' and 'muse_token' from regular /set completions
156
+ if key.startswith(text_after_trigger):
157
+ prev_value = get_value(key)
158
+ value_part = f" = {prev_value}" if prev_value is not None else " = "
159
+ completion_text = f"{key}{value_part}"
160
+
161
+ yield Completion(
162
+ completion_text,
163
+ start_position=start_position,
164
+ display_meta="",
165
+ )
166
+
167
+
168
+ class AttachmentPlaceholderProcessor(Processor):
169
+ """Display friendly placeholders for recognised attachments."""
170
+
171
+ _PLACEHOLDER_STYLE = "class:attachment-placeholder"
172
+ # Skip expensive path detection for very long input (likely pasted content)
173
+ _MAX_TEXT_LENGTH_FOR_REALTIME = 500
174
+
175
+ def apply_transformation(self, transformation_input):
176
+ document = transformation_input.document
177
+ text = document.text
178
+ if not text:
179
+ return Transformation(list(transformation_input.fragments))
180
+
181
+ # Skip real-time path detection for long text to avoid slowdown
182
+ if len(text) > self._MAX_TEXT_LENGTH_FOR_REALTIME:
183
+ return Transformation(list(transformation_input.fragments))
184
+
185
+ detections, _warnings = _detect_path_tokens(text)
186
+ replacements: list[tuple[int, int, str]] = []
187
+ search_cursor = 0
188
+ ESCAPE_MARKER = "\u0000ESCAPED_SPACE\u0000"
189
+ masked_text = text.replace(r"\ ", ESCAPE_MARKER)
190
+ token_view = list(_tokenise(masked_text))
191
+ for detection in detections:
192
+ display_text: str | None = None
193
+ if detection.path and detection.has_path():
194
+ suffix = detection.path.suffix.lower()
195
+ if suffix in DEFAULT_ACCEPTED_IMAGE_EXTENSIONS:
196
+ display_text = f"[{suffix.lstrip('.') or 'image'} image]"
197
+ elif suffix in DEFAULT_ACCEPTED_DOCUMENT_EXTENSIONS:
198
+ display_text = f"[{suffix.lstrip('.') or 'file'} document]"
199
+ else:
200
+ display_text = "[file attachment]"
201
+ elif detection.link is not None:
202
+ display_text = "[link]"
203
+
204
+ if not display_text:
205
+ continue
206
+
207
+ # Use token-span for robust lookup (handles escaped spaces)
208
+ span_tokens = token_view[detection.start_index : detection.consumed_until]
209
+ raw_span = " ".join(span_tokens).replace(ESCAPE_MARKER, r"\ ")
210
+ index = text.find(raw_span, search_cursor)
211
+ span_len = len(raw_span)
212
+ if index == -1:
213
+ # Fallback to placeholder string
214
+ placeholder = detection.placeholder
215
+ index = text.find(placeholder, search_cursor)
216
+ span_len = len(placeholder)
217
+ if index == -1:
218
+ continue
219
+ replacements.append((index, index + span_len, display_text))
220
+ search_cursor = index + span_len
221
+
222
+ if not replacements:
223
+ return Transformation(list(transformation_input.fragments))
224
+
225
+ replacements.sort(key=lambda item: item[0])
226
+
227
+ new_fragments: list[tuple[str, str]] = []
228
+ source_to_display_map: list[int] = []
229
+ display_to_source_map: list[int] = []
230
+
231
+ source_index = 0
232
+ display_index = 0
233
+
234
+ def append_plain_segment(segment: str) -> None:
235
+ nonlocal source_index, display_index
236
+ if not segment:
237
+ return
238
+ new_fragments.append(("", segment))
239
+ for _ in segment:
240
+ source_to_display_map.append(display_index)
241
+ display_to_source_map.append(source_index)
242
+ source_index += 1
243
+ display_index += 1
244
+
245
+ for start, end, replacement_text in replacements:
246
+ if start > source_index:
247
+ append_plain_segment(text[source_index:start])
248
+
249
+ placeholder = replacement_text or ""
250
+ placeholder_start = display_index
251
+ if placeholder:
252
+ new_fragments.append((self._PLACEHOLDER_STYLE, placeholder))
253
+ for _ in placeholder:
254
+ display_to_source_map.append(start)
255
+ display_index += 1
256
+
257
+ for _ in text[source_index:end]:
258
+ source_to_display_map.append(
259
+ placeholder_start if placeholder else display_index
260
+ )
261
+ source_index += 1
262
+
263
+ if source_index < len(text):
264
+ append_plain_segment(text[source_index:])
265
+
266
+ def source_to_display(pos: int) -> int:
267
+ if pos < 0:
268
+ return 0
269
+ if pos < len(source_to_display_map):
270
+ return source_to_display_map[pos]
271
+ return display_index
272
+
273
+ def display_to_source(pos: int) -> int:
274
+ if pos < 0:
275
+ return 0
276
+ if pos < len(display_to_source_map):
277
+ return display_to_source_map[pos]
278
+ return len(source_to_display_map)
279
+
280
+ return Transformation(
281
+ new_fragments,
282
+ source_to_display=source_to_display,
283
+ display_to_source=display_to_source,
284
+ )
285
+
286
+
287
+ class CDCompleter(Completer):
288
+ def __init__(self, trigger: str = "/cd"):
289
+ self.trigger = trigger
290
+
291
+ def get_completions(self, document, complete_event):
292
+ text_before_cursor = document.text_before_cursor
293
+ stripped_text = text_before_cursor.lstrip()
294
+
295
+ # Require a space after /cd before showing completions (consistency with other completers)
296
+ if not stripped_text.startswith(self.trigger + " "):
297
+ return
298
+
299
+ # Extract the directory path after /cd and space (up to cursor)
300
+ trigger_pos = text_before_cursor.find(self.trigger)
301
+ trigger_end = trigger_pos + len(self.trigger) + 1 # +1 for the space
302
+ dir_path = text_before_cursor[trigger_end:].lstrip()
303
+ start_position = -(len(dir_path))
304
+
305
+ try:
306
+ prefix = Path(dir_path).expanduser()
307
+ part = str(prefix.parent)
308
+ dirs, _ = list_directory(part)
309
+ dirnames = [d for d in dirs if d.startswith(prefix.name)]
310
+ base_dir = str(prefix.parent)
311
+
312
+ # Preserve the user's original prefix (e.g., ~/ or relative paths)
313
+ # Extract what the user originally typed (with ~ or ./ preserved)
314
+ if dir_path.startswith("~"):
315
+ # User typed something with ~, preserve it
316
+ user_prefix = "~" + os.sep
317
+ # For suggestion, we replace the expanded base_dir back with ~/
318
+ original_prefix = dir_path.rstrip(os.sep)
319
+ else:
320
+ user_prefix = None
321
+ original_prefix = None
322
+
323
+ for d in dirnames:
324
+ # Build the completion text so we keep the already-typed directory parts.
325
+ if user_prefix and original_prefix:
326
+ # Restore ~ prefix
327
+ suggestion = user_prefix + d + os.sep
328
+ elif base_dir and base_dir != ".":
329
+ suggestion = str(Path(base_dir) / d)
330
+ else:
331
+ suggestion = d
332
+ # Append trailing slash so the user can continue tabbing into sub-dirs.
333
+ suggestion = suggestion.rstrip(os.sep) + os.sep
334
+ yield Completion(
335
+ suggestion,
336
+ start_position=start_position,
337
+ display=d + os.sep,
338
+ display_meta="Directory",
339
+ )
340
+ except Exception:
341
+ # Silently ignore errors (e.g., permission issues, non-existent dir)
342
+ pass
343
+
344
+
345
+ class AgentCompleter(Completer):
346
+ """
347
+ A completer that triggers on '/agent' to show available agents.
348
+
349
+ Usage: /agent <agent-name>
350
+ """
351
+
352
+ def __init__(self, trigger: str = "/agent"):
353
+ self.trigger = trigger
354
+
355
+ def get_completions(self, document, complete_event):
356
+ cursor_position = document.cursor_position
357
+ text_before_cursor = document.text_before_cursor
358
+ stripped_text = text_before_cursor.lstrip()
359
+
360
+ # Require a space after /agent before showing completions
361
+ if not stripped_text.startswith(self.trigger + " "):
362
+ return
363
+
364
+ # Extract the input after /agent and space (up to cursor)
365
+ trigger_pos = text_before_cursor.find(self.trigger)
366
+ trigger_end = trigger_pos + len(self.trigger) + 1 # +1 for the space
367
+ text_after_trigger = text_before_cursor[trigger_end:cursor_position].lstrip()
368
+ start_position = -(len(text_after_trigger))
369
+
370
+ # Load all available agent names
371
+ try:
372
+ from code_muse.command_line.pin_command_completion import load_agent_names
373
+
374
+ agent_names = load_agent_names()
375
+ except Exception:
376
+ # If agent loading fails, return no completions
377
+ return
378
+
379
+ # Filter and yield agent completions
380
+ try:
381
+ from code_muse.command_line.pin_command_completion import (
382
+ _get_agent_display_meta,
383
+ )
384
+ except ImportError:
385
+ _get_agent_display_meta = lambda x: "default" # noqa: E731
386
+
387
+ for agent_name in agent_names:
388
+ if agent_name.lower().startswith(text_after_trigger.lower()):
389
+ yield Completion(
390
+ agent_name,
391
+ start_position=start_position,
392
+ display=agent_name,
393
+ display_meta=_get_agent_display_meta(agent_name),
394
+ )
395
+
396
+
397
+ class SlashCompleter(Completer):
398
+ """
399
+ A completer that triggers on '/' at the beginning of the line
400
+ to show all available slash commands.
401
+ """
402
+
403
+ def get_completions(self, document, complete_event):
404
+ text_before_cursor = document.text_before_cursor
405
+ stripped_text = text_before_cursor.lstrip()
406
+
407
+ # Only trigger if '/' is the first non-whitespace character
408
+ if not stripped_text.startswith("/"):
409
+ return
410
+
411
+ # Get the text after the initial slash
412
+ if len(stripped_text) == 1:
413
+ # User just typed '/', show all commands
414
+ partial = ""
415
+ start_position = 0 # Don't replace anything, just insert at cursor
416
+ else:
417
+ # User is typing a command after the slash
418
+ partial = stripped_text[1:] # text after '/'
419
+ start_position = -(len(partial)) # Replace what was typed after '/'
420
+
421
+ # Load all available commands
422
+ try:
423
+ commands = get_unique_commands()
424
+ except Exception:
425
+ # If command loading fails, return no completions
426
+ return
427
+
428
+ # Collect all primary commands and their aliases for proper alphabetical sorting
429
+ all_completions = []
430
+
431
+ # Convert partial to lowercase for case-insensitive matching
432
+ partial_lower = partial.lower()
433
+
434
+ for cmd in commands:
435
+ # Add primary command (case-insensitive matching)
436
+ if cmd.name.lower().startswith(partial_lower):
437
+ all_completions.append(
438
+ {
439
+ "text": cmd.name,
440
+ "display": f"/{cmd.name}",
441
+ "meta": cmd.description,
442
+ "sort_key": cmd.name.lower(), # Case-insensitive sort
443
+ }
444
+ )
445
+
446
+ # Add all aliases (case-insensitive matching)
447
+ for alias in cmd.aliases:
448
+ if alias.lower().startswith(partial_lower):
449
+ all_completions.append(
450
+ {
451
+ "text": alias,
452
+ "display": f"/{alias} (alias for /{cmd.name})",
453
+ "meta": cmd.description,
454
+ "sort_key": alias.lower(), # Sort by alias name, not primary command
455
+ }
456
+ )
457
+
458
+ # Also include custom commands from plugins (like claude-code-auth)
459
+ try:
460
+ from code_muse import callbacks, plugins
461
+
462
+ # Ensure plugins are loaded so custom commands are registered
463
+ plugins.load_plugin_callbacks()
464
+ custom_help_results = callbacks.on_custom_command_help()
465
+ for res in custom_help_results:
466
+ if not res:
467
+ continue
468
+ # Format 1: List of tuples (command_name, description)
469
+ if isinstance(res, list):
470
+ for item in res:
471
+ if isinstance(item, tuple) and len(item) == 2:
472
+ cmd_name = str(item[0])
473
+ description = str(item[1])
474
+ if cmd_name.lower().startswith(partial_lower):
475
+ all_completions.append(
476
+ {
477
+ "text": cmd_name,
478
+ "display": f"/{cmd_name}",
479
+ "meta": description,
480
+ "sort_key": cmd_name.lower(),
481
+ }
482
+ )
483
+ # Format 2: Single tuple (command_name, description)
484
+ elif isinstance(res, tuple) and len(res) == 2:
485
+ cmd_name = str(res[0])
486
+ description = str(res[1])
487
+ if cmd_name.lower().startswith(partial_lower):
488
+ all_completions.append(
489
+ {
490
+ "text": cmd_name,
491
+ "display": f"/{cmd_name}",
492
+ "meta": description,
493
+ "sort_key": cmd_name.lower(),
494
+ }
495
+ )
496
+ except Exception:
497
+ # If custom command loading fails, continue with registered commands only
498
+ pass
499
+
500
+ # Sort all completions alphabetically
501
+ all_completions.sort(key=lambda x: x["sort_key"])
502
+
503
+ # Yield the sorted completions
504
+ for completion in all_completions:
505
+ yield Completion(
506
+ completion["text"],
507
+ start_position=start_position,
508
+ display=completion["display"],
509
+ display_meta=completion["meta"],
510
+ )
511
+
512
+
513
+ def _strip_variation_selectors(text: str) -> str:
514
+ """Remove variation selectors (U+FE00-FE0F) from text.
515
+
516
+ These invisible characters modify emoji rendering but cause width
517
+ calculation mismatches between prompt_toolkit and terminal emulators.
518
+ """
519
+ return "".join(c for c in text if not (0xFE00 <= ord(c) <= 0xFE0F))
520
+
521
+
522
+ def _normalize_emoji_spacing(text: str) -> str:
523
+ """Normalize emoji spacing for consistent terminal rendering.
524
+
525
+ Some emojis have East Asian Width 'N' (Neutral) which terminals render
526
+ inconsistently. This adds a space after such emojis to prevent
527
+ the following character from overlapping.
528
+ """
529
+ import unicodedata
530
+
531
+ result = []
532
+ text = _strip_variation_selectors(text)
533
+ for char in text:
534
+ result.append(char)
535
+ # Add padding after Neutral-width emoji to prevent overlap
536
+ if (
537
+ 0x1F300 <= ord(char) <= 0x1FAFF
538
+ and unicodedata.east_asian_width(char) == "N"
539
+ ):
540
+ result.append(" ") # Extra space buffer
541
+ return "".join(result)
542
+
543
+
544
+ def get_prompt_with_active_model(base: str = ">>> "):
545
+ from code_muse.agents.agent_manager import get_current_agent
546
+
547
+ global_model = get_active_model() or "(default)"
548
+
549
+ # Get current agent information
550
+ current_agent = get_current_agent()
551
+ agent_display = current_agent.display_name if current_agent else "muse"
552
+
553
+ # Check if current agent has a pinned model
554
+ agent_model = None
555
+ if current_agent and hasattr(current_agent, "get_model_name"):
556
+ agent_model = current_agent.get_model_name()
557
+
558
+ # Determine which model to display
559
+ if agent_model and agent_model != global_model:
560
+ # Show both models when they differ
561
+ model_display = f"[{global_model} → {agent_model}]"
562
+ elif agent_model:
563
+ # Show only the agent model when pinned
564
+ model_display = f"[{agent_model}]"
565
+ else:
566
+ # Show only the global model when no agent model is pinned
567
+ model_display = f"[{global_model}]"
568
+
569
+ cwd = os.getcwd()
570
+ try:
571
+ home = str(Path.home())
572
+ cwd_display = "~" + cwd[len(home) :] if cwd.startswith(home) else cwd
573
+ except RuntimeError:
574
+ cwd_display = cwd
575
+ return FormattedText(
576
+ [
577
+ ("class:agent", f"{_normalize_emoji_spacing(agent_display)} "),
578
+ ("class:model", model_display + " "),
579
+ ("class:cwd", "(" + str(cwd_display) + ") "),
580
+ ("class:arrow", str(base)),
581
+ ]
582
+ )
583
+
584
+
585
+ async def get_input_with_combined_completion(
586
+ prompt_str=">>> ", history_file: str | None = None
587
+ ) -> str:
588
+ # Use SafeFileHistory to handle encoding errors gracefully on Windows
589
+ history = SafeFileHistory(history_file) if history_file else None
590
+ # Build the base completer list, then bolt on any plugin completers.
591
+ from code_muse.plugins.ollama_setup.completer import OllamaSetupCompleter
592
+
593
+ completer = merge_completers(
594
+ [
595
+ FilePathCompleter(symbol="@"),
596
+ ModelNameCompleter(trigger="/model"),
597
+ ModelNameCompleter(trigger="/m"),
598
+ CDCompleter(trigger="/cd"),
599
+ SetCompleter(trigger="/set"),
600
+ LoadContextCompleter(trigger="/load_context"),
601
+ PinCompleter(trigger="/pin_model"),
602
+ UnpinCompleter(trigger="/unpin"),
603
+ AgentCompleter(trigger="/agent"),
604
+ AgentCompleter(trigger="/a"),
605
+ SkillsCompleter(trigger="/skills"),
606
+ OllamaSetupCompleter(),
607
+ SlashCompleter(),
608
+ ]
609
+ )
610
+ # Add custom key bindings and multiline toggle
611
+ bindings = KeyBindings()
612
+
613
+ # Multiline mode state
614
+ multiline = {"enabled": False}
615
+
616
+ # Ctrl+X keybinding - exit with KeyboardInterrupt for shell command cancellation
617
+ @bindings.add(Keys.ControlX)
618
+ def _(event):
619
+ try:
620
+ event.app.exit(exception=KeyboardInterrupt)
621
+ except Exception:
622
+ # Ignore "Return value already set" errors when exit was already called
623
+ # This happens when user presses multiple exit keys in quick succession
624
+ pass
625
+
626
+ # Escape keybinding - exit with KeyboardInterrupt
627
+ @bindings.add(Keys.Escape)
628
+ def _(event):
629
+ try:
630
+ event.app.exit(exception=KeyboardInterrupt)
631
+ except Exception:
632
+ # Ignore "Return value already set" errors when exit was already called
633
+ pass
634
+
635
+ # NOTE: We intentionally do NOT override Ctrl+C here.
636
+ # prompt_toolkit's default Ctrl+C handler properly resets the terminal state on Windows.
637
+ # Overriding it with event.app.exit(exception=KeyboardInterrupt) can leave the terminal
638
+ # in a bad state where characters cannot be typed. Let prompt_toolkit handle Ctrl+C natively.
639
+
640
+ # Toggle multiline with Alt+M
641
+ @bindings.add(Keys.Escape, "m")
642
+ def _(event):
643
+ multiline["enabled"] = not multiline["enabled"]
644
+ status = "ON" if multiline["enabled"] else "OFF"
645
+ # Print status for user feedback (version-agnostic)
646
+ # Note: Using sys.stdout here for immediate feedback during input
647
+ sys.stdout.write(f"[multiline] {status}\n")
648
+ sys.stdout.flush()
649
+
650
+ # Also toggle multiline with F2 (more reliable across platforms)
651
+ @bindings.add("f2")
652
+ def _(event):
653
+ multiline["enabled"] = not multiline["enabled"]
654
+ status = "ON" if multiline["enabled"] else "OFF"
655
+ sys.stdout.write(f"[multiline] {status}\n")
656
+ sys.stdout.flush()
657
+
658
+ # Newline insert bindings — robust and explicit
659
+ # Ctrl+J (line feed) works in virtually all terminals; mark eager so it wins
660
+ @bindings.add("c-j", eager=True)
661
+ def _(event):
662
+ event.app.current_buffer.insert_text("\n")
663
+
664
+ # Also allow Ctrl+Enter for newline (terminal-dependent)
665
+ try:
666
+
667
+ @bindings.add("c-enter", eager=True)
668
+ def _(event):
669
+ event.app.current_buffer.insert_text("\n")
670
+
671
+ except Exception:
672
+ pass
673
+
674
+ # Enter behavior depends on multiline mode
675
+ @bindings.add("enter", filter=~is_searching, eager=True)
676
+ def _(event):
677
+ if multiline["enabled"]:
678
+ event.app.current_buffer.insert_text("\n")
679
+ else:
680
+ event.current_buffer.validate_and_handle()
681
+
682
+ # Backspace/Delete: trigger completions after deletion
683
+ # By default, complete_while_typing only triggers on character insertion,
684
+ # not deletion. This fixes completions not reappearing after backspace.
685
+ @bindings.add("c-h", eager=True) # Backspace (Ctrl+H)
686
+ @bindings.add("backspace", eager=True)
687
+ def handle_backspace_with_completion(event):
688
+ buffer = event.app.current_buffer
689
+ # Perform the deletion first
690
+ buffer.delete_before_cursor(count=1)
691
+ # Then trigger completion if text starts with '/'
692
+ text = buffer.text.lstrip()
693
+ if text.startswith("/"):
694
+ buffer.start_completion(select_first=False)
695
+
696
+ @bindings.add("delete", eager=True)
697
+ def handle_delete_with_completion(event):
698
+ buffer = event.app.current_buffer
699
+ # Perform the deletion first
700
+ buffer.delete(count=1)
701
+ # Then trigger completion if text starts with '/'
702
+ text = buffer.text.lstrip()
703
+ if text.startswith("/"):
704
+ buffer.start_completion(select_first=False)
705
+
706
+ # Handle bracketed paste - smart detection for text vs images.
707
+ # Most terminals (Windows included!) send Ctrl+V through bracketed paste.
708
+ # - If there's meaningful text content → paste as text (drag-and-drop file paths, copied text)
709
+ # - If text is empty/whitespace → check for clipboard image (image paste on Windows)
710
+ @bindings.add(Keys.BracketedPaste)
711
+ def handle_bracketed_paste(event):
712
+ """Handle bracketed paste - smart text vs image detection."""
713
+ pasted_data = event.data
714
+
715
+ # If we have meaningful text content, paste it (don't check for images)
716
+ # This handles drag-and-drop file paths and normal text paste
717
+ if pasted_data and pasted_data.strip():
718
+ # Normalize Windows line endings to Unix style
719
+ sanitized_data = pasted_data.replace("\r\n", "\n").replace("\r", "\n")
720
+ event.app.current_buffer.insert_text(sanitized_data)
721
+ return
722
+
723
+ # No meaningful text - check if clipboard has an image (Windows image paste!)
724
+ try:
725
+ if has_image_in_clipboard():
726
+ placeholder = capture_clipboard_image_to_pending()
727
+ if placeholder:
728
+ event.app.current_buffer.insert_text(placeholder + " ")
729
+ event.app.output.bell()
730
+ return
731
+ except Exception:
732
+ pass
733
+
734
+ # Fallback: if there was whitespace-only data, paste it
735
+ if pasted_data:
736
+ sanitized_data = pasted_data.replace("\r\n", "\n").replace("\r", "\n")
737
+ event.app.current_buffer.insert_text(sanitized_data)
738
+
739
+ # Fallback Ctrl+V for terminals without bracketed paste support
740
+ @bindings.add("c-v", eager=True)
741
+ async def handle_smart_paste(event):
742
+ """Handle Ctrl+V - auto-detect image vs text in clipboard."""
743
+ try:
744
+ # Check for image first (offload blocking Linux subprocess to thread)
745
+ # TODO: PEP 734 async bridge — make has_image_in_clipboard async
746
+ has_image = await asyncio.to_thread(has_image_in_clipboard)
747
+ if has_image:
748
+ placeholder = capture_clipboard_image_to_pending()
749
+ if placeholder:
750
+ event.app.current_buffer.insert_text(placeholder + " ")
751
+ # The placeholder itself is visible feedback - no need for extra output
752
+ # Use bell for audible feedback (works in most terminals)
753
+ event.app.output.bell()
754
+ return # Don't also paste text
755
+ except Exception:
756
+ pass # Fall through to text paste on any error
757
+
758
+ # No image (or error) - do normal text paste
759
+ # prompt_toolkit doesn't have built-in paste, so we handle it manually
760
+ try:
761
+ import platform
762
+ import subprocess
763
+
764
+ text = None
765
+ system = platform.system()
766
+
767
+ if system == "Darwin": # macOS
768
+ result = await asyncio.to_thread(
769
+ subprocess.run,
770
+ ["pbpaste"],
771
+ capture_output=True,
772
+ text=True,
773
+ timeout=2,
774
+ )
775
+ if result.returncode == 0:
776
+ text = result.stdout
777
+ elif system == "Windows":
778
+ # Windows - use powershell
779
+ result = await asyncio.to_thread(
780
+ subprocess.run,
781
+ ["powershell", "-command", "Get-Clipboard"],
782
+ capture_output=True,
783
+ text=True,
784
+ timeout=2,
785
+ )
786
+ if result.returncode == 0:
787
+ text = result.stdout
788
+ else: # Linux
789
+ # Try xclip first, then xsel
790
+ for cmd in [
791
+ ["xclip", "-selection", "clipboard", "-o"],
792
+ ["xsel", "--clipboard", "--output"],
793
+ ]:
794
+ try:
795
+ result = await asyncio.to_thread(
796
+ subprocess.run,
797
+ cmd,
798
+ capture_output=True,
799
+ text=True,
800
+ timeout=2,
801
+ )
802
+ if result.returncode == 0:
803
+ text = result.stdout
804
+ break
805
+ except FileNotFoundError:
806
+ continue
807
+
808
+ if text:
809
+ # Normalize Windows line endings to Unix style
810
+ text = text.replace("\r\n", "\n").replace("\r", "\n")
811
+ # Strip trailing newline that clipboard tools often add
812
+ text = text.rstrip("\n")
813
+ event.app.current_buffer.insert_text(text)
814
+ except Exception:
815
+ pass # Silently fail if text paste doesn't work
816
+
817
+ # F3 - dedicated image paste (shows error if no image)
818
+ @bindings.add("f3")
819
+ def handle_image_paste_f3(event):
820
+ """Handle F3 - paste image from clipboard (image-only, shows error if none)."""
821
+ try:
822
+ if has_image_in_clipboard():
823
+ placeholder = capture_clipboard_image_to_pending()
824
+ if placeholder:
825
+ event.app.current_buffer.insert_text(placeholder + " ")
826
+ # The placeholder itself is visible feedback
827
+ # Use bell for audible feedback (works in most terminals)
828
+ event.app.output.bell()
829
+ else:
830
+ # Insert a transient message that user can delete
831
+ event.app.current_buffer.insert_text("[⚠️ no image in clipboard] ")
832
+ event.app.output.bell()
833
+ except Exception:
834
+ event.app.current_buffer.insert_text("[❌ clipboard error] ")
835
+ event.app.output.bell()
836
+
837
+ session = PromptSession(
838
+ completer=completer,
839
+ history=history,
840
+ complete_while_typing=True,
841
+ key_bindings=bindings,
842
+ input_processors=[AttachmentPlaceholderProcessor()],
843
+ )
844
+ # If they pass a string, backward-compat: convert it to formatted_text
845
+ if isinstance(prompt_str, str):
846
+ from prompt_toolkit.formatted_text import FormattedText
847
+
848
+ prompt_str = FormattedText([(None, prompt_str)])
849
+ style = Style.from_dict(
850
+ {
851
+ # Keys must AVOID the 'class:' prefix – that prefix is used only when
852
+ # tagging tokens in `FormattedText`. See prompt_toolkit docs.
853
+ "muse": "bold ansibrightcyan",
854
+ "owner": "bold ansibrightblue",
855
+ "agent": "bold ansibrightblue",
856
+ "model": "bold ansibrightcyan",
857
+ "cwd": "bold ansibrightgreen",
858
+ "arrow": "bold ansibrightblue",
859
+ "attachment-placeholder": "italic ansicyan",
860
+ }
861
+ )
862
+ text = await session.prompt_async(prompt_str, style=style)
863
+ # NOTE: We used to call update_model_in_input(text) here to handle /model and /m
864
+ # commands at the prompt level, but that prevented the command handler from running
865
+ # and emitting success messages. Now we let all /model commands fall through to
866
+ # the command handler in main.py for consistent handling.
867
+ return text
868
+
869
+
870
+ if __name__ == "__main__":
871
+ print("Type '@' for path-completion or '/model' to pick a model. Ctrl+D to exit.")
872
+
873
+ async def main():
874
+ while True:
875
+ try:
876
+ inp = await get_input_with_combined_completion(
877
+ get_prompt_with_active_model(), history_file=COMMAND_HISTORY_FILE
878
+ )
879
+ print(f"You entered: {inp}")
880
+ except KeyboardInterrupt:
881
+ continue
882
+ except EOFError:
883
+ break
884
+ print("\nGoodbye!")
885
+
886
+ asyncio.run(main())