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.
- code_muse/__init__.py +26 -0
- code_muse/__main__.py +10 -0
- code_muse/agents/__init__.py +31 -0
- code_muse/agents/_builder.py +214 -0
- code_muse/agents/_compaction.py +506 -0
- code_muse/agents/_diagnostics.py +171 -0
- code_muse/agents/_history.py +382 -0
- code_muse/agents/_key_listeners.py +148 -0
- code_muse/agents/_non_streaming_render.py +148 -0
- code_muse/agents/_runtime.py +596 -0
- code_muse/agents/agent_creator_agent.py +603 -0
- code_muse/agents/agent_helios.py +47 -0
- code_muse/agents/agent_manager.py +740 -0
- code_muse/agents/agent_muse.py +78 -0
- code_muse/agents/agent_planning.py +44 -0
- code_muse/agents/agent_qa_melpomene.py +207 -0
- code_muse/agents/base_agent.py +194 -0
- code_muse/agents/event_stream_handler.py +361 -0
- code_muse/agents/json_agent.py +201 -0
- code_muse/agents/prompt_v3.py +521 -0
- code_muse/agents/subagent_stream_handler.py +273 -0
- code_muse/callbacks.py +941 -0
- code_muse/chatgpt_codex_client.py +333 -0
- code_muse/claude_cache_client.py +853 -0
- code_muse/cli_runner/__init__.py +319 -0
- code_muse/cli_runner/args.py +63 -0
- code_muse/cli_runner/loop.py +510 -0
- code_muse/cli_runner/resume.py +72 -0
- code_muse/cli_runner/runner.py +161 -0
- code_muse/command_line/__init__.py +1 -0
- code_muse/command_line/add_model_menu.py +1331 -0
- code_muse/command_line/agent_menu.py +674 -0
- code_muse/command_line/attachments.py +397 -0
- code_muse/command_line/autosave_menu.py +709 -0
- code_muse/command_line/clipboard.py +528 -0
- code_muse/command_line/colors_menu.py +530 -0
- code_muse/command_line/command_handler.py +262 -0
- code_muse/command_line/command_registry.py +150 -0
- code_muse/command_line/config_commands.py +711 -0
- code_muse/command_line/core_commands.py +740 -0
- code_muse/command_line/diff_menu.py +865 -0
- code_muse/command_line/file_path_completion.py +73 -0
- code_muse/command_line/load_context_completion.py +57 -0
- code_muse/command_line/model_picker_completion.py +512 -0
- code_muse/command_line/model_settings_menu.py +983 -0
- code_muse/command_line/onboarding_slides.py +162 -0
- code_muse/command_line/onboarding_wizard.py +337 -0
- code_muse/command_line/pagination.py +41 -0
- code_muse/command_line/pin_command_completion.py +329 -0
- code_muse/command_line/prompt_toolkit_completion.py +886 -0
- code_muse/command_line/session_commands.py +304 -0
- code_muse/command_line/shell_passthrough.py +145 -0
- code_muse/command_line/skills_completion.py +158 -0
- code_muse/command_line/types.py +18 -0
- code_muse/command_line/uc_menu.py +908 -0
- code_muse/command_line/utils.py +105 -0
- code_muse/command_line/wiggum_state.py +77 -0
- code_muse/config.py +1138 -0
- code_muse/config_agent.py +168 -0
- code_muse/config_appearance.py +241 -0
- code_muse/config_model.py +357 -0
- code_muse/config_security.py +73 -0
- code_muse/error_logging.py +132 -0
- code_muse/evals/__init__.py +35 -0
- code_muse/evals/eval_helpers.py +81 -0
- code_muse/evals/eval_runner.py +299 -0
- code_muse/evals/sample_evals/__init__.py +1 -0
- code_muse/evals/sample_evals/eval_frugal_reads.py +59 -0
- code_muse/evals/sample_evals/eval_memory_planning.py +31 -0
- code_muse/evals/sample_evals/eval_shell_efficiency.py +39 -0
- code_muse/evals/sample_evals/eval_tool_masking.py +33 -0
- code_muse/fs_scan_cache/__init__.py +31 -0
- code_muse/fs_scan_cache/invalidation_hooks.py +89 -0
- code_muse/fs_scan_cache/scan_cache_core.cpython-314-darwin.so +0 -0
- code_muse/fs_scan_cache/scan_cache_core.pyx +203 -0
- code_muse/fs_scan_cache/tool_integration.py +309 -0
- code_muse/fs_scan_cache/ttl_policy.py +44 -0
- code_muse/gemini_code_assist.py +383 -0
- code_muse/gemini_model.py +838 -0
- code_muse/hook_engine/README.md +105 -0
- code_muse/hook_engine/__init__.py +21 -0
- code_muse/hook_engine/aliases.py +153 -0
- code_muse/hook_engine/engine.py +221 -0
- code_muse/hook_engine/executor.py +347 -0
- code_muse/hook_engine/matcher.py +154 -0
- code_muse/hook_engine/models.py +245 -0
- code_muse/hook_engine/registry.py +114 -0
- code_muse/hook_engine/trust.py +268 -0
- code_muse/hook_engine/validator.py +144 -0
- code_muse/http_utils.py +360 -0
- code_muse/keymap.py +128 -0
- code_muse/list_filtering.py +26 -0
- code_muse/main.py +10 -0
- code_muse/messaging/__init__.py +259 -0
- code_muse/messaging/bus.py +621 -0
- code_muse/messaging/commands.py +166 -0
- code_muse/messaging/markdown_patches.py +57 -0
- code_muse/messaging/message_queue.py +397 -0
- code_muse/messaging/messages.py +591 -0
- code_muse/messaging/queue_console.py +269 -0
- code_muse/messaging/renderers.py +308 -0
- code_muse/messaging/rich_renderer.py +1158 -0
- code_muse/messaging/shimmer.py +154 -0
- code_muse/messaging/spinner/__init__.py +87 -0
- code_muse/messaging/spinner/console_spinner.py +250 -0
- code_muse/messaging/spinner/spinner_base.py +82 -0
- code_muse/messaging/subagent_console.py +458 -0
- code_muse/model_factory.py +1203 -0
- code_muse/model_switching.py +59 -0
- code_muse/model_utils.py +156 -0
- code_muse/models.json +66 -0
- code_muse/models_cache/__init__.py +26 -0
- code_muse/models_cache/blocking_lru_cache.py +98 -0
- code_muse/models_cache/cache_writer.py +86 -0
- code_muse/models_cache/sha256_hash.cpython-314-darwin.so +0 -0
- code_muse/models_cache/sha256_hash.pyx +34 -0
- code_muse/models_cache/startup_integration.py +75 -0
- code_muse/models_dev_api.json +1 -0
- code_muse/models_dev_parser.py +590 -0
- code_muse/motion.py +126 -0
- code_muse/plugins/__init__.py +471 -0
- code_muse/plugins/agent_skills/__init__.py +32 -0
- code_muse/plugins/agent_skills/config.py +176 -0
- code_muse/plugins/agent_skills/discovery.py +309 -0
- code_muse/plugins/agent_skills/downloader.py +389 -0
- code_muse/plugins/agent_skills/installer.py +19 -0
- code_muse/plugins/agent_skills/metadata.py +293 -0
- code_muse/plugins/agent_skills/prompt_builder.py +66 -0
- code_muse/plugins/agent_skills/register_callbacks.py +298 -0
- code_muse/plugins/agent_skills/remote_catalog.py +320 -0
- code_muse/plugins/agent_skills/skill_catalog.py +254 -0
- code_muse/plugins/agent_skills/skills_install_menu.py +690 -0
- code_muse/plugins/agent_skills/skills_menu.py +791 -0
- code_muse/plugins/autonomous_memory/__init__.py +39 -0
- code_muse/plugins/autonomous_memory/bm25_scorer.cpython-314-darwin.so +0 -0
- code_muse/plugins/autonomous_memory/bm25_scorer.cpython-314-x86_64-linux-gnu.so +0 -0
- code_muse/plugins/autonomous_memory/bm25_scorer.pyx +291 -0
- code_muse/plugins/autonomous_memory/consolidation.py +82 -0
- code_muse/plugins/autonomous_memory/extraction.py +382 -0
- code_muse/plugins/autonomous_memory/lease_lock.py +105 -0
- code_muse/plugins/autonomous_memory/memory_injection.py +59 -0
- code_muse/plugins/autonomous_memory/register_callbacks.py +268 -0
- code_muse/plugins/autonomous_memory/secret_scanner.py +62 -0
- code_muse/plugins/autonomous_memory/session_scanner.py +163 -0
- code_muse/plugins/aws_bedrock/__init__.py +14 -0
- code_muse/plugins/aws_bedrock/config.py +99 -0
- code_muse/plugins/aws_bedrock/register_callbacks.py +241 -0
- code_muse/plugins/aws_bedrock/utils.py +153 -0
- code_muse/plugins/azure_foundry/README.md +238 -0
- code_muse/plugins/azure_foundry/__init__.py +15 -0
- code_muse/plugins/azure_foundry/config.py +125 -0
- code_muse/plugins/azure_foundry/discovery.py +187 -0
- code_muse/plugins/azure_foundry/register_callbacks.py +495 -0
- code_muse/plugins/azure_foundry/token.py +180 -0
- code_muse/plugins/azure_foundry/utils.py +345 -0
- code_muse/plugins/build_filter/__init__.py +1 -0
- code_muse/plugins/build_filter/register_callbacks.py +201 -0
- code_muse/plugins/build_filter/strategies/__init__.py +1 -0
- code_muse/plugins/build_filter/strategies/build.py +397 -0
- code_muse/plugins/chatgpt_oauth/__init__.py +6 -0
- code_muse/plugins/chatgpt_oauth/config.py +52 -0
- code_muse/plugins/chatgpt_oauth/oauth_flow.py +338 -0
- code_muse/plugins/chatgpt_oauth/register_callbacks.py +172 -0
- code_muse/plugins/chatgpt_oauth/test_plugin.py +301 -0
- code_muse/plugins/chatgpt_oauth/utils.py +538 -0
- code_muse/plugins/checkpointing/__init__.py +29 -0
- code_muse/plugins/checkpointing/checkpoint_hook.py +51 -0
- code_muse/plugins/checkpointing/conversation_snapshots.py +117 -0
- code_muse/plugins/checkpointing/register_callbacks.py +51 -0
- code_muse/plugins/checkpointing/restore_command.py +263 -0
- code_muse/plugins/checkpointing/rewind_shortcut.py +88 -0
- code_muse/plugins/checkpointing/shadow_git.py +90 -0
- code_muse/plugins/claude_code_hooks/__init__.py +1 -0
- code_muse/plugins/claude_code_hooks/config.py +188 -0
- code_muse/plugins/claude_code_hooks/register_callbacks.py +208 -0
- code_muse/plugins/claude_code_oauth/README.md +167 -0
- code_muse/plugins/claude_code_oauth/SETUP.md +93 -0
- code_muse/plugins/claude_code_oauth/__init__.py +25 -0
- code_muse/plugins/claude_code_oauth/config.py +52 -0
- code_muse/plugins/claude_code_oauth/fast_mode.py +124 -0
- code_muse/plugins/claude_code_oauth/prompt_handler.py +63 -0
- code_muse/plugins/claude_code_oauth/register_callbacks.py +547 -0
- code_muse/plugins/claude_code_oauth/test_fast_mode.py +165 -0
- code_muse/plugins/claude_code_oauth/test_plugin.py +283 -0
- code_muse/plugins/claude_code_oauth/token_refresh_heartbeat.py +237 -0
- code_muse/plugins/claude_code_oauth/utils.py +664 -0
- code_muse/plugins/copilot_auth/__init__.py +11 -0
- code_muse/plugins/copilot_auth/config.py +91 -0
- code_muse/plugins/copilot_auth/reasoning_client.py +409 -0
- code_muse/plugins/copilot_auth/register_callbacks.py +461 -0
- code_muse/plugins/copilot_auth/utils.py +584 -0
- code_muse/plugins/custom_commands/__init__.py +14 -0
- code_muse/plugins/custom_commands/args_injection.py +82 -0
- code_muse/plugins/custom_commands/command_discovery.py +89 -0
- code_muse/plugins/custom_commands/command_toml_schema.py +71 -0
- code_muse/plugins/custom_commands/register_callbacks.py +176 -0
- code_muse/plugins/customizable_commands/__init__.py +0 -0
- code_muse/plugins/customizable_commands/register_callbacks.py +136 -0
- code_muse/plugins/destructive_command_guard/__init__.py +14 -0
- code_muse/plugins/destructive_command_guard/detector.py +375 -0
- code_muse/plugins/destructive_command_guard/register_callbacks.py +148 -0
- code_muse/plugins/example_custom_command/README.md +280 -0
- code_muse/plugins/example_custom_command/register_callbacks.py +51 -0
- code_muse/plugins/file_permission_handler/__init__.py +4 -0
- code_muse/plugins/file_permission_handler/register_callbacks.py +441 -0
- code_muse/plugins/filter_engine/__init__.py +30 -0
- code_muse/plugins/filter_engine/classifier.py +153 -0
- code_muse/plugins/filter_engine/content_detector.py +184 -0
- code_muse/plugins/filter_engine/dispatcher.py +244 -0
- code_muse/plugins/filter_engine/register_callbacks.py +188 -0
- code_muse/plugins/filter_engine/registry.py +279 -0
- code_muse/plugins/filter_engine/strategies/__init__.py +8 -0
- code_muse/plugins/filter_engine/strategies/ast_compressor.cpython-314-darwin.so +0 -0
- code_muse/plugins/filter_engine/strategies/ast_compressor.cpython-314-x86_64-linux-gnu.so +0 -0
- code_muse/plugins/filter_engine/strategies/ast_compressor.pyx +348 -0
- code_muse/plugins/filter_engine/strategies/ast_parser.py +167 -0
- code_muse/plugins/filter_engine/strategies/code.cpython-314-darwin.so +0 -0
- code_muse/plugins/filter_engine/strategies/code.cpython-314-x86_64-linux-gnu.so +0 -0
- code_muse/plugins/filter_engine/strategies/code.pyx +584 -0
- code_muse/plugins/filter_engine/strategies/git.cpython-314-darwin.so +0 -0
- code_muse/plugins/filter_engine/strategies/git.cpython-314-x86_64-linux-gnu.so +0 -0
- code_muse/plugins/filter_engine/strategies/git.pyx +438 -0
- code_muse/plugins/filter_engine/strategies/json_compressor.cpython-314-darwin.so +0 -0
- code_muse/plugins/filter_engine/strategies/json_compressor.pyx +253 -0
- code_muse/plugins/filter_engine/strategies/json_patterns.cpython-314-darwin.so +0 -0
- code_muse/plugins/filter_engine/strategies/json_patterns.pyx +178 -0
- code_muse/plugins/filter_engine/strategies/lint.cpython-314-darwin.so +0 -0
- code_muse/plugins/filter_engine/strategies/lint.cpython-314-x86_64-linux-gnu.so +0 -0
- code_muse/plugins/filter_engine/strategies/lint.pyx +626 -0
- code_muse/plugins/filter_engine/strategies/test.cpython-314-darwin.so +0 -0
- code_muse/plugins/filter_engine/strategies/test.cpython-314-x86_64-linux-gnu.so +0 -0
- code_muse/plugins/filter_engine/strategies/test.pyx +431 -0
- code_muse/plugins/filter_engine/verbosity.py +63 -0
- code_muse/plugins/force_push_guard/__init__.py +5 -0
- code_muse/plugins/force_push_guard/detector.py +96 -0
- code_muse/plugins/force_push_guard/register_callbacks.py +144 -0
- code_muse/plugins/force_push_guard/test_detector.py +143 -0
- code_muse/plugins/frontend_emitter/__init__.py +25 -0
- code_muse/plugins/frontend_emitter/emitter.py +121 -0
- code_muse/plugins/frontend_emitter/register_callbacks.py +259 -0
- code_muse/plugins/gac/__init__.py +4 -0
- code_muse/plugins/gac/git_ops.py +136 -0
- code_muse/plugins/gac/prompt.py +191 -0
- code_muse/plugins/gac/register_callbacks.py +82 -0
- code_muse/plugins/hook_creator/__init__.py +1 -0
- code_muse/plugins/hook_creator/register_callbacks.py +34 -0
- code_muse/plugins/hook_manager/__init__.py +1 -0
- code_muse/plugins/hook_manager/config.py +289 -0
- code_muse/plugins/hook_manager/hooks_menu.py +563 -0
- code_muse/plugins/hook_manager/register_callbacks.py +227 -0
- code_muse/plugins/hook_monitor/register_callbacks.py +36 -0
- code_muse/plugins/mindpack/__init__.py +0 -0
- code_muse/plugins/mindpack/factory.py +930 -0
- code_muse/plugins/mindpack/judge.py +573 -0
- code_muse/plugins/mindpack/memory.py +100 -0
- code_muse/plugins/mindpack/mindpack_menu.py +1552 -0
- code_muse/plugins/mindpack/orchestration.py +605 -0
- code_muse/plugins/mindpack/register_callbacks.py +175 -0
- code_muse/plugins/mindpack/schemas.py +358 -0
- code_muse/plugins/mindpack/tools.py +387 -0
- code_muse/plugins/oauth_muse_html.py +226 -0
- code_muse/plugins/ollama_setup/__init__.py +5 -0
- code_muse/plugins/ollama_setup/completer.py +36 -0
- code_muse/plugins/ollama_setup/register_callbacks.py +410 -0
- code_muse/plugins/plan_command/__init__.py +0 -0
- code_muse/plugins/plan_command/register_callbacks.py +206 -0
- code_muse/plugins/plan_mode/__init__.py +37 -0
- code_muse/plugins/plan_mode/mode_cycling.py +40 -0
- code_muse/plugins/plan_mode/plan_generation.py +68 -0
- code_muse/plugins/plan_mode/plan_hooks.py +74 -0
- code_muse/plugins/plan_mode/plan_mode_tools.py +138 -0
- code_muse/plugins/plan_mode/register_callbacks.py +121 -0
- code_muse/plugins/plugin_trust/register_callbacks.py +140 -0
- code_muse/plugins/policy_engine/__init__.py +46 -0
- code_muse/plugins/policy_engine/approval_flow_integration.py +59 -0
- code_muse/plugins/policy_engine/policy_evaluator.py +75 -0
- code_muse/plugins/policy_engine/policy_file_discovery.py +90 -0
- code_muse/plugins/policy_engine/policy_toml_schema.py +115 -0
- code_muse/plugins/policy_engine/register_callbacks.py +112 -0
- code_muse/plugins/pop_command/__init__.py +1 -0
- code_muse/plugins/pop_command/register_callbacks.py +189 -0
- code_muse/plugins/prompt_newline/__init__.py +13 -0
- code_muse/plugins/prompt_newline/config.py +19 -0
- code_muse/plugins/prompt_newline/register_callbacks.py +159 -0
- code_muse/plugins/safety_status/__init__.py +0 -0
- code_muse/plugins/safety_status/register_callbacks.py +113 -0
- code_muse/plugins/semantic_compression/__init__.py +6 -0
- code_muse/plugins/semantic_compression/compressor.py +295 -0
- code_muse/plugins/semantic_compression/config.py +123 -0
- code_muse/plugins/semantic_compression/register_callbacks.py +320 -0
- code_muse/plugins/shell_minimizer/__init__.py +50 -0
- code_muse/plugins/shell_minimizer/builtin_filters.toml +393 -0
- code_muse/plugins/shell_minimizer/pipeline.py +556 -0
- code_muse/plugins/shell_minimizer/primitives.py +482 -0
- code_muse/plugins/shell_minimizer/register_callbacks.py +276 -0
- code_muse/plugins/shell_safety/__init__.py +6 -0
- code_muse/plugins/shell_safety/agent_shell_safety.py +69 -0
- code_muse/plugins/shell_safety/command_cache.py +149 -0
- code_muse/plugins/shell_safety/register_callbacks.py +202 -0
- code_muse/plugins/synthetic_status/__init__.py +1 -0
- code_muse/plugins/synthetic_status/register_callbacks.py +128 -0
- code_muse/plugins/synthetic_status/status_api.py +145 -0
- code_muse/plugins/token_caching/__init__.py +21 -0
- code_muse/plugins/token_caching/cache_hit_tracking.py +128 -0
- code_muse/plugins/token_caching/cacheable_prefix_detection.py +28 -0
- code_muse/plugins/token_caching/register_callbacks.py +54 -0
- code_muse/plugins/token_caching/stats_display.py +35 -0
- code_muse/plugins/token_tracking/__init__.py +26 -0
- code_muse/plugins/token_tracking/database.py +381 -0
- code_muse/plugins/token_tracking/edit_analyzer.py +97 -0
- code_muse/plugins/token_tracking/record.py +55 -0
- code_muse/plugins/token_tracking/register_callbacks.py +277 -0
- code_muse/plugins/token_tracking/reports.py +329 -0
- code_muse/plugins/universal_constructor/__init__.py +13 -0
- code_muse/plugins/universal_constructor/models.py +136 -0
- code_muse/plugins/universal_constructor/register_callbacks.py +47 -0
- code_muse/plugins/universal_constructor/registry.py +390 -0
- code_muse/plugins/universal_constructor/runner.py +474 -0
- code_muse/plugins/universal_constructor/safety.py +440 -0
- code_muse/plugins/universal_constructor/sandbox.py +584 -0
- code_muse/provider_identity.py +105 -0
- code_muse/pydantic_patches.py +410 -0
- code_muse/reopenable_async_client.py +233 -0
- code_muse/round_robin_model.py +151 -0
- code_muse/secret_storage.py +74 -0
- code_muse/security/__init__.py +1 -0
- code_muse/security/redaction.cpython-314-darwin.so +0 -0
- code_muse/security/redaction.cpython-314-x86_64-linux-gnu.so +0 -0
- code_muse/security/redaction.pyx +135 -0
- code_muse/session_storage.py +565 -0
- code_muse/status_display.py +261 -0
- code_muse/stream_parser/__init__.py +76 -0
- code_muse/stream_parser/assistant_text_parser.py +90 -0
- code_muse/stream_parser/citation_parser.py +76 -0
- code_muse/stream_parser/inline_hidden_tag_parser.py +236 -0
- code_muse/stream_parser/proposed_plan_parser.py +158 -0
- code_muse/stream_parser/stream_text_chunk.py +23 -0
- code_muse/stream_parser/stream_text_parser.py +27 -0
- code_muse/stream_parser/tagged_line_parser.cpython-314-darwin.so +0 -0
- code_muse/stream_parser/tagged_line_parser.pyx +251 -0
- code_muse/stream_parser/utf8_stream_parser.cpython-314-darwin.so +0 -0
- code_muse/stream_parser/utf8_stream_parser.pyx +206 -0
- code_muse/summarization_agent.py +308 -0
- code_muse/terminal_utils.cpython-314-darwin.so +0 -0
- code_muse/terminal_utils.cpython-314-x86_64-linux-gnu.so +0 -0
- code_muse/terminal_utils.pyx +483 -0
- code_muse/tools/__init__.py +459 -0
- code_muse/tools/agent_tools.py +613 -0
- code_muse/tools/ask_user_question/__init__.py +26 -0
- code_muse/tools/ask_user_question/constants.py +73 -0
- code_muse/tools/ask_user_question/demo_tui.py +55 -0
- code_muse/tools/ask_user_question/handler.py +232 -0
- code_muse/tools/ask_user_question/models.py +302 -0
- code_muse/tools/ask_user_question/registration.py +37 -0
- code_muse/tools/ask_user_question/renderers.py +336 -0
- code_muse/tools/ask_user_question/terminal_ui.py +327 -0
- code_muse/tools/ask_user_question/theme.py +156 -0
- code_muse/tools/ask_user_question/tui_loop.py +422 -0
- code_muse/tools/background_jobs.py +99 -0
- code_muse/tools/browser/__init__.py +37 -0
- code_muse/tools/browser/browser_control.py +289 -0
- code_muse/tools/browser/browser_interactions.py +545 -0
- code_muse/tools/browser/browser_locators.py +640 -0
- code_muse/tools/browser/browser_manager.py +376 -0
- code_muse/tools/browser/browser_navigation.py +251 -0
- code_muse/tools/browser/browser_screenshot.py +180 -0
- code_muse/tools/browser/browser_scripts.py +462 -0
- code_muse/tools/browser/browser_workflows.py +222 -0
- code_muse/tools/chrome_cdp/__init__.py +1070 -0
- code_muse/tools/chrome_cdp/register_callbacks.py +61 -0
- code_muse/tools/command_runner.py +1401 -0
- code_muse/tools/common.py +1407 -0
- code_muse/tools/display.py +87 -0
- code_muse/tools/file_modifications.py +1099 -0
- code_muse/tools/file_operations.py +860 -0
- code_muse/tools/image_tools.py +185 -0
- code_muse/tools/meetin_proxy/__init__.py +243 -0
- code_muse/tools/meetin_proxy/capture_addon.py +82 -0
- code_muse/tools/meetin_proxy/proxy_manager.py +326 -0
- code_muse/tools/meetin_proxy/register_callbacks.py +45 -0
- code_muse/tools/path_policy.py +219 -0
- code_muse/tools/skills_tools.py +586 -0
- code_muse/tools/subagent_context.py +158 -0
- code_muse/tools/tools_content.py +50 -0
- code_muse/tools/universal_constructor.py +965 -0
- code_muse/uvx_detection.py +241 -0
- code_muse/version_checker.py +86 -0
- code_muse-0.0.1.data/data/code_muse/models.json +66 -0
- code_muse-0.0.1.data/data/code_muse/models_dev_api.json +1 -0
- code_muse-0.0.1.dist-info/METADATA +845 -0
- code_muse-0.0.1.dist-info/RECORD +394 -0
- code_muse-0.0.1.dist-info/WHEEL +4 -0
- code_muse-0.0.1.dist-info/entry_points.txt +2 -0
- 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.")
|