comate-cli 0.5.4__tar.gz → 0.5.5__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.5.4 → comate_cli-0.5.5}/PKG-INFO +1 -1
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/event_renderer.py +28 -6
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/history_printer.py +8 -1
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/rpc_stdio.py +1 -1
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/tool_view.py +2 -3
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/tui.py +50 -82
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/tui_parts/commands.py +1 -1
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/tui_parts/input_behavior.py +23 -28
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/tui_parts/key_bindings.py +63 -12
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/tui_parts/render_panels.py +49 -10
- {comate_cli-0.5.4 → comate_cli-0.5.5}/pyproject.toml +1 -1
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_completion_status_panel.py +3 -9
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_event_renderer.py +22 -9
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_event_renderer_boundary.py +13 -1
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_event_renderer_streaming.py +94 -2
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_history_printer.py +46 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_history_sync.py +2 -4
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_interrupt_exit_semantics.py +16 -2
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_question_key_bindings.py +15 -2
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_rpc_stdio_bridge.py +2 -1
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_task_panel_key_bindings.py +16 -1
- comate_cli-0.5.5/tests/test_tui_esc_queue.py +218 -0
- comate_cli-0.5.5/tests/test_tui_mcp_init_gate.py +80 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_tui_paste_placeholder.py +0 -170
- comate_cli-0.5.5/tests/test_tui_queue_preview.py +151 -0
- comate_cli-0.5.5/tests/test_tui_queue_sdk_source.py +273 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/uv.lock +1438 -1453
- comate_cli-0.5.4/tests/test_tui_mcp_init_gate.py +0 -107
- {comate_cli-0.5.4 → comate_cli-0.5.5}/.gitignore +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/README.md +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/__init__.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/__main__.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/main.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/mcp_cli.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/__init__.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/animations.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/app.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/assistant_render.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/codenames.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/custom_slash_commands.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/env_utils.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/error_display.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/figures.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/fragment_utils.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/input_geometry.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/layout_coordinator.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/logging_adapter.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/logo.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/markdown_render.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/mention_completer.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/message_style.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/models.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/path_context_hint.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/plugins/__init__.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/plugins/components/__init__.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/plugins/components/detail_view.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/plugins/components/plugin_list.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/plugins/components/search_box.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/plugins/components/tab_bar.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/plugins/marketplace_install_view.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/plugins/plugin_picker.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/plugins/tabs/__init__.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/plugins/tabs/discover_tab.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/plugins/tabs/errors_tab.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/plugins/tabs/installed_tab.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/plugins/tabs/marketplaces_tab.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/preflight.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/question_view.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/resume_selector.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/rewind_store.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/rpc_protocol.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/selection_menu.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/session_view.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/slash_commands.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/startup.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/status_bar.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/text_effects.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/tips.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/tui_parts/__init__.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/tui_parts/history_sync.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/tui_parts/mcp_connecting_view.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/tui_parts/slash_command_registry.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/comate_cli/terminal_agent/tui_parts/ui_mode.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/docs/superpowers/plans/2026-04-03-phrase-shuffle.md +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/docs/superpowers/specs/2026-04-01-conditional-diff-subtitle-design.md +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/docs/superpowers/specs/2026-04-03-phrase-shuffle-design.md +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/conftest.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_animator_shuffle.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_app_mcp_preload.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_app_preflight_gate.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_app_print_mode.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_app_shutdown.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_app_usage_line.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_cli_project_root.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_compact_command_semantics.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_completion_context_activation.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_context_command.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_custom_slash_commands.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_discover_tab.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_errors_tab.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_event_renderer_e2e.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_format_error.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_handle_error.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_input_behavior.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_input_history.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_installed_tab.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_layout_coordinator.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_logging_adapter.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_logo.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_main_args.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_markdown_render.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_marketplaces_tab.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_mcp_cli.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_mcp_slash_command.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_mention_completer.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_path_context_hint.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_plugin_slash_commands.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_plugin_tui_components.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_preflight.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_preflight_copilot.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_question_view.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_resume_selector.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_rewind_command_semantics.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_rewind_store.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_rpc_protocol.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_selection_menu.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_skills_slash_command.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_slash_argument_hint.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_slash_completer.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_slash_registry.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_status_bar.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_status_bar_transient.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_task_panel_format.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_task_panel_rendering.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_task_poll.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_tool_view.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_tui_elapsed_status.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_tui_split_invariance.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.5}/tests/test_update_check.py +0 -0
|
@@ -279,12 +279,13 @@ class EventRenderer:
|
|
|
279
279
|
self._show_thinking_cb: Callable[[], bool] = lambda: True
|
|
280
280
|
self._turn_received_text_delta: bool = False
|
|
281
281
|
self._turn_received_thinking_delta: bool = False
|
|
282
|
+
self._text_delta_started: bool = False
|
|
282
283
|
# ━━━━━ spec §4.1 新管道状态字段(Phase 1.2 引入,Phase 4-6 接入) ━━━━━
|
|
283
284
|
# Pipeline A: text line-commit
|
|
284
285
|
self._text_pending: str = ""
|
|
285
286
|
self._held_pipe_line: str | None = None
|
|
286
287
|
self._container: ContainerState | None = None
|
|
287
|
-
# Pipeline B: thinking
|
|
288
|
+
# Pipeline B: thinking line-commit + tail
|
|
288
289
|
self._thinking_batch: str = ""
|
|
289
290
|
# Pipeline C: loading 行 aux
|
|
290
291
|
self._loading_aux_text: str = ""
|
|
@@ -367,7 +368,7 @@ class EventRenderer:
|
|
|
367
368
|
self._loading_aux_text = ""
|
|
368
369
|
|
|
369
370
|
def _flush_thinking_batch(self) -> None:
|
|
370
|
-
"""spec §6.5:thinking
|
|
371
|
+
"""spec §6.5:thinking 残段 → HistoryEntry(受 _show_thinking_cb 过滤)。
|
|
371
372
|
即使 hidden 也清空 batch 避免持续累积。"""
|
|
372
373
|
text = self._thinking_batch
|
|
373
374
|
self._thinking_batch = ""
|
|
@@ -379,6 +380,22 @@ class EventRenderer:
|
|
|
379
380
|
HistoryEntry(entry_type="thinking", text=text)
|
|
380
381
|
)
|
|
381
382
|
|
|
383
|
+
def _consume_thinking_delta(self, delta: str) -> None:
|
|
384
|
+
"""spec §6.1:thinking delta 主入口。
|
|
385
|
+
|
|
386
|
+
与 text delta 对称:按完整行即时 commit 到 scrollback,未换行尾巴留在
|
|
387
|
+
_thinking_batch,等待 text/tool/turn 边界通过 _flush_thinking_batch 落盘。
|
|
388
|
+
"""
|
|
389
|
+
if not delta:
|
|
390
|
+
return
|
|
391
|
+
self._thinking_batch += delta
|
|
392
|
+
while "\n" in self._thinking_batch:
|
|
393
|
+
line, self._thinking_batch = self._thinking_batch.split("\n", 1)
|
|
394
|
+
if self._show_thinking_cb():
|
|
395
|
+
self._append_history_entry(
|
|
396
|
+
HistoryEntry(entry_type="thinking", text=line + "\n")
|
|
397
|
+
)
|
|
398
|
+
|
|
382
399
|
# ── 行级路由(spec §6.2,Task 4.1-4.4 渐进扩展)──
|
|
383
400
|
|
|
384
401
|
def _handle_complete_line(self, line: str) -> None:
|
|
@@ -457,7 +474,7 @@ class EventRenderer:
|
|
|
457
474
|
|
|
458
475
|
把 delta 累到 _text_pending,按 \\n 切完整行送 _handle_complete_line。
|
|
459
476
|
|
|
460
|
-
关键顺序保证:text 进入前先 flush 待 thinking
|
|
477
|
+
关键顺序保证:text 进入前先 flush 待 thinking 残段 —— 保证
|
|
461
478
|
scrollback 看到 "thinking → text" 而非 "text → thinking" 错序。
|
|
462
479
|
"""
|
|
463
480
|
if not delta:
|
|
@@ -467,6 +484,9 @@ class EventRenderer:
|
|
|
467
484
|
self._text_pending += delta
|
|
468
485
|
while "\n" in self._text_pending:
|
|
469
486
|
line, self._text_pending = self._text_pending.split("\n", 1)
|
|
487
|
+
if line == "" and not self._text_delta_started:
|
|
488
|
+
continue
|
|
489
|
+
self._text_delta_started = True
|
|
470
490
|
self._handle_complete_line(line)
|
|
471
491
|
|
|
472
492
|
# ── turn 边界(spec §6.4)──
|
|
@@ -509,6 +529,7 @@ class EventRenderer:
|
|
|
509
529
|
self._container = None
|
|
510
530
|
self._thinking_batch = ""
|
|
511
531
|
self._loading_aux_text = ""
|
|
532
|
+
self._text_delta_started = False
|
|
512
533
|
|
|
513
534
|
def _should_drop_duplicate_system_message(
|
|
514
535
|
self,
|
|
@@ -541,6 +562,7 @@ class EventRenderer:
|
|
|
541
562
|
self._pending_tool_starts.clear()
|
|
542
563
|
self._turn_received_text_delta = False
|
|
543
564
|
self._turn_received_thinking_delta = False
|
|
565
|
+
self._text_delta_started = False
|
|
544
566
|
# spec §6.4:清空新 5 字段(每 turn 起点保证状态干净)
|
|
545
567
|
self._text_pending = ""
|
|
546
568
|
self._held_pipe_line = None
|
|
@@ -630,6 +652,7 @@ class EventRenderer:
|
|
|
630
652
|
self._pending_tool_starts.clear()
|
|
631
653
|
self._turn_received_text_delta = False
|
|
632
654
|
self._turn_received_thinking_delta = False
|
|
655
|
+
self._text_delta_started = False
|
|
633
656
|
# spec §6.4:清空 5 个新字段(与 start_turn 同语义)
|
|
634
657
|
self._text_pending = ""
|
|
635
658
|
self._held_pipe_line = None
|
|
@@ -1444,10 +1467,9 @@ class EventRenderer:
|
|
|
1444
1467
|
self._turn_received_text_delta = True
|
|
1445
1468
|
case ThinkingDeltaEvent(delta=delta, message_id=_):
|
|
1446
1469
|
if delta:
|
|
1447
|
-
# spec §6.1:thinking delta →
|
|
1448
|
-
# text delta 来时)才一次性 commit
|
|
1449
|
-
self._thinking_batch += delta
|
|
1470
|
+
# spec §6.1:thinking delta → 按 \n 切完整行入队,残段留 batch
|
|
1450
1471
|
self._turn_received_thinking_delta = True
|
|
1472
|
+
self._consume_thinking_delta(delta)
|
|
1451
1473
|
case ToolCallStartEvent(tool_call_id=tool_call_id, tool=tool_name):
|
|
1452
1474
|
# tool 调用前先把 _text_pending / _thinking_batch 落到 scrollback,
|
|
1453
1475
|
# 保证说明文字出现在 tool UI 之前(无 \n 结尾的模型也适用)。
|
|
@@ -103,6 +103,7 @@ def render_history_group(
|
|
|
103
103
|
in_run_continuation = is_assistant and prev_was_assistant
|
|
104
104
|
next_entry = entries[idx + 1] if idx + 1 < len(entries) else None
|
|
105
105
|
next_is_assistant = next_entry is not None and next_entry.entry_type == "assistant"
|
|
106
|
+
next_is_thinking = next_entry is not None and next_entry.entry_type == "thinking"
|
|
106
107
|
|
|
107
108
|
# 跳过位于两个块标记 entry 之间的空 assistant entry(loose list 视觉对齐)
|
|
108
109
|
if is_assistant and _is_blank_assistant(entry):
|
|
@@ -127,7 +128,13 @@ def render_history_group(
|
|
|
127
128
|
line_text.append(" ", style="dim")
|
|
128
129
|
line_text.append(line, style="dim")
|
|
129
130
|
renderables.append(line_text)
|
|
130
|
-
|
|
131
|
+
is_batch_tail = next_entry is None
|
|
132
|
+
suppress_trailing_gap = (
|
|
133
|
+
is_batch_tail
|
|
134
|
+
and assume_run_continues_at_tail
|
|
135
|
+
)
|
|
136
|
+
if not next_is_thinking and not suppress_trailing_gap:
|
|
137
|
+
renderables.append(Text(""))
|
|
131
138
|
prev_was_assistant = False
|
|
132
139
|
continue
|
|
133
140
|
|
|
@@ -228,7 +228,7 @@ class StdioRPCBridge:
|
|
|
228
228
|
self._active_prompt_result = result_future
|
|
229
229
|
|
|
230
230
|
try:
|
|
231
|
-
await self._session.send(user_input)
|
|
231
|
+
_ = await self._session.send(user_input)
|
|
232
232
|
except Exception as exc:
|
|
233
233
|
self._clear_active_prompt(result_future)
|
|
234
234
|
await self._send(
|
|
@@ -93,11 +93,10 @@ def should_show_tool_in_scrollback(
|
|
|
93
93
|
return is_result and is_error
|
|
94
94
|
|
|
95
95
|
if lowered == "taskcreate":
|
|
96
|
-
return is_result
|
|
96
|
+
return is_result and is_error
|
|
97
97
|
|
|
98
98
|
if lowered == "taskupdate":
|
|
99
|
-
|
|
100
|
-
return is_result and (is_error or status == "completed")
|
|
99
|
+
return is_result and is_error
|
|
101
100
|
|
|
102
101
|
# Read-only task tools: only show errors.
|
|
103
102
|
if lowered in _SILENT_TASK_TOOLS:
|
|
@@ -9,7 +9,6 @@ import asyncio
|
|
|
9
9
|
import logging
|
|
10
10
|
import random
|
|
11
11
|
import time
|
|
12
|
-
from collections import deque
|
|
13
12
|
from collections.abc import Callable
|
|
14
13
|
from contextlib import suppress
|
|
15
14
|
from pathlib import Path
|
|
@@ -34,10 +33,14 @@ from comate_agent_sdk.agent import ChatSession
|
|
|
34
33
|
from comate_agent_sdk.agent.events import (
|
|
35
34
|
ExternalTurnScheduledEvent,
|
|
36
35
|
PlanApprovalRequiredEvent,
|
|
36
|
+
QueueDirtyEvent,
|
|
37
|
+
QueueDirtyReason,
|
|
38
|
+
QueuedMessageInjectedEvent,
|
|
37
39
|
SessionInitEvent,
|
|
38
40
|
StopEvent,
|
|
39
41
|
TeamMessageEvent,
|
|
40
42
|
)
|
|
43
|
+
from comate_agent_sdk.agent.queue_types import MessageOrigin
|
|
41
44
|
|
|
42
45
|
from comate_cli.terminal_agent.animations import (
|
|
43
46
|
DEFAULT_STATUS_PHRASES,
|
|
@@ -196,22 +199,21 @@ class TerminalAgentTUI(
|
|
|
196
199
|
"AGENT_SDK_TUI_TODO_PANEL_EXPANDED_MAX_LINES",
|
|
197
200
|
12,
|
|
198
201
|
)
|
|
199
|
-
self._message_queue_max_size = read_env_int(
|
|
200
|
-
"AGENT_SDK_TUI_MESSAGE_QUEUE_MAX_SIZE",
|
|
201
|
-
3,
|
|
202
|
-
)
|
|
203
202
|
self._queued_preview_max_chars = read_env_int(
|
|
204
203
|
"AGENT_SDK_TUI_QUEUED_PREVIEW_MAX_CHARS",
|
|
205
204
|
24,
|
|
206
205
|
)
|
|
206
|
+
self._queued_preview_max_rows = read_env_int(
|
|
207
|
+
"AGENT_SDK_TUI_QUEUED_PREVIEW_MAX_ROWS",
|
|
208
|
+
3,
|
|
209
|
+
)
|
|
207
210
|
|
|
208
211
|
self._busy = False
|
|
209
|
-
self._initializing = False # MCP 加载中
|
|
210
212
|
self._is_compacting = False
|
|
211
213
|
self._compact_task: asyncio.Task[Any] | None = None
|
|
212
214
|
self._compact_cancel_requested = False
|
|
213
215
|
self._pending_exit_after_compact_cancel = False
|
|
214
|
-
self.
|
|
216
|
+
self._queued_display_by_message_id: dict[str, str] = {}
|
|
215
217
|
self._waiting_for_input = False
|
|
216
218
|
self._pending_questions: list[dict[str, Any]] | None = None
|
|
217
219
|
self._pending_plan_approval: dict[str, str] | None = None
|
|
@@ -288,10 +290,6 @@ class TerminalAgentTUI(
|
|
|
288
290
|
self._ctrl_c_press_count: int = 0
|
|
289
291
|
ctrl_c_window_ms = read_env_int("AGENT_SDK_TUI_CTRL_C_EXIT_WINDOW_MS", 700)
|
|
290
292
|
self._ctrl_c_exit_window_seconds = ctrl_c_window_ms / 1000.0
|
|
291
|
-
self._mcp_init_gate_timeout_s = read_env_float(
|
|
292
|
-
"AGENT_SDK_TUI_MCP_INIT_GATE_TIMEOUT_S",
|
|
293
|
-
8.0,
|
|
294
|
-
)
|
|
295
293
|
self._mcp_init_cancel_timeout_s = read_env_float(
|
|
296
294
|
"AGENT_SDK_TUI_MCP_INIT_CANCEL_TIMEOUT_S",
|
|
297
295
|
1.0,
|
|
@@ -559,8 +557,8 @@ class TerminalAgentTUI(
|
|
|
559
557
|
"status.transient.warning": "bg:default italic fg:ansiyellow",
|
|
560
558
|
"input.placeholder": "bg:default #9CA3AF",
|
|
561
559
|
"auto-suggestion": "bg:default #94a3b8",
|
|
562
|
-
"queue": "bg
|
|
563
|
-
"queue.item": "bg
|
|
560
|
+
"queue": "bg:default #d8dee9",
|
|
561
|
+
"queue.item": "bg:default #cbd5e1",
|
|
564
562
|
"git-diff.added": "#4ade80",
|
|
565
563
|
"git-diff.removed": "#f87171",
|
|
566
564
|
"question.tabs": "bg:default #c7d2fe",
|
|
@@ -638,9 +636,7 @@ class TerminalAgentTUI(
|
|
|
638
636
|
)
|
|
639
637
|
|
|
640
638
|
def _should_show_queue_panel(self) -> bool:
|
|
641
|
-
return self._ui_mode == UIMode.NORMAL and
|
|
642
|
-
|
|
643
|
-
_QUEUED_SLASH_PREFIX = "__COMATE_QUEUED_SLASH__::"
|
|
639
|
+
return self._ui_mode == UIMode.NORMAL and bool(self._queued_human_messages())
|
|
644
640
|
|
|
645
641
|
def _build_slash_registry(self) -> None:
|
|
646
642
|
handlers: dict[str, Callable[[str], Any]] = {
|
|
@@ -729,26 +725,6 @@ class TerminalAgentTUI(
|
|
|
729
725
|
return None
|
|
730
726
|
return entry.spec
|
|
731
727
|
|
|
732
|
-
def _encode_queued_slash(self, raw_command: str) -> str:
|
|
733
|
-
return f"{self._QUEUED_SLASH_PREFIX}{raw_command}"
|
|
734
|
-
|
|
735
|
-
def _is_encoded_queued_slash(self, value: str) -> bool:
|
|
736
|
-
return value.startswith(self._QUEUED_SLASH_PREFIX)
|
|
737
|
-
|
|
738
|
-
def _decode_queued_slash(self, value: str) -> str:
|
|
739
|
-
if not self._is_encoded_queued_slash(value):
|
|
740
|
-
return value
|
|
741
|
-
return value[len(self._QUEUED_SLASH_PREFIX) :]
|
|
742
|
-
|
|
743
|
-
def _queued_preview_text(self, value: str) -> str:
|
|
744
|
-
return self._decode_queued_slash(value)
|
|
745
|
-
|
|
746
|
-
def _dispatch_queued_item(self, queued: str) -> None:
|
|
747
|
-
if self._is_encoded_queued_slash(queued):
|
|
748
|
-
self._schedule_background(self._execute_command(self._decode_queued_slash(queued)))
|
|
749
|
-
return
|
|
750
|
-
self._schedule_background(self._submit_user_message(queued))
|
|
751
|
-
|
|
752
728
|
def _create_input_history(self) -> FileHistory | InMemoryHistory:
|
|
753
729
|
"""Create persistent FileHistory based on cwd, with fallback to InMemoryHistory."""
|
|
754
730
|
try:
|
|
@@ -771,15 +747,16 @@ class TerminalAgentTUI(
|
|
|
771
747
|
def _input_placeholder_hint(self) -> str | None:
|
|
772
748
|
if self._ui_mode != UIMode.NORMAL:
|
|
773
749
|
return None
|
|
774
|
-
|
|
750
|
+
queued_messages = self._queued_human_messages()
|
|
751
|
+
if not queued_messages:
|
|
775
752
|
return None
|
|
776
753
|
input_text = str(getattr(getattr(self, "_input_area", None), "text", ""))
|
|
777
754
|
if input_text.strip():
|
|
778
755
|
return None
|
|
779
|
-
if len(
|
|
756
|
+
if len(queued_messages) >= 2:
|
|
780
757
|
return self._queued_input_hint
|
|
781
758
|
|
|
782
|
-
preview =
|
|
759
|
+
preview = self._queued_message_preview(queued_messages[0])
|
|
783
760
|
max_chars = max(8, int(self._queued_preview_max_chars))
|
|
784
761
|
if len(preview) > max_chars:
|
|
785
762
|
preview = f"{preview[: max_chars - 3]}..."
|
|
@@ -931,6 +908,28 @@ class TerminalAgentTUI(
|
|
|
931
908
|
await self._animation_controller.on_event(event)
|
|
932
909
|
if isinstance(event, SessionInitEvent):
|
|
933
910
|
self._initialized_session_id = str(event.session_id)
|
|
911
|
+
if isinstance(event, QueueDirtyEvent):
|
|
912
|
+
if event.reason in {
|
|
913
|
+
QueueDirtyReason.CANCELLED,
|
|
914
|
+
QueueDirtyReason.CLEARED,
|
|
915
|
+
}:
|
|
916
|
+
for message_id in event.message_ids:
|
|
917
|
+
self._queued_display_by_message_id.pop(
|
|
918
|
+
str(message_id),
|
|
919
|
+
None,
|
|
920
|
+
)
|
|
921
|
+
self._refresh_layers()
|
|
922
|
+
self._invalidate()
|
|
923
|
+
if isinstance(event, QueuedMessageInjectedEvent):
|
|
924
|
+
for message_id, origin in zip(event.message_ids, event.origins):
|
|
925
|
+
if origin is not MessageOrigin.HUMAN:
|
|
926
|
+
continue
|
|
927
|
+
display_text = self._queued_display_by_message_id.pop(
|
|
928
|
+
str(message_id),
|
|
929
|
+
None,
|
|
930
|
+
)
|
|
931
|
+
if display_text:
|
|
932
|
+
self._renderer.seed_user_message(display_text)
|
|
934
933
|
if isinstance(event, ExternalTurnScheduledEvent) and not self._busy:
|
|
935
934
|
self._session.run_controller.clear()
|
|
936
935
|
self._interrupt_requested_at = None
|
|
@@ -1013,14 +1012,6 @@ class TerminalAgentTUI(
|
|
|
1013
1012
|
|
|
1014
1013
|
self._refresh_layers()
|
|
1015
1014
|
|
|
1016
|
-
if (
|
|
1017
|
-
self._queued_messages
|
|
1018
|
-
and not waiting_for_input
|
|
1019
|
-
and plan_approval is None
|
|
1020
|
-
):
|
|
1021
|
-
queued = self._queued_messages.popleft()
|
|
1022
|
-
self._dispatch_queued_item(queued)
|
|
1023
|
-
|
|
1024
1015
|
waiting_for_input = False
|
|
1025
1016
|
questions = None
|
|
1026
1017
|
plan_approval = None
|
|
@@ -1285,17 +1276,16 @@ class TerminalAgentTUI(
|
|
|
1285
1276
|
if self._renderer.has_running_tools():
|
|
1286
1277
|
# Keep repainting for breathing animation.
|
|
1287
1278
|
self._render_dirty = True
|
|
1288
|
-
elif self._busy
|
|
1279
|
+
elif self._busy:
|
|
1289
1280
|
self._render_dirty = True
|
|
1290
1281
|
|
|
1291
1282
|
if self._render_dirty:
|
|
1292
1283
|
self._invalidate()
|
|
1293
1284
|
self._render_dirty = False
|
|
1294
1285
|
|
|
1295
|
-
# 动态帧率:busy
|
|
1286
|
+
# 动态帧率:busy/动画时降为 6fps(缓解 scrollback 污染),idle 时 4fps
|
|
1296
1287
|
fast = (
|
|
1297
1288
|
self._busy
|
|
1298
|
-
or self._initializing
|
|
1299
1289
|
or self._animator.is_active
|
|
1300
1290
|
or self._renderer.has_running_tools()
|
|
1301
1291
|
)
|
|
@@ -1408,45 +1398,23 @@ class TerminalAgentTUI(
|
|
|
1408
1398
|
logger.exception("Plugin init failed at startup; continuing without plugins")
|
|
1409
1399
|
|
|
1410
1400
|
if mcp_init is not None:
|
|
1411
|
-
self._initializing = True
|
|
1412
|
-
|
|
1413
1401
|
async def _do_init() -> None:
|
|
1414
|
-
init_task = asyncio.create_task(mcp_init(), name="terminal-mcp-init-worker")
|
|
1415
1402
|
try:
|
|
1416
|
-
await
|
|
1417
|
-
asyncio.shield(init_task),
|
|
1418
|
-
timeout=self._mcp_init_gate_timeout_s,
|
|
1419
|
-
)
|
|
1420
|
-
except asyncio.TimeoutError:
|
|
1421
|
-
logger.debug(
|
|
1422
|
-
"MCP init timed out after "
|
|
1423
|
-
f"{self._mcp_init_gate_timeout_s:.1f}s; continuing in background",
|
|
1424
|
-
)
|
|
1425
|
-
self._status_bar.show_transient(
|
|
1426
|
-
f"MCP: init timed out, continuing in background{ELLIPSIS}",
|
|
1427
|
-
duration_s=5.0,
|
|
1428
|
-
)
|
|
1429
|
-
# Don't cancel init_task — let it complete in background.
|
|
1430
|
-
# asyncio.shield above already protects it from wait_for's
|
|
1431
|
-
# auto-cancellation. When it finishes, runtime._mcp_manager
|
|
1432
|
-
# will be set and tools become available for the next turn.
|
|
1403
|
+
await mcp_init()
|
|
1433
1404
|
except asyncio.CancelledError:
|
|
1434
|
-
|
|
1435
|
-
init_task,
|
|
1436
|
-
timeout_s=self._mcp_init_cancel_timeout_s,
|
|
1437
|
-
task_name="terminal-mcp-init-worker",
|
|
1438
|
-
)
|
|
1439
|
-
return
|
|
1405
|
+
raise
|
|
1440
1406
|
except Exception as exc:
|
|
1441
|
-
logger.debug(
|
|
1407
|
+
logger.debug(
|
|
1408
|
+
f"MCP init failed in TUI bootstrap: {exc}",
|
|
1409
|
+
exc_info=True,
|
|
1410
|
+
)
|
|
1442
1411
|
finally:
|
|
1443
|
-
self._initializing = False
|
|
1444
|
-
if self._queued_messages and not self._busy:
|
|
1445
|
-
queued = self._queued_messages.popleft()
|
|
1446
|
-
self._dispatch_queued_item(queued)
|
|
1447
1412
|
self._refresh_layers()
|
|
1448
1413
|
|
|
1449
|
-
self._mcp_init_task = asyncio.create_task(
|
|
1414
|
+
self._mcp_init_task = asyncio.create_task(
|
|
1415
|
+
_do_init(),
|
|
1416
|
+
name="terminal-mcp-init",
|
|
1417
|
+
)
|
|
1450
1418
|
|
|
1451
1419
|
self._ui_tick_task = asyncio.create_task(
|
|
1452
1420
|
self._ui_tick(),
|
|
@@ -5,6 +5,8 @@ from typing import Any
|
|
|
5
5
|
|
|
6
6
|
from prompt_toolkit.document import Document
|
|
7
7
|
|
|
8
|
+
from comate_agent_sdk.agent.chat_session import ChatSessionQueueFullError
|
|
9
|
+
|
|
8
10
|
from comate_cli.terminal_agent.input_geometry import (
|
|
9
11
|
compute_visual_line_ranges,
|
|
10
12
|
index_for_visual_col,
|
|
@@ -333,8 +335,9 @@ class InputBehaviorMixin:
|
|
|
333
335
|
self._input_area.buffer.cancel_completion()
|
|
334
336
|
self._clear_input_area(save_to_history=True)
|
|
335
337
|
|
|
336
|
-
# busy
|
|
337
|
-
|
|
338
|
+
# busy 时:TUI 不再维护本地消息 deque,统一交给 SDK 队列。
|
|
339
|
+
# MCP preload 只在后台准备工具,不阻塞首条用户消息的回显与提交。
|
|
340
|
+
is_busy = self._busy
|
|
338
341
|
if is_busy:
|
|
339
342
|
if raw_text.startswith("/"):
|
|
340
343
|
normalized_slash = display_text.strip()
|
|
@@ -343,39 +346,31 @@ class InputBehaviorMixin:
|
|
|
343
346
|
entry = registry.resolve(parsed.name) if (registry is not None and parsed is not None) else None
|
|
344
347
|
|
|
345
348
|
if entry is not None and str(getattr(entry, "source", "")) == "custom":
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
f"消息队列已满({queue_size}/{self._message_queue_max_size}),请等待当前任务完成。",
|
|
350
|
-
severity="error",
|
|
351
|
-
)
|
|
352
|
-
self._refresh_layers()
|
|
353
|
-
return
|
|
354
|
-
|
|
355
|
-
encode_fn = getattr(self, "_encode_queued_slash", None)
|
|
356
|
-
queued_value = (
|
|
357
|
-
str(encode_fn(normalized_slash))
|
|
358
|
-
if callable(encode_fn)
|
|
359
|
-
else normalized_slash
|
|
349
|
+
self._renderer.append_system_message(
|
|
350
|
+
"当前任务运行中,custom slash command 不能排队执行,请等待当前任务结束后再运行。",
|
|
351
|
+
severity="error",
|
|
360
352
|
)
|
|
361
|
-
self.
|
|
362
|
-
self._invalidate()
|
|
353
|
+
self._refresh_layers()
|
|
363
354
|
return
|
|
364
355
|
|
|
365
356
|
self._schedule_background(self._execute_command(normalized_slash))
|
|
366
357
|
return
|
|
367
358
|
|
|
368
359
|
if not raw_text.startswith("/"):
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
360
|
+
async def _send_busy_message(content: str, display: str) -> None:
|
|
361
|
+
try:
|
|
362
|
+
message_id = await self._session.send(content)
|
|
363
|
+
self._queued_display_by_message_id[str(message_id)] = display
|
|
364
|
+
except ChatSessionQueueFullError:
|
|
365
|
+
self._renderer.append_system_message(
|
|
366
|
+
"消息队列已满,请等待当前任务完成。",
|
|
367
|
+
severity="error",
|
|
368
|
+
)
|
|
369
|
+
self._refresh_layers()
|
|
370
|
+
|
|
371
|
+
self._schedule_background(
|
|
372
|
+
_send_busy_message(submit_text, display_text)
|
|
373
|
+
)
|
|
379
374
|
return
|
|
380
375
|
|
|
381
376
|
if raw_text.startswith("/"):
|
|
@@ -6,6 +6,8 @@ from prompt_toolkit.filters import Condition, has_completions, has_focus
|
|
|
6
6
|
from prompt_toolkit.key_binding import KeyBindings
|
|
7
7
|
from prompt_toolkit.keys import Keys
|
|
8
8
|
|
|
9
|
+
from comate_agent_sdk.agent.queue_types import MessageOrigin
|
|
10
|
+
|
|
9
11
|
from comate_cli.terminal_agent.slash_commands import parse_slash_command_call
|
|
10
12
|
from comate_cli.terminal_agent.tui_parts.ui_mode import UIMode
|
|
11
13
|
|
|
@@ -334,18 +336,6 @@ class KeyBindingsMixin:
|
|
|
334
336
|
@bindings.add("up", filter=normal_mode & ~diff_panel_open & ~todo_panel_expanded)
|
|
335
337
|
def _active_prev_up(event) -> None:
|
|
336
338
|
buffer = event.current_buffer
|
|
337
|
-
# busy + 有 queue + input 为空 → 取回最近一条 queue,加载回 input
|
|
338
|
-
is_busy = self._busy or self._initializing
|
|
339
|
-
if is_busy and self._queued_messages and not buffer.text.strip():
|
|
340
|
-
queued = self._queued_messages.pop()
|
|
341
|
-
decode_fn = getattr(self, "_queued_preview_text", None)
|
|
342
|
-
if callable(decode_fn):
|
|
343
|
-
buffer.text = str(decode_fn(queued))
|
|
344
|
-
else:
|
|
345
|
-
buffer.text = str(queued)
|
|
346
|
-
buffer.cursor_position = len(buffer.text)
|
|
347
|
-
self._invalidate()
|
|
348
|
-
return
|
|
349
339
|
if self._move_completion_selection(buffer, backward=True):
|
|
350
340
|
return
|
|
351
341
|
if self._move_cursor_visual(buffer, backward=True):
|
|
@@ -375,6 +365,67 @@ class KeyBindingsMixin:
|
|
|
375
365
|
self._esc_last_pressed_at = now
|
|
376
366
|
return
|
|
377
367
|
|
|
368
|
+
is_busy = False
|
|
369
|
+
is_busy_fn = getattr(self._session, "is_busy", None)
|
|
370
|
+
if callable(is_busy_fn):
|
|
371
|
+
is_busy = bool(is_busy_fn())
|
|
372
|
+
else:
|
|
373
|
+
is_busy = bool(self._busy)
|
|
374
|
+
if is_busy:
|
|
375
|
+
self._session.run_controller.interrupt(reason="user_esc")
|
|
376
|
+
self._esc_press_count = 0
|
|
377
|
+
self._esc_last_pressed_at = now
|
|
378
|
+
return
|
|
379
|
+
|
|
380
|
+
peek_queue = getattr(self._session, "peek_queue", None)
|
|
381
|
+
queued_messages = tuple(peek_queue()) if callable(peek_queue) else ()
|
|
382
|
+
human_messages = [
|
|
383
|
+
message
|
|
384
|
+
for message in queued_messages
|
|
385
|
+
if getattr(message, "origin", None) == MessageOrigin.HUMAN
|
|
386
|
+
]
|
|
387
|
+
if human_messages:
|
|
388
|
+
def _editable_text(message) -> str: # type: ignore[no-untyped-def]
|
|
389
|
+
content = getattr(message, "content", "")
|
|
390
|
+
if isinstance(content, str):
|
|
391
|
+
return content
|
|
392
|
+
if isinstance(content, list):
|
|
393
|
+
pieces: list[str] = []
|
|
394
|
+
for part in content:
|
|
395
|
+
if isinstance(part, dict):
|
|
396
|
+
part_type = str(part.get("type", ""))
|
|
397
|
+
if part_type == "text":
|
|
398
|
+
pieces.append(str(part.get("text", "")))
|
|
399
|
+
elif "image" in part_type:
|
|
400
|
+
pieces.append("[image]")
|
|
401
|
+
else:
|
|
402
|
+
pieces.append(str(part))
|
|
403
|
+
return "\n".join(piece for piece in pieces if piece)
|
|
404
|
+
return str(content)
|
|
405
|
+
|
|
406
|
+
text = "\n\n".join(
|
|
407
|
+
piece
|
|
408
|
+
for piece in (_editable_text(message) for message in human_messages)
|
|
409
|
+
if piece
|
|
410
|
+
)
|
|
411
|
+
buffer.text = text
|
|
412
|
+
buffer.cursor_position = len(text)
|
|
413
|
+
message_ids = tuple(
|
|
414
|
+
str(getattr(message, "message_id"))
|
|
415
|
+
for message in human_messages
|
|
416
|
+
)
|
|
417
|
+
cancel_many = getattr(self._session, "cancel_many", None)
|
|
418
|
+
if callable(cancel_many):
|
|
419
|
+
cancel_many(message_ids)
|
|
420
|
+
queued_display = getattr(self, "_queued_display_by_message_id", None)
|
|
421
|
+
if isinstance(queued_display, dict):
|
|
422
|
+
for message_id in message_ids:
|
|
423
|
+
queued_display.pop(message_id, None)
|
|
424
|
+
self._esc_press_count = 0
|
|
425
|
+
self._esc_last_pressed_at = now
|
|
426
|
+
self._invalidate()
|
|
427
|
+
return
|
|
428
|
+
|
|
378
429
|
if now - self._esc_last_pressed_at > self._esc_clear_window_seconds:
|
|
379
430
|
self._esc_press_count = 0
|
|
380
431
|
self._esc_press_count += 1
|