comate-cli 0.7.0a1__tar.gz → 0.7.0a3__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 (160) hide show
  1. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/CHANGELOG.md +1 -1
  2. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/PKG-INFO +1 -1
  3. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/error_display.py +3 -7
  4. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/logging_adapter.py +0 -5
  5. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/mention_completer.py +72 -11
  6. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/resume_selector.py +3 -3
  7. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/tui.py +43 -0
  8. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/tui_parts/commands.py +36 -34
  9. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/pyproject.toml +1 -1
  10. comate_cli-0.7.0a3/tests/test_completion_context_activation.py +119 -0
  11. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_mention_completer.py +171 -0
  12. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/uv.lock +1 -1
  13. comate_cli-0.7.0a1/tests/test_completion_context_activation.py +0 -56
  14. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/.gitignore +0 -0
  15. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/README.md +0 -0
  16. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/bash-exit-code-green-dot-bug.md +0 -0
  17. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/__init__.py +0 -0
  18. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/__main__.py +0 -0
  19. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/main.py +0 -0
  20. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/mcp_cli.py +0 -0
  21. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/__init__.py +0 -0
  22. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/animations.py +0 -0
  23. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/app.py +0 -0
  24. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/assistant_render.py +0 -0
  25. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/codenames.py +0 -0
  26. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/custom_slash_commands.py +0 -0
  27. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/env_utils.py +0 -0
  28. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/event_renderer.py +0 -0
  29. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/figures.py +0 -0
  30. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/fragment_utils.py +0 -0
  31. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/history_printer.py +0 -0
  32. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/input_geometry.py +0 -0
  33. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/layout_coordinator.py +0 -0
  34. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/logo.py +0 -0
  35. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/markdown_render.py +0 -0
  36. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/message_style.py +0 -0
  37. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/models.py +0 -0
  38. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/path_context_hint.py +0 -0
  39. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/plugins/__init__.py +0 -0
  40. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/plugins/components/__init__.py +0 -0
  41. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/plugins/components/detail_view.py +0 -0
  42. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/plugins/components/plugin_list.py +0 -0
  43. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/plugins/components/search_box.py +0 -0
  44. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/plugins/components/tab_bar.py +0 -0
  45. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/plugins/marketplace_install_view.py +0 -0
  46. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/plugins/plugin_picker.py +0 -0
  47. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/plugins/tabs/__init__.py +0 -0
  48. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/plugins/tabs/discover_tab.py +0 -0
  49. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/plugins/tabs/errors_tab.py +0 -0
  50. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/plugins/tabs/installed_tab.py +0 -0
  51. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/plugins/tabs/marketplaces_tab.py +0 -0
  52. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/preflight.py +0 -0
  53. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/question_view.py +0 -0
  54. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/resume_picker.py +0 -0
  55. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/resume_preview.py +0 -0
  56. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/rpc_protocol.py +0 -0
  57. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/rpc_stdio.py +0 -0
  58. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/selection_menu.py +0 -0
  59. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/slash_commands.py +0 -0
  60. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/startup.py +0 -0
  61. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/status_bar.py +0 -0
  62. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/text_effects.py +0 -0
  63. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/tips.py +0 -0
  64. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/tool_result_formatters.py +0 -0
  65. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/tool_result_store.py +0 -0
  66. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/tool_result_viewer.py +0 -0
  67. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/tool_view.py +0 -0
  68. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/transcript_viewer.py +0 -0
  69. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/tui_parts/__init__.py +0 -0
  70. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/tui_parts/btw_view.py +0 -0
  71. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/tui_parts/history_sync.py +0 -0
  72. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/tui_parts/input_behavior.py +0 -0
  73. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/tui_parts/key_bindings.py +0 -0
  74. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/tui_parts/mcp_connecting_view.py +0 -0
  75. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/tui_parts/render_panels.py +0 -0
  76. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/tui_parts/slash_command_registry.py +0 -0
  77. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/comate_cli/terminal_agent/tui_parts/ui_mode.py +0 -0
  78. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/docs/hooks.md +0 -0
  79. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/docs/superpowers/plans/2026-04-03-phrase-shuffle.md +0 -0
  80. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/docs/superpowers/specs/2026-04-01-conditional-diff-subtitle-design.md +0 -0
  81. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/docs/superpowers/specs/2026-04-03-phrase-shuffle-design.md +0 -0
  82. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/conftest.py +0 -0
  83. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_animator_shuffle.py +0 -0
  84. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_app_mcp_preload.py +0 -0
  85. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_app_preflight_gate.py +0 -0
  86. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_app_print_mode.py +0 -0
  87. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_app_shutdown.py +0 -0
  88. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_app_usage_line.py +0 -0
  89. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_btw_slash_command.py +0 -0
  90. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_cli_project_root.py +0 -0
  91. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_compact_command_semantics.py +0 -0
  92. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_completion_status_panel.py +0 -0
  93. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_context_command.py +0 -0
  94. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_custom_slash_commands.py +0 -0
  95. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_discover_tab.py +0 -0
  96. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_errors_tab.py +0 -0
  97. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_event_renderer.py +0 -0
  98. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_event_renderer_boundary.py +0 -0
  99. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_event_renderer_e2e.py +0 -0
  100. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_event_renderer_log_boundary.py +0 -0
  101. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_event_renderer_log_queue.py +0 -0
  102. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_event_renderer_streaming.py +0 -0
  103. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_format_error.py +0 -0
  104. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_handle_error.py +0 -0
  105. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_history_printer.py +0 -0
  106. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_history_printer_log.py +0 -0
  107. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_history_sync.py +0 -0
  108. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_input_behavior.py +0 -0
  109. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_input_history.py +0 -0
  110. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_installed_tab.py +0 -0
  111. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_interrupt_exit_semantics.py +0 -0
  112. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_layout_coordinator.py +0 -0
  113. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_logging_adapter.py +0 -0
  114. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_logo.py +0 -0
  115. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_main_args.py +0 -0
  116. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_markdown_render.py +0 -0
  117. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_marketplaces_tab.py +0 -0
  118. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_mcp_cli.py +0 -0
  119. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_mcp_slash_command.py +0 -0
  120. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_path_context_hint.py +0 -0
  121. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_plugin_slash_commands.py +0 -0
  122. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_plugin_tui_components.py +0 -0
  123. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_preflight.py +0 -0
  124. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_preflight_copilot.py +0 -0
  125. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_question_key_bindings.py +0 -0
  126. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_question_view.py +0 -0
  127. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_resume_picker.py +0 -0
  128. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_resume_preview.py +0 -0
  129. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_resume_selector.py +0 -0
  130. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_rewind_command_semantics.py +0 -0
  131. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_rpc_protocol.py +0 -0
  132. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_rpc_stdio_bridge.py +0 -0
  133. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_selection_menu.py +0 -0
  134. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_session_query_token_summary.py +0 -0
  135. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_skills_slash_command.py +0 -0
  136. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_slash_argument_hint.py +0 -0
  137. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_slash_completer.py +0 -0
  138. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_slash_registry.py +0 -0
  139. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_status_bar.py +0 -0
  140. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_status_bar_transient.py +0 -0
  141. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_task_panel_format.py +0 -0
  142. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_task_panel_key_bindings.py +0 -0
  143. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_task_panel_rendering.py +0 -0
  144. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_task_poll.py +0 -0
  145. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_tool_result_formatters.py +0 -0
  146. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_tool_result_store.py +0 -0
  147. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_tool_result_viewer.py +0 -0
  148. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_tool_result_viewer_key_bindings.py +0 -0
  149. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_tool_view.py +0 -0
  150. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_transcript_viewer.py +0 -0
  151. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_tui_elapsed_status.py +0 -0
  152. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_tui_esc_queue.py +0 -0
  153. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_tui_mcp_init_gate.py +0 -0
  154. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_tui_paste_placeholder.py +0 -0
  155. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_tui_queue_preview.py +0 -0
  156. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_tui_queue_sdk_source.py +0 -0
  157. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_tui_split_invariance.py +0 -0
  158. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_tui_team_messages.py +0 -0
  159. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_tui_tool_result_registry_lifecycle.py +0 -0
  160. {comate_cli-0.7.0a1 → comate_cli-0.7.0a3}/tests/test_update_check.py +0 -0
@@ -17,6 +17,6 @@
17
17
  - Resume picker 重做:搜索、跨 cwd 切换、预览模式、视觉抛光
18
18
  - Auto compact 期间显式提示 `Compacting context`
19
19
  - Team / subagent 事件透出与渲染统一
20
- - 与 SDK 0.8.0a1 配套的 tool envelope / token 账本展示
20
+ - 与 SDK 0.8.0a2 配套的 tool envelope / token 账本展示
21
21
 
22
22
  详细变更请参考 git log。
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: comate-cli
3
- Version: 0.7.0a1
3
+ Version: 0.7.0a3
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
@@ -30,7 +30,7 @@ def format_error(exc: Exception) -> tuple[str, str, str]:
30
30
  "Context limit exceeded",
31
31
  "error",
32
32
  )
33
- return f"API error: {_truncate(exc_msg, 80)}", "API error", "error"
33
+ return f"API error: {exc_msg}", "API error", "error"
34
34
 
35
35
  if code == 401:
36
36
  return "Invalid or expired API key", "Auth failed", "error"
@@ -40,7 +40,7 @@ def format_error(exc: Exception) -> tuple[str, str, str]:
40
40
  return "Model not found or invalid API path", "Model not found", "error"
41
41
  if code and code >= 500:
42
42
  return f"Server error ({code})", "Server error", "warning"
43
- return f"API error: {_truncate(exc_msg, 80)}", "API error", "error"
43
+ return f"API error: {exc_msg}", "API error", "error"
44
44
 
45
45
  # Session errors
46
46
  if exc_type == "ChatSessionClosedError":
@@ -54,7 +54,7 @@ def format_error(exc: Exception) -> tuple[str, str, str]:
54
54
  return "Connection failed", "Connection failed", "error"
55
55
 
56
56
  # Generic fallback
57
- return f"Error: {_truncate(exc_msg, 80)}", "Error occurred", "error"
57
+ return f"Error: {exc_msg}", "Error occurred", "error"
58
58
 
59
59
 
60
60
  def _parse_context_size_error(msg: str) -> tuple[str, str] | None:
@@ -77,7 +77,3 @@ def _parse_context_size_error(msg: str) -> tuple[str, str] | None:
77
77
  return None # Type matches but can't parse numbers — fall through to generic 400
78
78
 
79
79
  return None
80
-
81
-
82
- def _truncate(s: str, max_len: int) -> str:
83
- return s[:max_len] + "..." if len(s) > max_len else s
@@ -113,11 +113,6 @@ class TUILoggingHandler(logging.Handler):
113
113
  parts = msg.split("timeout_ms=")
114
114
  msg = parts[0].rstrip(": ,")
115
115
 
116
- # 限制长度
117
- max_len = 100
118
- if len(msg) > max_len:
119
- msg = msg[:max_len] + "..."
120
-
121
116
  return msg
122
117
 
123
118
  def _get_message_key(self, record: logging.LogRecord) -> str:
@@ -3,8 +3,9 @@ from __future__ import annotations
3
3
  import os
4
4
  import re
5
5
  import subprocess
6
+ import threading
6
7
  import time
7
- from collections.abc import Iterable
8
+ from collections.abc import Callable, Iterable
8
9
  from dataclasses import dataclass
9
10
  from pathlib import Path, PurePosixPath
10
11
 
@@ -112,6 +113,9 @@ class LocalFileMentionCompleter(Completer):
112
113
 
113
114
  self._deep_cache_time: float = 0.0
114
115
  self._deep_cached_paths: list[str] = []
116
+ self._deep_cache_lock = threading.RLock()
117
+ self._deep_warmup_lock = threading.Lock()
118
+ self._deep_warmup_thread: threading.Thread | None = None
115
119
  self._directory_cached_paths: dict[str, tuple[float, list[str]]] = {}
116
120
 
117
121
  @classmethod
@@ -300,16 +304,68 @@ class LocalFileMentionCompleter(Completer):
300
304
  return self._top_cached_paths
301
305
 
302
306
  def _get_deep_paths(self) -> list[str]:
303
- now = time.monotonic()
304
- if now - self._deep_cache_time <= self._refresh_interval:
305
- return self._deep_cached_paths
307
+ with self._deep_cache_lock:
308
+ now = time.monotonic()
309
+ if now - self._deep_cache_time <= self._refresh_interval:
310
+ return self._deep_cached_paths
306
311
 
307
- file_paths = self._load_indexed_file_paths()
308
- paths = self._build_deep_paths(file_paths)
312
+ file_paths = self._load_indexed_file_paths()
313
+ paths = self._build_deep_paths(file_paths)
309
314
 
310
- self._deep_cached_paths = paths
311
- self._deep_cache_time = now
312
- return self._deep_cached_paths
315
+ self._deep_cached_paths = paths
316
+ self._deep_cache_time = time.monotonic()
317
+ return self._deep_cached_paths
318
+
319
+ def start_deep_cache_warmup(
320
+ self,
321
+ *,
322
+ on_complete: Callable[[], None] | None = None,
323
+ ) -> bool:
324
+ """Build the deep path cache in a daemon thread if it is stale."""
325
+ with self._deep_warmup_lock:
326
+ if (
327
+ self._deep_warmup_thread is not None
328
+ and self._deep_warmup_thread.is_alive()
329
+ ):
330
+ return False
331
+
332
+ with self._deep_cache_lock:
333
+ now = time.monotonic()
334
+ if now - self._deep_cache_time <= self._refresh_interval:
335
+ return False
336
+
337
+ def _warm() -> None:
338
+ try:
339
+ self._get_deep_paths()
340
+ except Exception:
341
+ logger.debug("deep mention cache warmup failed", exc_info=True)
342
+ finally:
343
+ with self._deep_warmup_lock:
344
+ if self._deep_warmup_thread is threading.current_thread():
345
+ self._deep_warmup_thread = None
346
+ if on_complete is not None:
347
+ try:
348
+ on_complete()
349
+ except Exception:
350
+ logger.debug(
351
+ "deep mention cache warmup callback failed",
352
+ exc_info=True,
353
+ )
354
+
355
+ thread = threading.Thread(
356
+ target=_warm,
357
+ name="comate-mention-index-warmup",
358
+ daemon=True,
359
+ )
360
+ with self._deep_warmup_lock:
361
+ if (
362
+ self._deep_warmup_thread is not None
363
+ and self._deep_warmup_thread.is_alive()
364
+ ):
365
+ return False
366
+ self._deep_warmup_thread = thread
367
+ thread.start()
368
+ return True
313
369
 
314
370
  def _load_indexed_file_paths(self) -> list[str]:
315
371
  git_paths = self._get_paths_from_git()
@@ -368,12 +424,17 @@ class LocalFileMentionCompleter(Completer):
368
424
  "%s mention index command exited with %s: %s",
369
425
  source_name,
370
426
  completed.returncode,
371
- completed.stderr.strip(),
427
+ (completed.stderr or "").strip(),
372
428
  )
373
429
  return None
374
430
 
431
+ stdout = completed.stdout
432
+ if stdout is None:
433
+ logger.debug("%s mention index command returned no stdout", source_name)
434
+ return None
435
+
375
436
  indexed_paths: set[str] = set()
376
- for line in completed.stdout.splitlines():
437
+ for line in stdout.splitlines():
377
438
  normalized = self._normalize_indexed_path(line)
378
439
  if normalized is None or not self._should_include_relative_path(normalized):
379
440
  continue
@@ -1,4 +1,4 @@
1
- """Resume picker 入口:把 SDK SessionInfo 喂给 ResumePickerApp"""
1
+ """Resume picker entry point: feeds SDK SessionInfo into ResumePickerApp."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -52,7 +52,7 @@ def list_resume_sessions_via_sdk(*, cwd: Path | str | None) -> list[ResumeSessio
52
52
  async def select_resume_session_id(console: Console, *, cwd: Path | None = None) -> str | None:
53
53
  items = list_resume_sessions_via_sdk(cwd=cwd)
54
54
  if not items:
55
- console.print("[dim]暂无可恢复会话。[/]")
55
+ console.print("[dim]No resumable sessions.[/]")
56
56
  return None
57
57
 
58
58
  cwd_by_session: dict[str, str | None] = {item.session_id: item.raw.cwd for item in items}
@@ -82,6 +82,6 @@ async def select_resume_session_id(console: Console, *, cwd: Path | None = None)
82
82
  with patch_stdout(raw=True):
83
83
  result = await app.run_async()
84
84
  if not isinstance(result, str) or not result.strip():
85
- console.print("[dim]已取消恢复。[/]")
85
+ console.print("[dim]Resume cancelled.[/]")
86
86
  return None
87
87
  return result
@@ -329,6 +329,11 @@ class TerminalAgentTUI(
329
329
  if self._busy:
330
330
  return
331
331
  doc = self._input_area.buffer.document
332
+ mention_context = self._mention_completer.extract_context(
333
+ doc.text_before_cursor
334
+ )
335
+ if mention_context is not None:
336
+ self._start_mention_cache_warmup()
332
337
  if self._completion_context_active(
333
338
  doc.text_before_cursor,
334
339
  doc.text_after_cursor,
@@ -1325,6 +1330,43 @@ class TerminalAgentTUI(
1325
1330
 
1326
1331
  task.add_done_callback(_done)
1327
1332
 
1333
+ def _start_mention_cache_warmup(self) -> None:
1334
+ mention_completer = getattr(self, "_mention_completer", None)
1335
+ start_warmup = getattr(mention_completer, "start_deep_cache_warmup", None)
1336
+ if not callable(start_warmup):
1337
+ return
1338
+ start_warmup(on_complete=self._on_mention_cache_warmed)
1339
+
1340
+ def _on_mention_cache_warmed(self) -> None:
1341
+ app = self._app
1342
+ if app is None or self._closing:
1343
+ return
1344
+ loop = getattr(app, "loop", None)
1345
+ if loop is None or loop.is_closed():
1346
+ return
1347
+ loop.call_soon_threadsafe(self._restart_active_mention_completion)
1348
+ app.invalidate()
1349
+
1350
+ def _restart_active_mention_completion(self) -> None:
1351
+ if self._closing or self._busy or self._ui_mode != UIMode.NORMAL:
1352
+ return
1353
+ input_area = getattr(self, "_input_area", None)
1354
+ if input_area is None:
1355
+ return
1356
+ buffer = input_area.buffer
1357
+ doc = buffer.document
1358
+ if not self._completion_context_active(
1359
+ doc.text_before_cursor,
1360
+ doc.text_after_cursor,
1361
+ ):
1362
+ return
1363
+ complete_state = buffer.complete_state
1364
+ if complete_state is not None:
1365
+ self._invalidate()
1366
+ return
1367
+ buffer.start_completion(select_first=False)
1368
+ self._invalidate()
1369
+
1328
1370
  def _refresh_layers(self) -> None:
1329
1371
  self._sync_focus_for_mode()
1330
1372
  self._render_dirty = True
@@ -1546,6 +1588,7 @@ class TerminalAgentTUI(
1546
1588
  if self._app is None:
1547
1589
  return
1548
1590
 
1591
+ self._start_mention_cache_warmup()
1549
1592
  self._refresh_layers()
1550
1593
 
1551
1594
  try:
@@ -43,12 +43,12 @@ if TYPE_CHECKING:
43
43
 
44
44
  class CommandsMixin:
45
45
  def _begin_llm_long_task(self) -> None:
46
- """标记 LLM 远端长任务开始:开启 busy 并记录运行起点。"""
46
+ """Mark the start of a long-running LLM task: enable busy state and record the start time."""
47
47
  self._set_busy(True)
48
48
  self._run_start_time = time.monotonic()
49
49
 
50
50
  def _end_llm_long_task(self) -> None:
51
- """标记 LLM 远端长任务结束:清理运行计时并退出 busy"""
51
+ """Mark the end of a long-running LLM task: clear the run timer and exit busy state."""
52
52
  self._run_start_time = None
53
53
  self._set_busy(False)
54
54
 
@@ -78,7 +78,7 @@ class CommandsMixin:
78
78
  def _cycle_agent_mode(self) -> None:
79
79
  if self._busy:
80
80
  self._renderer.append_system_message(
81
- "当前任务运行中,请在本轮结束后再切换模式。",
81
+ "A task is currently running. Please switch modes after this turn completes.",
82
82
  )
83
83
  self._refresh_layers()
84
84
  return
@@ -126,7 +126,7 @@ class CommandsMixin:
126
126
  is_busy = self._busy
127
127
  if is_busy and not entry.allow_when_busy:
128
128
  self._append_slash_command_result(
129
- f"当前任务运行中,暂不可执行 /{entry.spec.name}",
129
+ f"A task is currently running. /{entry.spec.name} is not available right now.",
130
130
  severity="error",
131
131
  )
132
132
  self._refresh_layers()
@@ -265,7 +265,7 @@ class CommandsMixin:
265
265
  self._open_mcp_server_list_menu(rows)
266
266
 
267
267
  def _slash_btw(self, args: str) -> None:
268
- """/btw <question> — 进入 BTW 面板,进行不污染主上下文的旁路问答。"""
268
+ """/btw <question> — Enter the BTW panel for side-channel Q&A without polluting the main context."""
269
269
  normalized = args.strip()
270
270
  if not normalized:
271
271
  self._append_slash_command_result(
@@ -274,7 +274,7 @@ class CommandsMixin:
274
274
  )
275
275
  return
276
276
  if self._ui_mode == UIMode.BTW:
277
- # 已经在 BTW 模式(理论上不可达:输入框被隐藏),防御性兜底
277
+ # Already in BTW mode (theoretically unreachable: input is hidden); defensive fallback.
278
278
  self._append_slash_command_result(
279
279
  "Already in /btw mode. Press Esc to exit first.",
280
280
  severity="error",
@@ -282,7 +282,7 @@ class CommandsMixin:
282
282
  return
283
283
 
284
284
  def _on_exit() -> None:
285
- # handle_escape 内部调用,UI mode 切换由 key binding 兜底完成。
285
+ # Called from inside handle_escape; UI mode switch is finalized by the key binding.
286
286
  self._ui_mode = UIMode.NORMAL
287
287
  self._sync_focus_for_mode()
288
288
  self._invalidate()
@@ -606,7 +606,7 @@ class CommandsMixin:
606
606
  *,
607
607
  enabled: bool,
608
608
  ) -> None:
609
- """启动 MCP server 启用/禁用流程。"""
609
+ """Start the MCP server enable/disable flow."""
610
610
  agent_runtime = self._session._agent
611
611
  row = rows_by_alias.get(alias, {})
612
612
  config_scope = str(row.get("config_scope", "")).strip().lower()
@@ -692,7 +692,7 @@ class CommandsMixin:
692
692
  self._invalidate()
693
693
 
694
694
  def _start_mcp_reconnect(self, alias: str, rows_by_alias: dict[str, dict[str, Any]]) -> None:
695
- """启动 MCP server 重连流程。"""
695
+ """Start the MCP server reconnect flow."""
696
696
  agent_runtime = self._session._agent
697
697
  row = rows_by_alias.get(alias, {})
698
698
  server_type = str(row.get("server_type", "")).strip().upper()
@@ -711,17 +711,17 @@ class CommandsMixin:
711
711
  else:
712
712
  message = f"Failed to reconnect to {alias}: {error_message}"
713
713
 
714
- # /mcp 命令和结果刷入 scrollback
714
+ # Flush the /mcp command and result into scrollback.
715
715
  self._renderer.seed_user_message("/mcp")
716
716
  self._renderer.append_subtitle(message)
717
717
 
718
718
  if success:
719
- # 成功:回到 NORMAL 模式,不返回菜单
719
+ # Success: return to NORMAL mode without going back to the menu.
720
720
  self._ui_mode = UIMode.NORMAL
721
721
  self._sync_focus_for_mode()
722
722
  self._refresh_layers()
723
723
  else:
724
- # 失败:回到 detail 菜单,方便用户再次重试
724
+ # Failure: return to the detail menu so the user can retry.
725
725
  refreshed_rows = self._collect_mcp_cached_rows()
726
726
  if refreshed_rows:
727
727
  refreshed_by_alias = {str(r["alias"]): r for r in refreshed_rows}
@@ -1050,14 +1050,14 @@ class CommandsMixin:
1050
1050
  )
1051
1051
  except CustomSlashExpandError as exc:
1052
1052
  self._append_slash_command_result(
1053
- f"/{command_name} 执行失败: {exc}",
1053
+ f"/{command_name} failed: {exc}",
1054
1054
  severity="error",
1055
1055
  )
1056
1056
  return
1057
1057
  except Exception as exc:
1058
1058
  logger.exception("custom slash command rendering failed")
1059
1059
  self._append_slash_command_result(
1060
- f"/{command_name} 执行失败: {exc}",
1060
+ f"/{command_name} failed: {exc}",
1061
1061
  severity="error",
1062
1062
  )
1063
1063
  return
@@ -1090,10 +1090,11 @@ class CommandsMixin:
1090
1090
  self._is_compacting = True
1091
1091
  self._compact_cancel_requested = False
1092
1092
  self._compact_task = asyncio.current_task()
1093
- # 不在此处提前 seed `> /compact` anchorcompact 必须 await 长任务,
1094
- # 若提前写入,user entry 的尾部空行会落到 scrollback 而无法被后续 drain
1095
- # `⎿` subtitle pop 掉(见 history_printer.py:223 的批内 pop 约束)。
1096
- # anchor 由结果阶段的 _append_slash_command_result subtitle 同批写入。
1093
+ # Do not pre-seed the `> /compact` anchor here: compact must await a long task,
1094
+ # and seeding it early would leave the user entry's trailing blank line in scrollback
1095
+ # where the subsequent drain's `⎿` subtitle cannot pop it
1096
+ # (see the in-batch pop constraint in history_printer.py:223).
1097
+ # The anchor is written together with the subtitle by _append_slash_command_result in the result phase.
1097
1098
  self._refresh_layers()
1098
1099
 
1099
1100
  try:
@@ -1132,19 +1133,19 @@ class CommandsMixin:
1132
1133
 
1133
1134
  if result.compacted:
1134
1135
  self._append_slash_command_result(
1135
- f"/compact completed: {result.tokens_before:,} {INJECTED_ARROW} {result.tokens_after:,} tokens",
1136
+ f"compact completed: {result.tokens_before:,} {INJECTED_ARROW} {result.tokens_after:,} tokens",
1136
1137
  )
1137
1138
  return
1138
1139
 
1139
1140
  if not result.attempted:
1140
1141
  self._append_slash_command_result(
1141
- f"/compact skipped: {result.reason}",
1142
+ f"compact skipped: {result.reason}",
1142
1143
  severity="warning",
1143
1144
  )
1144
1145
  return
1145
1146
 
1146
1147
  self._append_slash_command_result(
1147
- f"/compact made no changes: {result.reason}",
1148
+ f"compact made no changes: {result.reason}",
1148
1149
  severity="warning",
1149
1150
  )
1150
1151
 
@@ -1157,7 +1158,7 @@ class CommandsMixin:
1157
1158
  return
1158
1159
  if self._busy:
1159
1160
  self._append_slash_command_result(
1160
- "当前已有任务在运行,请稍后再执行 /rewind",
1161
+ "A task is currently running. Please try /rewind again later.",
1161
1162
  severity="error",
1162
1163
  )
1163
1164
  return
@@ -1165,8 +1166,9 @@ class CommandsMixin:
1165
1166
  targets = self._session.list_rewind_targets()
1166
1167
  if not targets:
1167
1168
  self._append_slash_command_result(
1168
- "暂时没有可以回到的文字消息。/rewind 目前支持回到本次会话中仍可恢复的普通文字输入;"
1169
- "图片、团队消息、队列自动插入的消息,以及已压缩进摘要的较早内容暂不支持。"
1169
+ "No rewind-able text messages are available. /rewind currently supports plain text "
1170
+ "messages from this session that can still be restored; images, team messages, "
1171
+ "queue-injected messages, and content already compacted into the summary are not supported."
1170
1172
  )
1171
1173
  return
1172
1174
  self._show_rewind_target_menu(targets)
@@ -1175,7 +1177,7 @@ class CommandsMixin:
1175
1177
  self._request_exit()
1176
1178
 
1177
1179
  def _slash_plugin(self, args: str) -> None:
1178
- """Handle /plugin command — 进入嵌入式 plugin picker 模式。"""
1180
+ """Handle /plugin command — enter the embedded plugin picker mode."""
1179
1181
  from comate_agent_sdk.plugins import (
1180
1182
  MarketplaceManager,
1181
1183
  PluginLoader,
@@ -1188,7 +1190,7 @@ class CommandsMixin:
1188
1190
  registry = create_plugin_registry()
1189
1191
  mkt_mgr = MarketplaceManager(registry)
1190
1192
 
1191
- # 快捷路径:/plugin marketplace add <repo> → 直接进入轻量安装面板
1193
+ # Shortcut: /plugin marketplace add <repo> → go straight to the lightweight install panel.
1192
1194
  if context.action == "add" and context.target_marketplace:
1193
1195
  self._install_view.enter(
1194
1196
  repo=context.target_marketplace,
@@ -1238,7 +1240,7 @@ class CommandsMixin:
1238
1240
  self._append_slash_command_result(msg)
1239
1241
 
1240
1242
  def _handle_plugin_action(self, action: PluginPickerAction | None) -> None:
1241
- """处理 plugin picker handler 返回值,切换回 NORMAL 模式。"""
1243
+ """Handle the plugin picker handler return value and switch back to NORMAL mode."""
1242
1244
  from comate_cli.terminal_agent.plugins.plugin_picker import PluginPickerAction
1243
1245
  if action is None or not isinstance(action, PluginPickerAction):
1244
1246
  return
@@ -1254,13 +1256,13 @@ class CommandsMixin:
1254
1256
  self._refresh_layers()
1255
1257
 
1256
1258
  def _exit_install_view(self) -> None:
1257
- """关闭 marketplace install 面板,切回 NORMAL 模式。"""
1259
+ """Close the marketplace install panel and switch back to NORMAL mode."""
1258
1260
  repo = self._install_view._repo
1259
1261
  result = self._install_view.take_result()
1260
1262
  self._ui_mode = UIMode.NORMAL
1261
1263
  self._sync_focus_for_mode()
1262
- # seed_user_message + append_subtitle 必须在同一次 drain 批次中,
1263
- # 否则 user entry 的尾部空行会在 scrollback 中残留。
1264
+ # seed_user_message + append_subtitle must occur in the same drain batch;
1265
+ # otherwise the user entry's trailing blank line will remain in scrollback.
1264
1266
  self._renderer.seed_user_message(f"/plugin marketplace add {repo}")
1265
1267
  if result.installed:
1266
1268
  self._renderer.append_subtitle(
@@ -1272,7 +1274,7 @@ class CommandsMixin:
1272
1274
  self._refresh_layers()
1273
1275
 
1274
1276
  def _on_marketplace_install_done(self) -> None:
1275
- """install_view 安装成功后的回调自动关闭面板。"""
1277
+ """Callback invoked after install_view succeedsautomatically closes the panel."""
1276
1278
  self._exit_install_view()
1277
1279
 
1278
1280
  def _slash_model(self, args: str) -> None:
@@ -1308,7 +1310,7 @@ class CommandsMixin:
1308
1310
  )
1309
1311
  logger.info(f"Model level switched: {event}")
1310
1312
 
1311
- # Update status bar model name - 使用 event 中的新模型名
1313
+ # Update status bar model name using the new model name from event.
1312
1314
  self._status_bar.set_model_name(new_model)
1313
1315
  self._invalidate()
1314
1316
  except Exception as e:
@@ -1524,7 +1526,7 @@ class CommandsMixin:
1524
1526
  return
1525
1527
  if self._busy:
1526
1528
  self._append_slash_command_result(
1527
- "当前已有任务在运行,请稍后再执行 /rewind",
1529
+ "A task is currently running. Please try /rewind again later.",
1528
1530
  severity="error",
1529
1531
  )
1530
1532
  self._refresh_layers()
@@ -1623,7 +1625,7 @@ class CommandsMixin:
1623
1625
  self._exit_selection_mode()
1624
1626
  return
1625
1627
 
1626
- # 回调可能已经切换了 ui_mode(如 MCP_CONNECTING),尊重回调的意图
1628
+ # The callback may have switched ui_mode (e.g., MCP_CONNECTING); respect its intent.
1627
1629
  if self._ui_mode == UIMode.SELECTION:
1628
1630
  self._exit_selection_mode()
1629
1631
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "comate-cli"
7
- version = "0.7.0a1"
7
+ version = "0.7.0a3"
8
8
  description = "Comate terminal CLI built on comate-agent-sdk"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -0,0 +1,119 @@
1
+ from __future__ import annotations
2
+
3
+ import tempfile
4
+ import unittest
5
+ from pathlib import Path
6
+ from types import SimpleNamespace
7
+
8
+ from prompt_toolkit.document import Document
9
+
10
+ from comate_cli.terminal_agent.mention_completer import LocalFileMentionCompleter
11
+ from comate_cli.terminal_agent.tui import TerminalAgentTUI
12
+ from comate_cli.terminal_agent.tui_parts import UIMode
13
+
14
+
15
+ class TestCompletionContextActivation(unittest.TestCase):
16
+ def setUp(self) -> None:
17
+ self._tmpdir = tempfile.TemporaryDirectory()
18
+ self.tui = TerminalAgentTUI.__new__(TerminalAgentTUI)
19
+ self.tui._mention_completer = LocalFileMentionCompleter(
20
+ Path(self._tmpdir.name),
21
+ refresh_interval=0.0,
22
+ limit=200,
23
+ )
24
+
25
+ def tearDown(self) -> None:
26
+ self._tmpdir.cleanup()
27
+
28
+ def test_slash_completion_requires_cursor_at_end(self) -> None:
29
+ self.assertTrue(self.tui._completion_context_active("/model", ""))
30
+ self.assertFalse(self.tui._completion_context_active("/model", " trailing"))
31
+
32
+ def test_mention_completion_triggers_at_line_end(self) -> None:
33
+ self.assertTrue(self.tui._completion_context_active("message @file", ""))
34
+
35
+ def test_mention_completion_triggers_at_line_start(self) -> None:
36
+ self.assertTrue(self.tui._completion_context_active("@file", " message"))
37
+
38
+ def test_mention_completion_triggers_in_middle_with_space_boundaries(self) -> None:
39
+ self.assertTrue(
40
+ self.tui._completion_context_active("message @file", " message")
41
+ )
42
+
43
+ def test_mention_completion_rejects_middle_without_right_space(self) -> None:
44
+ self.assertFalse(
45
+ self.tui._completion_context_active("message @file", "message")
46
+ )
47
+
48
+ def test_mention_completion_rejects_middle_without_left_space(self) -> None:
49
+ self.assertFalse(
50
+ self.tui._completion_context_active("message@file", " message")
51
+ )
52
+
53
+ def test_mention_completion_rejects_inside_token_editing(self) -> None:
54
+ self.assertFalse(
55
+ self.tui._completion_context_active("message @fi", "le message")
56
+ )
57
+
58
+ def test_mention_cache_warmup_registers_completion_callback(self) -> None:
59
+ callbacks = []
60
+
61
+ class _FakeMentionCompleter:
62
+ def start_deep_cache_warmup(self, *, on_complete):
63
+ callbacks.append(on_complete)
64
+ return True
65
+
66
+ self.tui._mention_completer = _FakeMentionCompleter()
67
+
68
+ self.tui._start_mention_cache_warmup()
69
+
70
+ self.assertEqual(len(callbacks), 1)
71
+ self.assertTrue(callable(callbacks[0]))
72
+
73
+ def test_warmup_callback_does_not_cancel_in_flight_completion(self) -> None:
74
+ class _FakeBuffer:
75
+ def __init__(self) -> None:
76
+ self.document = Document("@穆", cursor_position=2)
77
+ self.complete_state = SimpleNamespace(completions=[])
78
+ self.start_calls = 0
79
+
80
+ def start_completion(self, *, select_first: bool) -> None:
81
+ del select_first
82
+ self.start_calls += 1
83
+
84
+ buffer = _FakeBuffer()
85
+ self.tui._closing = False
86
+ self.tui._busy = False
87
+ self.tui._ui_mode = UIMode.NORMAL
88
+ self.tui._app = SimpleNamespace(invalidate=lambda: None)
89
+ self.tui._input_area = SimpleNamespace(buffer=buffer)
90
+
91
+ self.tui._restart_active_mention_completion()
92
+
93
+ self.assertEqual(buffer.start_calls, 0)
94
+
95
+ def test_warmup_callback_restarts_when_no_completion_is_active(self) -> None:
96
+ class _FakeBuffer:
97
+ def __init__(self) -> None:
98
+ self.document = Document("@穆", cursor_position=2)
99
+ self.complete_state = None
100
+ self.start_calls = 0
101
+
102
+ def start_completion(self, *, select_first: bool) -> None:
103
+ del select_first
104
+ self.start_calls += 1
105
+
106
+ buffer = _FakeBuffer()
107
+ self.tui._closing = False
108
+ self.tui._busy = False
109
+ self.tui._ui_mode = UIMode.NORMAL
110
+ self.tui._app = SimpleNamespace(invalidate=lambda: None)
111
+ self.tui._input_area = SimpleNamespace(buffer=buffer)
112
+
113
+ self.tui._restart_active_mention_completion()
114
+
115
+ self.assertEqual(buffer.start_calls, 1)
116
+
117
+
118
+ if __name__ == "__main__":
119
+ unittest.main(verbosity=2)