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,73 @@
1
+ import glob
2
+ import os
3
+ from collections.abc import Iterable
4
+ from pathlib import Path
5
+
6
+ from prompt_toolkit.completion import Completer, Completion
7
+ from prompt_toolkit.document import Document
8
+
9
+
10
+ class FilePathCompleter(Completer):
11
+ """A simple file path completer that works with a trigger symbol."""
12
+
13
+ def __init__(self, symbol: str = "@"):
14
+ self.symbol = symbol
15
+
16
+ def get_completions(
17
+ self, document: Document, complete_event
18
+ ) -> Iterable[Completion]:
19
+ text = document.text
20
+ cursor_position = document.cursor_position
21
+ text_before_cursor = text[:cursor_position]
22
+ if self.symbol not in text_before_cursor:
23
+ return
24
+ symbol_pos = text_before_cursor.rfind(self.symbol)
25
+ text_after_symbol = text_before_cursor[symbol_pos + len(self.symbol) :]
26
+ start_position = -(len(text_after_symbol))
27
+ try:
28
+ pattern = text_after_symbol + "*"
29
+ if not pattern.strip("*") or pattern.strip("*").endswith("/"):
30
+ base_path = pattern.strip("*")
31
+ if not base_path:
32
+ base_path = "."
33
+ if base_path.startswith("~"):
34
+ base_path = Path(base_path).expanduser()
35
+ if Path(base_path).is_dir():
36
+ paths = [
37
+ str(Path(base_path) / f)
38
+ for f in os.listdir(base_path)
39
+ if not f.startswith(".") or text_after_symbol.endswith(".")
40
+ ]
41
+ else:
42
+ paths = []
43
+ else:
44
+ paths = glob.glob(pattern)
45
+ if not pattern.startswith(".") and not pattern.startswith("*/."):
46
+ paths = [p for p in paths if not Path(p).name.startswith(".")]
47
+ paths.sort()
48
+ for path in paths:
49
+ p = Path(path)
50
+ is_dir = p.is_dir()
51
+ display = p.name
52
+ if p.is_absolute():
53
+ display_path = path
54
+ else:
55
+ if text_after_symbol.startswith("/"):
56
+ display_path = str(p.resolve())
57
+ elif text_after_symbol.startswith("~"):
58
+ home = Path.home()
59
+ if path.startswith(str(home)):
60
+ display_path = "~" + path[len(str(home)) :]
61
+ else:
62
+ display_path = path
63
+ else:
64
+ display_path = path
65
+ display_meta = "Directory" if is_dir else "File"
66
+ yield Completion(
67
+ display_path,
68
+ start_position=start_position,
69
+ display=display,
70
+ display_meta=display_meta,
71
+ )
72
+ except OSError, RuntimeError:
73
+ pass
@@ -0,0 +1,57 @@
1
+ from pathlib import Path
2
+
3
+ from prompt_toolkit.completion import Completer, Completion
4
+
5
+ from code_muse.config import CONFIG_DIR
6
+
7
+
8
+ class LoadContextCompleter(Completer):
9
+ def __init__(self, trigger: str = "/load_context"):
10
+ self.trigger = trigger
11
+
12
+ def get_completions(self, document, complete_event):
13
+ cursor_position = document.cursor_position
14
+ text_before_cursor = document.text_before_cursor
15
+ stripped_text_for_trigger_check = text_before_cursor.lstrip()
16
+
17
+ # If user types just /load_context (no space), suggest adding a space
18
+ if stripped_text_for_trigger_check == self.trigger:
19
+ yield Completion(
20
+ self.trigger + " ",
21
+ start_position=-len(self.trigger),
22
+ display=self.trigger + " ",
23
+ display_meta="load saved context",
24
+ )
25
+ return
26
+
27
+ # Require a space after /load_context before showing completions (consistency with other completers)
28
+ if not stripped_text_for_trigger_check.startswith(self.trigger + " "):
29
+ return
30
+
31
+ # Extract the session name after /load_context and space (up to cursor)
32
+ actual_trigger_pos = text_before_cursor.find(self.trigger)
33
+ trigger_end = actual_trigger_pos + len(self.trigger) + 1 # +1 for the space
34
+ session_filter = text_before_cursor[trigger_end:cursor_position].lstrip()
35
+ start_position = -(len(session_filter))
36
+
37
+ # Get available context files (both .json and .pkl)
38
+ try:
39
+ contexts_dir = Path(CONFIG_DIR) / "contexts"
40
+ if contexts_dir.exists():
41
+ seen: set[str] = set()
42
+ for ext in ("*.json", "*.pkl"):
43
+ for context_file in contexts_dir.glob(ext):
44
+ session_name = context_file.stem
45
+ if session_name in seen:
46
+ continue
47
+ seen.add(session_name)
48
+ if session_name.startswith(session_filter):
49
+ yield Completion(
50
+ session_name,
51
+ start_position=start_position,
52
+ display=session_name,
53
+ display_meta="saved context session",
54
+ )
55
+ except Exception:
56
+ # Silently ignore errors (e.g., permission issues, non-existent dir)
57
+ pass
@@ -0,0 +1,512 @@
1
+ from collections.abc import Iterable
2
+ from pathlib import Path
3
+
4
+ from prompt_toolkit import Application, PromptSession
5
+ from prompt_toolkit.completion import Completer, Completion
6
+ from prompt_toolkit.document import Document
7
+ from prompt_toolkit.history import FileHistory
8
+ from prompt_toolkit.key_binding import KeyBindings
9
+ from prompt_toolkit.layout import Layout, Window
10
+ from prompt_toolkit.layout.controls import FormattedTextControl
11
+
12
+ from code_muse.command_line.pagination import (
13
+ ensure_visible_page,
14
+ get_page_bounds,
15
+ get_page_for_index,
16
+ get_total_pages,
17
+ )
18
+ from code_muse.config import get_global_model_name
19
+ from code_muse.list_filtering import query_matches_text
20
+ from code_muse.model_switching import set_model_and_reload_agent
21
+
22
+ MODEL_PICKER_PAGE_SIZE = 15
23
+
24
+
25
+ def load_model_names():
26
+ """Load model names from the config that's fetched from the endpoint."""
27
+ from code_muse.model_factory import ModelFactory
28
+
29
+ models_config = ModelFactory.load_config()
30
+ return list(models_config.keys())
31
+
32
+
33
+ def get_active_model():
34
+ """
35
+ Returns the active model from the config using get_model_name().
36
+ This ensures consistency across the codebase by always using the config value.
37
+ """
38
+ return get_global_model_name()
39
+
40
+
41
+ def set_active_model(model_name: str):
42
+ """
43
+ Sets the active model name by updating the config (for persistence).
44
+ """
45
+ set_model_and_reload_agent(model_name)
46
+
47
+
48
+ class ModelNameCompleter(Completer):
49
+ """
50
+ A completer that triggers on '/model' to show available models from models.json.
51
+ Only '/model' (not just '/') will trigger the dropdown.
52
+ """
53
+
54
+ def __init__(self, trigger: str = "/model"):
55
+ self.trigger = trigger
56
+ self.model_names = load_model_names()
57
+
58
+ def get_completions(
59
+ self, document: Document, complete_event
60
+ ) -> Iterable[Completion]:
61
+ text = document.text
62
+ cursor_position = document.cursor_position
63
+ text_before_cursor = text[:cursor_position]
64
+
65
+ # Only trigger if /model is at the very beginning of the line and has a space after it
66
+ stripped_text = text_before_cursor.lstrip()
67
+ if not stripped_text.startswith(self.trigger + " "):
68
+ return
69
+
70
+ # Find where /model actually starts (after any leading whitespace)
71
+ symbol_pos = text_before_cursor.find(self.trigger)
72
+ text_after_trigger = text_before_cursor[
73
+ symbol_pos + len(self.trigger) + 1 :
74
+ ].lstrip()
75
+ start_position = -(len(text_after_trigger))
76
+
77
+ # Filter model names based on what's typed after /model (case-insensitive)
78
+ for model_name in self.model_names:
79
+ if text_after_trigger and not query_matches_text(
80
+ text_after_trigger, model_name
81
+ ):
82
+ continue # Skip models that don't match the typed text
83
+
84
+ meta = (
85
+ "Model (selected)"
86
+ if model_name.lower() == get_active_model().lower()
87
+ else "Model"
88
+ )
89
+ yield Completion(
90
+ model_name,
91
+ start_position=start_position,
92
+ display=model_name,
93
+ display_meta=meta,
94
+ )
95
+
96
+
97
+ def _find_matching_model(rest: str, model_names: list[str]) -> str | None:
98
+ """
99
+ Find the best matching model for the given input.
100
+
101
+ Priority:
102
+ 1. Exact match (case-insensitive)
103
+ 2. Input starts with a model name (longest/most specific wins)
104
+ 3. Model starts with input (prefix/completion match, longest wins)
105
+ """
106
+ rest_lower = rest.lower()
107
+
108
+ # First check for exact match
109
+ for model in model_names:
110
+ if rest_lower == model.lower():
111
+ return model
112
+
113
+ # Sort by length (longest first) so more specific matches win
114
+ sorted_models = sorted(model_names, key=len, reverse=True)
115
+
116
+ # Check if input starts with a model name (e.g. "gpt-5 tell me a joke")
117
+ for model in sorted_models:
118
+ model_lower = model.lower()
119
+ if rest_lower.startswith(model_lower) and (
120
+ len(rest_lower) == len(model_lower) or rest_lower[len(model_lower)] == " "
121
+ ):
122
+ return model
123
+
124
+ # Check for prefix/completion match (input is partial model name)
125
+ for model in sorted_models:
126
+ if model.lower().startswith(rest_lower):
127
+ return model
128
+
129
+ # Fall back to the same fuzzy matcher used by the completer.
130
+ for model in sorted_models:
131
+ if query_matches_text(rest, model):
132
+ return model
133
+
134
+ return None
135
+
136
+
137
+ def update_model_in_input(text: str) -> str | None:
138
+ # If input starts with /model or /m and a model name, set model and strip it out
139
+ content = text.strip()
140
+ model_names = load_model_names()
141
+
142
+ # Check for /model command (require space after /model, case-insensitive)
143
+ if content.lower().startswith("/model "):
144
+ # Find the actual /model command (case-insensitive)
145
+ model_cmd = content.split(" ", 1)[0] # Get the command part
146
+ rest = content[len(model_cmd) :].strip() # Remove the actual command
147
+
148
+ # Find the best matching model
149
+ model = _find_matching_model(rest, model_names)
150
+ if model:
151
+ # Found a matching model - now extract it properly
152
+ set_active_model(model)
153
+
154
+ # Find the actual model name in the original text (preserving case)
155
+ # We need to find where the model ends in the original rest string
156
+ model_end_idx = len(model)
157
+
158
+ # Build the full command+model part to remove
159
+ cmd_and_model_pattern = model_cmd + " " + rest[:model_end_idx]
160
+ idx = text.find(cmd_and_model_pattern)
161
+ if idx != -1:
162
+ new_text = (
163
+ text[:idx] + text[idx + len(cmd_and_model_pattern) :]
164
+ ).strip()
165
+ return new_text
166
+ return None
167
+
168
+ # Check for /m command (case-insensitive)
169
+ elif content.lower().startswith("/m ") and not content.lower().startswith(
170
+ "/model "
171
+ ):
172
+ # Find the actual /m command (case-insensitive)
173
+ m_cmd = content.split(" ", 1)[0] # Get the command part
174
+ rest = content[len(m_cmd) :].strip() # Remove the actual command
175
+
176
+ # Find the best matching model
177
+ model = _find_matching_model(rest, model_names)
178
+ if model:
179
+ # Found a matching model - now extract it properly
180
+ set_active_model(model)
181
+
182
+ # Find the actual model name in the original text (preserving case)
183
+ # We need to find where the model ends in the original rest string
184
+ model_end_idx = len(model)
185
+
186
+ # Build the full command+model part to remove
187
+ # Handle space variations in the original text
188
+ cmd_and_model_pattern = m_cmd + " " + rest[:model_end_idx]
189
+ idx = text.find(cmd_and_model_pattern)
190
+ if idx != -1:
191
+ new_text = (
192
+ text[:idx] + text[idx + len(cmd_and_model_pattern) :]
193
+ ).strip()
194
+ return new_text
195
+ return None
196
+
197
+ return None
198
+
199
+
200
+ class ModelSelectionMenu:
201
+ """Paginated interactive model picker for the /model command."""
202
+
203
+ def __init__(self, model_names: list[str | None] = None):
204
+ self.model_names = (
205
+ list(model_names) if model_names is not None else load_model_names()
206
+ )
207
+ self.current_model = get_active_model()
208
+ self.filter_text = ""
209
+ self.selected_index = 0
210
+ self.page = 0
211
+ self.page_size = MODEL_PICKER_PAGE_SIZE
212
+ self.result: str | None = None
213
+
214
+ if self.current_model in self.visible_model_names:
215
+ self.selected_index = self.visible_model_names.index(self.current_model)
216
+ self.page = get_page_for_index(self.selected_index, self.page_size)
217
+
218
+ @property
219
+ def total_pages(self) -> int:
220
+ return get_total_pages(len(self.visible_model_names), self.page_size)
221
+
222
+ @property
223
+ def page_start(self) -> int:
224
+ start, _ = get_page_bounds(
225
+ self.page, len(self.visible_model_names), self.page_size
226
+ )
227
+ return start
228
+
229
+ @property
230
+ def page_end(self) -> int:
231
+ _, end = get_page_bounds(
232
+ self.page, len(self.visible_model_names), self.page_size
233
+ )
234
+ return end
235
+
236
+ @property
237
+ def models_on_page(self) -> list[str]:
238
+ return self.visible_model_names[self.page_start : self.page_end]
239
+
240
+ @property
241
+ def visible_model_names(self) -> list[str]:
242
+ if not self.filter_text:
243
+ return self.model_names
244
+ return [
245
+ model_name
246
+ for model_name in self.model_names
247
+ if query_matches_text(self.filter_text, model_name)
248
+ ]
249
+
250
+ def _get_selected_model_name(self) -> str | None:
251
+ if 0 <= self.selected_index < len(self.visible_model_names):
252
+ return self.visible_model_names[self.selected_index]
253
+ return None
254
+
255
+ def _ensure_selection_visible(self) -> None:
256
+ self.page = ensure_visible_page(
257
+ self.selected_index,
258
+ self.page,
259
+ len(self.visible_model_names),
260
+ self.page_size,
261
+ )
262
+
263
+ def _set_filter_text(self, value: str) -> None:
264
+ selected_model = self._get_selected_model_name()
265
+ self.filter_text = value
266
+ visible_models = self.visible_model_names
267
+ if not visible_models:
268
+ self.selected_index = 0
269
+ self.page = 0
270
+ return
271
+
272
+ if selected_model and selected_model in visible_models:
273
+ self.selected_index = visible_models.index(selected_model)
274
+ elif self.current_model in visible_models:
275
+ self.selected_index = visible_models.index(self.current_model)
276
+ else:
277
+ self.selected_index = 0
278
+ self._ensure_selection_visible()
279
+
280
+ def _append_filter_char(self, value: str) -> None:
281
+ self._set_filter_text(self.filter_text + value)
282
+
283
+ def _delete_filter_char(self) -> None:
284
+ if self.filter_text:
285
+ self._set_filter_text(self.filter_text[:-1])
286
+
287
+ def _accept_selection(self) -> bool:
288
+ """Store the currently selected visible model if one is available."""
289
+ selected_model = self._get_selected_model_name()
290
+ if selected_model is None:
291
+ return False
292
+ self.result = selected_model
293
+ return True
294
+
295
+ def _move_up(self) -> None:
296
+ if self.selected_index > 0:
297
+ self.selected_index -= 1
298
+ self._ensure_selection_visible()
299
+
300
+ def _move_down(self) -> None:
301
+ if self.selected_index < len(self.visible_model_names) - 1:
302
+ self.selected_index += 1
303
+ self._ensure_selection_visible()
304
+
305
+ def _page_up(self) -> None:
306
+ if self.page > 0:
307
+ self.page -= 1
308
+ self.selected_index = self.page_start
309
+
310
+ def _page_down(self) -> None:
311
+ if self.page < self.total_pages - 1:
312
+ self.page += 1
313
+ self.selected_index = self.page_start
314
+
315
+ def _render(self):
316
+ lines = [("bold cyan", " 🤖 Select Active Model")]
317
+ filter_label = self.filter_text or "type to filter"
318
+ lines.append(("fg:ansibrightblack", f"\n Filter: {filter_label}"))
319
+ if self.total_pages > 1:
320
+ lines.append(
321
+ ("fg:ansibrightblack", f" (Page {self.page + 1}/{self.total_pages})")
322
+ )
323
+ lines.append(("", "\n"))
324
+
325
+ if not self.visible_model_names:
326
+ empty_message = (
327
+ "No models match the current filter."
328
+ if self.filter_text
329
+ else "No models available."
330
+ )
331
+ lines.append(("fg:ansiyellow", f"\n {empty_message}\n"))
332
+ lines.append(("fg:ansibrightblack", " Type "))
333
+ lines.append(("", "Adjust filter\n"))
334
+ lines.append(("fg:ansibrightblack", " Backspace "))
335
+ lines.append(("", "Delete filter char\n"))
336
+ if self.filter_text:
337
+ lines.append(("fg:ansibrightblack", " Ctrl+U "))
338
+ lines.append(("", "Clear filter\n"))
339
+ lines.append(("fg:ansiyellow", " Esc "))
340
+ lines.append(("", "Exit\n"))
341
+ return lines
342
+
343
+ lines.append(("fg:ansibrightblack", f"\n Current: {self.current_model}\n\n"))
344
+
345
+ for offset, model_name in enumerate(self.models_on_page):
346
+ absolute_index = self.page_start + offset
347
+ is_selected = absolute_index == self.selected_index
348
+ is_current = model_name == self.current_model
349
+
350
+ prefix = " › " if is_selected else " "
351
+ style = "fg:ansiwhite bold" if is_selected else "fg:ansibrightblack"
352
+ lines.append((style, f"{prefix}{model_name}"))
353
+ if is_current:
354
+ lines.append(("fg:ansigreen", " (active)"))
355
+ lines.append(("", "\n"))
356
+
357
+ lines.append(("", "\n"))
358
+ lines.append(("fg:ansibrightblack", " ↑/↓ "))
359
+ lines.append(("", "Navigate\n"))
360
+ if self.total_pages > 1:
361
+ lines.append(("fg:ansibrightblack", " PgUp/PgDn "))
362
+ lines.append(("", "Change page\n"))
363
+ lines.append(("fg:ansibrightblack", " Type "))
364
+ lines.append(("", "Filter models\n"))
365
+ lines.append(("fg:ansibrightblack", " Backspace "))
366
+ lines.append(("", "Delete filter char\n"))
367
+ lines.append(("fg:ansibrightblack", " Ctrl+U "))
368
+ lines.append(("", "Clear filter\n"))
369
+ lines.append(("fg:ansigreen", " Enter "))
370
+ lines.append(("", "Select model\n"))
371
+ lines.append(("fg:ansiyellow", " Esc "))
372
+ lines.append(("", "Cancel\n"))
373
+ return lines
374
+
375
+ async def run_async(self) -> str | None:
376
+ control = FormattedTextControl(lambda: self._render())
377
+ kb = KeyBindings()
378
+
379
+ def refresh() -> None:
380
+ control.text = self._render()
381
+
382
+ @kb.add("up")
383
+ @kb.add("c-p")
384
+ def _(event):
385
+ self._move_up()
386
+ refresh()
387
+ event.app.invalidate()
388
+
389
+ @kb.add("down")
390
+ @kb.add("c-n")
391
+ def _(event):
392
+ self._move_down()
393
+ refresh()
394
+ event.app.invalidate()
395
+
396
+ @kb.add("pageup")
397
+ @kb.add("left")
398
+ def _(event):
399
+ self._page_up()
400
+ refresh()
401
+ event.app.invalidate()
402
+
403
+ @kb.add("pagedown")
404
+ @kb.add("right")
405
+ def _(event):
406
+ self._page_down()
407
+ refresh()
408
+ event.app.invalidate()
409
+
410
+ @kb.add("backspace")
411
+ def _(event):
412
+ if not self.filter_text:
413
+ return
414
+ self._delete_filter_char()
415
+ refresh()
416
+ event.app.invalidate()
417
+
418
+ @kb.add("c-u")
419
+ def _(event):
420
+ if not self.filter_text:
421
+ return
422
+ self._set_filter_text("")
423
+ refresh()
424
+ event.app.invalidate()
425
+
426
+ @kb.add("<any>")
427
+ def _(event):
428
+ if not event.data or not event.data.isprintable():
429
+ return
430
+ self._append_filter_char(event.data)
431
+ refresh()
432
+ event.app.invalidate()
433
+
434
+ @kb.add("enter")
435
+ def _(event):
436
+ if not self._accept_selection():
437
+ return
438
+ event.app.exit()
439
+
440
+ @kb.add("escape")
441
+ @kb.add("c-c")
442
+ def _(event):
443
+ self.result = None
444
+ event.app.exit()
445
+
446
+ app = Application(
447
+ layout=Layout(Window(content=control, wrap_lines=True)),
448
+ key_bindings=kb,
449
+ full_screen=False,
450
+ )
451
+ await app.run_async()
452
+ return self.result
453
+
454
+
455
+ def _build_legacy_picker_choices(
456
+ model_names: list[str], current_model: str
457
+ ) -> list[str]:
458
+ """Build simple picker labels for test and non-interactive fallback paths."""
459
+ choices = []
460
+ for model_name in model_names:
461
+ suffix = " (current)" if model_name == current_model else ""
462
+ choices.append(f"{model_name}{suffix}")
463
+ return choices
464
+
465
+
466
+ def _normalize_legacy_picker_choice(choice: str) -> str:
467
+ """Extract the model name from a legacy picker label."""
468
+ return choice.removesuffix(" (current)")
469
+
470
+
471
+ async def interactive_model_picker() -> str | None:
472
+ """Run the paginated interactive model picker used by /model."""
473
+ from code_muse.tools.command_runner import set_awaiting_user_input
474
+
475
+ set_awaiting_user_input(True)
476
+ try:
477
+ try:
478
+ return await ModelSelectionMenu().run_async()
479
+ except EOFError:
480
+ model_names = load_model_names()
481
+ current_model = get_active_model()
482
+ choices = _build_legacy_picker_choices(model_names, current_model)
483
+ if not choices:
484
+ return None
485
+
486
+ from code_muse.tools.common import arrow_select_async
487
+
488
+ try:
489
+ selected = await arrow_select_async("Select Active Model", choices)
490
+ except KeyboardInterrupt:
491
+ return None
492
+ return _normalize_legacy_picker_choice(selected)
493
+ finally:
494
+ set_awaiting_user_input(False)
495
+
496
+
497
+ async def get_input_with_model_completion(
498
+ prompt_str: str = ">>> ",
499
+ trigger: str = "/model",
500
+ history_file: str | None = None,
501
+ ) -> str:
502
+ history = FileHistory(Path(history_file).expanduser()) if history_file else None
503
+ session = PromptSession(
504
+ completer=ModelNameCompleter(trigger),
505
+ history=history,
506
+ complete_while_typing=True,
507
+ )
508
+ text = await session.prompt_async(prompt_str)
509
+ possibly_stripped = update_model_in_input(text)
510
+ if possibly_stripped is not None:
511
+ return possibly_stripped
512
+ return text