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.
Files changed (190) hide show
  1. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/PKG-INFO +1 -1
  2. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/animations.py +29 -5
  3. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/app.py +10 -1
  4. comate_cli-0.7.0a10/comate_cli/terminal_agent/config/model.py +52 -0
  5. comate_cli-0.7.0a10/comate_cli/terminal_agent/config/picker.py +232 -0
  6. comate_cli-0.7.0a10/comate_cli/terminal_agent/config/picker_state.py +210 -0
  7. comate_cli-0.7.0a10/comate_cli/terminal_agent/config/store.py +173 -0
  8. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/error_display.py +22 -0
  9. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/event_renderer.py +28 -7
  10. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/history_printer.py +1 -2
  11. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/logging_adapter.py +59 -2
  12. comate_cli-0.7.0a10/comate_cli/terminal_agent/plugins/tabs/__init__.py +0 -0
  13. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/slash_commands.py +5 -0
  14. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/tool_result_formatters.py +125 -8
  15. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/tool_view.py +49 -2
  16. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/tui.py +12 -1
  17. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/tui_parts/commands.py +40 -0
  18. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/tui_parts/history_sync.py +2 -0
  19. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/tui_parts/input_behavior.py +3 -0
  20. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/tui_parts/key_bindings.py +42 -0
  21. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/tui_parts/render_panels.py +46 -11
  22. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/tui_parts/ui_mode.py +1 -0
  23. comate_cli-0.7.0a10/docs/superpowers/plans/2026-05-22-log-style-implementation-plan.md +444 -0
  24. comate_cli-0.7.0a10/docs/superpowers/specs/2026-05-22-comate-cli-log-styling-design.md +71 -0
  25. comate_cli-0.7.0a10/docs/superpowers/specs/2026-05-22-log-style-optimization-design.md +114 -0
  26. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/pyproject.toml +1 -1
  27. comate_cli-0.7.0a10/tests/config/__init__.py +0 -0
  28. comate_cli-0.7.0a10/tests/config/test_model.py +88 -0
  29. comate_cli-0.7.0a10/tests/config/test_picker_state.py +205 -0
  30. comate_cli-0.7.0a10/tests/config/test_picker_ui.py +42 -0
  31. comate_cli-0.7.0a10/tests/config/test_roundtrip.py +75 -0
  32. comate_cli-0.7.0a10/tests/config/test_store_load.py +112 -0
  33. comate_cli-0.7.0a10/tests/config/test_store_save.py +103 -0
  34. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_completion_status_panel.py +43 -0
  35. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_event_renderer.py +91 -0
  36. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_event_renderer_streaming.py +32 -1
  37. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_format_error.py +52 -0
  38. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_history_printer_tool_fold.py +8 -0
  39. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_history_sync.py +114 -0
  40. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_logging_adapter.py +235 -0
  41. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_task_panel_rendering.py +3 -3
  42. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_tool_result_formatters.py +71 -1
  43. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_tool_view.py +13 -0
  44. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_transcript_viewer.py +44 -0
  45. comate_cli-0.7.0a10/tests/test_tui_thinking_display.py +71 -0
  46. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/uv.lock +1441 -1457
  47. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/.gitignore +0 -0
  48. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/AGENTS.md +0 -0
  49. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/CHANGELOG.md +0 -0
  50. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/README.md +0 -0
  51. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/bash-exit-code-green-dot-bug.md +0 -0
  52. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/__init__.py +0 -0
  53. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/__main__.py +0 -0
  54. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/main.py +0 -0
  55. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/mcp_cli.py +0 -0
  56. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/__init__.py +0 -0
  57. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/assistant_render.py +0 -0
  58. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/codenames.py +0 -0
  59. {comate_cli-0.7.0a8/comate_cli/terminal_agent/plugins → comate_cli-0.7.0a10/comate_cli/terminal_agent/config}/__init__.py +0 -0
  60. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/custom_slash_commands.py +0 -0
  61. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/env_utils.py +0 -0
  62. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/figures.py +0 -0
  63. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/fragment_utils.py +0 -0
  64. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/input_geometry.py +0 -0
  65. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/layout_coordinator.py +0 -0
  66. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/logo.py +0 -0
  67. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/markdown_render.py +0 -0
  68. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/mention_completer.py +0 -0
  69. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/message_style.py +0 -0
  70. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/models.py +0 -0
  71. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/path_context_hint.py +0 -0
  72. {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
  73. {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
  74. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/plugins/components/detail_view.py +0 -0
  75. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/plugins/components/plugin_list.py +0 -0
  76. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/plugins/components/search_box.py +0 -0
  77. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/plugins/components/tab_bar.py +0 -0
  78. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/plugins/marketplace_install_view.py +0 -0
  79. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/plugins/plugin_picker.py +0 -0
  80. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/plugins/tabs/discover_tab.py +0 -0
  81. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/plugins/tabs/errors_tab.py +0 -0
  82. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/plugins/tabs/installed_tab.py +0 -0
  83. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/plugins/tabs/marketplaces_tab.py +0 -0
  84. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/preflight.py +0 -0
  85. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/question_view.py +0 -0
  86. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/resume_picker.py +0 -0
  87. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/resume_preview.py +0 -0
  88. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/resume_selector.py +0 -0
  89. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/rpc_protocol.py +0 -0
  90. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/rpc_stdio.py +0 -0
  91. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/selection_menu.py +0 -0
  92. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/startup.py +0 -0
  93. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/status_bar.py +0 -0
  94. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/text_effects.py +0 -0
  95. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/tips.py +0 -0
  96. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/tool_fold.py +0 -0
  97. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/tool_result_store.py +0 -0
  98. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/tool_result_viewer.py +0 -0
  99. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/transcript_viewer.py +0 -0
  100. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/tui_parts/__init__.py +0 -0
  101. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/tui_parts/btw_view.py +0 -0
  102. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/tui_parts/mcp_connecting_view.py +0 -0
  103. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/comate_cli/terminal_agent/tui_parts/slash_command_registry.py +0 -0
  104. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/docs/hooks.md +0 -0
  105. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/docs/superpowers/plans/2026-04-03-phrase-shuffle.md +0 -0
  106. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/docs/superpowers/specs/2026-04-01-conditional-diff-subtitle-design.md +0 -0
  107. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/docs/superpowers/specs/2026-04-03-phrase-shuffle-design.md +0 -0
  108. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/report.md +0 -0
  109. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/conftest.py +0 -0
  110. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/fixtures/fake_mcp_misbehaving_stdout.py +0 -0
  111. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_animator_shuffle.py +0 -0
  112. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_app_mcp_preload.py +0 -0
  113. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_app_preflight_gate.py +0 -0
  114. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_app_print_mode.py +0 -0
  115. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_app_shutdown.py +0 -0
  116. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_app_usage_line.py +0 -0
  117. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_btw_slash_command.py +0 -0
  118. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_cli_project_root.py +0 -0
  119. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_compact_command_semantics.py +0 -0
  120. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_completion_context_activation.py +0 -0
  121. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_context_command.py +0 -0
  122. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_custom_slash_commands.py +0 -0
  123. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_discover_tab.py +0 -0
  124. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_errors_tab.py +0 -0
  125. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_event_renderer_boundary.py +0 -0
  126. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_event_renderer_e2e.py +0 -0
  127. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_event_renderer_log_boundary.py +0 -0
  128. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_event_renderer_log_queue.py +0 -0
  129. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_event_renderer_tool_fold.py +0 -0
  130. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_handle_error.py +0 -0
  131. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_history_printer.py +0 -0
  132. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_history_printer_log.py +0 -0
  133. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_history_printer_subtitle_position.py +0 -0
  134. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_history_sync_tool_fold.py +0 -0
  135. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_input_behavior.py +0 -0
  136. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_input_history.py +0 -0
  137. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_installed_tab.py +0 -0
  138. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_interrupt_exit_semantics.py +0 -0
  139. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_layout_coordinator.py +0 -0
  140. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_logo.py +0 -0
  141. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_main_args.py +0 -0
  142. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_markdown_render.py +0 -0
  143. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_marketplaces_tab.py +0 -0
  144. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_mcp_cli.py +0 -0
  145. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_mcp_slash_command.py +0 -0
  146. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_mention_completer.py +0 -0
  147. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_path_context_hint.py +0 -0
  148. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_plugin_slash_commands.py +0 -0
  149. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_plugin_tui_components.py +0 -0
  150. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_preflight.py +0 -0
  151. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_preflight_copilot.py +0 -0
  152. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_question_key_bindings.py +0 -0
  153. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_question_view.py +0 -0
  154. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_resume_picker.py +0 -0
  155. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_resume_preview.py +0 -0
  156. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_resume_selector.py +0 -0
  157. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_rewind_command_semantics.py +0 -0
  158. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_rpc_protocol.py +0 -0
  159. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_rpc_stdio_bridge.py +0 -0
  160. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_selection_menu.py +0 -0
  161. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_session_query_token_summary.py +0 -0
  162. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_shutdown_noise_guard.py +0 -0
  163. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_shutdown_noise_integration.py +0 -0
  164. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_skills_slash_command.py +0 -0
  165. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_slash_argument_hint.py +0 -0
  166. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_slash_clear.py +0 -0
  167. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_slash_completer.py +0 -0
  168. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_slash_registry.py +0 -0
  169. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_status_bar.py +0 -0
  170. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_status_bar_transient.py +0 -0
  171. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_task_panel_format.py +0 -0
  172. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_task_panel_key_bindings.py +0 -0
  173. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_task_poll.py +0 -0
  174. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_tool_fold.py +0 -0
  175. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_tool_fold_panel.py +0 -0
  176. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_tool_result_store.py +0 -0
  177. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_tool_result_viewer.py +0 -0
  178. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_tool_result_viewer_key_bindings.py +0 -0
  179. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_transcript_viewer_tool_fold.py +0 -0
  180. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_tui_elapsed_status.py +0 -0
  181. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_tui_esc_queue.py +0 -0
  182. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_tui_mcp_init_gate.py +0 -0
  183. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_tui_paste_newline_guard.py +0 -0
  184. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_tui_paste_placeholder.py +0 -0
  185. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_tui_queue_preview.py +0 -0
  186. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_tui_queue_sdk_source.py +0 -0
  187. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_tui_split_invariance.py +0 -0
  188. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_tui_team_messages.py +0 -0
  189. {comate_cli-0.7.0a8 → comate_cli-0.7.0a10}/tests/test_tui_tool_result_registry_lifecycle.py +0 -0
  190. {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.0a8
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, TextEvent, ToolCallEvent, ToolResultEvent, UserQuestionEvent
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"{breathing_dot_glyph(now_monotonic)} ",
392
- style=f"bold {dot_color}",
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, # 启用流式 Task(实时显示嵌套工具调用)
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