comate-cli 0.7.0a8__tar.gz → 0.7.0a10__tar.gz
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.
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/PKG-INFO +1 -1
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/animations.py +29 -5
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/app.py +10 -1
- comate_cli-0.7.0a10/comate_cli/terminal_agent/config/model.py +52 -0
- comate_cli-0.7.0a10/comate_cli/terminal_agent/config/picker.py +232 -0
- comate_cli-0.7.0a10/comate_cli/terminal_agent/config/picker_state.py +210 -0
- comate_cli-0.7.0a10/comate_cli/terminal_agent/config/store.py +173 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/error_display.py +22 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/event_renderer.py +28 -7
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/history_printer.py +1 -2
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/logging_adapter.py +59 -2
- comate_cli-0.7.0a10/comate_cli/terminal_agent/plugins/tabs/__init__.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/slash_commands.py +5 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/tool_result_formatters.py +125 -8
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/tool_view.py +49 -2
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/tui.py +12 -1
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/tui_parts/commands.py +40 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/tui_parts/history_sync.py +2 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/tui_parts/input_behavior.py +3 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/tui_parts/key_bindings.py +42 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/tui_parts/render_panels.py +46 -11
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/tui_parts/ui_mode.py +1 -0
- comate_cli-0.7.0a10/docs/superpowers/plans/2026-05-22-log-style-implementation-plan.md +444 -0
- comate_cli-0.7.0a10/docs/superpowers/specs/2026-05-22-comate-cli-log-styling-design.md +71 -0
- comate_cli-0.7.0a10/docs/superpowers/specs/2026-05-22-log-style-optimization-design.md +114 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/pyproject.toml +1 -1
- comate_cli-0.7.0a10/tests/config/__init__.py +0 -0
- comate_cli-0.7.0a10/tests/config/test_model.py +88 -0
- comate_cli-0.7.0a10/tests/config/test_picker_state.py +205 -0
- comate_cli-0.7.0a10/tests/config/test_picker_ui.py +42 -0
- comate_cli-0.7.0a10/tests/config/test_roundtrip.py +75 -0
- comate_cli-0.7.0a10/tests/config/test_store_load.py +112 -0
- comate_cli-0.7.0a10/tests/config/test_store_save.py +103 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_completion_status_panel.py +43 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_event_renderer.py +91 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_event_renderer_streaming.py +32 -1
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_format_error.py +52 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_history_printer_tool_fold.py +8 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_history_sync.py +114 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_logging_adapter.py +235 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_task_panel_rendering.py +3 -3
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_tool_result_formatters.py +71 -1
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_tool_view.py +13 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_transcript_viewer.py +44 -0
- comate_cli-0.7.0a10/tests/test_tui_thinking_display.py +71 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/uv.lock +1441 -1457
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/.gitignore +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/AGENTS.md +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/CHANGELOG.md +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/README.md +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/bash-exit-code-green-dot-bug.md +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/__init__.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/__main__.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/main.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/mcp_cli.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/__init__.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/assistant_render.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/codenames.py +0 -0
- {comate_cli-0.7.0a8/comate_cli/terminal_agent/plugins → comate_cli-0.7.0a10/comate_cli/terminal_agent/config}/__init__.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/custom_slash_commands.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/env_utils.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/figures.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/fragment_utils.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/input_geometry.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/layout_coordinator.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/logo.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/markdown_render.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/mention_completer.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/message_style.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/models.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/path_context_hint.py +0 -0
- {comate_cli-0.7.0a8/comate_cli/terminal_agent/plugins/components → comate_cli-0.7.0a10/comate_cli/terminal_agent/plugins}/__init__.py +0 -0
- {comate_cli-0.7.0a8/comate_cli/terminal_agent/plugins/tabs → comate_cli-0.7.0a10/comate_cli/terminal_agent/plugins/components}/__init__.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/plugins/components/detail_view.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/plugins/components/plugin_list.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/plugins/components/search_box.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/plugins/components/tab_bar.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/plugins/marketplace_install_view.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/plugins/plugin_picker.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/plugins/tabs/discover_tab.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/plugins/tabs/errors_tab.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/plugins/tabs/installed_tab.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/plugins/tabs/marketplaces_tab.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/preflight.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/question_view.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/resume_picker.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/resume_preview.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/resume_selector.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/rpc_protocol.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/rpc_stdio.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/selection_menu.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/startup.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/status_bar.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/text_effects.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/tips.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/tool_fold.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/tool_result_store.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/tool_result_viewer.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/transcript_viewer.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/tui_parts/__init__.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/tui_parts/btw_view.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/tui_parts/mcp_connecting_view.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/tui_parts/slash_command_registry.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/docs/hooks.md +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/docs/superpowers/plans/2026-04-03-phrase-shuffle.md +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/docs/superpowers/specs/2026-04-01-conditional-diff-subtitle-design.md +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/docs/superpowers/specs/2026-04-03-phrase-shuffle-design.md +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/report.md +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/conftest.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/fixtures/fake_mcp_misbehaving_stdout.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_animator_shuffle.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_app_mcp_preload.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_app_preflight_gate.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_app_print_mode.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_app_shutdown.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_app_usage_line.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_btw_slash_command.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_cli_project_root.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_compact_command_semantics.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_completion_context_activation.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_context_command.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_custom_slash_commands.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_discover_tab.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_errors_tab.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_event_renderer_boundary.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_event_renderer_e2e.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_event_renderer_log_boundary.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_event_renderer_log_queue.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_event_renderer_tool_fold.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_handle_error.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_history_printer.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_history_printer_log.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_history_printer_subtitle_position.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_history_sync_tool_fold.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_input_behavior.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_input_history.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_installed_tab.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_interrupt_exit_semantics.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_layout_coordinator.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_logo.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_main_args.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_markdown_render.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_marketplaces_tab.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_mcp_cli.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_mcp_slash_command.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_mention_completer.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_path_context_hint.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_plugin_slash_commands.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_plugin_tui_components.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_preflight.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_preflight_copilot.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_question_key_bindings.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_question_view.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_resume_picker.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_resume_preview.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_resume_selector.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_rewind_command_semantics.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_rpc_protocol.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_rpc_stdio_bridge.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_selection_menu.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_session_query_token_summary.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_shutdown_noise_guard.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_shutdown_noise_integration.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_skills_slash_command.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_slash_argument_hint.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_slash_clear.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_slash_completer.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_slash_registry.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_status_bar.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_status_bar_transient.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_task_panel_format.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_task_panel_key_bindings.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_task_poll.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_tool_fold.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_tool_fold_panel.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_tool_result_store.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_tool_result_viewer.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_tool_result_viewer_key_bindings.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_transcript_viewer_tool_fold.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_tui_elapsed_status.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_tui_esc_queue.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_tui_mcp_init_gate.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_tui_paste_newline_guard.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_tui_paste_placeholder.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_tui_queue_preview.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_tui_queue_sdk_source.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_tui_split_invariance.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_tui_team_messages.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_tui_tool_result_registry_lifecycle.py +0 -0
- {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_update_check.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: comate-cli
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.0a10
|
|
4
4
|
Summary: Comate terminal CLI built on comate-agent-sdk
|
|
5
5
|
Project-URL: Homepage, https://github.com/AndyLee1024/agent-sdk
|
|
6
6
|
Project-URL: Repository, https://github.com/AndyLee1024/agent-sdk
|
|
@@ -9,7 +9,7 @@ from enum import Enum
|
|
|
9
9
|
from rich.console import RenderableType
|
|
10
10
|
from rich.text import Text
|
|
11
11
|
|
|
12
|
-
from comate_agent_sdk.agent.events import StopEvent,
|
|
12
|
+
from comate_agent_sdk.agent.events import StopEvent, ToolCallEvent, ToolResultEvent, UserQuestionEvent
|
|
13
13
|
|
|
14
14
|
from comate_cli.terminal_agent.figures import (
|
|
15
15
|
BREATH_DOT_GLYPHS as _FIGURES_BREATH_DOT_GLYPHS,
|
|
@@ -238,6 +238,27 @@ BREATH_DOT_COLORS: tuple[str, ...] = (
|
|
|
238
238
|
)
|
|
239
239
|
BREATH_DOT_GLYPHS: tuple[str, ...] = _FIGURES_BREATH_DOT_GLYPHS
|
|
240
240
|
|
|
241
|
+
LOADING_SPINNER_FRAMES: tuple[str, ...] = (
|
|
242
|
+
"⠋",
|
|
243
|
+
"⠙",
|
|
244
|
+
"⠹",
|
|
245
|
+
"⠸",
|
|
246
|
+
"⠼",
|
|
247
|
+
"⠴",
|
|
248
|
+
"⠦",
|
|
249
|
+
"⠧",
|
|
250
|
+
"⠇",
|
|
251
|
+
"⠏",
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
HIDDEN_THINKING_BADGES: tuple[str, ...] = (
|
|
255
|
+
"ultra thinking",
|
|
256
|
+
"deep reasoning",
|
|
257
|
+
"thinking deeper",
|
|
258
|
+
"extended thinking",
|
|
259
|
+
"working through it",
|
|
260
|
+
)
|
|
261
|
+
|
|
241
262
|
|
|
242
263
|
def breathing_dot_color(frame: int) -> str:
|
|
243
264
|
"""Return the breathing dot color for a given animation frame."""
|
|
@@ -252,6 +273,11 @@ def breathing_dot_glyph(now_monotonic: float | None = None) -> str:
|
|
|
252
273
|
return BREATH_DOT_GLYPHS[phase]
|
|
253
274
|
|
|
254
275
|
|
|
276
|
+
def loading_spinner_glyph(frame: int) -> str:
|
|
277
|
+
"""Return the compact spinner glyph for the main loading line."""
|
|
278
|
+
return LOADING_SPINNER_FRAMES[frame % len(LOADING_SPINNER_FRAMES)]
|
|
279
|
+
|
|
280
|
+
|
|
255
281
|
def _lerp_rgb(
|
|
256
282
|
start_rgb: tuple[int, int, int],
|
|
257
283
|
end_rgb: tuple[int, int, int],
|
|
@@ -385,11 +411,9 @@ class SubmissionAnimator:
|
|
|
385
411
|
return Text("")
|
|
386
412
|
|
|
387
413
|
phrase = self._status_hint if self._status_hint else (self._shuffled[self._phrase_idx] + ELLIPSIS)
|
|
388
|
-
dot_color = breathing_dot_color(self._frame)
|
|
389
|
-
now_monotonic = time.monotonic()
|
|
390
414
|
dot = Text(
|
|
391
|
-
f"{
|
|
392
|
-
style=
|
|
415
|
+
f"{loading_spinner_glyph(self._frame)} ",
|
|
416
|
+
style="bold #9CA3AF",
|
|
393
417
|
)
|
|
394
418
|
sweep = _cyan_sweep_text(phrase, frame=self._frame)
|
|
395
419
|
return Text.assemble(dot, sweep)
|
|
@@ -186,14 +186,23 @@ async def add(a: int, b: int) -> int:
|
|
|
186
186
|
|
|
187
187
|
|
|
188
188
|
def _build_agent(*, project_root: Path | None = None) -> Agent:
|
|
189
|
+
from comate_agent_sdk.agent.compaction import CompactionConfig
|
|
190
|
+
from comate_cli.terminal_agent.config import store
|
|
191
|
+
|
|
189
192
|
resolved_project_root = project_root or _resolve_cli_project_root()
|
|
193
|
+
snapshot = store.load()
|
|
190
194
|
|
|
191
195
|
return Agent(
|
|
192
196
|
config=AgentConfig(
|
|
193
197
|
role="software_engineering",
|
|
194
198
|
cwd=resolved_project_root,
|
|
195
199
|
env_options=EnvOptions(system_env=True, git_env=True),
|
|
196
|
-
use_streaming_task=True,
|
|
200
|
+
use_streaming_task=True,
|
|
201
|
+
memory_background_enabled=snapshot.memory_background_enabled,
|
|
202
|
+
memory_dreaming_enabled=snapshot.memory_dreaming_enabled,
|
|
203
|
+
compaction=CompactionConfig(
|
|
204
|
+
threshold_ratio=snapshot.compaction_threshold_ratio,
|
|
205
|
+
),
|
|
197
206
|
)
|
|
198
207
|
)
|
|
199
208
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""ConfigSnapshot:/config UI、store、startup 三处共享的偏好快照。
|
|
2
|
+
|
|
3
|
+
Revision 2: 5 项偏好(display_thinking, model_level, compaction, memory_background, memory_dreaming)。
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, replace
|
|
8
|
+
from typing import Literal
|
|
9
|
+
|
|
10
|
+
ModelLevelPreset = Literal["LOW", "MID", "HIGH"]
|
|
11
|
+
ThresholdPreset = Literal["conservative", "balanced", "aggressive"]
|
|
12
|
+
|
|
13
|
+
COMPACT_PRESETS: dict[str, float] = {
|
|
14
|
+
"conservative": 0.65,
|
|
15
|
+
"balanced": 0.80,
|
|
16
|
+
"aggressive": 0.92,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, slots=True)
|
|
21
|
+
class ConfigSnapshot:
|
|
22
|
+
"""5 项 CLI 偏好的不可变快照。"""
|
|
23
|
+
|
|
24
|
+
display_thinking: bool = False
|
|
25
|
+
model_level: ModelLevelPreset = "MID"
|
|
26
|
+
compaction_threshold_ratio: float = 0.80
|
|
27
|
+
compaction_threshold_preset: ThresholdPreset = "balanced"
|
|
28
|
+
memory_background_enabled: bool = True
|
|
29
|
+
memory_dreaming_enabled: bool = True
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def default(cls) -> "ConfigSnapshot":
|
|
33
|
+
return cls()
|
|
34
|
+
|
|
35
|
+
def with_display_thinking(self, enabled: bool) -> "ConfigSnapshot":
|
|
36
|
+
return replace(self, display_thinking=enabled)
|
|
37
|
+
|
|
38
|
+
def with_model_level(self, level: ModelLevelPreset) -> "ConfigSnapshot":
|
|
39
|
+
return replace(self, model_level=level)
|
|
40
|
+
|
|
41
|
+
def with_threshold_preset(self, preset: ThresholdPreset) -> "ConfigSnapshot":
|
|
42
|
+
return replace(
|
|
43
|
+
self,
|
|
44
|
+
compaction_threshold_preset=preset,
|
|
45
|
+
compaction_threshold_ratio=COMPACT_PRESETS[preset],
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def with_memory_background(self, enabled: bool) -> "ConfigSnapshot":
|
|
49
|
+
return replace(self, memory_background_enabled=enabled)
|
|
50
|
+
|
|
51
|
+
def with_memory_dreaming(self, enabled: bool) -> "ConfigSnapshot":
|
|
52
|
+
return replace(self, memory_dreaming_enabled=enabled)
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""/config slash command 的嵌入式 TUI 面板。
|
|
2
|
+
|
|
3
|
+
仿 plugins/plugin_picker.py:PluginPickerUI 的模式:
|
|
4
|
+
enter() → container → focus_target() → handle_*。
|
|
5
|
+
|
|
6
|
+
Revision 2: 5 行 ConfigField。
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
from prompt_toolkit.layout.containers import HSplit, Window
|
|
13
|
+
from prompt_toolkit.layout.controls import FormattedTextControl
|
|
14
|
+
|
|
15
|
+
from comate_cli.terminal_agent.figures import (
|
|
16
|
+
EFFORT_HIGH,
|
|
17
|
+
EFFORT_LOW,
|
|
18
|
+
HEAVY_HORIZONTAL,
|
|
19
|
+
PLAY_ICON,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from .model import ConfigSnapshot
|
|
23
|
+
from .picker_state import (
|
|
24
|
+
ConfigField,
|
|
25
|
+
ConfigPickerState,
|
|
26
|
+
PickerExit,
|
|
27
|
+
PickerExitKind,
|
|
28
|
+
ViewMode,
|
|
29
|
+
)
|
|
30
|
+
from . import store
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
_FIELD_LABELS: dict[ConfigField, str] = {
|
|
35
|
+
ConfigField.DISPLAY_THINKING: "Thinking display",
|
|
36
|
+
ConfigField.MODEL_LEVEL: "Default model level",
|
|
37
|
+
ConfigField.AUTO_COMPACT_THRESHOLD: "Auto compact threshold",
|
|
38
|
+
ConfigField.MEMORY_BACKGROUND: "Memory background",
|
|
39
|
+
ConfigField.MEMORY_DREAMING: "Memory dreaming",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
_FIELD_ORDER = (
|
|
43
|
+
ConfigField.DISPLAY_THINKING,
|
|
44
|
+
ConfigField.MODEL_LEVEL,
|
|
45
|
+
ConfigField.AUTO_COMPACT_THRESHOLD,
|
|
46
|
+
ConfigField.MEMORY_BACKGROUND,
|
|
47
|
+
ConfigField.MEMORY_DREAMING,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
_ENUM_FIELDS = {ConfigField.MODEL_LEVEL, ConfigField.AUTO_COMPACT_THRESHOLD}
|
|
51
|
+
_BOOL_FIELDS = {ConfigField.DISPLAY_THINKING, ConfigField.MEMORY_BACKGROUND, ConfigField.MEMORY_DREAMING}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ConfigPickerUI:
|
|
55
|
+
"""嵌入式 /config 面板。tui.py 持有单实例,ConditionalContainer 控制可见性。"""
|
|
56
|
+
|
|
57
|
+
def __init__(self) -> None:
|
|
58
|
+
self._state: ConfigPickerState | None = None
|
|
59
|
+
self._content_window: Window | None = None
|
|
60
|
+
self._root: HSplit | None = None
|
|
61
|
+
|
|
62
|
+
# ---- entry / lifecycle ---------------------------------------------
|
|
63
|
+
def enter(self, snapshot: ConfigSnapshot) -> None:
|
|
64
|
+
self._state = ConfigPickerState(origin=snapshot)
|
|
65
|
+
self._state.save_callback = store.save # type: ignore[assignment]
|
|
66
|
+
|
|
67
|
+
def take_result(self) -> tuple[bool, tuple[str, ...]]:
|
|
68
|
+
"""返回 (saved, error_messages)。供 tui.py 在 mode 切回 NORMAL 后落 history。"""
|
|
69
|
+
if self._state is None:
|
|
70
|
+
return (False, ())
|
|
71
|
+
return (self._state.save_called, self._state.last_save_error_messages)
|
|
72
|
+
|
|
73
|
+
# ---- container ------------------------------------------------------
|
|
74
|
+
@property
|
|
75
|
+
def container(self) -> HSplit:
|
|
76
|
+
if self._root is None:
|
|
77
|
+
self._root = self._build_layout()
|
|
78
|
+
return self._root
|
|
79
|
+
|
|
80
|
+
def focus_target(self) -> Window:
|
|
81
|
+
if self._content_window is None:
|
|
82
|
+
self._build_layout()
|
|
83
|
+
assert self._content_window is not None
|
|
84
|
+
return self._content_window
|
|
85
|
+
|
|
86
|
+
# ---- delegated handlers --------------------------------------------
|
|
87
|
+
def handle_up(self) -> None:
|
|
88
|
+
if self._state:
|
|
89
|
+
self._state.handle_up()
|
|
90
|
+
|
|
91
|
+
def handle_down(self) -> None:
|
|
92
|
+
if self._state:
|
|
93
|
+
self._state.handle_down()
|
|
94
|
+
|
|
95
|
+
def handle_tab(self) -> None:
|
|
96
|
+
if self._state:
|
|
97
|
+
self._state.handle_tab()
|
|
98
|
+
|
|
99
|
+
def handle_enter(self) -> PickerExit | None:
|
|
100
|
+
if self._state is None:
|
|
101
|
+
return None
|
|
102
|
+
return self._state.handle_enter() # type: ignore[return-value]
|
|
103
|
+
|
|
104
|
+
def handle_escape(self) -> PickerExit | None:
|
|
105
|
+
if self._state is None:
|
|
106
|
+
return PickerExit(PickerExitKind.CLEAN_EXIT)
|
|
107
|
+
return self._state.handle_escape()
|
|
108
|
+
|
|
109
|
+
def handle_ctrl_s(self) -> PickerExit | None:
|
|
110
|
+
if self._state is None:
|
|
111
|
+
return None
|
|
112
|
+
return self._state.handle_ctrl_s()
|
|
113
|
+
|
|
114
|
+
# ---- fragment builders ---------------------------------------------
|
|
115
|
+
def _header_fragments(self) -> list[tuple[str, str]]:
|
|
116
|
+
return [("class:selection.title", " Config")]
|
|
117
|
+
|
|
118
|
+
def _content_fragments(self) -> list[tuple[str, str]]:
|
|
119
|
+
if self._state is None:
|
|
120
|
+
return [("", "")]
|
|
121
|
+
|
|
122
|
+
if self._state.view_mode == ViewMode.MAIN:
|
|
123
|
+
return self._main_fragments()
|
|
124
|
+
return self._subview_fragments()
|
|
125
|
+
|
|
126
|
+
def _main_fragments(self) -> list[tuple[str, str]]:
|
|
127
|
+
state = self._state
|
|
128
|
+
rows: list[tuple[str, str]] = []
|
|
129
|
+
for f in _FIELD_ORDER:
|
|
130
|
+
focused = f == state.focused_field
|
|
131
|
+
cursor = PLAY_ICON if focused else " "
|
|
132
|
+
marker = EFFORT_HIGH if focused else EFFORT_LOW
|
|
133
|
+
prefix = f" {cursor} {marker} "
|
|
134
|
+
name = _FIELD_LABELS[f]
|
|
135
|
+
value = self._render_field_value(f, state.draft)
|
|
136
|
+
suffix = " \u21b5" if f in _ENUM_FIELDS else ""
|
|
137
|
+
row_style = (
|
|
138
|
+
"class:selection.option.selected"
|
|
139
|
+
if focused
|
|
140
|
+
else "class:selection.option"
|
|
141
|
+
)
|
|
142
|
+
value_style = (
|
|
143
|
+
"class:selection.description.selected"
|
|
144
|
+
if focused
|
|
145
|
+
else "class:selection.description"
|
|
146
|
+
)
|
|
147
|
+
rows.append((row_style, f"{prefix}{name:<26}"))
|
|
148
|
+
rows.append((value_style, f"{value}{suffix}\n"))
|
|
149
|
+
return rows
|
|
150
|
+
|
|
151
|
+
def _render_field_value(self, f: ConfigField, snap: ConfigSnapshot) -> str:
|
|
152
|
+
if f == ConfigField.DISPLAY_THINKING:
|
|
153
|
+
return f"[ {'on ' if snap.display_thinking else 'off'} ]"
|
|
154
|
+
if f == ConfigField.MODEL_LEVEL:
|
|
155
|
+
return f"[ {snap.model_level} ]"
|
|
156
|
+
if f == ConfigField.AUTO_COMPACT_THRESHOLD:
|
|
157
|
+
return f"[ {snap.compaction_threshold_preset} ({snap.compaction_threshold_ratio:.2f}) ]"
|
|
158
|
+
if f == ConfigField.MEMORY_BACKGROUND:
|
|
159
|
+
return f"[ {'on ' if snap.memory_background_enabled else 'off'} ]"
|
|
160
|
+
if f == ConfigField.MEMORY_DREAMING:
|
|
161
|
+
return f"[ {'on ' if snap.memory_dreaming_enabled else 'off'} ]"
|
|
162
|
+
return ""
|
|
163
|
+
|
|
164
|
+
def _subview_fragments(self) -> list[tuple[str, str]]:
|
|
165
|
+
state = self._state
|
|
166
|
+
if state is None or state.view_mode != ViewMode.SUBVIEW or state.subview_field is None:
|
|
167
|
+
return []
|
|
168
|
+
title = _FIELD_LABELS[state.subview_field]
|
|
169
|
+
rows: list[tuple[str, str]] = [
|
|
170
|
+
("class:selection.title", f" {title}\n"),
|
|
171
|
+
("", "\n"),
|
|
172
|
+
]
|
|
173
|
+
for idx, opt in enumerate(state.subview_options()):
|
|
174
|
+
focused = idx == state.subview_cursor
|
|
175
|
+
cursor = PLAY_ICON if focused else " "
|
|
176
|
+
marker = EFFORT_HIGH if focused else EFFORT_LOW
|
|
177
|
+
prefix = f" {cursor} {marker} "
|
|
178
|
+
style = (
|
|
179
|
+
"class:selection.option.selected"
|
|
180
|
+
if focused
|
|
181
|
+
else "class:selection.option"
|
|
182
|
+
)
|
|
183
|
+
rows.append((style, f"{prefix}{opt.label}\n"))
|
|
184
|
+
desc_style = (
|
|
185
|
+
"class:selection.description.selected"
|
|
186
|
+
if focused
|
|
187
|
+
else "class:selection.description"
|
|
188
|
+
)
|
|
189
|
+
rows.append((desc_style, f" {opt.description}\n"))
|
|
190
|
+
return rows
|
|
191
|
+
|
|
192
|
+
def _footer_fragments(self) -> list[tuple[str, str]]:
|
|
193
|
+
if self._state is None:
|
|
194
|
+
return []
|
|
195
|
+
if self._state.view_mode == ViewMode.MAIN:
|
|
196
|
+
if self._state.dirty_warning_active:
|
|
197
|
+
return [("class:warning", " Press Esc again to discard, Ctrl+S to save")]
|
|
198
|
+
if self._state.last_save_error_messages:
|
|
199
|
+
return [("class:error", " Save failed: " + " | ".join(self._state.last_save_error_messages))]
|
|
200
|
+
return [
|
|
201
|
+
(
|
|
202
|
+
"class:selection.hint",
|
|
203
|
+
" Tab cycle \u00b7 Enter open \u00b7 Ctrl+S save \u00b7 Esc cancel",
|
|
204
|
+
)
|
|
205
|
+
]
|
|
206
|
+
return [("class:selection.hint", " Enter confirm \u00b7 Esc back")]
|
|
207
|
+
|
|
208
|
+
def _build_layout(self) -> HSplit:
|
|
209
|
+
header = Window(
|
|
210
|
+
content=FormattedTextControl(self._header_fragments),
|
|
211
|
+
height=1,
|
|
212
|
+
dont_extend_height=True,
|
|
213
|
+
style="class:selection.title",
|
|
214
|
+
)
|
|
215
|
+
divider = Window(
|
|
216
|
+
height=1,
|
|
217
|
+
char=HEAVY_HORIZONTAL,
|
|
218
|
+
style="class:selection.divider",
|
|
219
|
+
)
|
|
220
|
+
self._content_window = Window(
|
|
221
|
+
content=FormattedTextControl(self._content_fragments, focusable=True),
|
|
222
|
+
wrap_lines=True,
|
|
223
|
+
always_hide_cursor=True,
|
|
224
|
+
style="class:selection.body",
|
|
225
|
+
)
|
|
226
|
+
footer = Window(
|
|
227
|
+
content=FormattedTextControl(self._footer_fragments),
|
|
228
|
+
height=1,
|
|
229
|
+
dont_extend_height=True,
|
|
230
|
+
style="class:selection.body",
|
|
231
|
+
)
|
|
232
|
+
return HSplit([header, divider, self._content_window, footer])
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""ConfigPicker 的纯逻辑状态机——不依赖 prompt_toolkit。
|
|
2
|
+
|
|
3
|
+
Revision 2: 5 行 ConfigField。
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import Callable
|
|
10
|
+
|
|
11
|
+
from .model import (
|
|
12
|
+
COMPACT_PRESETS,
|
|
13
|
+
ConfigSnapshot,
|
|
14
|
+
ModelLevelPreset,
|
|
15
|
+
ThresholdPreset,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ConfigField(Enum):
|
|
20
|
+
DISPLAY_THINKING = "display_thinking"
|
|
21
|
+
MODEL_LEVEL = "model_level"
|
|
22
|
+
AUTO_COMPACT_THRESHOLD = "auto_compact_threshold"
|
|
23
|
+
MEMORY_BACKGROUND = "memory_background"
|
|
24
|
+
MEMORY_DREAMING = "memory_dreaming"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ViewMode(Enum):
|
|
28
|
+
MAIN = "main"
|
|
29
|
+
SUBVIEW = "subview"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class PickerExitKind(Enum):
|
|
33
|
+
SAVED = "saved"
|
|
34
|
+
CLEAN_EXIT = "clean_exit" # 无 dirty,直接退出
|
|
35
|
+
DISCARDED = "discarded" # dirty 但用户两次 esc 丢弃
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True, slots=True)
|
|
39
|
+
class SubviewOption:
|
|
40
|
+
value: str
|
|
41
|
+
label: str
|
|
42
|
+
description: str
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True, slots=True)
|
|
46
|
+
class PickerExit:
|
|
47
|
+
kind: PickerExitKind
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
_FIELD_ORDER = (
|
|
51
|
+
ConfigField.DISPLAY_THINKING,
|
|
52
|
+
ConfigField.MODEL_LEVEL,
|
|
53
|
+
ConfigField.AUTO_COMPACT_THRESHOLD,
|
|
54
|
+
ConfigField.MEMORY_BACKGROUND,
|
|
55
|
+
ConfigField.MEMORY_DREAMING,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
_THRESHOLD_VALUES: tuple[ThresholdPreset, ...] = ("conservative", "balanced", "aggressive")
|
|
59
|
+
_THRESHOLD_DESCRIPTIONS: dict[str, str] = {
|
|
60
|
+
"conservative": "0.65 — compact more often; smaller per-call ctx.",
|
|
61
|
+
"balanced": "0.80 — default. Compact when ctx >= 80% utilized.",
|
|
62
|
+
"aggressive": "0.92 — compact late; larger ctx per call.",
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
_MODEL_LEVEL_VALUES: tuple[ModelLevelPreset, ...] = ("LOW", "MID", "HIGH")
|
|
66
|
+
_MODEL_LEVEL_DESCRIPTIONS: dict[str, str] = {
|
|
67
|
+
"LOW": "Fastest, cheapest. Good for simple tasks.",
|
|
68
|
+
"MID": "Default. Balanced speed and capability.",
|
|
69
|
+
"HIGH": "Most capable. For complex reasoning tasks.",
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class ConfigPickerState:
|
|
75
|
+
"""ConfigPickerUI 的全部业务状态——可独立单元测试。"""
|
|
76
|
+
|
|
77
|
+
origin: ConfigSnapshot
|
|
78
|
+
draft: ConfigSnapshot = field(init=False)
|
|
79
|
+
focused_field: ConfigField = field(init=False, default=ConfigField.DISPLAY_THINKING)
|
|
80
|
+
view_mode: ViewMode = field(init=False, default=ViewMode.MAIN)
|
|
81
|
+
subview_field: ConfigField | None = field(init=False, default=None)
|
|
82
|
+
subview_cursor: int = field(init=False, default=0)
|
|
83
|
+
dirty_warning_active: bool = field(init=False, default=False)
|
|
84
|
+
save_called: bool = field(init=False, default=False)
|
|
85
|
+
last_save_error_messages: tuple[str, ...] = field(init=False, default=())
|
|
86
|
+
|
|
87
|
+
save_callback: Callable[[ConfigSnapshot], object] | None = None
|
|
88
|
+
|
|
89
|
+
def __post_init__(self) -> None:
|
|
90
|
+
self.draft = self.origin
|
|
91
|
+
|
|
92
|
+
# ---- queries --------------------------------------------------------
|
|
93
|
+
def is_dirty(self) -> bool:
|
|
94
|
+
return self.draft != self.origin
|
|
95
|
+
|
|
96
|
+
def subview_options(self) -> list[SubviewOption]:
|
|
97
|
+
if self.view_mode != ViewMode.SUBVIEW or self.subview_field is None:
|
|
98
|
+
return []
|
|
99
|
+
if self.subview_field == ConfigField.AUTO_COMPACT_THRESHOLD:
|
|
100
|
+
return [
|
|
101
|
+
SubviewOption(v, v.capitalize(), _THRESHOLD_DESCRIPTIONS[v])
|
|
102
|
+
for v in _THRESHOLD_VALUES
|
|
103
|
+
]
|
|
104
|
+
if self.subview_field == ConfigField.MODEL_LEVEL:
|
|
105
|
+
return [
|
|
106
|
+
SubviewOption(v, v, _MODEL_LEVEL_DESCRIPTIONS[v])
|
|
107
|
+
for v in _MODEL_LEVEL_VALUES
|
|
108
|
+
]
|
|
109
|
+
return []
|
|
110
|
+
|
|
111
|
+
# ---- key handlers ---------------------------------------------------
|
|
112
|
+
def handle_up(self) -> None:
|
|
113
|
+
if self.view_mode == ViewMode.MAIN:
|
|
114
|
+
idx = _FIELD_ORDER.index(self.focused_field)
|
|
115
|
+
self.focused_field = _FIELD_ORDER[(idx - 1) % len(_FIELD_ORDER)]
|
|
116
|
+
else:
|
|
117
|
+
self.subview_cursor = (self.subview_cursor - 1) % len(self.subview_options())
|
|
118
|
+
self.dirty_warning_active = False
|
|
119
|
+
|
|
120
|
+
def handle_down(self) -> None:
|
|
121
|
+
if self.view_mode == ViewMode.MAIN:
|
|
122
|
+
idx = _FIELD_ORDER.index(self.focused_field)
|
|
123
|
+
self.focused_field = _FIELD_ORDER[(idx + 1) % len(_FIELD_ORDER)]
|
|
124
|
+
else:
|
|
125
|
+
self.subview_cursor = (self.subview_cursor + 1) % len(self.subview_options())
|
|
126
|
+
self.dirty_warning_active = False
|
|
127
|
+
|
|
128
|
+
def handle_tab(self) -> None:
|
|
129
|
+
if self.view_mode != ViewMode.MAIN:
|
|
130
|
+
return
|
|
131
|
+
self.dirty_warning_active = False
|
|
132
|
+
f = self.focused_field
|
|
133
|
+
if f == ConfigField.DISPLAY_THINKING:
|
|
134
|
+
self.draft = self.draft.with_display_thinking(not self.draft.display_thinking)
|
|
135
|
+
elif f == ConfigField.MEMORY_BACKGROUND:
|
|
136
|
+
self.draft = self.draft.with_memory_background(not self.draft.memory_background_enabled)
|
|
137
|
+
elif f == ConfigField.MEMORY_DREAMING:
|
|
138
|
+
self.draft = self.draft.with_memory_dreaming(not self.draft.memory_dreaming_enabled)
|
|
139
|
+
elif f == ConfigField.AUTO_COMPACT_THRESHOLD:
|
|
140
|
+
cur = _THRESHOLD_VALUES.index(self.draft.compaction_threshold_preset)
|
|
141
|
+
next_v = _THRESHOLD_VALUES[(cur + 1) % len(_THRESHOLD_VALUES)]
|
|
142
|
+
self.draft = self.draft.with_threshold_preset(next_v)
|
|
143
|
+
elif f == ConfigField.MODEL_LEVEL:
|
|
144
|
+
cur = _MODEL_LEVEL_VALUES.index(self.draft.model_level)
|
|
145
|
+
next_v = _MODEL_LEVEL_VALUES[(cur + 1) % len(_MODEL_LEVEL_VALUES)]
|
|
146
|
+
self.draft = self.draft.with_model_level(next_v)
|
|
147
|
+
|
|
148
|
+
def handle_enter(self) -> PickerExit | None:
|
|
149
|
+
if self.view_mode == ViewMode.MAIN:
|
|
150
|
+
f = self.focused_field
|
|
151
|
+
if f in (ConfigField.AUTO_COMPACT_THRESHOLD, ConfigField.MODEL_LEVEL):
|
|
152
|
+
self.view_mode = ViewMode.SUBVIEW
|
|
153
|
+
self.subview_field = f
|
|
154
|
+
self.subview_cursor = self._initial_subview_cursor(f)
|
|
155
|
+
else:
|
|
156
|
+
# bool 行:Enter 等价 Tab
|
|
157
|
+
self.handle_tab()
|
|
158
|
+
else:
|
|
159
|
+
# SUBVIEW: commit selection to draft, return to MAIN.
|
|
160
|
+
opts = self.subview_options()
|
|
161
|
+
if not opts:
|
|
162
|
+
self._return_to_main()
|
|
163
|
+
return None
|
|
164
|
+
chosen = opts[self.subview_cursor].value
|
|
165
|
+
if self.subview_field == ConfigField.AUTO_COMPACT_THRESHOLD:
|
|
166
|
+
self.draft = self.draft.with_threshold_preset(chosen) # type: ignore[arg-type]
|
|
167
|
+
elif self.subview_field == ConfigField.MODEL_LEVEL:
|
|
168
|
+
self.draft = self.draft.with_model_level(chosen) # type: ignore[arg-type]
|
|
169
|
+
self._return_to_main()
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
def handle_escape(self) -> PickerExit | None:
|
|
173
|
+
if self.view_mode == ViewMode.SUBVIEW:
|
|
174
|
+
self._return_to_main()
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
# MAIN
|
|
178
|
+
if not self.is_dirty():
|
|
179
|
+
return PickerExit(PickerExitKind.CLEAN_EXIT)
|
|
180
|
+
if not self.dirty_warning_active:
|
|
181
|
+
self.dirty_warning_active = True
|
|
182
|
+
return None
|
|
183
|
+
return PickerExit(PickerExitKind.DISCARDED)
|
|
184
|
+
|
|
185
|
+
def handle_ctrl_s(self) -> PickerExit | None:
|
|
186
|
+
if self.view_mode == ViewMode.SUBVIEW:
|
|
187
|
+
return None
|
|
188
|
+
if self.save_callback is None:
|
|
189
|
+
raise RuntimeError("save_callback 未注入;ConfigPickerUI 接入时必须设置")
|
|
190
|
+
result = self.save_callback(self.draft)
|
|
191
|
+
self.dirty_warning_active = False
|
|
192
|
+
if getattr(result, "ok", False):
|
|
193
|
+
self.save_called = True
|
|
194
|
+
self.last_save_error_messages = ()
|
|
195
|
+
return PickerExit(PickerExitKind.SAVED)
|
|
196
|
+
self.last_save_error_messages = tuple(getattr(result, "error_messages", ()) or ())
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
# ---- helpers --------------------------------------------------------
|
|
200
|
+
def _initial_subview_cursor(self, f: ConfigField) -> int:
|
|
201
|
+
if f == ConfigField.AUTO_COMPACT_THRESHOLD:
|
|
202
|
+
return _THRESHOLD_VALUES.index(self.draft.compaction_threshold_preset)
|
|
203
|
+
if f == ConfigField.MODEL_LEVEL:
|
|
204
|
+
return _MODEL_LEVEL_VALUES.index(self.draft.model_level)
|
|
205
|
+
return 0
|
|
206
|
+
|
|
207
|
+
def _return_to_main(self) -> None:
|
|
208
|
+
self.view_mode = ViewMode.MAIN
|
|
209
|
+
self.subview_field = None
|
|
210
|
+
self.subview_cursor = 0
|