comate-cli 0.5.4__tar.gz → 0.5.6__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.6}/PKG-INFO +1 -1
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/event_renderer.py +29 -7
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/history_printer.py +8 -1
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/rpc_stdio.py +1 -1
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/selection_menu.py +17 -13
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/tool_view.py +2 -3
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/tui.py +158 -87
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/tui_parts/commands.py +83 -14
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/tui_parts/input_behavior.py +23 -28
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/tui_parts/key_bindings.py +74 -12
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/tui_parts/render_panels.py +49 -10
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/tui_parts/slash_command_registry.py +1 -1
- {comate_cli-0.5.4 → comate_cli-0.5.6}/pyproject.toml +1 -1
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_completion_status_panel.py +3 -9
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_event_renderer.py +22 -9
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_event_renderer_boundary.py +13 -1
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_event_renderer_streaming.py +104 -2
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_history_printer.py +46 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_history_sync.py +2 -4
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_interrupt_exit_semantics.py +16 -2
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_question_key_bindings.py +15 -2
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_rpc_stdio_bridge.py +2 -1
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_selection_menu.py +34 -0
- comate_cli-0.5.6/tests/test_skills_slash_command.py +386 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_task_panel_key_bindings.py +16 -1
- comate_cli-0.5.6/tests/test_tui_esc_queue.py +257 -0
- comate_cli-0.5.6/tests/test_tui_mcp_init_gate.py +80 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_tui_paste_placeholder.py +0 -170
- comate_cli-0.5.6/tests/test_tui_queue_preview.py +151 -0
- comate_cli-0.5.6/tests/test_tui_queue_sdk_source.py +340 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/uv.lock +1438 -1453
- comate_cli-0.5.4/tests/test_skills_slash_command.py +0 -205
- comate_cli-0.5.4/tests/test_tui_mcp_init_gate.py +0 -107
- {comate_cli-0.5.4 → comate_cli-0.5.6}/.gitignore +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/README.md +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/__init__.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/__main__.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/main.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/mcp_cli.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/__init__.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/animations.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/app.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/assistant_render.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/codenames.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/custom_slash_commands.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/env_utils.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/error_display.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/figures.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/fragment_utils.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/input_geometry.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/layout_coordinator.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/logging_adapter.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/logo.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/markdown_render.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/mention_completer.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/message_style.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/models.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/path_context_hint.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/plugins/__init__.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/plugins/components/__init__.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/plugins/components/detail_view.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/plugins/components/plugin_list.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/plugins/components/search_box.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/plugins/components/tab_bar.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/plugins/marketplace_install_view.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/plugins/plugin_picker.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/plugins/tabs/__init__.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/plugins/tabs/discover_tab.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/plugins/tabs/errors_tab.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/plugins/tabs/installed_tab.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/plugins/tabs/marketplaces_tab.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/preflight.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/question_view.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/resume_selector.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/rewind_store.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/rpc_protocol.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/session_view.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/slash_commands.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/startup.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/status_bar.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/text_effects.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/tips.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/tui_parts/__init__.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/tui_parts/history_sync.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/tui_parts/mcp_connecting_view.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/comate_cli/terminal_agent/tui_parts/ui_mode.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/docs/superpowers/plans/2026-04-03-phrase-shuffle.md +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/docs/superpowers/specs/2026-04-01-conditional-diff-subtitle-design.md +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/docs/superpowers/specs/2026-04-03-phrase-shuffle-design.md +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/conftest.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_animator_shuffle.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_app_mcp_preload.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_app_preflight_gate.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_app_print_mode.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_app_shutdown.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_app_usage_line.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_cli_project_root.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_compact_command_semantics.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_completion_context_activation.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_context_command.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_custom_slash_commands.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_discover_tab.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_errors_tab.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_event_renderer_e2e.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_format_error.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_handle_error.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_input_behavior.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_input_history.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_installed_tab.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_layout_coordinator.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_logging_adapter.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_logo.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_main_args.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_markdown_render.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_marketplaces_tab.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_mcp_cli.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_mcp_slash_command.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_mention_completer.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_path_context_hint.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_plugin_slash_commands.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_plugin_tui_components.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_preflight.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_preflight_copilot.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_question_view.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_resume_selector.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_rewind_command_semantics.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_rewind_store.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_rpc_protocol.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_slash_argument_hint.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_slash_completer.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_slash_registry.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_status_bar.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_status_bar_transient.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_task_panel_format.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_task_panel_rendering.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_task_poll.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_tool_view.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_tui_elapsed_status.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/tests/test_tui_split_invariance.py +0 -0
- {comate_cli-0.5.4 → comate_cli-0.5.6}/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 结尾的模型也适用)。
|
|
@@ -1625,7 +1647,7 @@ class EventRenderer:
|
|
|
1625
1647
|
return (True, None)
|
|
1626
1648
|
if reason == "waiting_for_plan_approval":
|
|
1627
1649
|
return (False, None)
|
|
1628
|
-
if reason == "interrupted":
|
|
1650
|
+
if reason == "interrupted" and "error" not in event.metadata:
|
|
1629
1651
|
self._append_history_entry(
|
|
1630
1652
|
HistoryEntry(entry_type="system", text="Current task interrupted.", severity="warning")
|
|
1631
1653
|
)
|
|
@@ -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(
|
|
@@ -288,14 +288,13 @@ class SelectionMenuUI:
|
|
|
288
288
|
desc_style = "class:selection.description.selected" if is_selected else "class:selection.description"
|
|
289
289
|
|
|
290
290
|
if self._state.layout_mode == "inline":
|
|
291
|
-
fragments.
|
|
292
|
-
(
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
),
|
|
291
|
+
fragments.extend(
|
|
292
|
+
self._inline_option_fragments(
|
|
293
|
+
option=option,
|
|
294
|
+
cursor=cursor,
|
|
295
|
+
marker=marker,
|
|
296
|
+
line_style=line_style,
|
|
297
|
+
desc_style=desc_style,
|
|
299
298
|
)
|
|
300
299
|
)
|
|
301
300
|
fragments.append(("", "\n"))
|
|
@@ -331,13 +330,15 @@ class SelectionMenuUI:
|
|
|
331
330
|
)
|
|
332
331
|
return fragments
|
|
333
332
|
|
|
334
|
-
def
|
|
333
|
+
def _inline_option_fragments(
|
|
335
334
|
self,
|
|
336
335
|
*,
|
|
337
336
|
option: SelectionOption,
|
|
338
337
|
cursor: str,
|
|
339
338
|
marker: str,
|
|
340
|
-
|
|
339
|
+
line_style: str,
|
|
340
|
+
desc_style: str,
|
|
341
|
+
) -> list[tuple[str, str]]:
|
|
341
342
|
total_width = max(20, int(self._state.inline_total_width or 100))
|
|
342
343
|
prefix = f" {cursor} {marker} "
|
|
343
344
|
prefix_width = get_cwidth(prefix)
|
|
@@ -357,13 +358,16 @@ class SelectionMenuUI:
|
|
|
357
358
|
name_padding = " " * max(0, name_width - get_cwidth(name_text))
|
|
358
359
|
|
|
359
360
|
if desc_width <= 0:
|
|
360
|
-
return fit_single_line(f"{prefix}{name_text}", total_width)
|
|
361
|
+
return [(line_style, fit_single_line(f"{prefix}{name_text}", total_width))]
|
|
361
362
|
|
|
362
363
|
description = option.description.strip()
|
|
363
364
|
desc_text = fit_single_line(description, desc_width) if description else ""
|
|
364
365
|
if desc_text:
|
|
365
|
-
return
|
|
366
|
-
|
|
366
|
+
return [
|
|
367
|
+
(line_style, f"{prefix}{name_text}{name_padding}"),
|
|
368
|
+
(desc_style, f"{' ' * gap_width}{desc_text}"),
|
|
369
|
+
]
|
|
370
|
+
return [(line_style, f"{prefix}{name_text}{name_padding}")]
|
|
367
371
|
|
|
368
372
|
def _normalized_page_size(self) -> int:
|
|
369
373
|
options_count = len(self._state.options)
|
|
@@ -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
|
|
@@ -220,6 +222,7 @@ class TerminalAgentTUI(
|
|
|
220
222
|
self._renderer._show_thinking_cb = lambda: self._show_thinking
|
|
221
223
|
|
|
222
224
|
self._custom_slash_commands: dict[str, CustomSlashCommand] = {}
|
|
225
|
+
self._skill_slash_command_names: set[str] = set()
|
|
223
226
|
self._slash_registry = SlashCommandRegistry()
|
|
224
227
|
self._build_slash_registry()
|
|
225
228
|
self._slash_completer = SlashCommandCompleter(
|
|
@@ -235,6 +238,7 @@ class TerminalAgentTUI(
|
|
|
235
238
|
deduplicate=True,
|
|
236
239
|
)
|
|
237
240
|
)
|
|
241
|
+
self._sync_skill_slash_commands()
|
|
238
242
|
self._loading_frame = 0
|
|
239
243
|
self._fallback_loading_phrase = (
|
|
240
244
|
random.choice(DEFAULT_STATUS_PHRASES)
|
|
@@ -288,10 +292,6 @@ class TerminalAgentTUI(
|
|
|
288
292
|
self._ctrl_c_press_count: int = 0
|
|
289
293
|
ctrl_c_window_ms = read_env_int("AGENT_SDK_TUI_CTRL_C_EXIT_WINDOW_MS", 700)
|
|
290
294
|
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
295
|
self._mcp_init_cancel_timeout_s = read_env_float(
|
|
296
296
|
"AGENT_SDK_TUI_MCP_INIT_CANCEL_TIMEOUT_S",
|
|
297
297
|
1.0,
|
|
@@ -559,8 +559,8 @@ class TerminalAgentTUI(
|
|
|
559
559
|
"status.transient.warning": "bg:default italic fg:ansiyellow",
|
|
560
560
|
"input.placeholder": "bg:default #9CA3AF",
|
|
561
561
|
"auto-suggestion": "bg:default #94a3b8",
|
|
562
|
-
"queue": "bg
|
|
563
|
-
"queue.item": "bg
|
|
562
|
+
"queue": "bg:default #d8dee9",
|
|
563
|
+
"queue.item": "bg:default #cbd5e1",
|
|
564
564
|
"git-diff.added": "#4ade80",
|
|
565
565
|
"git-diff.removed": "#f87171",
|
|
566
566
|
"question.tabs": "bg:default #c7d2fe",
|
|
@@ -638,9 +638,7 @@ class TerminalAgentTUI(
|
|
|
638
638
|
)
|
|
639
639
|
|
|
640
640
|
def _should_show_queue_panel(self) -> bool:
|
|
641
|
-
return self._ui_mode == UIMode.NORMAL and
|
|
642
|
-
|
|
643
|
-
_QUEUED_SLASH_PREFIX = "__COMATE_QUEUED_SLASH__::"
|
|
641
|
+
return self._ui_mode == UIMode.NORMAL and bool(self._queued_human_messages())
|
|
644
642
|
|
|
645
643
|
def _build_slash_registry(self) -> None:
|
|
646
644
|
handlers: dict[str, Callable[[str], Any]] = {
|
|
@@ -723,31 +721,61 @@ class TerminalAgentTUI(
|
|
|
723
721
|
argument_hint=command.argument_hint,
|
|
724
722
|
)
|
|
725
723
|
|
|
726
|
-
def
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
724
|
+
def _sync_skill_slash_commands(self) -> None:
|
|
725
|
+
"""同步当前 runtime skills 到 slash command registry。"""
|
|
726
|
+
for name in list(getattr(self, "_skill_slash_command_names", set())):
|
|
727
|
+
self._slash_registry.unregister(name)
|
|
728
|
+
self._skill_slash_command_names = set()
|
|
731
729
|
|
|
732
|
-
|
|
733
|
-
|
|
730
|
+
agent = getattr(self._session, "_agent", None)
|
|
731
|
+
runtime_state = getattr(agent, "_runtime_state", None)
|
|
732
|
+
skills = list(getattr(runtime_state, "skills", []) or [])
|
|
733
|
+
seen_names: set[str] = set()
|
|
734
734
|
|
|
735
|
-
|
|
736
|
-
|
|
735
|
+
for skill in skills:
|
|
736
|
+
if not bool(getattr(skill, "user_invocable", True)):
|
|
737
|
+
continue
|
|
737
738
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
739
|
+
name = str(getattr(skill, "name", "")).strip()
|
|
740
|
+
if not name or name in seen_names:
|
|
741
|
+
continue
|
|
742
|
+
seen_names.add(name)
|
|
742
743
|
|
|
743
|
-
|
|
744
|
-
|
|
744
|
+
if self._slash_registry.resolve(name) is not None:
|
|
745
|
+
logger.warning(
|
|
746
|
+
"Skill '%s' conflicts with an existing slash command, skipping",
|
|
747
|
+
name,
|
|
748
|
+
)
|
|
749
|
+
continue
|
|
745
750
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
+
spec = SlashCommandSpec(
|
|
752
|
+
name=name,
|
|
753
|
+
description=str(getattr(skill, "description", "") or "").strip(),
|
|
754
|
+
execution_kind="hybrid",
|
|
755
|
+
argument_hint=getattr(skill, "argument_hint", None),
|
|
756
|
+
)
|
|
757
|
+
|
|
758
|
+
async def _handler(args: str, *, skill_name: str = name) -> None:
|
|
759
|
+
await self._slash_skill(skill_name=skill_name, args=args)
|
|
760
|
+
|
|
761
|
+
self._slash_registry.register(
|
|
762
|
+
spec=spec,
|
|
763
|
+
handler=_handler,
|
|
764
|
+
allow_when_busy=False,
|
|
765
|
+
source="skill",
|
|
766
|
+
argument_hint=spec.argument_hint,
|
|
767
|
+
)
|
|
768
|
+
self._skill_slash_command_names.add(name)
|
|
769
|
+
|
|
770
|
+
slash_completer = getattr(self, "_slash_completer", None)
|
|
771
|
+
if slash_completer is not None:
|
|
772
|
+
slash_completer.update_commands(self._slash_registry.command_specs())
|
|
773
|
+
|
|
774
|
+
def _resolve_slash_spec(self, name: str) -> SlashCommandSpec | None:
|
|
775
|
+
entry = self._slash_registry.resolve(name)
|
|
776
|
+
if entry is None:
|
|
777
|
+
return None
|
|
778
|
+
return entry.spec
|
|
751
779
|
|
|
752
780
|
def _create_input_history(self) -> FileHistory | InMemoryHistory:
|
|
753
781
|
"""Create persistent FileHistory based on cwd, with fallback to InMemoryHistory."""
|
|
@@ -771,15 +799,16 @@ class TerminalAgentTUI(
|
|
|
771
799
|
def _input_placeholder_hint(self) -> str | None:
|
|
772
800
|
if self._ui_mode != UIMode.NORMAL:
|
|
773
801
|
return None
|
|
774
|
-
|
|
802
|
+
queued_messages = self._queued_human_messages()
|
|
803
|
+
if not queued_messages:
|
|
775
804
|
return None
|
|
776
805
|
input_text = str(getattr(getattr(self, "_input_area", None), "text", ""))
|
|
777
806
|
if input_text.strip():
|
|
778
807
|
return None
|
|
779
|
-
if len(
|
|
808
|
+
if len(queued_messages) >= 2:
|
|
780
809
|
return self._queued_input_hint
|
|
781
810
|
|
|
782
|
-
preview =
|
|
811
|
+
preview = self._queued_message_preview(queued_messages[0])
|
|
783
812
|
max_chars = max(8, int(self._queued_preview_max_chars))
|
|
784
813
|
if len(preview) > max_chars:
|
|
785
814
|
preview = f"{preview[: max_chars - 3]}..."
|
|
@@ -927,10 +956,63 @@ class TerminalAgentTUI(
|
|
|
927
956
|
async for event in self._session.events():
|
|
928
957
|
if self._closing:
|
|
929
958
|
break
|
|
959
|
+
stop_error: Exception | None = None
|
|
930
960
|
self._maybe_cancel_tool_result_flash(event)
|
|
931
961
|
await self._animation_controller.on_event(event)
|
|
932
962
|
if isinstance(event, SessionInitEvent):
|
|
933
963
|
self._initialized_session_id = str(event.session_id)
|
|
964
|
+
if isinstance(event, QueueDirtyEvent):
|
|
965
|
+
if event.reason in {
|
|
966
|
+
QueueDirtyReason.CANCELLED,
|
|
967
|
+
QueueDirtyReason.CLEARED,
|
|
968
|
+
}:
|
|
969
|
+
for message_id in event.message_ids:
|
|
970
|
+
self._queued_display_by_message_id.pop(
|
|
971
|
+
str(message_id),
|
|
972
|
+
None,
|
|
973
|
+
)
|
|
974
|
+
if event.reason is QueueDirtyReason.CONSUMED:
|
|
975
|
+
consumed_human_displays: list[str] = []
|
|
976
|
+
for message_id, origin in zip(event.message_ids, event.origins):
|
|
977
|
+
if origin is not MessageOrigin.HUMAN:
|
|
978
|
+
continue
|
|
979
|
+
display_text = self._queued_display_by_message_id.pop(
|
|
980
|
+
str(message_id),
|
|
981
|
+
None,
|
|
982
|
+
)
|
|
983
|
+
if display_text:
|
|
984
|
+
consumed_human_displays.append(display_text)
|
|
985
|
+
if consumed_human_displays and not self._busy:
|
|
986
|
+
self._session.run_controller.clear()
|
|
987
|
+
self._interrupt_requested_at = None
|
|
988
|
+
self._waiting_for_input = False
|
|
989
|
+
self._pending_questions = None
|
|
990
|
+
self._pending_plan_approval = None
|
|
991
|
+
self._set_busy(True)
|
|
992
|
+
self._run_start_time = time.monotonic()
|
|
993
|
+
self._fallback_loading_phrase = (
|
|
994
|
+
random.choice(DEFAULT_STATUS_PHRASES)
|
|
995
|
+
if DEFAULT_STATUS_PHRASES
|
|
996
|
+
else f"Thinking{ELLIPSIS}"
|
|
997
|
+
)
|
|
998
|
+
self._fallback_phrase_refresh_at = time.monotonic() + 3.0
|
|
999
|
+
self._renderer.start_turn()
|
|
1000
|
+
await self._animation_controller.start()
|
|
1001
|
+
self._last_turn_user_preview = consumed_human_displays[-1]
|
|
1002
|
+
for display_text in consumed_human_displays:
|
|
1003
|
+
self._renderer.seed_user_message(display_text)
|
|
1004
|
+
self._refresh_layers()
|
|
1005
|
+
self._invalidate()
|
|
1006
|
+
if isinstance(event, QueuedMessageInjectedEvent):
|
|
1007
|
+
for message_id, origin in zip(event.message_ids, event.origins):
|
|
1008
|
+
if origin is not MessageOrigin.HUMAN:
|
|
1009
|
+
continue
|
|
1010
|
+
display_text = self._queued_display_by_message_id.pop(
|
|
1011
|
+
str(message_id),
|
|
1012
|
+
None,
|
|
1013
|
+
)
|
|
1014
|
+
if display_text:
|
|
1015
|
+
self._renderer.seed_user_message(display_text)
|
|
934
1016
|
if isinstance(event, ExternalTurnScheduledEvent) and not self._busy:
|
|
935
1017
|
self._session.run_controller.clear()
|
|
936
1018
|
self._interrupt_requested_at = None
|
|
@@ -954,6 +1036,14 @@ class TerminalAgentTUI(
|
|
|
954
1036
|
}
|
|
955
1037
|
if isinstance(event, StopEvent):
|
|
956
1038
|
stop_reason = str(event.reason or "")
|
|
1039
|
+
raw_stop_error = event.metadata.get("error")
|
|
1040
|
+
if raw_stop_error is not None:
|
|
1041
|
+
stop_error = (
|
|
1042
|
+
raw_stop_error
|
|
1043
|
+
if isinstance(raw_stop_error, Exception)
|
|
1044
|
+
else Exception(str(raw_stop_error))
|
|
1045
|
+
)
|
|
1046
|
+
stop_reason = "error"
|
|
957
1047
|
if event.reason == "shutdown_request":
|
|
958
1048
|
shutdown_recipients = event.metadata.get("shutdown_recipients", [])
|
|
959
1049
|
if shutdown_recipients:
|
|
@@ -969,6 +1059,15 @@ class TerminalAgentTUI(
|
|
|
969
1059
|
waiting_for_input = True
|
|
970
1060
|
if new_questions is not None:
|
|
971
1061
|
questions = new_questions
|
|
1062
|
+
if stop_error is not None:
|
|
1063
|
+
from comate_cli.terminal_agent.error_display import format_error
|
|
1064
|
+
|
|
1065
|
+
message, transient_summary, severity = format_error(stop_error)
|
|
1066
|
+
self._renderer.append_system_message(message, severity=severity)
|
|
1067
|
+
self._status_bar.show_transient(
|
|
1068
|
+
transient_summary,
|
|
1069
|
+
severity=severity,
|
|
1070
|
+
)
|
|
972
1071
|
self._refresh_layers()
|
|
973
1072
|
if isinstance(event, StopEvent):
|
|
974
1073
|
background_turn = self._background_turn_source is not None
|
|
@@ -1013,14 +1112,6 @@ class TerminalAgentTUI(
|
|
|
1013
1112
|
|
|
1014
1113
|
self._refresh_layers()
|
|
1015
1114
|
|
|
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
1115
|
waiting_for_input = False
|
|
1025
1116
|
questions = None
|
|
1026
1117
|
plan_approval = None
|
|
@@ -1285,17 +1376,16 @@ class TerminalAgentTUI(
|
|
|
1285
1376
|
if self._renderer.has_running_tools():
|
|
1286
1377
|
# Keep repainting for breathing animation.
|
|
1287
1378
|
self._render_dirty = True
|
|
1288
|
-
elif self._busy
|
|
1379
|
+
elif self._busy:
|
|
1289
1380
|
self._render_dirty = True
|
|
1290
1381
|
|
|
1291
1382
|
if self._render_dirty:
|
|
1292
1383
|
self._invalidate()
|
|
1293
1384
|
self._render_dirty = False
|
|
1294
1385
|
|
|
1295
|
-
# 动态帧率:busy
|
|
1386
|
+
# 动态帧率:busy/动画时降为 6fps(缓解 scrollback 污染),idle 时 4fps
|
|
1296
1387
|
fast = (
|
|
1297
1388
|
self._busy
|
|
1298
|
-
or self._initializing
|
|
1299
1389
|
or self._animator.is_active
|
|
1300
1390
|
or self._renderer.has_running_tools()
|
|
1301
1391
|
)
|
|
@@ -1408,45 +1498,23 @@ class TerminalAgentTUI(
|
|
|
1408
1498
|
logger.exception("Plugin init failed at startup; continuing without plugins")
|
|
1409
1499
|
|
|
1410
1500
|
if mcp_init is not None:
|
|
1411
|
-
self._initializing = True
|
|
1412
|
-
|
|
1413
1501
|
async def _do_init() -> None:
|
|
1414
|
-
init_task = asyncio.create_task(mcp_init(), name="terminal-mcp-init-worker")
|
|
1415
1502
|
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.
|
|
1503
|
+
await mcp_init()
|
|
1433
1504
|
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
|
|
1505
|
+
raise
|
|
1440
1506
|
except Exception as exc:
|
|
1441
|
-
logger.debug(
|
|
1507
|
+
logger.debug(
|
|
1508
|
+
f"MCP init failed in TUI bootstrap: {exc}",
|
|
1509
|
+
exc_info=True,
|
|
1510
|
+
)
|
|
1442
1511
|
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
1512
|
self._refresh_layers()
|
|
1448
1513
|
|
|
1449
|
-
self._mcp_init_task = asyncio.create_task(
|
|
1514
|
+
self._mcp_init_task = asyncio.create_task(
|
|
1515
|
+
_do_init(),
|
|
1516
|
+
name="terminal-mcp-init",
|
|
1517
|
+
)
|
|
1450
1518
|
|
|
1451
1519
|
self._ui_tick_task = asyncio.create_task(
|
|
1452
1520
|
self._ui_tick(),
|
|
@@ -1519,6 +1587,9 @@ class TerminalAgentTUI(
|
|
|
1519
1587
|
|
|
1520
1588
|
# Commands → SlashCommandRegistry with namespace prefix
|
|
1521
1589
|
for cmd in plugin.commands:
|
|
1590
|
+
if cmd.name in getattr(self, "_skill_slash_command_names", set()):
|
|
1591
|
+
self._slash_registry.unregister(cmd.name)
|
|
1592
|
+
self._skill_slash_command_names.discard(cmd.name)
|
|
1522
1593
|
spec = SlashCommandSpec(
|
|
1523
1594
|
name=cmd.name, # already namespaced: "plugin-name:cmd-name"
|
|
1524
1595
|
description=cmd.description,
|
|
@@ -1576,8 +1647,10 @@ class TerminalAgentTUI(
|
|
|
1576
1647
|
if any(p.agents for p in plugins):
|
|
1577
1648
|
rebuild_agent_tool(agent)
|
|
1578
1649
|
|
|
1650
|
+
if any(p.skills for p in plugins):
|
|
1651
|
+
self._sync_skill_slash_commands()
|
|
1579
1652
|
# Refresh tab-completion so new plugin commands appear
|
|
1580
|
-
|
|
1653
|
+
elif any(p.commands for p in plugins):
|
|
1581
1654
|
self._slash_completer.update_commands(self._slash_registry.command_specs())
|
|
1582
1655
|
|
|
1583
1656
|
def _clear_plugin_resources(self) -> None:
|
|
@@ -1615,10 +1688,6 @@ class TerminalAgentTUI(
|
|
|
1615
1688
|
self._custom_slash_commands.pop(cmd_name, None)
|
|
1616
1689
|
self._plugin_command_names = set()
|
|
1617
1690
|
|
|
1618
|
-
# Refresh tab-completion so removed commands disappear
|
|
1619
|
-
if had_commands:
|
|
1620
|
-
self._slash_completer.update_commands(self._slash_registry.command_specs())
|
|
1621
|
-
|
|
1622
1691
|
# Remove plugin MCP servers from frozen config
|
|
1623
1692
|
has_mcp = any(p.mcp_servers for p in loaded)
|
|
1624
1693
|
if has_mcp and agent.config.mcp_servers:
|
|
@@ -1642,3 +1711,5 @@ class TerminalAgentTUI(
|
|
|
1642
1711
|
rebuild_skill_tool(agent)
|
|
1643
1712
|
if had_agents:
|
|
1644
1713
|
rebuild_agent_tool(agent)
|
|
1714
|
+
if had_skills or had_commands:
|
|
1715
|
+
self._sync_skill_slash_commands()
|