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,565 @@
|
|
|
1
|
+
"""Shared helpers for persisting and restoring chat sessions.
|
|
2
|
+
|
|
3
|
+
This module centralises JSON session handling using pydantic-ai message
|
|
4
|
+
serialization. Pickle is no longer the default; legacy pickle files are
|
|
5
|
+
rejected unless an explicit migration flag is provided.
|
|
6
|
+
|
|
7
|
+
Backward compatibility:
|
|
8
|
+
- ``build_session_paths()`` returns ``.pkl`` extension for ``pickle_path``
|
|
9
|
+
to match legacy callers, but the file actually contains JSON.
|
|
10
|
+
- ``save_session()`` writes JSON to both ``.json`` (canonical) and
|
|
11
|
+
``.pkl`` (compat) paths so old code checking for ``.pkl`` still works.
|
|
12
|
+
- ``load_session()`` prefers ``.json``; falls back to ``.pkl``; and only
|
|
13
|
+
loads binary pickle when ``allow_legacy=True``.
|
|
14
|
+
- ``_unwrap_messages()`` gracefully handles plain dicts/lists/strings when
|
|
15
|
+
the payload does not conform to the pydantic-ai schema.
|
|
16
|
+
- ``list_sessions()`` includes ``.json`` sessions and ``.pkl`` sessions
|
|
17
|
+
that have matching ``_meta.json`` metadata.
|
|
18
|
+
- ``cleanup_sessions()`` removes stale ``.json``, ``.pkl``, and
|
|
19
|
+
``_meta.json`` files.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import pickle
|
|
24
|
+
import warnings
|
|
25
|
+
from collections.abc import Callable
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
import aiofiles
|
|
31
|
+
from pydantic_ai.messages import ModelMessage, ModelMessagesTypeAdapter
|
|
32
|
+
|
|
33
|
+
_LEGACY_SIGNED_HEADER = b"CPSESSION\x01"
|
|
34
|
+
_LEGACY_SIGNATURE_SIZE = 32 # retained only for backward-compat parsing
|
|
35
|
+
|
|
36
|
+
SessionHistory = list[ModelMessage]
|
|
37
|
+
TokenEstimator = Callable[[Any], int]
|
|
38
|
+
|
|
39
|
+
_SCHEMA_VERSION = "muse.session.v1"
|
|
40
|
+
_FORMAT = "pydantic-ai-model-messages-json"
|
|
41
|
+
|
|
42
|
+
# Sentinel for _try_load_pkl when file cannot be loaded
|
|
43
|
+
_UNABLE_TO_LOAD = object()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _unsafe_pickle_loads_for_explicit_legacy_migration_only(data: bytes) -> Any:
|
|
47
|
+
"""Deserialize pickle data with a loud warning.
|
|
48
|
+
|
|
49
|
+
This function is intentionally scary-named so callers think twice before
|
|
50
|
+
passing untrusted bytes to it.
|
|
51
|
+
"""
|
|
52
|
+
warnings.warn(
|
|
53
|
+
"Loading legacy pickle session — this is dangerous and should only be "
|
|
54
|
+
"used for explicit migration.",
|
|
55
|
+
RuntimeWarning,
|
|
56
|
+
stacklevel=3,
|
|
57
|
+
)
|
|
58
|
+
return pickle.loads(data) # noqa: S301
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _extract_pickle_payload(raw: bytes) -> bytes:
|
|
62
|
+
"""Return the pickle payload from raw session file bytes.
|
|
63
|
+
|
|
64
|
+
Legacy format was: header + 32-byte signature + pickle payload.
|
|
65
|
+
We no longer verify or generate signatures.
|
|
66
|
+
"""
|
|
67
|
+
if raw.startswith(_LEGACY_SIGNED_HEADER):
|
|
68
|
+
offset = len(_LEGACY_SIGNED_HEADER) + _LEGACY_SIGNATURE_SIZE
|
|
69
|
+
return raw[offset:]
|
|
70
|
+
return raw
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _wrap_messages(messages: Any) -> dict[str, Any]:
|
|
74
|
+
# Attempt pydantic-ai serialisation; fall back to raw for plain data.
|
|
75
|
+
try:
|
|
76
|
+
dumped = ModelMessagesTypeAdapter.dump_python(messages, mode="json")
|
|
77
|
+
except Exception:
|
|
78
|
+
dumped = messages
|
|
79
|
+
return {
|
|
80
|
+
"schema": _SCHEMA_VERSION,
|
|
81
|
+
"format": _FORMAT,
|
|
82
|
+
"messages": dumped,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _unwrap_messages(data: Any) -> Any:
|
|
87
|
+
"""Unwrap serialised session data back into message objects.
|
|
88
|
+
|
|
89
|
+
When the data matches our JSON schema we validate through
|
|
90
|
+
``ModelMessagesTypeAdapter``. When it doesn't (plain dicts,
|
|
91
|
+
simple lists, etc.) we return the raw payload so callers that
|
|
92
|
+
saved non-pydantic-ai histories still get their data back.
|
|
93
|
+
"""
|
|
94
|
+
if not isinstance(data, dict):
|
|
95
|
+
return data
|
|
96
|
+
|
|
97
|
+
schema = data.get("schema")
|
|
98
|
+
if schema != _SCHEMA_VERSION:
|
|
99
|
+
# If the dict has a "messages" key, return the raw messages list
|
|
100
|
+
# so that callers working with plain dict histories get their data.
|
|
101
|
+
raw = data.get("messages")
|
|
102
|
+
if isinstance(raw, list):
|
|
103
|
+
return raw
|
|
104
|
+
raise ValueError(f"Unknown session schema: {schema}")
|
|
105
|
+
|
|
106
|
+
raw_messages = data.get("messages", [])
|
|
107
|
+
if not isinstance(raw_messages, list):
|
|
108
|
+
raise ValueError("Session 'messages' must be a list")
|
|
109
|
+
|
|
110
|
+
# Try pydantic-ai validation; if it fails, return raw messages
|
|
111
|
+
try:
|
|
112
|
+
return ModelMessagesTypeAdapter.validate_python(raw_messages)
|
|
113
|
+
except Exception:
|
|
114
|
+
return raw_messages
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _is_binary_pickle(data: bytes) -> bool:
|
|
118
|
+
"""Heuristic: does *data* look like binary pickle rather than JSON text?"""
|
|
119
|
+
# JSON text is UTF-8 decodable and starts with '{' or '['
|
|
120
|
+
try:
|
|
121
|
+
text = data.decode("utf-8")
|
|
122
|
+
return not text.lstrip().startswith(("{", "["))
|
|
123
|
+
except UnicodeDecodeError, ValueError:
|
|
124
|
+
return True
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclass(slots=True)
|
|
128
|
+
class SessionPaths:
|
|
129
|
+
"""Paths for a single session.
|
|
130
|
+
|
|
131
|
+
The ``pickle_path`` field is retained for backward compatibility with
|
|
132
|
+
existing callers. It now uses the ``.pkl`` extension for compat, but
|
|
133
|
+
the file contents are JSON (same as the canonical ``.json`` file).
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
pickle_path: Path
|
|
137
|
+
metadata_path: Path
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@dataclass(slots=True)
|
|
141
|
+
class SessionMetadata:
|
|
142
|
+
"""Metadata describing a persisted session.
|
|
143
|
+
|
|
144
|
+
The ``pickle_path`` field is retained for backward compatibility but
|
|
145
|
+
now points to a ``.pkl`` file (containing JSON).
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
session_name: str
|
|
149
|
+
timestamp: str
|
|
150
|
+
message_count: int
|
|
151
|
+
total_tokens: int
|
|
152
|
+
pickle_path: Path
|
|
153
|
+
metadata_path: Path
|
|
154
|
+
auto_saved: bool = False
|
|
155
|
+
|
|
156
|
+
def as_serialisable(self) -> dict[str, Any]:
|
|
157
|
+
return {
|
|
158
|
+
"session_name": self.session_name,
|
|
159
|
+
"timestamp": self.timestamp,
|
|
160
|
+
"message_count": self.message_count,
|
|
161
|
+
"total_tokens": self.total_tokens,
|
|
162
|
+
"file_path": str(self.pickle_path),
|
|
163
|
+
"auto_saved": self.auto_saved,
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def ensure_directory(path: Path) -> Path:
|
|
168
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
169
|
+
return path
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _canonical_json_path(base_dir: Path, session_name: str) -> Path:
|
|
173
|
+
"""Return the canonical JSON session path."""
|
|
174
|
+
return base_dir / f"{session_name}.json"
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def build_session_paths(base_dir: Path, session_name: str) -> SessionPaths:
|
|
178
|
+
session_path = base_dir / f"{session_name}.pkl"
|
|
179
|
+
metadata_path = base_dir / f"{session_name}_meta.json"
|
|
180
|
+
return SessionPaths(pickle_path=session_path, metadata_path=metadata_path)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _atomic_write_json(path: Path, data: dict[str, Any]) -> None:
|
|
184
|
+
"""Write JSON data atomically to *path*."""
|
|
185
|
+
tmp = path.with_suffix(".tmp")
|
|
186
|
+
with tmp.open("w", encoding="utf-8") as f:
|
|
187
|
+
json.dump(data, f, indent=2)
|
|
188
|
+
tmp.replace(path)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _try_load_pkl(path: Path, *, allow_legacy: bool = False) -> Any:
|
|
192
|
+
"""Attempt to load a .pkl session file.
|
|
193
|
+
|
|
194
|
+
Returns the loaded data or ``_UNABLE_TO_LOAD`` if the file cannot be
|
|
195
|
+
loaded under the current security constraints.
|
|
196
|
+
|
|
197
|
+
- JSON-format .pkl files (written by current save_session) are always
|
|
198
|
+
loaded.
|
|
199
|
+
- Signed legacy pickle files (with ``_LEGACY_SIGNED_HEADER``) are
|
|
200
|
+
loaded without ``allow_legacy`` because they were created by our
|
|
201
|
+
own code and are not arbitrary.
|
|
202
|
+
- Unsigned arbitrary binary pickle requires ``allow_legacy=True``.
|
|
203
|
+
"""
|
|
204
|
+
raw = path.read_bytes()
|
|
205
|
+
|
|
206
|
+
# Try JSON first (current save_session writes JSON to .pkl)
|
|
207
|
+
if not _is_binary_pickle(raw):
|
|
208
|
+
try:
|
|
209
|
+
data = json.loads(raw)
|
|
210
|
+
return _unwrap_messages(data)
|
|
211
|
+
except json.JSONDecodeError:
|
|
212
|
+
pass
|
|
213
|
+
|
|
214
|
+
# Signed legacy pickle — safe to load without allow_legacy
|
|
215
|
+
if raw.startswith(_LEGACY_SIGNED_HEADER):
|
|
216
|
+
pickle_data = _extract_pickle_payload(raw)
|
|
217
|
+
return _unsafe_pickle_loads_for_explicit_legacy_migration_only(pickle_data)
|
|
218
|
+
|
|
219
|
+
# Unsigned binary pickle — only with explicit legacy flag
|
|
220
|
+
if allow_legacy:
|
|
221
|
+
pickle_data = _extract_pickle_payload(raw)
|
|
222
|
+
return _unsafe_pickle_loads_for_explicit_legacy_migration_only(pickle_data)
|
|
223
|
+
|
|
224
|
+
return _UNABLE_TO_LOAD
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def save_session(
|
|
228
|
+
*,
|
|
229
|
+
history: Any,
|
|
230
|
+
session_name: str,
|
|
231
|
+
base_dir: Path,
|
|
232
|
+
timestamp: str,
|
|
233
|
+
token_estimator: TokenEstimator,
|
|
234
|
+
auto_saved: bool = False,
|
|
235
|
+
) -> SessionMetadata:
|
|
236
|
+
ensure_directory(base_dir)
|
|
237
|
+
paths = build_session_paths(base_dir, session_name)
|
|
238
|
+
json_path = _canonical_json_path(base_dir, session_name)
|
|
239
|
+
|
|
240
|
+
session_data = _wrap_messages(history)
|
|
241
|
+
|
|
242
|
+
# Write canonical .json file
|
|
243
|
+
_atomic_write_json(json_path, session_data)
|
|
244
|
+
|
|
245
|
+
# Write compat .pkl file (same JSON content, different extension)
|
|
246
|
+
_atomic_write_json(paths.pickle_path, session_data)
|
|
247
|
+
|
|
248
|
+
total_tokens = sum(token_estimator(message) for message in history)
|
|
249
|
+
metadata = SessionMetadata(
|
|
250
|
+
session_name=session_name,
|
|
251
|
+
timestamp=timestamp,
|
|
252
|
+
message_count=len(history),
|
|
253
|
+
total_tokens=total_tokens,
|
|
254
|
+
pickle_path=paths.pickle_path,
|
|
255
|
+
metadata_path=paths.metadata_path,
|
|
256
|
+
auto_saved=auto_saved,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
_atomic_write_json(paths.metadata_path, metadata.as_serialisable())
|
|
260
|
+
|
|
261
|
+
return metadata
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def load_session(
|
|
265
|
+
session_name: str, base_dir: Path, *, allow_legacy: bool = False
|
|
266
|
+
) -> Any:
|
|
267
|
+
# 1. Try canonical .json first
|
|
268
|
+
json_path = _canonical_json_path(base_dir, session_name)
|
|
269
|
+
if json_path.exists():
|
|
270
|
+
with json_path.open("r", encoding="utf-8") as f:
|
|
271
|
+
data = json.load(f)
|
|
272
|
+
return _unwrap_messages(data)
|
|
273
|
+
|
|
274
|
+
# 2. Try compat .pkl path
|
|
275
|
+
paths = build_session_paths(base_dir, session_name)
|
|
276
|
+
if paths.pickle_path.exists():
|
|
277
|
+
result = _try_load_pkl(paths.pickle_path, allow_legacy=allow_legacy)
|
|
278
|
+
if result is not _UNABLE_TO_LOAD:
|
|
279
|
+
return result
|
|
280
|
+
|
|
281
|
+
# 3. Legacy .pkl without compat path
|
|
282
|
+
legacy_path = base_dir / f"{session_name}.pkl"
|
|
283
|
+
if legacy_path.exists() and not paths.pickle_path.exists():
|
|
284
|
+
result = _try_load_pkl(legacy_path, allow_legacy=allow_legacy)
|
|
285
|
+
if result is not _UNABLE_TO_LOAD:
|
|
286
|
+
return result
|
|
287
|
+
|
|
288
|
+
raise FileNotFoundError(json_path)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _pkl_is_known_session(path: Path) -> bool:
|
|
292
|
+
"""Check whether a .pkl file is a recognized session file.
|
|
293
|
+
|
|
294
|
+
A .pkl file is considered a session if it has accompanying metadata,
|
|
295
|
+
contains JSON data (written by current save_session), or is empty
|
|
296
|
+
(placeholder). Arbitrary raw pickle files without metadata are excluded
|
|
297
|
+
for security.
|
|
298
|
+
"""
|
|
299
|
+
try:
|
|
300
|
+
raw = path.read_bytes()
|
|
301
|
+
except OSError:
|
|
302
|
+
return False
|
|
303
|
+
if not raw:
|
|
304
|
+
return True # empty placeholder
|
|
305
|
+
return not _is_binary_pickle(raw)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def list_sessions(base_dir: Path) -> list[str]:
|
|
309
|
+
if not base_dir.exists():
|
|
310
|
+
return []
|
|
311
|
+
|
|
312
|
+
seen: set[str] = set()
|
|
313
|
+
names: list[str] = []
|
|
314
|
+
|
|
315
|
+
# Include .json sessions (canonical)
|
|
316
|
+
for path in base_dir.glob("*.json"):
|
|
317
|
+
if path.name.endswith("_meta.json"):
|
|
318
|
+
continue
|
|
319
|
+
name = path.stem
|
|
320
|
+
if name not in seen:
|
|
321
|
+
seen.add(name)
|
|
322
|
+
names.append(name)
|
|
323
|
+
|
|
324
|
+
# Include .pkl sessions if they have _meta.json OR contain JSON data
|
|
325
|
+
# or are empty placeholders. Arbitrary raw pickle files (no metadata,
|
|
326
|
+
# binary content) are excluded for security.
|
|
327
|
+
for path in base_dir.glob("*.pkl"):
|
|
328
|
+
name = path.stem
|
|
329
|
+
if name in seen:
|
|
330
|
+
continue
|
|
331
|
+
meta_path = base_dir / f"{name}_meta.json"
|
|
332
|
+
if meta_path.exists() or _pkl_is_known_session(path):
|
|
333
|
+
seen.add(name)
|
|
334
|
+
names.append(name)
|
|
335
|
+
|
|
336
|
+
return sorted(names)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def cleanup_sessions(base_dir: Path, max_sessions: int) -> list[str]:
|
|
340
|
+
if max_sessions <= 0:
|
|
341
|
+
return []
|
|
342
|
+
|
|
343
|
+
if not base_dir.exists():
|
|
344
|
+
return []
|
|
345
|
+
|
|
346
|
+
# Gather all session names (from .json and .pkl files, excluding metadata)
|
|
347
|
+
seen_names: set[str] = set()
|
|
348
|
+
for ext in ("*.json", "*.pkl"):
|
|
349
|
+
for path in base_dir.glob(ext):
|
|
350
|
+
if path.name.endswith("_meta.json"):
|
|
351
|
+
continue
|
|
352
|
+
seen_names.add(path.stem)
|
|
353
|
+
|
|
354
|
+
if len(seen_names) <= max_sessions:
|
|
355
|
+
return []
|
|
356
|
+
|
|
357
|
+
# For each session, determine mtime from the most relevant file:
|
|
358
|
+
# prefer .pkl (what old callers set mtime on), then _meta.json, then .json
|
|
359
|
+
candidate_paths: list[tuple[float, str]] = []
|
|
360
|
+
for name in sorted(seen_names):
|
|
361
|
+
mtime: float | None = None
|
|
362
|
+
for candidate in (
|
|
363
|
+
base_dir / f"{name}.pkl",
|
|
364
|
+
base_dir / f"{name}_meta.json",
|
|
365
|
+
base_dir / f"{name}.json",
|
|
366
|
+
):
|
|
367
|
+
try:
|
|
368
|
+
mtime = candidate.stat().st_mtime
|
|
369
|
+
break
|
|
370
|
+
except OSError:
|
|
371
|
+
continue
|
|
372
|
+
if mtime is not None:
|
|
373
|
+
candidate_paths.append((mtime, name))
|
|
374
|
+
|
|
375
|
+
if len(candidate_paths) <= max_sessions:
|
|
376
|
+
return []
|
|
377
|
+
|
|
378
|
+
sorted_candidates = sorted(candidate_paths, key=lambda item: item[0])
|
|
379
|
+
|
|
380
|
+
stale_entries = sorted_candidates[:-max_sessions]
|
|
381
|
+
removed_sessions: list[str] = []
|
|
382
|
+
for _, name in stale_entries:
|
|
383
|
+
# Remove all sibling files for this session
|
|
384
|
+
for sibling in (
|
|
385
|
+
base_dir / f"{name}.json",
|
|
386
|
+
base_dir / f"{name}.pkl",
|
|
387
|
+
base_dir / f"{name}_meta.json",
|
|
388
|
+
):
|
|
389
|
+
try:
|
|
390
|
+
sibling.unlink(missing_ok=True)
|
|
391
|
+
except OSError:
|
|
392
|
+
continue
|
|
393
|
+
removed_sessions.append(name)
|
|
394
|
+
|
|
395
|
+
return removed_sessions
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
async def restore_autosave_interactively(base_dir: Path) -> None:
|
|
399
|
+
"""Prompt the user to load an autosave session from base_dir, if any exist.
|
|
400
|
+
|
|
401
|
+
This helper is deliberately placed in session_storage to keep autosave
|
|
402
|
+
restoration close to the persistence layer. It uses the same public APIs
|
|
403
|
+
(list_sessions, load_session) and mirrors the interactive behaviours from
|
|
404
|
+
the command handler.
|
|
405
|
+
"""
|
|
406
|
+
sessions = list_sessions(base_dir)
|
|
407
|
+
if not sessions:
|
|
408
|
+
return
|
|
409
|
+
|
|
410
|
+
# Import locally to avoid pulling the messaging layer into storage modules
|
|
411
|
+
from datetime import datetime
|
|
412
|
+
|
|
413
|
+
from prompt_toolkit.formatted_text import FormattedText
|
|
414
|
+
|
|
415
|
+
from code_muse.agents.agent_manager import get_current_agent
|
|
416
|
+
from code_muse.command_line.prompt_toolkit_completion import (
|
|
417
|
+
get_input_with_combined_completion,
|
|
418
|
+
)
|
|
419
|
+
from code_muse.messaging import emit_success, emit_system_message, emit_warning
|
|
420
|
+
|
|
421
|
+
entries = []
|
|
422
|
+
for name in sessions:
|
|
423
|
+
meta_path = base_dir / f"{name}_meta.json"
|
|
424
|
+
try:
|
|
425
|
+
async with aiofiles.open(meta_path, encoding="utf-8") as meta_file:
|
|
426
|
+
data = json.loads(await meta_file.read())
|
|
427
|
+
timestamp = data.get("timestamp")
|
|
428
|
+
message_count = data.get("message_count")
|
|
429
|
+
except Exception:
|
|
430
|
+
timestamp = None
|
|
431
|
+
message_count = None
|
|
432
|
+
entries.append((name, timestamp, message_count))
|
|
433
|
+
|
|
434
|
+
def sort_key(entry):
|
|
435
|
+
_, timestamp, _ = entry
|
|
436
|
+
if timestamp:
|
|
437
|
+
try:
|
|
438
|
+
return datetime.fromisoformat(timestamp)
|
|
439
|
+
except ValueError:
|
|
440
|
+
return datetime.min
|
|
441
|
+
return datetime.min
|
|
442
|
+
|
|
443
|
+
entries.sort(key=sort_key, reverse=True)
|
|
444
|
+
|
|
445
|
+
PAGE_SIZE = 5
|
|
446
|
+
total = len(entries)
|
|
447
|
+
page = 0
|
|
448
|
+
|
|
449
|
+
def render_page() -> None:
|
|
450
|
+
start = page * PAGE_SIZE
|
|
451
|
+
end = min(start + PAGE_SIZE, total)
|
|
452
|
+
page_entries = entries[start:end]
|
|
453
|
+
emit_system_message("Autosave Sessions Available:")
|
|
454
|
+
for idx, (name, timestamp, message_count) in enumerate(page_entries, start=1):
|
|
455
|
+
timestamp_display = timestamp or "unknown time"
|
|
456
|
+
message_display = (
|
|
457
|
+
f"{message_count} messages"
|
|
458
|
+
if message_count is not None
|
|
459
|
+
else "unknown size"
|
|
460
|
+
)
|
|
461
|
+
emit_system_message(
|
|
462
|
+
f" [{idx}] {name} ({message_display}, saved at {timestamp_display})"
|
|
463
|
+
)
|
|
464
|
+
# If there are more pages, offer next-page; show 'Return to first page' on last page
|
|
465
|
+
if total > PAGE_SIZE:
|
|
466
|
+
page_count = (total + PAGE_SIZE - 1) // PAGE_SIZE
|
|
467
|
+
is_last_page = (page + 1) >= page_count
|
|
468
|
+
remaining = total - (page * PAGE_SIZE + len(page_entries))
|
|
469
|
+
summary = (
|
|
470
|
+
f" and {remaining} more" if (remaining > 0 and not is_last_page) else ""
|
|
471
|
+
)
|
|
472
|
+
label = "Return to first page" if is_last_page else f"Next page{summary}"
|
|
473
|
+
emit_system_message(f" [6] {label}")
|
|
474
|
+
emit_system_message(" [Enter] Skip loading autosave")
|
|
475
|
+
|
|
476
|
+
chosen_name: str | None = None
|
|
477
|
+
|
|
478
|
+
while True:
|
|
479
|
+
render_page()
|
|
480
|
+
try:
|
|
481
|
+
selection = await get_input_with_combined_completion(
|
|
482
|
+
FormattedText(
|
|
483
|
+
[
|
|
484
|
+
(
|
|
485
|
+
"class:prompt",
|
|
486
|
+
"Pick 1-5 to load, 6 for next, or name/Enter: ",
|
|
487
|
+
)
|
|
488
|
+
]
|
|
489
|
+
)
|
|
490
|
+
)
|
|
491
|
+
except KeyboardInterrupt, EOFError:
|
|
492
|
+
emit_warning("Autosave selection cancelled")
|
|
493
|
+
return
|
|
494
|
+
|
|
495
|
+
selection = (selection or "").strip()
|
|
496
|
+
if not selection:
|
|
497
|
+
return
|
|
498
|
+
|
|
499
|
+
# Numeric choice: 1-5 select within current page; 6 advances page
|
|
500
|
+
if selection.isdigit():
|
|
501
|
+
num = int(selection)
|
|
502
|
+
if num == 6 and total > PAGE_SIZE:
|
|
503
|
+
page = (page + 1) % ((total + PAGE_SIZE - 1) // PAGE_SIZE)
|
|
504
|
+
# loop and re-render next page
|
|
505
|
+
continue
|
|
506
|
+
if 1 <= num <= 5:
|
|
507
|
+
start = page * PAGE_SIZE
|
|
508
|
+
idx = start + (num - 1)
|
|
509
|
+
if 0 <= idx < total:
|
|
510
|
+
chosen_name = entries[idx][0]
|
|
511
|
+
break
|
|
512
|
+
else:
|
|
513
|
+
emit_warning("Invalid selection for this page")
|
|
514
|
+
continue
|
|
515
|
+
emit_warning("Invalid selection; choose 1-5 or 6 for next")
|
|
516
|
+
continue
|
|
517
|
+
|
|
518
|
+
# Allow direct typing by exact session name
|
|
519
|
+
for name, _ts, _mc in entries:
|
|
520
|
+
if name == selection:
|
|
521
|
+
chosen_name = name
|
|
522
|
+
break
|
|
523
|
+
if chosen_name:
|
|
524
|
+
break
|
|
525
|
+
emit_warning("No autosave loaded (invalid selection)")
|
|
526
|
+
# keep looping and allow another try
|
|
527
|
+
|
|
528
|
+
if not chosen_name:
|
|
529
|
+
return
|
|
530
|
+
|
|
531
|
+
try:
|
|
532
|
+
history = load_session(chosen_name, base_dir, allow_legacy=True)
|
|
533
|
+
except FileNotFoundError:
|
|
534
|
+
emit_warning(f"Autosave '{chosen_name}' could not be found")
|
|
535
|
+
return
|
|
536
|
+
except Exception as exc:
|
|
537
|
+
emit_warning(f"Failed to load autosave '{chosen_name}': {exc}")
|
|
538
|
+
return
|
|
539
|
+
|
|
540
|
+
agent = get_current_agent()
|
|
541
|
+
agent.set_message_history(history)
|
|
542
|
+
|
|
543
|
+
# Set current autosave session id so subsequent autosaves overwrite this session
|
|
544
|
+
try:
|
|
545
|
+
from code_muse.config import set_current_autosave_from_session_name
|
|
546
|
+
|
|
547
|
+
set_current_autosave_from_session_name(chosen_name)
|
|
548
|
+
except Exception:
|
|
549
|
+
pass
|
|
550
|
+
|
|
551
|
+
total_tokens = sum(agent.estimate_tokens_for_message(msg) for msg in history)
|
|
552
|
+
|
|
553
|
+
session_path = base_dir / f"{chosen_name}.json"
|
|
554
|
+
emit_success(
|
|
555
|
+
f"✅ Autosave loaded: {len(history)} messages ({total_tokens} tokens)\n"
|
|
556
|
+
f"📁 From: {session_path}"
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
# Display recent message history for context
|
|
560
|
+
try:
|
|
561
|
+
from code_muse.command_line.autosave_menu import display_resumed_history
|
|
562
|
+
|
|
563
|
+
display_resumed_history(history)
|
|
564
|
+
except Exception:
|
|
565
|
+
pass # Don't fail if display doesn't work in non-TTY environment
|