comate-cli 0.6.0__tar.gz → 0.6.1__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.6.0 → comate_cli-0.6.1}/PKG-INFO +1 -1
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/app.py +3 -2
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/event_renderer.py +66 -7
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/history_printer.py +101 -6
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/logging_adapter.py +11 -34
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/models.py +12 -1
- comate_cli-0.6.1/comate_cli/terminal_agent/tips.py +21 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/tui.py +16 -2
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/tui_parts/commands.py +133 -94
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/tui_parts/render_panels.py +41 -4
- {comate_cli-0.6.0 → comate_cli-0.6.1}/pyproject.toml +1 -1
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_compact_command_semantics.py +119 -5
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_completion_status_panel.py +40 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_context_command.py +43 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_event_renderer.py +24 -7
- comate_cli-0.6.1/tests/test_event_renderer_log_boundary.py +175 -0
- comate_cli-0.6.1/tests/test_event_renderer_log_queue.py +114 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_event_renderer_streaming.py +24 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_history_printer.py +2 -2
- comate_cli-0.6.1/tests/test_history_printer_log.py +199 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_logging_adapter.py +69 -17
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_update_check.py +49 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/uv.lock +1453 -1437
- comate_cli-0.6.0/comate_cli/terminal_agent/tips.py +0 -15
- {comate_cli-0.6.0 → comate_cli-0.6.1}/.gitignore +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/README.md +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/__init__.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/__main__.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/main.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/mcp_cli.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/__init__.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/animations.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/assistant_render.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/codenames.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/custom_slash_commands.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/env_utils.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/error_display.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/figures.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/fragment_utils.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/input_geometry.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/layout_coordinator.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/logo.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/markdown_render.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/mention_completer.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/message_style.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/path_context_hint.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/plugins/__init__.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/plugins/components/__init__.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/plugins/components/detail_view.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/plugins/components/plugin_list.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/plugins/components/search_box.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/plugins/components/tab_bar.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/plugins/marketplace_install_view.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/plugins/plugin_picker.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/plugins/tabs/__init__.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/plugins/tabs/discover_tab.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/plugins/tabs/errors_tab.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/plugins/tabs/installed_tab.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/plugins/tabs/marketplaces_tab.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/preflight.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/question_view.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/resume_selector.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/rewind_store.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/rpc_protocol.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/rpc_stdio.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/selection_menu.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/session_view.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/slash_commands.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/startup.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/status_bar.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/text_effects.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/tool_view.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/tui_parts/__init__.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/tui_parts/history_sync.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/tui_parts/input_behavior.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/tui_parts/key_bindings.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/tui_parts/mcp_connecting_view.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/tui_parts/slash_command_registry.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/comate_cli/terminal_agent/tui_parts/ui_mode.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/docs/superpowers/plans/2026-04-03-phrase-shuffle.md +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/docs/superpowers/specs/2026-04-01-conditional-diff-subtitle-design.md +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/docs/superpowers/specs/2026-04-03-phrase-shuffle-design.md +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/conftest.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_animator_shuffle.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_app_mcp_preload.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_app_preflight_gate.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_app_print_mode.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_app_shutdown.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_app_usage_line.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_cli_project_root.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_completion_context_activation.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_custom_slash_commands.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_discover_tab.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_errors_tab.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_event_renderer_boundary.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_event_renderer_e2e.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_format_error.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_handle_error.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_history_sync.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_input_behavior.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_input_history.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_installed_tab.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_interrupt_exit_semantics.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_layout_coordinator.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_logo.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_main_args.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_markdown_render.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_marketplaces_tab.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_mcp_cli.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_mcp_slash_command.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_mention_completer.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_path_context_hint.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_plugin_slash_commands.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_plugin_tui_components.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_preflight.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_preflight_copilot.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_question_key_bindings.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_question_view.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_resume_selector.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_rewind_command_semantics.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_rewind_store.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_rpc_protocol.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_rpc_stdio_bridge.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_selection_menu.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_skills_slash_command.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_slash_argument_hint.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_slash_completer.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_slash_registry.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_status_bar.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_status_bar_transient.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_task_panel_format.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_task_panel_key_bindings.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_task_panel_rendering.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_task_poll.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_tool_view.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_tui_elapsed_status.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_tui_esc_queue.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_tui_mcp_init_gate.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_tui_paste_placeholder.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_tui_queue_preview.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_tui_queue_sdk_source.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_tui_split_invariance.py +0 -0
- {comate_cli-0.6.0 → comate_cli-0.6.1}/tests/test_tui_team_messages.py +0 -0
|
@@ -22,7 +22,6 @@ from comate_agent_sdk.tools import tool
|
|
|
22
22
|
|
|
23
23
|
from comate_cli.terminal_agent.event_renderer import EventRenderer
|
|
24
24
|
from comate_cli.terminal_agent.logo import print_logo
|
|
25
|
-
from comate_cli.terminal_agent.tips import TIPS
|
|
26
25
|
from comate_cli.terminal_agent.preflight import run_preflight_if_needed
|
|
27
26
|
from comate_cli.terminal_agent.resume_selector import select_resume_session_id
|
|
28
27
|
from comate_cli.terminal_agent.rpc_stdio import StdioRPCBridge
|
|
@@ -357,7 +356,6 @@ async def run(
|
|
|
357
356
|
return
|
|
358
357
|
|
|
359
358
|
print_logo(console, project_root=project_root)
|
|
360
|
-
console.print(f"[dim]💡 Tip: {random.choice(TIPS)}[/dim]")
|
|
361
359
|
|
|
362
360
|
if resume_select and not resume_session_id:
|
|
363
361
|
selected_session_id = await select_resume_session_id(console, cwd=project_root)
|
|
@@ -373,6 +371,9 @@ async def run(
|
|
|
373
371
|
logging_session = setup_tui_logging(renderer, project_root=project_root)
|
|
374
372
|
|
|
375
373
|
session, mode = _resolve_session(agent, resume_session_id, cwd=project_root)
|
|
374
|
+
# setup_tui_logging 在 _resolve_session 前安装,用于捕获 session 初始化期 warning/error。
|
|
375
|
+
# 这些日志没有交互 anchor,需在恢复历史或首次用户输入前落为 standalone log。
|
|
376
|
+
renderer.flush_pending_logs()
|
|
376
377
|
|
|
377
378
|
# 版本检查:带超时,不阻塞启动
|
|
378
379
|
try:
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
import re
|
|
5
|
+
import threading
|
|
5
6
|
import time
|
|
6
7
|
from collections import deque
|
|
7
8
|
from dataclasses import dataclass, field
|
|
@@ -280,6 +281,7 @@ class EventRenderer:
|
|
|
280
281
|
self._turn_received_text_delta: bool = False
|
|
281
282
|
self._turn_received_thinking_delta: bool = False
|
|
282
283
|
self._text_delta_started: bool = False
|
|
284
|
+
self._active_text_message_id: str | None = None
|
|
283
285
|
# ━━━━━ spec §4.1 新管道状态字段(Phase 1.2 引入,Phase 4-6 接入) ━━━━━
|
|
284
286
|
# Pipeline A: text line-commit
|
|
285
287
|
self._text_pending: str = ""
|
|
@@ -289,10 +291,14 @@ class EventRenderer:
|
|
|
289
291
|
self._thinking_batch: str = ""
|
|
290
292
|
# Pipeline C: loading 行 aux
|
|
291
293
|
self._loading_aux_text: str = ""
|
|
294
|
+
self._pending_logs: list[tuple[Literal["warning", "error"], str]] = []
|
|
295
|
+
self._pending_logs_lock = threading.Lock()
|
|
292
296
|
|
|
293
297
|
def _append_history_entry(self, entry: HistoryEntry) -> None:
|
|
294
298
|
self._history.append(entry)
|
|
295
299
|
self._last_history_append_at = time.monotonic()
|
|
300
|
+
if entry.entry_type == "tool_result":
|
|
301
|
+
self.flush_pending_logs()
|
|
296
302
|
|
|
297
303
|
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
298
304
|
# spec §6 流式新管道方法集(Phase 1-6 新增;Phase 7 接入 handle_event)
|
|
@@ -484,7 +490,7 @@ class EventRenderer:
|
|
|
484
490
|
self._text_pending += delta
|
|
485
491
|
while "\n" in self._text_pending:
|
|
486
492
|
line, self._text_pending = self._text_pending.split("\n", 1)
|
|
487
|
-
if line == "" and not self._text_delta_started:
|
|
493
|
+
if line.strip() == "" and not self._text_delta_started:
|
|
488
494
|
continue
|
|
489
495
|
self._text_delta_started = True
|
|
490
496
|
self._handle_complete_line(line)
|
|
@@ -530,6 +536,7 @@ class EventRenderer:
|
|
|
530
536
|
self._thinking_batch = ""
|
|
531
537
|
self._loading_aux_text = ""
|
|
532
538
|
self._text_delta_started = False
|
|
539
|
+
self._active_text_message_id = None
|
|
533
540
|
|
|
534
541
|
def _should_drop_duplicate_system_message(
|
|
535
542
|
self,
|
|
@@ -563,6 +570,7 @@ class EventRenderer:
|
|
|
563
570
|
self._turn_received_text_delta = False
|
|
564
571
|
self._turn_received_thinking_delta = False
|
|
565
572
|
self._text_delta_started = False
|
|
573
|
+
self._active_text_message_id = None
|
|
566
574
|
# spec §6.4:清空新 5 字段(每 turn 起点保证状态干净)
|
|
567
575
|
self._text_pending = ""
|
|
568
576
|
self._held_pipe_line = None
|
|
@@ -570,6 +578,7 @@ class EventRenderer:
|
|
|
570
578
|
self._thinking_batch = ""
|
|
571
579
|
self._loading_aux_text = ""
|
|
572
580
|
self._rebuild_loading_line()
|
|
581
|
+
self.flush_pending_logs()
|
|
573
582
|
|
|
574
583
|
def seed_user_message(self, content: str) -> None:
|
|
575
584
|
normalized = content.strip()
|
|
@@ -578,9 +587,10 @@ class EventRenderer:
|
|
|
578
587
|
self._flush_assistant_segment()
|
|
579
588
|
self._append_history_entry(HistoryEntry(entry_type="user", text=normalized))
|
|
580
589
|
self._maybe_append_file_ref_hint(normalized)
|
|
590
|
+
self.flush_pending_logs()
|
|
581
591
|
|
|
582
592
|
def _maybe_append_file_ref_hint(self, text: str) -> None:
|
|
583
|
-
"""Append
|
|
593
|
+
"""Append dim ⎿ hints for valid @path references."""
|
|
584
594
|
if self._project_root is None:
|
|
585
595
|
return
|
|
586
596
|
for match in FILE_REF_PATTERN.finditer(text):
|
|
@@ -591,7 +601,7 @@ class EventRenderer:
|
|
|
591
601
|
self._append_history_entry(
|
|
592
602
|
HistoryEntry(entry_type="file_ref", text=f"Listed directory {raw_path}")
|
|
593
603
|
)
|
|
594
|
-
|
|
604
|
+
continue
|
|
595
605
|
if candidate.is_file():
|
|
596
606
|
hint = f"Read {raw_path}"
|
|
597
607
|
if candidate.stat().st_size <= _FILE_REF_MAX_COUNT_BYTES:
|
|
@@ -602,7 +612,7 @@ class EventRenderer:
|
|
|
602
612
|
except OSError:
|
|
603
613
|
pass
|
|
604
614
|
self._append_history_entry(HistoryEntry(entry_type="file_ref", text=hint))
|
|
605
|
-
|
|
615
|
+
continue
|
|
606
616
|
except OSError:
|
|
607
617
|
continue
|
|
608
618
|
|
|
@@ -614,6 +624,7 @@ class EventRenderer:
|
|
|
614
624
|
# text_pending / thinking)保证 scrollback 看到完整内容
|
|
615
625
|
self._force_flush_all()
|
|
616
626
|
self._flush_assistant_segment()
|
|
627
|
+
self.flush_pending_logs()
|
|
617
628
|
self._pending_tool_starts.clear()
|
|
618
629
|
self._rebuild_loading_line()
|
|
619
630
|
|
|
@@ -653,6 +664,7 @@ class EventRenderer:
|
|
|
653
664
|
self._turn_received_text_delta = False
|
|
654
665
|
self._turn_received_thinking_delta = False
|
|
655
666
|
self._text_delta_started = False
|
|
667
|
+
self._active_text_message_id = None
|
|
656
668
|
# spec §6.4:清空 5 个新字段(与 start_turn 同语义)
|
|
657
669
|
self._text_pending = ""
|
|
658
670
|
self._held_pipe_line = None
|
|
@@ -664,6 +676,8 @@ class EventRenderer:
|
|
|
664
676
|
self._current_task_title = None
|
|
665
677
|
self._task_started_at_monotonic = None
|
|
666
678
|
self._latest_diff_lines = None
|
|
679
|
+
with self._pending_logs_lock:
|
|
680
|
+
self._pending_logs.clear()
|
|
667
681
|
|
|
668
682
|
@property
|
|
669
683
|
def latest_diff_lines(self) -> list[str] | None:
|
|
@@ -718,12 +732,19 @@ class EventRenderer:
|
|
|
718
732
|
拼接到 spinner phrase。无容器时返回 ""。"""
|
|
719
733
|
return self._loading_aux_text
|
|
720
734
|
|
|
721
|
-
def append_subtitle(
|
|
735
|
+
def append_subtitle(
|
|
736
|
+
self,
|
|
737
|
+
text: str,
|
|
738
|
+
*,
|
|
739
|
+
severity: Literal["info", "warning", "error"] = "info",
|
|
740
|
+
) -> None:
|
|
722
741
|
"""Append a ⎿ subtitle entry, visually attached to the preceding entry."""
|
|
723
742
|
normalized = text.strip()
|
|
724
743
|
if not normalized:
|
|
725
744
|
return
|
|
726
|
-
self._append_history_entry(
|
|
745
|
+
self._append_history_entry(
|
|
746
|
+
HistoryEntry(entry_type="file_ref", text=normalized, severity=severity)
|
|
747
|
+
)
|
|
727
748
|
|
|
728
749
|
def append_system_message(
|
|
729
750
|
self,
|
|
@@ -744,6 +765,39 @@ class EventRenderer:
|
|
|
744
765
|
HistoryEntry(entry_type="system", text=normalized, severity=severity)
|
|
745
766
|
)
|
|
746
767
|
|
|
768
|
+
def enqueue_log(
|
|
769
|
+
self,
|
|
770
|
+
*,
|
|
771
|
+
severity: Literal["warning", "error"],
|
|
772
|
+
message: str,
|
|
773
|
+
) -> None:
|
|
774
|
+
"""线程安全入口:只入队,不调度 prompt_toolkit,不立即写 scrollback。"""
|
|
775
|
+
normalized = message.strip()
|
|
776
|
+
if not normalized:
|
|
777
|
+
return
|
|
778
|
+
with self._pending_logs_lock:
|
|
779
|
+
self._pending_logs.append((severity, normalized))
|
|
780
|
+
|
|
781
|
+
def flush_pending_logs(self) -> None:
|
|
782
|
+
"""Drain pending log,按当前 history anchor 决定 attached 形态。"""
|
|
783
|
+
with self._pending_logs_lock:
|
|
784
|
+
if not self._pending_logs:
|
|
785
|
+
return
|
|
786
|
+
drained = list(self._pending_logs)
|
|
787
|
+
self._pending_logs.clear()
|
|
788
|
+
|
|
789
|
+
has_anchor = bool(self._history)
|
|
790
|
+
for severity, message in drained:
|
|
791
|
+
self._append_history_entry(
|
|
792
|
+
HistoryEntry(
|
|
793
|
+
entry_type="log",
|
|
794
|
+
text=message,
|
|
795
|
+
severity=severity,
|
|
796
|
+
attached=has_anchor,
|
|
797
|
+
)
|
|
798
|
+
)
|
|
799
|
+
has_anchor = True
|
|
800
|
+
|
|
747
801
|
@staticmethod
|
|
748
802
|
def _team_event_key(
|
|
749
803
|
*,
|
|
@@ -943,6 +997,7 @@ class EventRenderer:
|
|
|
943
997
|
HistoryEntry(entry_type="assistant", text=self._assistant_buffer)
|
|
944
998
|
)
|
|
945
999
|
self._assistant_buffer = ""
|
|
1000
|
+
self.flush_pending_logs()
|
|
946
1001
|
|
|
947
1002
|
def _append_assistant_text(self, text: str) -> None:
|
|
948
1003
|
self._assistant_buffer += text
|
|
@@ -1460,8 +1515,12 @@ class EventRenderer:
|
|
|
1460
1515
|
match event:
|
|
1461
1516
|
case SessionInitEvent(session_id=_):
|
|
1462
1517
|
pass
|
|
1463
|
-
case TextDeltaEvent(delta=delta, message_id=
|
|
1518
|
+
case TextDeltaEvent(delta=delta, message_id=message_id):
|
|
1464
1519
|
if delta:
|
|
1520
|
+
normalized_message_id = str(message_id or "").strip()
|
|
1521
|
+
if normalized_message_id and normalized_message_id != self._active_text_message_id:
|
|
1522
|
+
self._active_text_message_id = normalized_message_id
|
|
1523
|
+
self._text_delta_started = False
|
|
1465
1524
|
# spec §6.1:text delta → 累到 _text_pending → 按 \n 切完整行入队
|
|
1466
1525
|
self._consume_text_delta(delta)
|
|
1467
1526
|
self._turn_received_text_delta = True
|
|
@@ -19,14 +19,50 @@ from comate_cli.terminal_agent.message_style import ASSISTANT_PREFIX
|
|
|
19
19
|
from comate_cli.terminal_agent.models import HistoryEntry
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
def _render_subtitle_line(
|
|
23
|
-
|
|
22
|
+
def _render_subtitle_line(
|
|
23
|
+
subtitle: str,
|
|
24
|
+
*,
|
|
25
|
+
error: bool = False,
|
|
26
|
+
warning: bool = False,
|
|
27
|
+
) -> Text:
|
|
28
|
+
"""Render a ⎿ subtitle line for tool results / attached log entries."""
|
|
29
|
+
content_lines = str(subtitle).splitlines() or [""]
|
|
30
|
+
text_style = _subtitle_text_style(error=error, warning=warning)
|
|
31
|
+
|
|
24
32
|
line = Text()
|
|
25
33
|
line.append(f" {BOTTOM_LEFT_CROP} ", style="#555555")
|
|
34
|
+
line.append(f" {content_lines[0]}", style=text_style)
|
|
35
|
+
for continuation in content_lines[1:]:
|
|
36
|
+
line.append("\n")
|
|
37
|
+
line.append(" ")
|
|
38
|
+
line.append(continuation, style=text_style)
|
|
39
|
+
return line
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _subtitle_text_style(*, error: bool = False, warning: bool = False) -> str:
|
|
26
43
|
if error:
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
44
|
+
return "bold #FF6B6B"
|
|
45
|
+
if warning:
|
|
46
|
+
return "#E8B830"
|
|
47
|
+
return "dim"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _render_subtitle_continuation_line(
|
|
51
|
+
subtitle: str,
|
|
52
|
+
*,
|
|
53
|
+
error: bool = False,
|
|
54
|
+
warning: bool = False,
|
|
55
|
+
) -> Text:
|
|
56
|
+
"""Render a consecutive attached subtitle without repeating the ⎿ glyph."""
|
|
57
|
+
content_lines = str(subtitle).splitlines() or [""]
|
|
58
|
+
text_style = _subtitle_text_style(error=error, warning=warning)
|
|
59
|
+
|
|
60
|
+
line = Text()
|
|
61
|
+
for idx, content in enumerate(content_lines):
|
|
62
|
+
if idx > 0:
|
|
63
|
+
line.append("\n")
|
|
64
|
+
line.append(" ")
|
|
65
|
+
line.append(content, style=text_style)
|
|
30
66
|
return line
|
|
31
67
|
|
|
32
68
|
|
|
@@ -100,6 +136,7 @@ def render_history_group(
|
|
|
100
136
|
# 后续行用 2 空格缩进保持视觉对齐。`prev_was_assistant` 把 run 状态跨 drain 传过来。
|
|
101
137
|
renderables: list[Any] = []
|
|
102
138
|
needs_leading_gap_after_thinking = prev_was_thinking
|
|
139
|
+
subtitle_chain_active = False
|
|
103
140
|
for idx, entry in enumerate(entries):
|
|
104
141
|
is_assistant = entry.entry_type == "assistant"
|
|
105
142
|
in_run_continuation = is_assistant and prev_was_assistant
|
|
@@ -116,6 +153,7 @@ def render_history_group(
|
|
|
116
153
|
and next_entry is not None
|
|
117
154
|
and _starts_with_block_marker(next_entry)
|
|
118
155
|
):
|
|
156
|
+
subtitle_chain_active = False
|
|
119
157
|
continue
|
|
120
158
|
|
|
121
159
|
# Thinking entries: 灰色显示,无前缀。thinking 流式文本里的空白行
|
|
@@ -125,6 +163,7 @@ def render_history_group(
|
|
|
125
163
|
content_lines = [line for line in content.splitlines() if line.strip()]
|
|
126
164
|
if not content_lines:
|
|
127
165
|
prev_was_assistant = False
|
|
166
|
+
subtitle_chain_active = False
|
|
128
167
|
continue
|
|
129
168
|
needs_leading_gap_after_thinking = False
|
|
130
169
|
line_text = Text()
|
|
@@ -143,6 +182,7 @@ def render_history_group(
|
|
|
143
182
|
if not next_is_thinking and not suppress_trailing_gap:
|
|
144
183
|
renderables.append(Text(""))
|
|
145
184
|
prev_was_assistant = False
|
|
185
|
+
subtitle_chain_active = False
|
|
146
186
|
continue
|
|
147
187
|
|
|
148
188
|
if needs_leading_gap_after_thinking:
|
|
@@ -156,6 +196,7 @@ def render_history_group(
|
|
|
156
196
|
renderables.append(line_text)
|
|
157
197
|
renderables.append(Text(""))
|
|
158
198
|
prev_was_assistant = False
|
|
199
|
+
subtitle_chain_active = False
|
|
159
200
|
continue
|
|
160
201
|
|
|
161
202
|
# System entries: 按 severity 区分视觉样式,缩进 2 空格与消息内容列对齐
|
|
@@ -176,15 +217,28 @@ def render_history_group(
|
|
|
176
217
|
renderables.append(line_text)
|
|
177
218
|
renderables.append(Text(""))
|
|
178
219
|
prev_was_assistant = False
|
|
220
|
+
subtitle_chain_active = False
|
|
179
221
|
continue
|
|
180
222
|
|
|
181
223
|
# File reference hint: reuse subtitle style, attached to preceding user message
|
|
182
224
|
if entry.entry_type == "file_ref":
|
|
183
225
|
if renderables and isinstance(renderables[-1], Text) and not renderables[-1].plain:
|
|
184
226
|
renderables.pop()
|
|
185
|
-
|
|
227
|
+
render_subtitle = (
|
|
228
|
+
_render_subtitle_continuation_line
|
|
229
|
+
if subtitle_chain_active
|
|
230
|
+
else _render_subtitle_line
|
|
231
|
+
)
|
|
232
|
+
renderables.append(
|
|
233
|
+
render_subtitle(
|
|
234
|
+
str(entry.text),
|
|
235
|
+
error=(entry.severity == "error"),
|
|
236
|
+
warning=(entry.severity == "warning"),
|
|
237
|
+
)
|
|
238
|
+
)
|
|
186
239
|
renderables.append(Text(""))
|
|
187
240
|
prev_was_assistant = False
|
|
241
|
+
subtitle_chain_active = True
|
|
188
242
|
continue
|
|
189
243
|
|
|
190
244
|
if entry.entry_type == "tool_result":
|
|
@@ -206,6 +260,7 @@ def render_history_group(
|
|
|
206
260
|
renderables.append(line)
|
|
207
261
|
renderables.append(Text(""))
|
|
208
262
|
prev_was_assistant = False
|
|
263
|
+
subtitle_chain_active = bool(entry.subtitle)
|
|
209
264
|
continue
|
|
210
265
|
|
|
211
266
|
content = str(entry.text)
|
|
@@ -224,6 +279,45 @@ def render_history_group(
|
|
|
224
279
|
renderables.append(_render_subtitle_line(entry.subtitle, error=is_error))
|
|
225
280
|
renderables.append(Text(""))
|
|
226
281
|
prev_was_assistant = False
|
|
282
|
+
subtitle_chain_active = bool(entry.subtitle)
|
|
283
|
+
continue
|
|
284
|
+
|
|
285
|
+
if entry.entry_type == "log":
|
|
286
|
+
if entry.attached:
|
|
287
|
+
if renderables and isinstance(renderables[-1], Text) and not renderables[-1].plain:
|
|
288
|
+
renderables.pop()
|
|
289
|
+
render_subtitle = (
|
|
290
|
+
_render_subtitle_continuation_line
|
|
291
|
+
if subtitle_chain_active
|
|
292
|
+
else _render_subtitle_line
|
|
293
|
+
)
|
|
294
|
+
renderables.append(
|
|
295
|
+
render_subtitle(
|
|
296
|
+
str(entry.text),
|
|
297
|
+
error=(entry.severity == "error"),
|
|
298
|
+
warning=(entry.severity == "warning"),
|
|
299
|
+
)
|
|
300
|
+
)
|
|
301
|
+
subtitle_chain_active = True
|
|
302
|
+
else:
|
|
303
|
+
prefix_char = HEAVY_MULTIPLICATION if entry.severity == "error" else BLACK_CIRCLE
|
|
304
|
+
prefix_style = "bold #FF6B6B" if entry.severity == "error" else "#E8B830"
|
|
305
|
+
content = str(entry.text)
|
|
306
|
+
content_lines = content.splitlines() or [""]
|
|
307
|
+
line_text = Text()
|
|
308
|
+
line_text.append(f"{prefix_char} ", style=prefix_style)
|
|
309
|
+
line_text.append(content_lines[0], style=prefix_style)
|
|
310
|
+
for line in content_lines[1:]:
|
|
311
|
+
line_text.append("\n")
|
|
312
|
+
line_text.append(" ")
|
|
313
|
+
line_text.append(line, style=prefix_style)
|
|
314
|
+
renderables.append(line_text)
|
|
315
|
+
subtitle_chain_active = False
|
|
316
|
+
|
|
317
|
+
next_is_log = next_entry is not None and next_entry.entry_type == "log"
|
|
318
|
+
if not next_is_log:
|
|
319
|
+
renderables.append(Text(""))
|
|
320
|
+
prev_was_assistant = False
|
|
227
321
|
continue
|
|
228
322
|
|
|
229
323
|
if hasattr(entry.text, "__rich_console__"):
|
|
@@ -264,6 +358,7 @@ def render_history_group(
|
|
|
264
358
|
if not (is_assistant and next_is_assistant) and not suppress_trailing_gap:
|
|
265
359
|
renderables.append(Text(""))
|
|
266
360
|
prev_was_assistant = is_assistant
|
|
361
|
+
subtitle_chain_active = False
|
|
267
362
|
|
|
268
363
|
if not renderables:
|
|
269
364
|
return None
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""TUI logging adapter -
|
|
1
|
+
"""TUI logging adapter - 将 warning/error 日志送入 renderer pending 队列。"""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import logging
|
|
@@ -7,9 +7,7 @@ import sys
|
|
|
7
7
|
import threading
|
|
8
8
|
import time
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from typing import TYPE_CHECKING
|
|
11
|
-
|
|
12
|
-
from prompt_toolkit.application import run_in_terminal
|
|
10
|
+
from typing import TYPE_CHECKING, Literal
|
|
13
11
|
|
|
14
12
|
if TYPE_CHECKING:
|
|
15
13
|
from comate_cli.terminal_agent.event_renderer import EventRenderer
|
|
@@ -52,7 +50,7 @@ class TUILoggingHandler(logging.Handler):
|
|
|
52
50
|
特性:
|
|
53
51
|
- WARNING/ERROR 显示给用户,带颜色标识
|
|
54
52
|
- 首次显示机制:相同消息只显示一次
|
|
55
|
-
-
|
|
53
|
+
- 仅入 EventRenderer pending 队列,由明确 boundary 写入 scrollback
|
|
56
54
|
"""
|
|
57
55
|
|
|
58
56
|
def __init__(self, renderer: EventRenderer) -> None:
|
|
@@ -81,11 +79,13 @@ class TUILoggingHandler(logging.Handler):
|
|
|
81
79
|
self._shown_messages.add(msg_key)
|
|
82
80
|
|
|
83
81
|
# 按日志级别映射 severity,渲染层负责视觉区分
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
82
|
+
severity: Literal["warning", "error"] = (
|
|
83
|
+
"error" if record.levelno >= logging.ERROR else "warning"
|
|
84
|
+
)
|
|
85
|
+
self._renderer.enqueue_log(
|
|
86
|
+
severity=severity,
|
|
87
|
+
message=msg,
|
|
88
|
+
)
|
|
89
89
|
|
|
90
90
|
except Exception:
|
|
91
91
|
self.handleError(record)
|
|
@@ -129,29 +129,6 @@ class TUILoggingHandler(logging.Handler):
|
|
|
129
129
|
# 其他消息用完整内容作为 key
|
|
130
130
|
return f"{record.name}:{msg[:50]}"
|
|
131
131
|
|
|
132
|
-
def _append_to_tui(self, msg: str, *, severity: str = "warning") -> None:
|
|
133
|
-
"""将消息追加到 TUI(通过 run_in_terminal 避免破坏界面)"""
|
|
134
|
-
def _append() -> None:
|
|
135
|
-
self._renderer.append_system_message(msg, severity=severity) # type: ignore[arg-type]
|
|
136
|
-
|
|
137
|
-
# 使用 run_in_terminal 确保不破坏 prompt_toolkit 的输入
|
|
138
|
-
# 如果没有运行中的真实 Application,直接调用(测试或初始化阶段)
|
|
139
|
-
try:
|
|
140
|
-
from prompt_toolkit.application import get_app
|
|
141
|
-
from prompt_toolkit.application.dummy import DummyApplication
|
|
142
|
-
app = get_app()
|
|
143
|
-
if isinstance(app, DummyApplication):
|
|
144
|
-
# DummyApplication,直接调用
|
|
145
|
-
_append()
|
|
146
|
-
else:
|
|
147
|
-
# run_in_terminal() 自身已经会调度 Future;这里不能再交给
|
|
148
|
-
# create_background_task() 二次包装,否则会触发重复执行竞态。
|
|
149
|
-
run_in_terminal(_append, in_executor=False)
|
|
150
|
-
except Exception:
|
|
151
|
-
# 没有 app 或导入失败,直接调用
|
|
152
|
-
_append()
|
|
153
|
-
|
|
154
|
-
|
|
155
132
|
class _DropTerminalStreamFilter(logging.Filter):
|
|
156
133
|
def filter(self, record: logging.LogRecord) -> bool:
|
|
157
134
|
del record
|
|
@@ -348,7 +325,7 @@ def setup_tui_logging(
|
|
|
348
325
|
setattr(tui_handler, _COMATE_LOGGING_SESSION_MARKER, True)
|
|
349
326
|
root.addHandler(tui_handler)
|
|
350
327
|
|
|
351
|
-
# 3. 临时静默直写终端的 handler
|
|
328
|
+
# 3. 临时静默直写终端的 handler,避免污染主 TUI 输出
|
|
352
329
|
muted_handlers: list[tuple[logging.Handler, logging.Filter]] = []
|
|
353
330
|
for handler in list(root.handlers):
|
|
354
331
|
if handler in {file_handler, tui_handler}:
|
|
@@ -8,7 +8,17 @@ if TYPE_CHECKING:
|
|
|
8
8
|
from rich.text import Text
|
|
9
9
|
|
|
10
10
|
ToolStatus = Literal["running", "success", "error"]
|
|
11
|
-
HistoryEntryType = Literal[
|
|
11
|
+
HistoryEntryType = Literal[
|
|
12
|
+
"user",
|
|
13
|
+
"assistant",
|
|
14
|
+
"tool_call",
|
|
15
|
+
"tool_result",
|
|
16
|
+
"system",
|
|
17
|
+
"thinking",
|
|
18
|
+
"elapsed",
|
|
19
|
+
"file_ref",
|
|
20
|
+
"log",
|
|
21
|
+
]
|
|
12
22
|
|
|
13
23
|
|
|
14
24
|
class LoadingStateType(Enum):
|
|
@@ -81,6 +91,7 @@ class HistoryEntry:
|
|
|
81
91
|
text: str | Text # 支持普通字符串或 Rich Text 对象
|
|
82
92
|
severity: Literal["info", "warning", "error"] = "info"
|
|
83
93
|
subtitle: str | None = None
|
|
94
|
+
attached: bool = False
|
|
84
95
|
|
|
85
96
|
|
|
86
97
|
@dataclass(frozen=True)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Tips displayed while a turn is running."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
LOADING_TIPS: tuple[str, ...] = (
|
|
5
|
+
"Use /context when a task starts to feel fuzzy.",
|
|
6
|
+
"Attach @files to make the model read the right surface first.",
|
|
7
|
+
"Use Shift+Tab to switch between act and plan mode.",
|
|
8
|
+
"Use /compact before a long follow-up in the same session.",
|
|
9
|
+
"Press Ctrl+C once to interrupt the current operation.",
|
|
10
|
+
"Use /rewind when the last turn took the wrong branch.",
|
|
11
|
+
"Ask for source-level diagnosis before changing brittle code.",
|
|
12
|
+
"Name the failing boundary before patching behavior.",
|
|
13
|
+
"Prefer one visible behavior change per small patch.",
|
|
14
|
+
"Keep queue messages invisible until they are consumed.",
|
|
15
|
+
"Check scrollback output separately from live TUI state.",
|
|
16
|
+
"For TUI bugs, separate layout state from history output.",
|
|
17
|
+
"Use @path hints when the answer depends on local files.",
|
|
18
|
+
"Review tests around the boundary you are about to touch.",
|
|
19
|
+
"Use /model when the task needs a different reasoning tier.",
|
|
20
|
+
"Small focused prompts usually beat broad fuzzy requests.",
|
|
21
|
+
)
|
|
@@ -71,6 +71,7 @@ from comate_cli.terminal_agent.slash_commands import (
|
|
|
71
71
|
SlashCommandSpec,
|
|
72
72
|
)
|
|
73
73
|
from comate_cli.terminal_agent.status_bar import StatusBar
|
|
74
|
+
from comate_cli.terminal_agent.tips import LOADING_TIPS
|
|
74
75
|
from comate_cli.terminal_agent.tui_parts import (
|
|
75
76
|
CommandsMixin,
|
|
76
77
|
HistorySyncMixin,
|
|
@@ -83,6 +84,8 @@ from comate_cli.terminal_agent.tui_parts import (
|
|
|
83
84
|
|
|
84
85
|
logger = logging.getLogger(__name__)
|
|
85
86
|
|
|
87
|
+
_LOADING_TIP_DELAY_SECONDS = 2.5
|
|
88
|
+
|
|
86
89
|
_TASK_POLL_INTERVAL_S = 2.0
|
|
87
90
|
_INPUT_HISTORY_DIR = Path.home() / ".agent" / "history"
|
|
88
91
|
_INPUT_HISTORY_MAX_ENTRIES = 200
|
|
@@ -250,6 +253,8 @@ class TerminalAgentTUI(
|
|
|
250
253
|
else f"Flibbertigibbeting{ELLIPSIS}"
|
|
251
254
|
)
|
|
252
255
|
self._fallback_phrase_refresh_at = 0.0
|
|
256
|
+
self._loading_tip_text = ""
|
|
257
|
+
self._loading_tip_show_at_monotonic: float | None = None
|
|
253
258
|
|
|
254
259
|
self._tool_result_flash_seconds = read_env_float(
|
|
255
260
|
"AGENT_SDK_TUI_TOOL_RESULT_FLASH_SECONDS",
|
|
@@ -876,7 +881,16 @@ class TerminalAgentTUI(
|
|
|
876
881
|
)
|
|
877
882
|
|
|
878
883
|
def _set_busy(self, value: bool) -> None:
|
|
884
|
+
was_busy = self._busy
|
|
879
885
|
self._busy = value
|
|
886
|
+
if value and not was_busy:
|
|
887
|
+
self._loading_tip_text = random.choice(LOADING_TIPS) if LOADING_TIPS else ""
|
|
888
|
+
self._loading_tip_show_at_monotonic = (
|
|
889
|
+
time.monotonic() + _LOADING_TIP_DELAY_SECONDS
|
|
890
|
+
)
|
|
891
|
+
elif not value:
|
|
892
|
+
self._loading_tip_text = ""
|
|
893
|
+
self._loading_tip_show_at_monotonic = None
|
|
880
894
|
self._render_dirty = True
|
|
881
895
|
self._invalidate()
|
|
882
896
|
|
|
@@ -1447,13 +1461,13 @@ class TerminalAgentTUI(
|
|
|
1447
1461
|
if not self._compact_cancel_requested:
|
|
1448
1462
|
self._compact_cancel_requested = True
|
|
1449
1463
|
self._renderer.append_system_message(
|
|
1450
|
-
"
|
|
1464
|
+
"Cancelling /compact. The app will exit after rollback finishes.",
|
|
1451
1465
|
severity="warning",
|
|
1452
1466
|
)
|
|
1453
1467
|
self._compact_task.cancel()
|
|
1454
1468
|
else:
|
|
1455
1469
|
self._renderer.append_system_message(
|
|
1456
|
-
"
|
|
1470
|
+
"/compact cancellation already requested. Waiting for rollback to finish.",
|
|
1457
1471
|
severity="warning",
|
|
1458
1472
|
)
|
|
1459
1473
|
self._refresh_layers()
|