comate-cli 0.7.0a5__tar.gz → 0.7.0a6__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.0a6/CHANGELOG.md +46 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/PKG-INFO +26 -1
- comate_cli-0.7.0a6/README.md +41 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/main.py +96 -2
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/app.py +7 -5
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/tui.py +6 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/tui_parts/input_behavior.py +41 -4
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/tui_parts/key_bindings.py +3 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/pyproject.toml +1 -1
- comate_cli-0.7.0a6/tests/fixtures/fake_mcp_misbehaving_stdout.py +75 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_app_shutdown.py +66 -1
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_compact_command_semantics.py +4 -4
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_preflight.py +3 -2
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_rewind_command_semantics.py +4 -1
- comate_cli-0.7.0a6/tests/test_shutdown_noise_guard.py +185 -0
- comate_cli-0.7.0a6/tests/test_shutdown_noise_integration.py +116 -0
- comate_cli-0.7.0a6/tests/test_tui_paste_newline_guard.py +198 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/uv.lock +2 -2
- comate_cli-0.7.0a5/CHANGELOG.md +0 -22
- comate_cli-0.7.0a5/README.md +0 -16
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/.gitignore +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/bash-exit-code-green-dot-bug.md +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/__init__.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/__main__.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/mcp_cli.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/__init__.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/animations.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/assistant_render.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/codenames.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/custom_slash_commands.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/env_utils.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/error_display.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/event_renderer.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/figures.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/fragment_utils.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/history_printer.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/input_geometry.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/layout_coordinator.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/logging_adapter.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/logo.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/markdown_render.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/mention_completer.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/message_style.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/models.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/path_context_hint.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/plugins/__init__.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/plugins/components/__init__.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/plugins/components/detail_view.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/plugins/components/plugin_list.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/plugins/components/search_box.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/plugins/components/tab_bar.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/plugins/marketplace_install_view.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/plugins/plugin_picker.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/plugins/tabs/__init__.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/plugins/tabs/discover_tab.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/plugins/tabs/errors_tab.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/plugins/tabs/installed_tab.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/plugins/tabs/marketplaces_tab.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/preflight.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/question_view.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/resume_picker.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/resume_preview.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/resume_selector.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/rpc_protocol.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/rpc_stdio.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/selection_menu.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/slash_commands.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/startup.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/status_bar.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/text_effects.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/tips.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/tool_result_formatters.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/tool_result_store.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/tool_result_viewer.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/tool_view.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/transcript_viewer.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/tui_parts/__init__.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/tui_parts/btw_view.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/tui_parts/commands.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/tui_parts/history_sync.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/tui_parts/mcp_connecting_view.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/tui_parts/render_panels.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/tui_parts/slash_command_registry.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/tui_parts/ui_mode.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/docs/hooks.md +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/docs/superpowers/plans/2026-04-03-phrase-shuffle.md +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/docs/superpowers/specs/2026-04-01-conditional-diff-subtitle-design.md +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/docs/superpowers/specs/2026-04-03-phrase-shuffle-design.md +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/conftest.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_animator_shuffle.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_app_mcp_preload.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_app_preflight_gate.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_app_print_mode.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_app_usage_line.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_btw_slash_command.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_cli_project_root.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_completion_context_activation.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_completion_status_panel.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_context_command.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_custom_slash_commands.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_discover_tab.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_errors_tab.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_event_renderer.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_event_renderer_boundary.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_event_renderer_e2e.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_event_renderer_log_boundary.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_event_renderer_log_queue.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_event_renderer_streaming.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_format_error.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_handle_error.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_history_printer.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_history_printer_log.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_history_sync.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_input_behavior.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_input_history.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_installed_tab.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_interrupt_exit_semantics.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_layout_coordinator.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_logging_adapter.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_logo.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_main_args.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_markdown_render.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_marketplaces_tab.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_mcp_cli.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_mcp_slash_command.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_mention_completer.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_path_context_hint.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_plugin_slash_commands.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_plugin_tui_components.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_preflight_copilot.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_question_key_bindings.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_question_view.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_resume_picker.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_resume_preview.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_resume_selector.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_rpc_protocol.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_rpc_stdio_bridge.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_selection_menu.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_session_query_token_summary.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_skills_slash_command.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_slash_argument_hint.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_slash_completer.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_slash_registry.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_status_bar.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_status_bar_transient.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_task_panel_format.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_task_panel_key_bindings.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_task_panel_rendering.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_task_poll.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_tool_result_formatters.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_tool_result_store.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_tool_result_viewer.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_tool_result_viewer_key_bindings.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_tool_view.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_transcript_viewer.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_tui_elapsed_status.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_tui_esc_queue.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_tui_mcp_init_gate.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_tui_paste_placeholder.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_tui_queue_preview.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_tui_queue_sdk_source.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_tui_split_invariance.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_tui_team_messages.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_tui_tool_result_registry_lifecycle.py +0 -0
- {comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/tests/test_update_check.py +0 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Windows 上 Ctrl+C 退出时 `ERROR:mcp.client.stdio:Failed to parse JSONRPC message from server`
|
|
8
|
+
和 traceback 漏到终端的问题。修复包含两部分:`app.py` finally 顺序调整,
|
|
9
|
+
以及 SDK stdio MCP 关闭路径切换到主动信号升级链。
|
|
10
|
+
- Windows 上 Ctrl+C 退出时 `Exception ignored in: <BaseSubprocessTransport.__del__>`
|
|
11
|
+
和 `ValueError: I/O operation on closed pipe` 漏到终端的问题。现在由
|
|
12
|
+
`_ShutdownNoiseGuard` catch-all 接管 `sys.unraisablehook` 并写入 agent log。
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- `_ShutdownNoiseGuard` 扩展为四合一接管器:SIGINT、`sys.unraisablehook`、
|
|
17
|
+
`sys.excepthook`、`warnings.showwarning`。捕获到的噪音统一路由到独立的
|
|
18
|
+
`comate.noise` logger(`propagate=False`),写入 `~/.comate/logs/agent.log`。
|
|
19
|
+
- 新增 `COMATE_DEBUG_NOISE=1` 环境变量逃生口,开发排障时可关闭噪音接管器,
|
|
20
|
+
查看旧版本的原始 stderr 输出。
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- TUI 分支退出时先执行 `_graceful_shutdown(...)`,再关闭 `logging_session`,
|
|
25
|
+
让现有 logging 屏蔽机制覆盖 MCP teardown 全程。
|
|
26
|
+
|
|
27
|
+
## 0.7.0a1 - 2026-05-19 (alpha)
|
|
28
|
+
|
|
29
|
+
> 这是一个 **alpha 预发布版本**,用于内部 / 早期测试。
|
|
30
|
+
> `pip install comate-cli` 默认不会安装到本版本,需要显式 `pip install comate-cli==0.7.0a1` 或 `pip install --pre comate-cli`。
|
|
31
|
+
|
|
32
|
+
### Breaking Changes
|
|
33
|
+
|
|
34
|
+
- 依赖 `comate-agent-sdk>=0.8.0a1`,与旧 SDK 不兼容。
|
|
35
|
+
- 旧版本生成的 resume jsonl / context 快照不保证可以正常回放,**建议从空 session 重新开始**。
|
|
36
|
+
|
|
37
|
+
### 累积变更
|
|
38
|
+
|
|
39
|
+
自 0.6.x 起累积大量 TUI 与会话能力改进,涵盖:
|
|
40
|
+
|
|
41
|
+
- Resume picker 重做:搜索、跨 cwd 切换、预览模式、视觉抛光
|
|
42
|
+
- Auto compact 期间显式提示 `Compacting context`
|
|
43
|
+
- Team / subagent 事件透出与渲染统一
|
|
44
|
+
- 与 SDK 0.8.0a2 配套的 tool envelope / token 账本展示
|
|
45
|
+
|
|
46
|
+
详细变更请参考 git log。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: comate-cli
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.0a6
|
|
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
|
|
@@ -41,3 +41,28 @@ Or install globally:
|
|
|
41
41
|
uv tool install comate-cli
|
|
42
42
|
comate
|
|
43
43
|
```
|
|
44
|
+
|
|
45
|
+
## 排错 / Troubleshooting
|
|
46
|
+
|
|
47
|
+
### 看不到错误信息但行为异常
|
|
48
|
+
|
|
49
|
+
comate 在生产模式下会把若干无害但吵闹的 shutdown 噪音写入
|
|
50
|
+
`~/.comate/logs/agent.log`,而不是显示在终端里。这包括 Windows asyncio
|
|
51
|
+
shutdown 的 `ResourceWarning`、MCP 服务器违规向 stdout 写日志导致的 JSONRPC
|
|
52
|
+
解析失败,以及解释器退出阶段的 unraisable 异常。
|
|
53
|
+
|
|
54
|
+
如果你怀疑隐藏了真实错误,可以按下面顺序排查:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# 1. 看 noise 通道的最近 50 条
|
|
58
|
+
grep -E "\[unraisable\]|\[excepthook\]|\[warning\]" ~/.comate/logs/agent.log | tail -50
|
|
59
|
+
|
|
60
|
+
# 2. 看完整 agent.log 末尾
|
|
61
|
+
tail -200 ~/.comate/logs/agent.log
|
|
62
|
+
|
|
63
|
+
# 3. 临时关掉噪音接管器,看原始输出(开发场景)
|
|
64
|
+
COMATE_DEBUG_NOISE=1 comate
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`COMATE_DEBUG_NOISE=1` 设置后,`_ShutdownNoiseGuard` 会跳过 install,
|
|
68
|
+
错误会像旧版本一样直接打印到 stderr。
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# comate-cli
|
|
2
|
+
|
|
3
|
+
Comate terminal CLI package.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
uvx comate-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or install globally:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
uv tool install comate-cli
|
|
15
|
+
comate
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## 排错 / Troubleshooting
|
|
19
|
+
|
|
20
|
+
### 看不到错误信息但行为异常
|
|
21
|
+
|
|
22
|
+
comate 在生产模式下会把若干无害但吵闹的 shutdown 噪音写入
|
|
23
|
+
`~/.comate/logs/agent.log`,而不是显示在终端里。这包括 Windows asyncio
|
|
24
|
+
shutdown 的 `ResourceWarning`、MCP 服务器违规向 stdout 写日志导致的 JSONRPC
|
|
25
|
+
解析失败,以及解释器退出阶段的 unraisable 异常。
|
|
26
|
+
|
|
27
|
+
如果你怀疑隐藏了真实错误,可以按下面顺序排查:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# 1. 看 noise 通道的最近 50 条
|
|
31
|
+
grep -E "\[unraisable\]|\[excepthook\]|\[warning\]" ~/.comate/logs/agent.log | tail -50
|
|
32
|
+
|
|
33
|
+
# 2. 看完整 agent.log 末尾
|
|
34
|
+
tail -200 ~/.comate/logs/agent.log
|
|
35
|
+
|
|
36
|
+
# 3. 临时关掉噪音接管器,看原始输出(开发场景)
|
|
37
|
+
COMATE_DEBUG_NOISE=1 comate
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`COMATE_DEBUG_NOISE=1` 设置后,`_ShutdownNoiseGuard` 会跳过 install,
|
|
41
|
+
错误会像旧版本一样直接打印到 stderr。
|
|
@@ -3,9 +3,11 @@ from __future__ import annotations
|
|
|
3
3
|
import atexit
|
|
4
4
|
import asyncio
|
|
5
5
|
import logging
|
|
6
|
+
import os
|
|
6
7
|
import signal
|
|
7
8
|
import subprocess
|
|
8
9
|
import sys
|
|
10
|
+
import warnings
|
|
9
11
|
try:
|
|
10
12
|
import termios
|
|
11
13
|
except ImportError:
|
|
@@ -68,10 +70,21 @@ class _ShutdownNoiseGuard:
|
|
|
68
70
|
|
|
69
71
|
def __init__(self) -> None:
|
|
70
72
|
self._shutdown_armed = False
|
|
73
|
+
self._installed = False
|
|
71
74
|
self._orig_unraisablehook = sys.unraisablehook
|
|
75
|
+
self._orig_excepthook = sys.excepthook
|
|
76
|
+
self._orig_showwarning = warnings.showwarning
|
|
77
|
+
self._noise_logger = self._build_noise_logger()
|
|
72
78
|
|
|
73
79
|
def install(self) -> None:
|
|
80
|
+
if self._installed:
|
|
81
|
+
return
|
|
82
|
+
if os.environ.get("COMATE_DEBUG_NOISE", "").strip().lower() in ("1", "true", "yes"):
|
|
83
|
+
return
|
|
74
84
|
sys.unraisablehook = self._unraisablehook
|
|
85
|
+
sys.excepthook = self._excepthook
|
|
86
|
+
warnings.showwarning = self._showwarning
|
|
87
|
+
self._installed = True
|
|
75
88
|
|
|
76
89
|
def begin_shutdown(self) -> None:
|
|
77
90
|
if self._shutdown_armed:
|
|
@@ -88,13 +101,94 @@ class _ShutdownNoiseGuard:
|
|
|
88
101
|
except Exception as exc:
|
|
89
102
|
logger.debug(f"Failed to switch SIGINT to SIG_IGN during shutdown: {exc}")
|
|
90
103
|
|
|
104
|
+
def _route(
|
|
105
|
+
self,
|
|
106
|
+
channel: str,
|
|
107
|
+
*,
|
|
108
|
+
exc: BaseException | None = None,
|
|
109
|
+
msg: str = "",
|
|
110
|
+
**extra: object,
|
|
111
|
+
) -> None:
|
|
112
|
+
try:
|
|
113
|
+
if exc is not None:
|
|
114
|
+
self._noise_logger.warning(
|
|
115
|
+
"[%s] %s: %s",
|
|
116
|
+
channel,
|
|
117
|
+
type(exc).__name__,
|
|
118
|
+
exc,
|
|
119
|
+
exc_info=(type(exc), exc, exc.__traceback__),
|
|
120
|
+
extra={"noise_channel": channel, **extra},
|
|
121
|
+
)
|
|
122
|
+
else:
|
|
123
|
+
self._noise_logger.warning(
|
|
124
|
+
"[%s] %s",
|
|
125
|
+
channel,
|
|
126
|
+
msg,
|
|
127
|
+
extra={"noise_channel": channel, **extra},
|
|
128
|
+
)
|
|
129
|
+
except Exception:
|
|
130
|
+
pass
|
|
131
|
+
|
|
91
132
|
def _unraisablehook(self, unraisable: object) -> None:
|
|
92
133
|
exc_value = getattr(unraisable, "exc_value", None)
|
|
93
134
|
if self._shutdown_armed and isinstance(exc_value, KeyboardInterrupt):
|
|
94
|
-
logger.debug("Suppressed unraisable KeyboardInterrupt during shutdown")
|
|
95
135
|
return
|
|
136
|
+
obj = getattr(unraisable, "object", None)
|
|
137
|
+
self._route("unraisable", exc=exc_value, object_repr=repr(obj))
|
|
138
|
+
|
|
139
|
+
def _excepthook(
|
|
140
|
+
self,
|
|
141
|
+
exc_type: type[BaseException],
|
|
142
|
+
exc_value: BaseException,
|
|
143
|
+
exc_tb: object,
|
|
144
|
+
) -> None:
|
|
145
|
+
self._route("excepthook", exc=exc_value)
|
|
146
|
+
|
|
147
|
+
def _showwarning(
|
|
148
|
+
self,
|
|
149
|
+
message: Warning | str,
|
|
150
|
+
category: type[Warning],
|
|
151
|
+
filename: str,
|
|
152
|
+
lineno: int,
|
|
153
|
+
file: object | None = None,
|
|
154
|
+
line: str | None = None,
|
|
155
|
+
) -> None:
|
|
156
|
+
self._route(
|
|
157
|
+
"warning",
|
|
158
|
+
msg=str(message),
|
|
159
|
+
category=category.__name__,
|
|
160
|
+
warning_filename=str(filename),
|
|
161
|
+
warning_lineno=lineno,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
def _build_noise_logger(self) -> logging.Logger:
|
|
165
|
+
log = logging.getLogger("comate.noise")
|
|
166
|
+
log.setLevel(logging.WARNING)
|
|
167
|
+
log.propagate = False
|
|
168
|
+
if log.handlers:
|
|
169
|
+
return log
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
from concurrent_log_handler import ConcurrentRotatingFileHandler
|
|
173
|
+
|
|
174
|
+
log_dir = os.path.join(os.path.expanduser("~"), ".comate", "logs")
|
|
175
|
+
os.makedirs(log_dir, exist_ok=True)
|
|
176
|
+
handler = ConcurrentRotatingFileHandler(
|
|
177
|
+
os.path.join(log_dir, "agent.log"),
|
|
178
|
+
maxBytes=10 * 1024 * 1024,
|
|
179
|
+
backupCount=3,
|
|
180
|
+
encoding="utf-8",
|
|
181
|
+
)
|
|
182
|
+
handler.setFormatter(
|
|
183
|
+
logging.Formatter(
|
|
184
|
+
"%(asctime)s %(levelname)s %(name)s [%(noise_channel)s]: %(message)s"
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
log.addHandler(handler)
|
|
188
|
+
except Exception:
|
|
189
|
+
log.addHandler(logging.NullHandler())
|
|
96
190
|
|
|
97
|
-
|
|
191
|
+
return log
|
|
98
192
|
|
|
99
193
|
|
|
100
194
|
def _usage_text() -> str:
|
|
@@ -415,11 +415,13 @@ async def run(
|
|
|
415
415
|
exc_info=True,
|
|
416
416
|
)
|
|
417
417
|
finally:
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
418
|
+
try:
|
|
419
|
+
if active_session is session:
|
|
420
|
+
await _graceful_shutdown(active_session)
|
|
421
|
+
else:
|
|
422
|
+
await _graceful_shutdown(session, active_session)
|
|
423
|
+
finally:
|
|
424
|
+
logging_session.close()
|
|
423
425
|
|
|
424
426
|
if usage_line:
|
|
425
427
|
console.print(f"[dim]{usage_line}[/]")
|
|
@@ -274,6 +274,12 @@ class TerminalAgentTUI(
|
|
|
274
274
|
"AGENT_SDK_TUI_PASTE_PLACEHOLDER_THRESHOLD_CHARS",
|
|
275
275
|
500,
|
|
276
276
|
)
|
|
277
|
+
paste_guard_window_ms = read_env_int(
|
|
278
|
+
"AGENT_SDK_TUI_PASTE_GUARD_WINDOW_MS",
|
|
279
|
+
120,
|
|
280
|
+
)
|
|
281
|
+
self._paste_guard_window_seconds = paste_guard_window_ms / 1000.0
|
|
282
|
+
self._paste_guard_active_until = 0.0
|
|
277
283
|
self._paste_placeholder_text: str | None = None
|
|
278
284
|
self._active_paste_token: str | None = None
|
|
279
285
|
self._paste_payload_by_token: dict[str, str] = {}
|
{comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/tui_parts/input_behavior.py
RENAMED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import time
|
|
3
4
|
from bisect import bisect_right
|
|
4
5
|
from typing import Any
|
|
5
6
|
|
|
@@ -212,6 +213,35 @@ class InputBehaviorMixin:
|
|
|
212
213
|
self._active_paste_token = None
|
|
213
214
|
self._paste_placeholder_text = None
|
|
214
215
|
self._paste_payload_by_token.clear()
|
|
216
|
+
self._paste_guard_active_until = 0.0
|
|
217
|
+
|
|
218
|
+
def _paste_guard_window(self) -> float:
|
|
219
|
+
return max(0.05, float(getattr(self, "_paste_guard_window_seconds", 0.12)))
|
|
220
|
+
|
|
221
|
+
def _mark_paste_like_input(self, _text: str = "") -> None:
|
|
222
|
+
self._paste_guard_active_until = time.monotonic() + self._paste_guard_window()
|
|
223
|
+
|
|
224
|
+
def _extend_paste_guard(self) -> None:
|
|
225
|
+
self._mark_paste_like_input("")
|
|
226
|
+
|
|
227
|
+
def _is_paste_guard_active(self) -> bool:
|
|
228
|
+
return time.monotonic() < float(
|
|
229
|
+
getattr(self, "_paste_guard_active_until", 0.0)
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
def _should_treat_enter_as_pasted_newline(self, buffer: Any) -> bool:
|
|
233
|
+
if not self._is_paste_guard_active():
|
|
234
|
+
return False
|
|
235
|
+
if getattr(buffer, "complete_state", None) is not None:
|
|
236
|
+
return False
|
|
237
|
+
return True
|
|
238
|
+
|
|
239
|
+
def _handle_normal_enter_key(self, buffer: Any) -> bool:
|
|
240
|
+
if not self._should_treat_enter_as_pasted_newline(buffer):
|
|
241
|
+
return False
|
|
242
|
+
buffer.insert_text("\n")
|
|
243
|
+
self._extend_paste_guard()
|
|
244
|
+
return True
|
|
215
245
|
|
|
216
246
|
@staticmethod
|
|
217
247
|
def _find_inserted_segment(
|
|
@@ -258,14 +288,23 @@ class InputBehaviorMixin:
|
|
|
258
288
|
def _handle_large_paste(self, buffer: Any) -> bool:
|
|
259
289
|
if self._suppress_input_change_hook:
|
|
260
290
|
return False
|
|
261
|
-
if self._busy:
|
|
262
|
-
return False
|
|
263
291
|
|
|
264
292
|
text = str(buffer.text)
|
|
265
293
|
previous_text = self._last_input_text
|
|
266
294
|
self._last_input_len = len(text)
|
|
267
295
|
self._last_input_text = text
|
|
268
296
|
|
|
297
|
+
threshold = max(1, int(self._paste_threshold_chars))
|
|
298
|
+
segment = self._find_inserted_segment(previous_text, text)
|
|
299
|
+
inserted_text = segment[2] if segment is not None else ""
|
|
300
|
+
if inserted_text and ("\n" in inserted_text or "\r" in inserted_text):
|
|
301
|
+
self._mark_paste_like_input(inserted_text)
|
|
302
|
+
if len(inserted_text) >= threshold:
|
|
303
|
+
self._mark_paste_like_input(inserted_text)
|
|
304
|
+
|
|
305
|
+
if self._busy:
|
|
306
|
+
return False
|
|
307
|
+
|
|
269
308
|
placeholder = self._paste_placeholder_text
|
|
270
309
|
if self._active_paste_token is not None and placeholder and placeholder in text:
|
|
271
310
|
return False
|
|
@@ -276,8 +315,6 @@ class InputBehaviorMixin:
|
|
|
276
315
|
):
|
|
277
316
|
self._clear_paste_state()
|
|
278
317
|
|
|
279
|
-
threshold = max(1, int(self._paste_threshold_chars))
|
|
280
|
-
segment = self._find_inserted_segment(previous_text, text)
|
|
281
318
|
if segment is None:
|
|
282
319
|
return False
|
|
283
320
|
start, end, inserted_text = segment
|
{comate_cli-0.7.0a5 → comate_cli-0.7.0a6}/comate_cli/terminal_agent/tui_parts/key_bindings.py
RENAMED
|
@@ -411,6 +411,9 @@ class KeyBindingsMixin:
|
|
|
411
411
|
@bindings.add("enter", filter=normal_mode, eager=True)
|
|
412
412
|
def _enter(event) -> None:
|
|
413
413
|
buffer = event.current_buffer
|
|
414
|
+
if self._handle_normal_enter_key(buffer):
|
|
415
|
+
return
|
|
416
|
+
|
|
414
417
|
cs = buffer.complete_state
|
|
415
418
|
|
|
416
419
|
# 菜单打开时:先接受补全
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""故意违反 MCP stdio 规范的假服务器。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import signal
|
|
6
|
+
import sys
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
_running = True
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _read_loop() -> None:
|
|
15
|
+
"""从 stdin 读 JSONRPC 请求,对 initialize 回一个最小响应。"""
|
|
16
|
+
for line in sys.stdin:
|
|
17
|
+
line = line.strip()
|
|
18
|
+
if not line:
|
|
19
|
+
continue
|
|
20
|
+
try:
|
|
21
|
+
req = json.loads(line)
|
|
22
|
+
except Exception:
|
|
23
|
+
continue
|
|
24
|
+
if req.get("method") == "initialize":
|
|
25
|
+
resp = {
|
|
26
|
+
"jsonrpc": "2.0",
|
|
27
|
+
"id": req.get("id"),
|
|
28
|
+
"result": {
|
|
29
|
+
"protocolVersion": "2024-11-05",
|
|
30
|
+
"capabilities": {},
|
|
31
|
+
"serverInfo": {
|
|
32
|
+
"name": "fake-misbehaving",
|
|
33
|
+
"version": "0.0.1",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
sys.stdout.write(json.dumps(resp) + "\n")
|
|
38
|
+
sys.stdout.flush()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _noise_loop() -> None:
|
|
42
|
+
"""每 50ms 向 stdout 写一行彩色日志。"""
|
|
43
|
+
counter = 0
|
|
44
|
+
while _running:
|
|
45
|
+
sys.stdout.write(
|
|
46
|
+
f"\x1b[90m2026-05-19T02:29:{counter:02d}Z "
|
|
47
|
+
"[INFO] internal status tick\x1b[0m\n"
|
|
48
|
+
)
|
|
49
|
+
sys.stdout.flush()
|
|
50
|
+
counter = (counter + 1) % 60
|
|
51
|
+
time.sleep(0.05)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _on_signal(signum, frame) -> None:
|
|
55
|
+
global _running
|
|
56
|
+
_running = False
|
|
57
|
+
try:
|
|
58
|
+
sys.stdout.write(
|
|
59
|
+
"\x1b[90m2026-05-19T02:29:99Z "
|
|
60
|
+
"[INFO] shutting down gracefully\x1b[0m\n"
|
|
61
|
+
)
|
|
62
|
+
sys.stdout.flush()
|
|
63
|
+
except Exception:
|
|
64
|
+
pass
|
|
65
|
+
raise SystemExit(0)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
signal.signal(signal.SIGINT, _on_signal)
|
|
70
|
+
if sys.platform == "win32":
|
|
71
|
+
signal.signal(signal.SIGTERM, _on_signal)
|
|
72
|
+
threading.Thread(target=_read_loop, daemon=True).start()
|
|
73
|
+
threading.Thread(target=_noise_loop, daemon=True).start()
|
|
74
|
+
while True:
|
|
75
|
+
time.sleep(0.5)
|
|
@@ -2,7 +2,8 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import unittest
|
|
4
4
|
from contextlib import contextmanager
|
|
5
|
-
from
|
|
5
|
+
from types import SimpleNamespace
|
|
6
|
+
from unittest.mock import AsyncMock, MagicMock, call, patch
|
|
6
7
|
|
|
7
8
|
from comate_cli.terminal_agent import app as app_module
|
|
8
9
|
|
|
@@ -20,6 +21,70 @@ class _FakeSession:
|
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
class TestAppShutdown(unittest.IsolatedAsyncioTestCase):
|
|
24
|
+
async def test_run_closes_logging_after_graceful_shutdown(self) -> None:
|
|
25
|
+
call_log: list[str] = []
|
|
26
|
+
|
|
27
|
+
fake_logging_session = MagicMock()
|
|
28
|
+
fake_logging_session.close.side_effect = lambda: call_log.append("logging_close")
|
|
29
|
+
|
|
30
|
+
fake_session = MagicMock()
|
|
31
|
+
fake_session.session_id = "session-1"
|
|
32
|
+
fake_session.runtime = SimpleNamespace(_mcp_manager=None)
|
|
33
|
+
fake_session.get_mode.return_value = "default"
|
|
34
|
+
fake_session.get_usage = AsyncMock(return_value=object())
|
|
35
|
+
|
|
36
|
+
class _FakeStatusBar:
|
|
37
|
+
def __init__(self, session) -> None:
|
|
38
|
+
self.session = session
|
|
39
|
+
|
|
40
|
+
def set_mode(self, mode) -> None:
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
async def refresh(self) -> None:
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
def show_transient(self, message: str, *, duration_s: float) -> None:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
class _FakeTUI:
|
|
50
|
+
initialized_session_id = None
|
|
51
|
+
|
|
52
|
+
def __init__(self, session, status_bar, renderer) -> None:
|
|
53
|
+
self.session = session
|
|
54
|
+
|
|
55
|
+
def add_resume_history(self, mode: str) -> None:
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
async def run(self, *, mcp_init) -> None:
|
|
59
|
+
call_log.append("tui_run")
|
|
60
|
+
|
|
61
|
+
async def fake_preflight(*args, **kwargs):
|
|
62
|
+
return SimpleNamespace(should_abort_launch=False)
|
|
63
|
+
|
|
64
|
+
async def fake_graceful(*sessions) -> None:
|
|
65
|
+
call_log.append("graceful_shutdown")
|
|
66
|
+
|
|
67
|
+
with (
|
|
68
|
+
patch.object(app_module, "_resolve_cli_project_root", return_value=SimpleNamespace()),
|
|
69
|
+
patch.object(app_module, "run_preflight_if_needed", side_effect=fake_preflight),
|
|
70
|
+
patch.object(app_module, "_build_agent", return_value=object()),
|
|
71
|
+
patch.object(app_module, "print_logo"),
|
|
72
|
+
patch.object(app_module, "EventRenderer", return_value=MagicMock()),
|
|
73
|
+
patch(
|
|
74
|
+
"comate_cli.terminal_agent.logging_adapter.setup_tui_logging",
|
|
75
|
+
return_value=fake_logging_session,
|
|
76
|
+
),
|
|
77
|
+
patch.object(app_module, "_resolve_session", return_value=(fake_session, "new")),
|
|
78
|
+
patch.object(app_module, "_check_update", return_value=None),
|
|
79
|
+
patch.object(app_module, "StatusBar", _FakeStatusBar),
|
|
80
|
+
patch.object(app_module, "TerminalAgentTUI", _FakeTUI),
|
|
81
|
+
patch.object(app_module, "_format_exit_usage_line", return_value=None),
|
|
82
|
+
patch.object(app_module, "_graceful_shutdown", side_effect=fake_graceful),
|
|
83
|
+
):
|
|
84
|
+
await app_module.run()
|
|
85
|
+
|
|
86
|
+
self.assertEqual(call_log, ["tui_run", "graceful_shutdown", "logging_close"])
|
|
87
|
+
|
|
23
88
|
async def test_graceful_shutdown_deduplicates_sessions_and_flushes(self) -> None:
|
|
24
89
|
events: list[str] = []
|
|
25
90
|
|
|
@@ -149,7 +149,7 @@ class TestCompactCommandSemantics(unittest.TestCase):
|
|
|
149
149
|
self.assertEqual(commands._status_bar.refresh_calls, 1)
|
|
150
150
|
self.assertTrue(
|
|
151
151
|
any(
|
|
152
|
-
kind == "subtitle" and "
|
|
152
|
+
kind == "subtitle" and "compact completed:" in msg
|
|
153
153
|
for kind, msg, _ in commands._renderer.events
|
|
154
154
|
)
|
|
155
155
|
)
|
|
@@ -167,7 +167,7 @@ class TestCompactCommandSemantics(unittest.TestCase):
|
|
|
167
167
|
asyncio.run(commands._execute_command("/compact"))
|
|
168
168
|
|
|
169
169
|
self.assertEqual(commands._renderer.events[-1][0], "subtitle")
|
|
170
|
-
self.assertEqual(commands._renderer.events[-1][1], "
|
|
170
|
+
self.assertEqual(commands._renderer.events[-1][1], "compact skipped: disabled")
|
|
171
171
|
self.assertEqual(commands._renderer.events[-1][2], "warning")
|
|
172
172
|
|
|
173
173
|
def test_compact_reports_no_changes_in_english(self) -> None:
|
|
@@ -184,7 +184,7 @@ class TestCompactCommandSemantics(unittest.TestCase):
|
|
|
184
184
|
self.assertEqual(commands._renderer.events[-1][0], "subtitle")
|
|
185
185
|
self.assertEqual(
|
|
186
186
|
commands._renderer.events[-1][1],
|
|
187
|
-
"
|
|
187
|
+
"compact made no changes: The context was left unchanged.",
|
|
188
188
|
)
|
|
189
189
|
self.assertEqual(commands._renderer.events[-1][2], "warning")
|
|
190
190
|
|
|
@@ -205,7 +205,7 @@ class TestCompactCommandSemantics(unittest.TestCase):
|
|
|
205
205
|
("user", "/compact", "info"),
|
|
206
206
|
(
|
|
207
207
|
"subtitle",
|
|
208
|
-
"
|
|
208
|
+
"compact made no changes: The context was left unchanged.",
|
|
209
209
|
"warning",
|
|
210
210
|
),
|
|
211
211
|
],
|
|
@@ -196,11 +196,11 @@ class TestPreflightMerge(unittest.TestCase):
|
|
|
196
196
|
self.assertIn("zai_global", preset_map)
|
|
197
197
|
self.assertEqual(
|
|
198
198
|
preset_map["zai_cn"].base_url,
|
|
199
|
-
"https://open.bigmodel.cn/api/
|
|
199
|
+
"https://open.bigmodel.cn/api/coding/paas/v4",
|
|
200
200
|
)
|
|
201
201
|
self.assertEqual(
|
|
202
202
|
preset_map["zai_global"].base_url,
|
|
203
|
-
"https://api.z.ai/api/
|
|
203
|
+
"https://api.z.ai/api/coding/paas/v4",
|
|
204
204
|
)
|
|
205
205
|
|
|
206
206
|
def test_provider_presets_are_brand_only(self) -> None:
|
|
@@ -216,6 +216,7 @@ class TestPreflightMerge(unittest.TestCase):
|
|
|
216
216
|
"OpenAI and compatible",
|
|
217
217
|
"Anthropic and compatible",
|
|
218
218
|
"GitHub Copilot Plan",
|
|
219
|
+
"Xiaomi MiMo Token Plan",
|
|
219
220
|
],
|
|
220
221
|
)
|
|
221
222
|
|
|
@@ -144,7 +144,10 @@ class TestRewindCommandSemantics(unittest.TestCase):
|
|
|
144
144
|
|
|
145
145
|
asyncio.run(commands._slash_rewind(""))
|
|
146
146
|
|
|
147
|
-
self.assertIn(
|
|
147
|
+
self.assertIn(
|
|
148
|
+
"No rewind-able text messages are available",
|
|
149
|
+
commands._renderer.messages[-1][0],
|
|
150
|
+
)
|
|
148
151
|
self.assertNotIn("active replay", commands._renderer.messages[-1][0])
|
|
149
152
|
self.assertNotIn("meta", commands._renderer.messages[-1][0])
|
|
150
153
|
|