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,1552 @@
1
+ """Interactive terminal UI for configuring MindPack experts.
2
+
3
+ Provides a split-panel interface for browsing, adding, editing,
4
+ and deleting MindPack expert descriptors — following the same
5
+ UI patterns as agent_menu.py.
6
+ """
7
+
8
+ import asyncio
9
+ import logging
10
+ import re
11
+ import sys
12
+ import unicodedata
13
+
14
+ from prompt_toolkit.application import Application
15
+ from prompt_toolkit.key_binding import KeyBindings
16
+ from prompt_toolkit.layout import Dimension, Layout, VSplit, Window
17
+ from prompt_toolkit.layout.controls import FormattedTextControl
18
+ from prompt_toolkit.widgets import Frame
19
+
20
+ from code_muse.command_line.model_picker_completion import load_model_names
21
+ from code_muse.command_line.pagination import (
22
+ ensure_visible_page,
23
+ get_page_bounds,
24
+ get_page_for_index,
25
+ get_total_pages,
26
+ )
27
+ from code_muse.messaging import emit_info, emit_success, emit_warning
28
+ from code_muse.plugins.mindpack.schemas import ExpertDescriptor, ProfileDescriptor
29
+ from code_muse.plugins.mindpack.tools import orchestrator
30
+ from code_muse.tools.command_runner import set_awaiting_user_input
31
+ from code_muse.tools.common import arrow_select_async
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+ PAGE_SIZE = 10 # Experts per page
36
+
37
+ # ---------------------------------------------------------------------------
38
+ # Preset expert templates
39
+ # ---------------------------------------------------------------------------
40
+
41
+ PRESET_EXPERTS = [
42
+ ExpertDescriptor(
43
+ name="SecurityReviewer",
44
+ speciality="security analysis & vulnerability assessment",
45
+ system_prompt_fragment=(
46
+ "You are SecurityReviewer, the security specialist. "
47
+ "Your job is to identify security vulnerabilities, "
48
+ "check for OWASP Top 10 issues, validate authentication/authorization "
49
+ "flows, and ensure secure coding practices are followed. "
50
+ "Focus on: input validation, SQL injection, XSS, CSRF, "
51
+ "authentication bypasses, and data exposure risks."
52
+ ),
53
+ model="strong",
54
+ ),
55
+ ExpertDescriptor(
56
+ name="PerfReviewer",
57
+ speciality="performance analysis & optimization",
58
+ system_prompt_fragment=(
59
+ "You are PerfReviewer, the performance specialist. "
60
+ "Your job is to identify performance bottlenecks, "
61
+ "analyze algorithmic complexity, review database query efficiency, "
62
+ "and suggest optimization strategies. "
63
+ "Focus on: time complexity, memory usage, N+1 queries, "
64
+ "caching opportunities, and scalability concerns."
65
+ ),
66
+ model="medium",
67
+ ),
68
+ ExpertDescriptor(
69
+ name="UXReviewer",
70
+ speciality="user experience & accessibility review",
71
+ system_prompt_fragment=(
72
+ "You are UXReviewer, the user experience specialist. "
73
+ "Your job is to review code changes from a user-centric perspective, "
74
+ "check accessibility compliance (WCAG), validate error handling UX, "
75
+ "and ensure intuitive interfaces. "
76
+ "Focus on: accessibility (a11y), error messages, loading states, "
77
+ "responsive design, and user flow clarity."
78
+ ),
79
+ model="medium",
80
+ ),
81
+ ExpertDescriptor(
82
+ name="APReviewer",
83
+ speciality="API design & REST conventions",
84
+ system_prompt_fragment=(
85
+ "You are APIReviewer, the API design specialist. "
86
+ "Your job is to review API endpoints for REST compliance, "
87
+ "validate request/response schemas, check HTTP status codes, "
88
+ "and ensure consistent API conventions. "
89
+ "Focus on: RESTful design, OpenAPI schema validation, "
90
+ "versioning strategy, error response format, and idempotency."
91
+ ),
92
+ model="strong",
93
+ ),
94
+ ExpertDescriptor(
95
+ name="DBReviewer",
96
+ speciality="database schema & query optimization",
97
+ system_prompt_fragment=(
98
+ "You are DBReviewer, the database specialist. "
99
+ "Your job is to review schema migrations, analyze query performance, "
100
+ "check indexing strategy, and validate data integrity constraints. "
101
+ "Focus on: migration safety, index usage, N+1 queries, "
102
+ "transaction boundaries, and normalization vs. performance tradeoffs."
103
+ ),
104
+ model="medium",
105
+ ),
106
+ ]
107
+
108
+ # ---------------------------------------------------------------------------
109
+ # MindPack default settings (used by settings panel)
110
+ # ---------------------------------------------------------------------------
111
+
112
+ _DEFAULT_SETTINGS = {
113
+ "spawn_mode": "fixed",
114
+ "default_expert_count": "5",
115
+ }
116
+
117
+
118
+ def _sanitize_display_text(text: str) -> str:
119
+ """Remove or replace characters that cause terminal rendering issues.
120
+
121
+ Args:
122
+ text: Text that may contain emojis or wide characters
123
+
124
+ Returns:
125
+ Sanitized text safe for prompt_toolkit rendering
126
+ """
127
+ result = []
128
+ for char in text:
129
+ cat = unicodedata.category(char)
130
+ safe_categories = (
131
+ "Lu",
132
+ "Ll",
133
+ "Lt",
134
+ "Lm",
135
+ "Lo",
136
+ "Nd",
137
+ "Nl",
138
+ "No",
139
+ "Pc",
140
+ "Pd",
141
+ "Ps",
142
+ "Pe",
143
+ "Pi",
144
+ "Pf",
145
+ "Po",
146
+ "Zs",
147
+ "Sm",
148
+ "Sc",
149
+ "Sk",
150
+ )
151
+ if cat in safe_categories:
152
+ result.append(char)
153
+
154
+ cleaned = " ".join("".join(result).split())
155
+ return cleaned
156
+
157
+
158
+ # ---------------------------------------------------------------------------
159
+ # Expert list helpers
160
+ # ---------------------------------------------------------------------------
161
+
162
+
163
+ def _get_expert_entries() -> list[ExpertDescriptor]:
164
+ """Return the current expert registry, sorted by name."""
165
+ return sorted(orchestrator.expert_registry, key=lambda e: e.name.lower())
166
+
167
+
168
+ def _get_profile_entries() -> list[ProfileDescriptor]:
169
+ """Return the current profile registry, sorted by name
170
+ with 'Default' always last."""
171
+ profiles = orchestrator.profile_registry
172
+ default = [p for p in profiles if p.name == "Default"]
173
+ others = sorted(
174
+ [p for p in profiles if p.name != "Default"],
175
+ key=lambda p: p.name.lower(),
176
+ )
177
+ return others + default
178
+
179
+
180
+ def _get_expert_entries_for_profile(profile_name: str) -> list[ExpertDescriptor]:
181
+ """Return experts belonging to a specific profile, sorted by name."""
182
+ return sorted(
183
+ orchestrator.get_experts_for_profile(profile_name),
184
+ key=lambda e: e.name.lower(),
185
+ )
186
+
187
+
188
+ # ---------------------------------------------------------------------------
189
+ # Menu panel rendering
190
+ # ---------------------------------------------------------------------------
191
+
192
+
193
+ def _render_menu_panel(
194
+ entries: list[ExpertDescriptor],
195
+ page: int,
196
+ selected_idx: int,
197
+ ) -> list:
198
+ """Render the left menu panel with pagination.
199
+
200
+ Args:
201
+ entries: list of ExpertDescriptor
202
+ page: Current page number (0-indexed)
203
+ selected_idx: Currently selected index (global)
204
+
205
+ Returns:
206
+ list of (style, text) tuples for FormattedTextControl
207
+ """
208
+ lines: list[tuple[str, str]] = []
209
+ total_pages = get_total_pages(len(entries), PAGE_SIZE)
210
+ start_idx, end_idx = get_page_bounds(page, len(entries), PAGE_SIZE)
211
+
212
+ lines.append(("bold", "Experts"))
213
+ lines.append(("fg:ansibrightblack", f" (Page {page + 1}/{total_pages})"))
214
+ lines.append(("", "\n\n"))
215
+
216
+ if not entries:
217
+ lines.append(("fg:yellow", " No experts configured."))
218
+ lines.append(("", "\n\n"))
219
+ else:
220
+ for i in range(start_idx, end_idx):
221
+ expert = entries[i]
222
+ is_selected = i == selected_idx
223
+
224
+ safe_name = _sanitize_display_text(expert.name)
225
+ safe_speciality = _sanitize_display_text(expert.speciality)
226
+
227
+ if is_selected:
228
+ lines.append(("fg:ansigreen", "> "))
229
+ lines.append(("fg:ansigreen bold", safe_name))
230
+ else:
231
+ lines.append(("", " "))
232
+ lines.append(("", safe_name))
233
+
234
+ # Show model indicator if set
235
+ if expert.model:
236
+ safe_model = _sanitize_display_text(expert.model)
237
+ lines.append(("fg:ansiyellow", f" -> {safe_model}"))
238
+
239
+ lines.append(("", "\n"))
240
+
241
+ # Second line: speciality (dimmed)
242
+ if is_selected:
243
+ lines.append(("fg:ansibrightgreen", " "))
244
+ else:
245
+ lines.append(("fg:ansibrightblack", " "))
246
+ lines.append(("fg:ansibrightblack", safe_speciality))
247
+ lines.append(("", "\n"))
248
+
249
+ # Navigation hints
250
+ lines.append(("", "\n"))
251
+ lines.append(("fg:ansibrightblack", " Up/Dn "))
252
+ lines.append(("", "Navigate\n"))
253
+ lines.append(("fg:ansibrightblack", " Lt/Rt "))
254
+ lines.append(("", "Page\n"))
255
+ lines.append(("fg:green", " Enter "))
256
+ lines.append(("", "Edit expert\n"))
257
+ lines.append(("fg:ansibrightblack", " A "))
258
+ lines.append(("", "Add expert\n"))
259
+ lines.append(("fg:ansibrightblack", " D "))
260
+ lines.append(("", "Delete expert\n"))
261
+ lines.append(("fg:ansibrightblack", " C "))
262
+ lines.append(("", "Settings\n"))
263
+ lines.append(("fg:ansibrightred", " Ctrl+C "))
264
+ lines.append(("", "Exit"))
265
+
266
+ return lines
267
+
268
+
269
+ def _render_profile_menu_panel(
270
+ profiles: list[ProfileDescriptor],
271
+ page: int,
272
+ selected_idx: int,
273
+ ) -> list:
274
+ """Render the left menu panel for profile selection.
275
+
276
+ Args:
277
+ profiles: list of ProfileDescriptor
278
+ page: Current page number (0-indexed)
279
+ selected_idx: Currently selected index (global)
280
+
281
+ Returns:
282
+ list of (style, text) tuples for FormattedTextControl
283
+ """
284
+ lines: list[tuple[str, str]] = []
285
+ total_pages = get_total_pages(len(profiles), PAGE_SIZE)
286
+ start_idx, end_idx = get_page_bounds(page, len(profiles), PAGE_SIZE)
287
+
288
+ lines.append(("bold", "Profiles"))
289
+ lines.append(("fg:ansibrightblack", f" (Page {page + 1}/{total_pages})"))
290
+ lines.append(("", "\n\n"))
291
+
292
+ if not profiles:
293
+ lines.append(("fg:yellow", " No profiles configured."))
294
+ lines.append(("", "\n\n"))
295
+ else:
296
+ for i in range(start_idx, end_idx):
297
+ profile = profiles[i]
298
+ is_selected = i == selected_idx
299
+
300
+ safe_name = _sanitize_display_text(profile.name)
301
+
302
+ if is_selected:
303
+ lines.append(("fg:ansigreen", "> "))
304
+ lines.append(("fg:ansigreen bold", safe_name))
305
+ else:
306
+ lines.append(("", " "))
307
+ lines.append(("", safe_name))
308
+
309
+ # Expert count badge
310
+ count = len(profile.expert_names)
311
+ lines.append(("fg:ansiyellow", f" ({count} experts)"))
312
+ lines.append(("", "\n"))
313
+
314
+ # Description line (dimmed)
315
+ safe_desc = (
316
+ _sanitize_display_text(profile.description)
317
+ if profile.description
318
+ else "(no description)"
319
+ )
320
+ if is_selected:
321
+ lines.append(("fg:ansibrightgreen", " "))
322
+ else:
323
+ lines.append(("fg:ansibrightblack", " "))
324
+ lines.append(("fg:ansibrightblack", safe_desc))
325
+ lines.append(("", "\n"))
326
+
327
+ # Navigation hints
328
+ lines.append(("", "\n"))
329
+ lines.append(("fg:ansibrightblack", " Up/Dn "))
330
+ lines.append(("", "Navigate\n"))
331
+ lines.append(("fg:ansibrightblack", " Lt/Rt "))
332
+ lines.append(("", "Page\n"))
333
+ lines.append(("fg:green", " Enter "))
334
+ lines.append(("", "Open experts\n"))
335
+ lines.append(("fg:green", " A "))
336
+ lines.append(("", "Activate & exit\n"))
337
+ lines.append(("fg:ansibrightblack", " N "))
338
+ lines.append(("", "Add profile\n"))
339
+ lines.append(("fg:ansibrightblack", " D "))
340
+ lines.append(("", "Delete profile\n"))
341
+ lines.append(("fg:ansibrightblack", " E "))
342
+ lines.append(("", "Edit profile\n"))
343
+ lines.append(("fg:ansibrightred", " Ctrl+C "))
344
+ lines.append(("", "Exit"))
345
+
346
+ return lines
347
+
348
+
349
+ # ---------------------------------------------------------------------------
350
+ # Preview panel rendering
351
+ # ---------------------------------------------------------------------------
352
+
353
+
354
+ def _render_profile_preview_panel(profile: ProfileDescriptor | None) -> list:
355
+ """Render the right preview panel showing experts in the selected profile.
356
+
357
+ Args:
358
+ profile: ProfileDescriptor or None
359
+
360
+ Returns:
361
+ list of (style, text) tuples for FormattedTextControl
362
+ """
363
+ lines: list[tuple[str, str]] = []
364
+
365
+ lines.append(("dim cyan", " PROFILE PREVIEW"))
366
+ lines.append(("", "\n\n"))
367
+
368
+ if not profile:
369
+ lines.append(("fg:yellow", " No profile selected."))
370
+ lines.append(("", "\n"))
371
+ return lines
372
+
373
+ safe_name = _sanitize_display_text(profile.name)
374
+ safe_desc = (
375
+ _sanitize_display_text(profile.description)
376
+ if profile.description
377
+ else "(no description)"
378
+ )
379
+
380
+ # Profile name
381
+ lines.append(("bold", "Profile: "))
382
+ lines.append(("fg:ansigreen", safe_name))
383
+ lines.append(("", "\n\n"))
384
+
385
+ # Description
386
+ lines.append(("bold", "Description: "))
387
+ lines.append(("fg:ansicyan", safe_desc))
388
+ lines.append(("", "\n\n"))
389
+
390
+ # Experts
391
+ lines.append(("bold", f"Experts ({len(profile.expert_names)}):"))
392
+ lines.append(("", "\n"))
393
+
394
+ if not profile.expert_names:
395
+ lines.append(("fg:ansibrightblack", " (no experts — add some via Edit)"))
396
+ lines.append(("", "\n"))
397
+ else:
398
+ experts = orchestrator.get_experts_for_profile(profile.name)
399
+ for expert in experts:
400
+ safe_en = _sanitize_display_text(expert.name)
401
+ safe_es = _sanitize_display_text(expert.speciality)
402
+ lines.append(("fg:ansiyellow", f" • {safe_en}"))
403
+ lines.append(("fg:ansibrightblack", f" — {safe_es}"))
404
+ lines.append(("", "\n"))
405
+
406
+ return lines
407
+
408
+
409
+ def _render_preview_panel(expert: ExpertDescriptor | None) -> list:
410
+ """Render the right preview panel with expert details.
411
+
412
+ Args:
413
+ expert: ExpertDescriptor or None
414
+
415
+ Returns:
416
+ list of (style, text) tuples for FormattedTextControl
417
+ """
418
+ lines: list[tuple[str, str]] = []
419
+
420
+ lines.append(("dim cyan", " EXPERT DETAILS"))
421
+ lines.append(("", "\n\n"))
422
+
423
+ if not expert:
424
+ lines.append(("fg:yellow", " No expert selected."))
425
+ lines.append(("", "\n"))
426
+ return lines
427
+
428
+ safe_name = _sanitize_display_text(expert.name)
429
+ safe_speciality = _sanitize_display_text(expert.speciality)
430
+
431
+ # Name
432
+ lines.append(("bold", "Name: "))
433
+ lines.append(("", safe_name))
434
+ lines.append(("", "\n\n"))
435
+
436
+ # Speciality
437
+ lines.append(("bold", "Speciality: "))
438
+ lines.append(("fg:ansicyan", safe_speciality))
439
+ lines.append(("", "\n\n"))
440
+
441
+ # Model
442
+ lines.append(("bold", "Model: "))
443
+ if expert.model:
444
+ safe_model = _sanitize_display_text(expert.model)
445
+ lines.append(("fg:ansiyellow", safe_model))
446
+ else:
447
+ lines.append(("fg:ansibrightblack", "default"))
448
+ lines.append(("", "\n\n"))
449
+
450
+ # Max experts override
451
+ lines.append(("bold", "Max Experts Override: "))
452
+ if expert.max_experts_override is not None:
453
+ lines.append(("", str(expert.max_experts_override)))
454
+ else:
455
+ lines.append(("fg:ansibrightblack", "none"))
456
+ lines.append(("", "\n\n"))
457
+
458
+ # System prompt fragment
459
+ lines.append(("bold", "System Prompt Fragment:"))
460
+ lines.append(("", "\n"))
461
+
462
+ safe_fragment = _sanitize_display_text(expert.system_prompt_fragment)
463
+ # Word-wrap the fragment
464
+ words = safe_fragment.split()
465
+ current_line = ""
466
+ for word in words:
467
+ if len(current_line) + len(word) + 1 > 55:
468
+ lines.append(("fg:ansibrightblack", current_line))
469
+ lines.append(("", "\n"))
470
+ current_line = word
471
+ else:
472
+ if current_line == "":
473
+ current_line = word
474
+ else:
475
+ current_line += " " + word
476
+ if current_line.strip():
477
+ lines.append(("fg:ansibrightblack", current_line))
478
+ lines.append(("", "\n"))
479
+
480
+ lines.append(("", "\n"))
481
+ return lines
482
+
483
+
484
+ # ---------------------------------------------------------------------------
485
+ # Expert add / edit prompts
486
+ # ---------------------------------------------------------------------------
487
+
488
+
489
+ async def _prompt_text(label: str, default: str = "") -> str | None:
490
+ """Prompt the user for a text input using prompt_toolkit.
491
+
492
+ Returns None if the user cancels (Ctrl+C / Ctrl+D).
493
+ """
494
+ from prompt_toolkit import PromptSession
495
+
496
+ session: PromptSession = PromptSession()
497
+ try:
498
+ result = await session.prompt_async(f"{label}: ", default=default)
499
+ return result.strip() if result else None
500
+ except KeyboardInterrupt, EOFError:
501
+ return None
502
+
503
+
504
+ async def _prompt_model(current_model: str | None = None) -> str | None:
505
+ """Prompt the user to select a model, or clear the model override.
506
+
507
+ Returns the model name, "(clear)" to remove the override, or None on cancel.
508
+ """
509
+ try:
510
+ model_names = load_model_names() or []
511
+ except Exception as exc:
512
+ emit_warning(f"Failed to load models: {exc}")
513
+ return None
514
+
515
+ choices = ["(clear model override)"] + model_names
516
+
517
+ try:
518
+ choice = await arrow_select_async("Select model (or clear override)", choices)
519
+ except KeyboardInterrupt:
520
+ emit_info("Model selection cancelled")
521
+ return None
522
+
523
+ if choice == "(clear model override)":
524
+ return "(clear)"
525
+ return choice
526
+
527
+
528
+ # ---------------------------------------------------------------------------
529
+ # Final prompt extraction
530
+ # ---------------------------------------------------------------------------
531
+
532
+ _FINAL_PROMPT_RE = re.compile(r"\[FINAL_PROMPT\](.*?)\[/FINAL_PROMPT\]", re.DOTALL)
533
+
534
+
535
+ def _try_extract_final_prompt(text: str) -> str | None:
536
+ """Extract the content between [FINAL_PROMPT]...[/FINAL_PROMPT] tags.
537
+
538
+ Returns the stripped content if found, otherwise None.
539
+ """
540
+ match = _FINAL_PROMPT_RE.search(text)
541
+ if match:
542
+ return match.group(1).strip()
543
+ return None
544
+
545
+
546
+ # ---------------------------------------------------------------------------
547
+ # Interactive agent chat loop
548
+ # ---------------------------------------------------------------------------
549
+
550
+ _MAX_CHAT_TURNS = 20 # Safety cap to prevent infinite loops
551
+
552
+
553
+ async def _interactive_agent_chat(
554
+ agent_name: str,
555
+ initial_prompt: str,
556
+ ) -> str:
557
+ """Run an interactive multi-turn chat with a named agent.
558
+
559
+ Uses the standard ``_runtime.run`` path so streaming, tool calls,
560
+ and cancellation all work identically to the main CLI.
561
+
562
+ Args:
563
+ agent_name: The agent to load (e.g. "agent-creator").
564
+ initial_prompt: The first user message to send.
565
+
566
+ Returns:
567
+ The extracted final prompt text, or empty string on cancel/failure.
568
+ """
569
+ from prompt_toolkit import PromptSession
570
+
571
+ from code_muse.agents._runtime import run as agent_run
572
+ from code_muse.agents.agent_manager import load_agent
573
+
574
+ # Load the agent through the standard path
575
+ try:
576
+ agent = load_agent(agent_name)
577
+ except ValueError as exc:
578
+ emit_warning(f"Could not load agent '{agent_name}': {exc}")
579
+ return ""
580
+
581
+ session: PromptSession = PromptSession()
582
+ last_response_text = ""
583
+
584
+ # --- Turn 0: the initial prompt ---
585
+ emit_info(f"🤖 Chatting with {agent.display_name} (type 'done' to finish)\n")
586
+
587
+ try:
588
+ result = await agent_run(agent, initial_prompt)
589
+ except KeyboardInterrupt, asyncio.CancelledError:
590
+ emit_info("Chat cancelled.")
591
+ return ""
592
+ except Exception as exc:
593
+ emit_warning(f"Agent error: {exc}")
594
+ return ""
595
+
596
+ if result is not None:
597
+ last_response_text = getattr(result, "output", None) or str(result)
598
+ extracted = _try_extract_final_prompt(last_response_text)
599
+ if extracted is not None:
600
+ return extracted
601
+
602
+ # --- Subsequent turns ---
603
+ for _ in range(_MAX_CHAT_TURNS - 1):
604
+ try:
605
+ user_input = await session.prompt_async("agent-creator > ")
606
+ except KeyboardInterrupt, EOFError:
607
+ emit_info("Chat ended.")
608
+ return ""
609
+
610
+ if user_input is None or user_input.strip().lower() in ("exit", "quit", "done"):
611
+ # Return whatever we last extracted, or empty
612
+ extracted = _try_extract_final_prompt(last_response_text)
613
+ return extracted if extracted is not None else last_response_text.strip()
614
+
615
+ if not user_input.strip():
616
+ continue
617
+
618
+ try:
619
+ result = await agent_run(agent, user_input.strip())
620
+ except KeyboardInterrupt, asyncio.CancelledError:
621
+ emit_info("Chat cancelled.")
622
+ return ""
623
+ except Exception as exc:
624
+ emit_warning(f"Agent error: {exc}")
625
+ continue
626
+
627
+ if result is not None:
628
+ last_response_text = getattr(result, "output", None) or str(result)
629
+ extracted = _try_extract_final_prompt(last_response_text)
630
+ if extracted is not None:
631
+ return extracted
632
+
633
+ emit_warning("Reached maximum chat turns; exiting.")
634
+ extracted = _try_extract_final_prompt(last_response_text)
635
+ return extracted if extracted is not None else last_response_text.strip()
636
+
637
+
638
+ async def _generate_system_prompt_with_agent_creator(name: str, speciality: str) -> str:
639
+ """Interactively generate a system prompt for a MindPack expert.
640
+
641
+ Starts a multi-turn chat with Agent Creator so the user can
642
+ iterate on the system prompt. When the agent wraps its final
643
+ output in ``[FINAL_PROMPT]...[/FINAL_PROMPT]`` tags the chat
644
+ exits automatically and the enclosed text is returned.
645
+
646
+ Args:
647
+ name: The expert's name (e.g., "SecurityReviewer")
648
+ speciality: The expert's speciality (e.g., "security analysis")
649
+
650
+ Returns:
651
+ The generated system prompt text, or empty string on failure/cancel.
652
+ """
653
+ initial_prompt = (
654
+ f"Create a system prompt for a MindPack expert named '{name}' "
655
+ f"that specializes in: {speciality}.\n\n"
656
+ f"This expert will be part of a multi-expert advisory panel in Muse. "
657
+ f"The system prompt should define the expert's role, perspective, "
658
+ f"and how they should analyze problems and provide recommendations.\n\n"
659
+ f"Ask me any clarifying questions about the expert's behavior, "
660
+ f"tone, constraints, or specific focus areas. Iterate with me until "
661
+ f"we're both satisfied. When we agree on the final version, output "
662
+ f"the complete system prompt wrapped in [FINAL_PROMPT]...[/FINAL_PROMPT] "
663
+ f'tags. The system prompt should be in second person ("You are...") '
664
+ f"and should be comprehensive but concise."
665
+ )
666
+
667
+ # Exit alternate-screen so the chat renders in the normal terminal
668
+ sys.stdout.write("\033[?1049l")
669
+ sys.stdout.flush()
670
+ await asyncio.sleep(0.05)
671
+
672
+ try:
673
+ result_text = await _interactive_agent_chat("agent-creator", initial_prompt)
674
+ finally:
675
+ # Re-enter alternate-screen for the MindPack menu
676
+ sys.stdout.write("\033[?1049h")
677
+ sys.stdout.write("\033[2J\033[H")
678
+ sys.stdout.flush()
679
+ await asyncio.sleep(0.05)
680
+
681
+ if result_text:
682
+ emit_success(f"System prompt generated for '{name}'")
683
+ return result_text
684
+
685
+ emit_info("Agent Creator chat ended without a final prompt.")
686
+ return ""
687
+
688
+
689
+ async def _add_expert_flow() -> ExpertDescriptor | None:
690
+ """Interactive flow to add a new expert.
691
+
692
+ Offers preset templates or custom creation.
693
+ Returns a new ExpertDescriptor, or None if cancelled.
694
+ """
695
+ # Ask: preset or custom?
696
+ try:
697
+ choice = await arrow_select_async(
698
+ "Add expert: choose template or custom",
699
+ ["Custom expert (manual input)", "Use preset template"],
700
+ )
701
+ except KeyboardInterrupt:
702
+ emit_info("Add expert cancelled.")
703
+ return None
704
+
705
+ if choice == "Use preset template":
706
+ # Show preset selector
707
+ preset_choices = [
708
+ f"{e.name} — {e.speciality} (model: {e.model or 'default'})"
709
+ for e in PRESET_EXPERTS
710
+ ]
711
+ try:
712
+ preset_choice = await arrow_select_async(
713
+ "Select preset expert", preset_choices
714
+ )
715
+ except KeyboardInterrupt:
716
+ emit_info("Add expert cancelled.")
717
+ return None
718
+
719
+ # Find selected preset
720
+ idx = preset_choices.index(preset_choice)
721
+ preset = PRESET_EXPERTS[idx]
722
+
723
+ # Check for duplicate names
724
+ existing_names = [e.name for e in orchestrator.expert_registry]
725
+ if preset.name in existing_names:
726
+ emit_warning(f"Expert '{preset.name}' already exists. Edit it instead.")
727
+ return None
728
+
729
+ emit_success(f"Added preset expert: {preset.name}")
730
+ return preset
731
+
732
+ # Custom expert creation
733
+ name = await _prompt_text("Expert name")
734
+ if name is None or not name:
735
+ emit_info("Add expert cancelled.")
736
+ return None
737
+
738
+ existing_names = [e.name for e in orchestrator.expert_registry]
739
+ if name in existing_names:
740
+ emit_warning(f"Expert '{name}' already exists. Edit it instead.")
741
+ return None
742
+
743
+ speciality = await _prompt_text("Speciality")
744
+ if speciality is None or not speciality:
745
+ emit_info("Add expert cancelled.")
746
+ return None
747
+
748
+ model_choice = await _prompt_model()
749
+ model = None
750
+ if model_choice and model_choice != "(clear)":
751
+ model = model_choice
752
+
753
+ # System prompt: manual entry or Agent Creator
754
+ try:
755
+ prompt_choice = await arrow_select_async(
756
+ "System prompt fragment",
757
+ [
758
+ "Enter manually",
759
+ "Generate with Agent Creator 🏗️",
760
+ "Skip (leave empty)",
761
+ ],
762
+ )
763
+ except KeyboardInterrupt:
764
+ emit_info("Add expert cancelled.")
765
+ return None
766
+
767
+ if prompt_choice == "Generate with Agent Creator 🏗️":
768
+ system_prompt_fragment = await _generate_system_prompt_with_agent_creator(
769
+ name=name, speciality=speciality
770
+ )
771
+ elif prompt_choice == "Enter manually":
772
+ system_prompt_fragment = await _prompt_text("System prompt fragment") or ""
773
+ else:
774
+ system_prompt_fragment = ""
775
+
776
+ return ExpertDescriptor(
777
+ name=name,
778
+ speciality=speciality,
779
+ system_prompt_fragment=system_prompt_fragment,
780
+ model=model,
781
+ )
782
+
783
+
784
+ async def _edit_expert_flow(expert: ExpertDescriptor) -> ExpertDescriptor | None:
785
+ """Interactive flow to edit an existing expert.
786
+
787
+ Returns the updated ExpertDescriptor, or None if cancelled.
788
+ """
789
+ new_name = await _prompt_text("Name", default=expert.name)
790
+ if new_name is None:
791
+ emit_info("Edit cancelled.")
792
+ return None
793
+
794
+ # Check for duplicate names (if name changed)
795
+ if new_name != expert.name:
796
+ existing_names = [e.name for e in orchestrator.expert_registry]
797
+ if new_name in existing_names:
798
+ emit_warning(f"Expert name '{new_name}' already exists.")
799
+ return None
800
+
801
+ new_speciality = await _prompt_text("Speciality", default=expert.speciality)
802
+ if new_speciality is None:
803
+ emit_info("Edit cancelled.")
804
+ return None
805
+
806
+ model_choice = await _prompt_model(current_model=expert.model)
807
+ model = expert.model
808
+ if model_choice is not None:
809
+ model = None if model_choice == "(clear)" else model_choice
810
+
811
+ # System prompt: manual entry or Agent Creator
812
+ try:
813
+ prompt_choice = await arrow_select_async(
814
+ "System prompt fragment",
815
+ [
816
+ "Keep existing",
817
+ "Edit manually",
818
+ "Generate with Agent Creator 🏗️",
819
+ ],
820
+ )
821
+ except KeyboardInterrupt:
822
+ emit_info("Edit cancelled.")
823
+ return None
824
+
825
+ if prompt_choice == "Generate with Agent Creator 🏗️":
826
+ new_fragment = await _generate_system_prompt_with_agent_creator(
827
+ name=new_name, speciality=new_speciality
828
+ )
829
+ elif prompt_choice == "Edit manually":
830
+ new_fragment = await _prompt_text(
831
+ "System prompt fragment", default=expert.system_prompt_fragment
832
+ )
833
+ if new_fragment is None:
834
+ emit_info("Edit cancelled.")
835
+ return None
836
+ else:
837
+ # Keep existing
838
+ new_fragment = expert.system_prompt_fragment
839
+
840
+ max_override_str = await _prompt_text(
841
+ "Max experts override (empty = none)",
842
+ default=str(expert.max_experts_override)
843
+ if expert.max_experts_override is not None
844
+ else "",
845
+ )
846
+ max_experts_override = expert.max_experts_override
847
+ if max_override_str is not None:
848
+ if max_override_str.strip() == "":
849
+ max_experts_override = None
850
+ else:
851
+ try:
852
+ max_experts_override = int(max_override_str)
853
+ except ValueError:
854
+ emit_warning(
855
+ "Invalid number for max_experts_override, keeping previous value."
856
+ )
857
+
858
+ return ExpertDescriptor(
859
+ name=new_name,
860
+ speciality=new_speciality,
861
+ system_prompt_fragment=new_fragment,
862
+ model=model,
863
+ max_experts_override=max_experts_override,
864
+ )
865
+
866
+
867
+ async def _add_profile_flow() -> ProfileDescriptor | None:
868
+ """Interactive flow to add a new profile.
869
+
870
+ Returns a new ProfileDescriptor, or None if cancelled.
871
+ """
872
+ name = await _prompt_text("Profile name")
873
+ if name is None or not name:
874
+ emit_info("Add profile cancelled.")
875
+ return None
876
+
877
+ existing_names = [p.name for p in orchestrator.profile_registry]
878
+ if name in existing_names:
879
+ emit_warning(f"Profile '{name}' already exists.")
880
+ return None
881
+
882
+ description = await _prompt_text("Description (optional)") or ""
883
+
884
+ # Multi-select experts from the full registry
885
+ all_experts = sorted(orchestrator.expert_registry, key=lambda e: e.name.lower())
886
+ expert_choices = [f"{e.name} — {e.speciality}" for e in all_experts]
887
+
888
+ if not expert_choices:
889
+ emit_warning("No experts available. Add experts first.")
890
+ return None
891
+
892
+ selected_expert_names: list[str] = []
893
+ try:
894
+ # Use arrow_select_async for each expert (simple pick-one-at-a-time with Done)
895
+ while True:
896
+ remaining = [
897
+ f"{e.name} — {e.speciality}"
898
+ for e in all_experts
899
+ if e.name not in selected_expert_names
900
+ ]
901
+ if not remaining:
902
+ emit_info("All experts selected.")
903
+ break
904
+
905
+ choice = await arrow_select_async(
906
+ f"Select experts for '{name}' ({len(selected_expert_names)} selected) — choose 'Done' to finish",
907
+ ["✅ Done"] + remaining,
908
+ )
909
+ if choice == "✅ Done" or choice is None:
910
+ break
911
+
912
+ # Extract expert name from the choice string
913
+ expert_name = choice.split(" — ")[0]
914
+ if expert_name not in selected_expert_names:
915
+ selected_expert_names.append(expert_name)
916
+ emit_info(f"Added '{expert_name}' to profile.")
917
+ except KeyboardInterrupt:
918
+ if not selected_expert_names:
919
+ emit_info("Add profile cancelled.")
920
+ return None
921
+
922
+ if not selected_expert_names:
923
+ emit_warning("Profile must have at least one expert.")
924
+ return None
925
+
926
+ return ProfileDescriptor(
927
+ name=name,
928
+ description=description,
929
+ expert_names=selected_expert_names,
930
+ )
931
+
932
+
933
+ async def _edit_profile_flow(profile: ProfileDescriptor) -> ProfileDescriptor | None:
934
+ """Interactive flow to edit an existing profile.
935
+
936
+ Returns the updated ProfileDescriptor, or None if cancelled.
937
+ """
938
+ new_name = await _prompt_text("Name", default=profile.name)
939
+ if new_name is None:
940
+ emit_info("Edit cancelled.")
941
+ return None
942
+
943
+ if new_name != profile.name:
944
+ existing_names = [
945
+ p.name for p in orchestrator.profile_registry if p.name != profile.name
946
+ ]
947
+ if new_name in existing_names:
948
+ emit_warning(f"Profile name '{new_name}' already exists.")
949
+ return None
950
+
951
+ new_description = (
952
+ await _prompt_text("Description (optional)", default=profile.description) or ""
953
+ )
954
+
955
+ # Multi-select experts (current selection pre-populated)
956
+ all_experts = sorted(orchestrator.expert_registry, key=lambda e: e.name.lower())
957
+ selected_expert_names = list(profile.expert_names)
958
+
959
+ try:
960
+ while True:
961
+ remaining = [
962
+ f"{e.name} — {e.speciality}"
963
+ for e in all_experts
964
+ if e.name not in selected_expert_names
965
+ ]
966
+ # Build display showing current selection status
967
+ status = (
968
+ ", ".join(selected_expert_names) if selected_expert_names else "(none)"
969
+ )
970
+ options = ["✅ Done"]
971
+ if selected_expert_names:
972
+ options.append("🗑️ Remove an expert")
973
+ options.extend(remaining)
974
+
975
+ choice = await arrow_select_async(
976
+ f"Experts for '{new_name}' — current: {status}",
977
+ options,
978
+ )
979
+ if choice == "✅ Done" or choice is None:
980
+ break
981
+ elif choice == "🗑️ Remove an expert":
982
+ # Pick which expert to remove
983
+ remove_choices = selected_expert_names[:]
984
+ to_remove = await arrow_select_async(
985
+ "Remove which expert?", remove_choices
986
+ )
987
+ if to_remove:
988
+ selected_expert_names.remove(to_remove)
989
+ emit_info(f"Removed '{to_remove}' from profile.")
990
+ else:
991
+ expert_name = choice.split(" — ")[0]
992
+ if expert_name not in selected_expert_names:
993
+ selected_expert_names.append(expert_name)
994
+ emit_info(f"Added '{expert_name}' to profile.")
995
+ except KeyboardInterrupt:
996
+ emit_info("Edit cancelled (changes discarded).")
997
+ return None
998
+
999
+ return ProfileDescriptor(
1000
+ name=new_name,
1001
+ description=new_description,
1002
+ expert_names=selected_expert_names,
1003
+ )
1004
+
1005
+
1006
+ # ---------------------------------------------------------------------------
1007
+ # Settings configuration
1008
+ # ---------------------------------------------------------------------------
1009
+
1010
+
1011
+ async def _configure_settings() -> None:
1012
+ """Show settings configuration for MindPack."""
1013
+ choices = [
1014
+ f"Spawn mode: {_DEFAULT_SETTINGS['spawn_mode']}",
1015
+ f"Default expert count: {_DEFAULT_SETTINGS['default_expert_count']}",
1016
+ "Done",
1017
+ ]
1018
+
1019
+ try:
1020
+ choice = await arrow_select_async("MindPack Settings", choices)
1021
+ except KeyboardInterrupt:
1022
+ return
1023
+
1024
+ if choice.startswith("Spawn mode"):
1025
+ mode_options = [
1026
+ "fixed",
1027
+ "adaptive",
1028
+ "same_agent_replicas",
1029
+ "multi_model_replicas",
1030
+ "multi_agent",
1031
+ "hybrid",
1032
+ ]
1033
+ try:
1034
+ mode = await arrow_select_async("Select spawn mode", mode_options)
1035
+ _DEFAULT_SETTINGS["spawn_mode"] = mode
1036
+ emit_success(f"Spawn mode set to '{mode}'")
1037
+ except KeyboardInterrupt:
1038
+ pass
1039
+ elif choice.startswith("Default expert count"):
1040
+ count_str = await _prompt_text(
1041
+ "Default expert count",
1042
+ default=_DEFAULT_SETTINGS["default_expert_count"],
1043
+ )
1044
+ if count_str is not None:
1045
+ try:
1046
+ int(count_str)
1047
+ _DEFAULT_SETTINGS["default_expert_count"] = count_str
1048
+ emit_success(f"Default expert count set to {count_str}")
1049
+ except ValueError:
1050
+ emit_warning("Invalid number.")
1051
+ # "Done" — just return
1052
+
1053
+
1054
+ # ---------------------------------------------------------------------------
1055
+ # Main interactive menu
1056
+ # ---------------------------------------------------------------------------
1057
+
1058
+
1059
+ async def interactive_profile_selector_menu() -> str | None | bool:
1060
+ """Show profile selector as the first screen when entering /mindpack.
1061
+
1062
+ Returns:
1063
+ The name of the selected profile to open, or None to exit.
1064
+ - True if a profile was activated and the caller should exit.
1065
+
1066
+ Supports browsing, selecting, adding, editing, and deleting profiles.
1067
+ """
1068
+ profiles = _get_profile_entries()
1069
+
1070
+ # State
1071
+ selected_idx = [0]
1072
+ current_page = [0]
1073
+ pending_action = [None] # 'open', 'add', 'edit', 'delete', or None
1074
+
1075
+ total_pages = [get_total_pages(len(profiles), PAGE_SIZE)]
1076
+
1077
+ def get_current_profile() -> ProfileDescriptor | None:
1078
+ if 0 <= selected_idx[0] < len(profiles):
1079
+ return profiles[selected_idx[0]]
1080
+ return None
1081
+
1082
+ def refresh_profiles(selected_name: str | None = None) -> None:
1083
+ nonlocal profiles
1084
+ profiles = _get_profile_entries()
1085
+ total_pages[0] = get_total_pages(len(profiles), PAGE_SIZE)
1086
+
1087
+ if not profiles:
1088
+ selected_idx[0] = 0
1089
+ current_page[0] = 0
1090
+ return
1091
+
1092
+ if selected_name:
1093
+ for idx, p in enumerate(profiles):
1094
+ if p.name == selected_name:
1095
+ selected_idx[0] = idx
1096
+ break
1097
+ else:
1098
+ selected_idx[0] = min(selected_idx[0], len(profiles) - 1)
1099
+ else:
1100
+ selected_idx[0] = min(selected_idx[0], len(profiles) - 1)
1101
+
1102
+ current_page[0] = get_page_for_index(selected_idx[0], PAGE_SIZE)
1103
+
1104
+ # Build UI
1105
+ menu_control = FormattedTextControl(text="")
1106
+ preview_control = FormattedTextControl(text="")
1107
+
1108
+ def update_display():
1109
+ menu_control.text = _render_profile_menu_panel(
1110
+ profiles, current_page[0], selected_idx[0]
1111
+ )
1112
+ preview_control.text = _render_profile_preview_panel(get_current_profile())
1113
+
1114
+ menu_window = Window(
1115
+ content=menu_control, wrap_lines=False, width=Dimension(weight=35)
1116
+ )
1117
+ preview_window = Window(
1118
+ content=preview_control, wrap_lines=False, width=Dimension(weight=65)
1119
+ )
1120
+
1121
+ menu_frame = Frame(
1122
+ menu_window, width=Dimension(weight=35), title="MindPack Profiles"
1123
+ )
1124
+ preview_frame = Frame(preview_window, width=Dimension(weight=65), title="Preview")
1125
+
1126
+ root_container = VSplit([menu_frame, preview_frame])
1127
+
1128
+ # Key bindings
1129
+ kb = KeyBindings()
1130
+
1131
+ @kb.add("up")
1132
+ def _(event):
1133
+ if selected_idx[0] > 0:
1134
+ selected_idx[0] -= 1
1135
+ current_page[0] = ensure_visible_page(
1136
+ selected_idx[0], current_page[0], len(profiles), PAGE_SIZE
1137
+ )
1138
+ update_display()
1139
+
1140
+ @kb.add("down")
1141
+ def _(event):
1142
+ if selected_idx[0] < len(profiles) - 1:
1143
+ selected_idx[0] += 1
1144
+ current_page[0] = ensure_visible_page(
1145
+ selected_idx[0], current_page[0], len(profiles), PAGE_SIZE
1146
+ )
1147
+ update_display()
1148
+
1149
+ @kb.add("left")
1150
+ def _(event):
1151
+ if current_page[0] > 0:
1152
+ current_page[0] -= 1
1153
+ selected_idx[0] = current_page[0] * PAGE_SIZE
1154
+ update_display()
1155
+
1156
+ @kb.add("right")
1157
+ def _(event):
1158
+ if current_page[0] < total_pages[0] - 1:
1159
+ current_page[0] += 1
1160
+ selected_idx[0] = current_page[0] * PAGE_SIZE
1161
+ update_display()
1162
+
1163
+ @kb.add("a")
1164
+ def _(event):
1165
+ if get_current_profile():
1166
+ pending_action[0] = "activate"
1167
+ event.app.exit()
1168
+
1169
+ @kb.add("n")
1170
+ def _(event):
1171
+ pending_action[0] = "add"
1172
+ event.app.exit()
1173
+
1174
+ @kb.add("d")
1175
+ def _(event):
1176
+ if get_current_profile():
1177
+ pending_action[0] = "delete"
1178
+ event.app.exit()
1179
+
1180
+ @kb.add("e")
1181
+ def _(event):
1182
+ if get_current_profile():
1183
+ pending_action[0] = "edit"
1184
+ event.app.exit()
1185
+
1186
+ @kb.add("enter")
1187
+ def _(event):
1188
+ if get_current_profile():
1189
+ pending_action[0] = "open"
1190
+ event.app.exit()
1191
+
1192
+ @kb.add("c-c")
1193
+ def _(event):
1194
+ pending_action[0] = None
1195
+ event.app.exit()
1196
+
1197
+ layout = Layout(root_container)
1198
+ app = Application(
1199
+ layout=layout,
1200
+ key_bindings=kb,
1201
+ full_screen=False,
1202
+ mouse_support=False,
1203
+ )
1204
+
1205
+ set_awaiting_user_input(True)
1206
+
1207
+ # Enter alternate screen buffer
1208
+ sys.stdout.write("\033[?1049h")
1209
+ sys.stdout.write("\033[2J\033[H")
1210
+ sys.stdout.flush()
1211
+ await asyncio.sleep(0.05)
1212
+
1213
+ try:
1214
+ while True:
1215
+ pending_action[0] = None
1216
+ update_display()
1217
+
1218
+ sys.stdout.write("\033[2J\033[H")
1219
+ sys.stdout.flush()
1220
+
1221
+ await app.run_async()
1222
+
1223
+ if pending_action[0] == "activate":
1224
+ profile = get_current_profile()
1225
+ if profile:
1226
+ orchestrator.set_active_profile(profile.name)
1227
+ emit_success(f"Profile '{profile.name}' activated. Exiting.")
1228
+ return True
1229
+ continue
1230
+
1231
+ if pending_action[0] == "open":
1232
+ profile = get_current_profile()
1233
+ if profile:
1234
+ orchestrator.set_active_profile(profile.name)
1235
+ emit_success(f"Active profile set to '{profile.name}'")
1236
+ return profile.name
1237
+ continue
1238
+
1239
+ if pending_action[0] == "add":
1240
+ new_profile = await _add_profile_flow()
1241
+ if new_profile is not None:
1242
+ orchestrator.register_profile(new_profile)
1243
+ orchestrator.save_profiles()
1244
+ emit_success(f"Profile '{new_profile.name}' added.")
1245
+ refresh_profiles(
1246
+ selected_name=new_profile.name if new_profile else None
1247
+ )
1248
+ continue
1249
+
1250
+ if pending_action[0] == "edit":
1251
+ profile = get_current_profile()
1252
+ if profile:
1253
+ updated = await _edit_profile_flow(profile)
1254
+ if updated is not None:
1255
+ orchestrator.remove_profile(profile.name)
1256
+ orchestrator.register_profile(updated)
1257
+ orchestrator.save_profiles()
1258
+ emit_success(f"Profile '{updated.name}' updated.")
1259
+ refresh_profiles(
1260
+ selected_name=updated.name if updated else profile.name
1261
+ )
1262
+ continue
1263
+
1264
+ if pending_action[0] == "delete":
1265
+ profile = get_current_profile()
1266
+ if profile:
1267
+ try:
1268
+ confirm = await arrow_select_async(
1269
+ f"Delete profile '{profile.name}'?",
1270
+ ["No, cancel", "Yes, delete"],
1271
+ )
1272
+ except KeyboardInterrupt:
1273
+ confirm = "No, cancel"
1274
+
1275
+ if confirm == "Yes, delete":
1276
+ orchestrator.remove_profile(profile.name)
1277
+ orchestrator.save_profiles()
1278
+ emit_success(f"Profile '{profile.name}' deleted.")
1279
+ else:
1280
+ emit_info("Delete cancelled.")
1281
+ refresh_profiles()
1282
+ continue
1283
+
1284
+ # Ctrl+C — exit
1285
+ return None
1286
+
1287
+ finally:
1288
+ sys.stdout.write("\033[?1049l")
1289
+ sys.stdout.flush()
1290
+ set_awaiting_user_input(False)
1291
+
1292
+
1293
+ async def interactive_mindpack_menu(profile_name: str | None = None) -> None:
1294
+ """Show interactive terminal UI for managing MindPack experts.
1295
+
1296
+ Supports browsing, adding, editing, and deleting experts with
1297
+ a split-panel layout and live preview.
1298
+
1299
+ Args:
1300
+ profile_name: If set, filters experts to only those in this profile.
1301
+ """
1302
+ if profile_name:
1303
+ entries = _get_expert_entries_for_profile(profile_name)
1304
+ else:
1305
+ entries = _get_expert_entries()
1306
+
1307
+ # State
1308
+ selected_idx = [0]
1309
+ current_page = [0]
1310
+ pending_action = [None] # 'add', 'edit', 'delete', 'settings', or None
1311
+
1312
+ total_pages = [get_total_pages(len(entries), PAGE_SIZE)]
1313
+
1314
+ def get_current_expert() -> ExpertDescriptor | None:
1315
+ if 0 <= selected_idx[0] < len(entries):
1316
+ return entries[selected_idx[0]]
1317
+ return None
1318
+
1319
+ def refresh_entries(selected_name: str | None = None) -> None:
1320
+ nonlocal entries
1321
+ if profile_name:
1322
+ entries = _get_expert_entries_for_profile(profile_name)
1323
+ else:
1324
+ entries = _get_expert_entries()
1325
+ total_pages[0] = get_total_pages(len(entries), PAGE_SIZE)
1326
+
1327
+ if not entries:
1328
+ selected_idx[0] = 0
1329
+ current_page[0] = 0
1330
+ return
1331
+
1332
+ if selected_name:
1333
+ for idx, expert in enumerate(entries):
1334
+ if expert.name == selected_name:
1335
+ selected_idx[0] = idx
1336
+ break
1337
+ else:
1338
+ selected_idx[0] = min(selected_idx[0], len(entries) - 1)
1339
+ else:
1340
+ selected_idx[0] = min(selected_idx[0], len(entries) - 1)
1341
+
1342
+ current_page[0] = get_page_for_index(selected_idx[0], PAGE_SIZE)
1343
+
1344
+ # Build UI
1345
+ menu_control = FormattedTextControl(text="")
1346
+ preview_control = FormattedTextControl(text="")
1347
+
1348
+ def update_display():
1349
+ """Update both panels."""
1350
+ menu_control.text = _render_menu_panel(
1351
+ entries, current_page[0], selected_idx[0]
1352
+ )
1353
+ preview_control.text = _render_preview_panel(get_current_expert())
1354
+
1355
+ menu_window = Window(
1356
+ content=menu_control, wrap_lines=False, width=Dimension(weight=35)
1357
+ )
1358
+ preview_window = Window(
1359
+ content=preview_control, wrap_lines=False, width=Dimension(weight=65)
1360
+ )
1361
+
1362
+ title = f"MindPack Experts — {profile_name}" if profile_name else "MindPack Experts"
1363
+ menu_frame = Frame(menu_window, width=Dimension(weight=35), title=title)
1364
+ preview_frame = Frame(preview_window, width=Dimension(weight=65), title="Preview")
1365
+
1366
+ root_container = VSplit(
1367
+ [
1368
+ menu_frame,
1369
+ preview_frame,
1370
+ ]
1371
+ )
1372
+
1373
+ # Key bindings
1374
+ kb = KeyBindings()
1375
+
1376
+ @kb.add("up")
1377
+ def _(event):
1378
+ if selected_idx[0] > 0:
1379
+ selected_idx[0] -= 1
1380
+ current_page[0] = ensure_visible_page(
1381
+ selected_idx[0],
1382
+ current_page[0],
1383
+ len(entries),
1384
+ PAGE_SIZE,
1385
+ )
1386
+ update_display()
1387
+
1388
+ @kb.add("down")
1389
+ def _(event):
1390
+ if selected_idx[0] < len(entries) - 1:
1391
+ selected_idx[0] += 1
1392
+ current_page[0] = ensure_visible_page(
1393
+ selected_idx[0],
1394
+ current_page[0],
1395
+ len(entries),
1396
+ PAGE_SIZE,
1397
+ )
1398
+ update_display()
1399
+
1400
+ @kb.add("left")
1401
+ def _(event):
1402
+ if current_page[0] > 0:
1403
+ current_page[0] -= 1
1404
+ selected_idx[0] = current_page[0] * PAGE_SIZE
1405
+ update_display()
1406
+
1407
+ @kb.add("right")
1408
+ def _(event):
1409
+ if current_page[0] < total_pages[0] - 1:
1410
+ current_page[0] += 1
1411
+ selected_idx[0] = current_page[0] * PAGE_SIZE
1412
+ update_display()
1413
+
1414
+ @kb.add("a")
1415
+ def _(event):
1416
+ pending_action[0] = "add"
1417
+ event.app.exit()
1418
+
1419
+ @kb.add("d")
1420
+ def _(event):
1421
+ if get_current_expert():
1422
+ pending_action[0] = "delete"
1423
+ event.app.exit()
1424
+
1425
+ @kb.add("c")
1426
+ def _(event):
1427
+ pending_action[0] = "settings"
1428
+ event.app.exit()
1429
+
1430
+ @kb.add("enter")
1431
+ def _(event):
1432
+ if get_current_expert():
1433
+ pending_action[0] = "edit"
1434
+ event.app.exit()
1435
+
1436
+ @kb.add("c-c")
1437
+ def _(event):
1438
+ pending_action[0] = None
1439
+ event.app.exit()
1440
+
1441
+ layout = Layout(root_container)
1442
+ app = Application(
1443
+ layout=layout,
1444
+ key_bindings=kb,
1445
+ full_screen=False,
1446
+ mouse_support=False,
1447
+ )
1448
+
1449
+ set_awaiting_user_input(True)
1450
+
1451
+ # Enter alternate screen buffer once for entire session
1452
+ sys.stdout.write("\033[?1049h")
1453
+ sys.stdout.write("\033[2J\033[H")
1454
+ sys.stdout.flush()
1455
+ await asyncio.sleep(0.05)
1456
+
1457
+ try:
1458
+ while True:
1459
+ pending_action[0] = None
1460
+ update_display()
1461
+
1462
+ # Clear the current buffer
1463
+ sys.stdout.write("\033[2J\033[H")
1464
+ sys.stdout.flush()
1465
+
1466
+ # Run application
1467
+ await app.run_async()
1468
+
1469
+ if pending_action[0] == "add":
1470
+ new_expert = await _add_expert_flow()
1471
+ if new_expert is not None:
1472
+ orchestrator.register_expert(new_expert)
1473
+ orchestrator.save_experts()
1474
+ # If inside a profile, auto-add to the current profile
1475
+ if profile_name:
1476
+ for p in orchestrator.profile_registry:
1477
+ if p.name == profile_name:
1478
+ if new_expert.name not in p.expert_names:
1479
+ p.expert_names.append(new_expert.name)
1480
+ orchestrator.save_profiles()
1481
+ emit_info(
1482
+ f"Expert '{new_expert.name}' added to profile '{profile_name}'."
1483
+ )
1484
+ break
1485
+ emit_success(f"Expert '{new_expert.name}' added.")
1486
+ refresh_entries(selected_name=new_expert.name if new_expert else None)
1487
+ continue
1488
+
1489
+ if pending_action[0] == "edit":
1490
+ expert = get_current_expert()
1491
+ if expert:
1492
+ updated = await _edit_expert_flow(expert)
1493
+ if updated is not None:
1494
+ # Remove old and register new (name may have changed)
1495
+ orchestrator.remove_expert(expert.name)
1496
+ orchestrator.register_expert(updated)
1497
+ orchestrator.save_experts()
1498
+ emit_success(f"Expert '{updated.name}' updated.")
1499
+ refresh_entries(
1500
+ selected_name=updated.name if updated else expert.name
1501
+ )
1502
+ continue
1503
+
1504
+ if pending_action[0] == "delete":
1505
+ expert = get_current_expert()
1506
+ if expert:
1507
+ try:
1508
+ confirm = await arrow_select_async(
1509
+ f"Delete expert '{expert.name}'?",
1510
+ ["No, cancel", "Yes, delete"],
1511
+ )
1512
+ except KeyboardInterrupt:
1513
+ confirm = "No, cancel"
1514
+
1515
+ if confirm == "Yes, delete":
1516
+ removed = orchestrator.remove_expert(expert.name)
1517
+ if removed:
1518
+ # If inside a profile, remove from profile too
1519
+ if profile_name:
1520
+ for p in orchestrator.profile_registry:
1521
+ if p.name == profile_name:
1522
+ if expert.name in p.expert_names:
1523
+ p.expert_names.remove(expert.name)
1524
+ orchestrator.save_profiles()
1525
+ break
1526
+ orchestrator.save_experts()
1527
+ emit_success(f"Expert '{expert.name}' deleted.")
1528
+ else:
1529
+ emit_warning(
1530
+ f"Expert '{expert.name}' not found in registry."
1531
+ )
1532
+ else:
1533
+ emit_info("Delete cancelled.")
1534
+ refresh_entries()
1535
+ continue
1536
+
1537
+ if pending_action[0] == "settings":
1538
+ await _configure_settings()
1539
+ continue
1540
+
1541
+ # No pending action (Ctrl+C) — exit loop
1542
+ break
1543
+
1544
+ finally:
1545
+ # Exit alternate screen buffer once at end
1546
+ sys.stdout.write("\033[?1049l")
1547
+ sys.stdout.flush()
1548
+ # Reset awaiting input flag
1549
+ set_awaiting_user_input(False)
1550
+
1551
+ # Clear exit message
1552
+ emit_info("Exited MindPack expert configuration.")