comate-cli 0.4.8__tar.gz → 0.5.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.4.8 → comate_cli-0.5.1}/.gitignore +1 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/PKG-INFO +1 -1
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/event_renderer.py +71 -24
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/logging_adapter.py +65 -4
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/tui.py +31 -4
- {comate_cli-0.4.8 → comate_cli-0.5.1}/pyproject.toml +1 -1
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_event_renderer.py +36 -0
- comate_cli-0.5.1/tests/test_logging_adapter.py +166 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/uv.lock +25 -2
- comate_cli-0.4.8/tests/test_logging_adapter.py +0 -68
- {comate_cli-0.4.8 → comate_cli-0.5.1}/README.md +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/__init__.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/__main__.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/main.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/mcp_cli.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/__init__.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/animations.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/app.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/assistant_render.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/codenames.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/custom_slash_commands.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/env_utils.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/error_display.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/fragment_utils.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/history_printer.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/input_geometry.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/layout_coordinator.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/logo.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/markdown_render.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/mention_completer.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/message_style.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/models.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/path_context_hint.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/plugins/__init__.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/plugins/components/__init__.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/plugins/components/detail_view.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/plugins/components/plugin_list.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/plugins/components/search_box.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/plugins/components/tab_bar.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/plugins/marketplace_install_view.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/plugins/plugin_picker.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/plugins/tabs/__init__.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/plugins/tabs/discover_tab.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/plugins/tabs/errors_tab.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/plugins/tabs/installed_tab.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/plugins/tabs/marketplaces_tab.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/preflight.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/question_view.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/resume_selector.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/rewind_store.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/rpc_protocol.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/rpc_stdio.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/selection_menu.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/session_view.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/slash_commands.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/startup.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/status_bar.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/text_effects.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/tips.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/tool_view.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/tui_parts/__init__.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/tui_parts/commands.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/tui_parts/history_sync.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/tui_parts/input_behavior.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/tui_parts/key_bindings.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/tui_parts/mcp_connecting_view.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/tui_parts/render_panels.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/tui_parts/slash_command_registry.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/comate_cli/terminal_agent/tui_parts/ui_mode.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/docs/superpowers/plans/2026-04-03-phrase-shuffle.md +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/docs/superpowers/specs/2026-04-01-conditional-diff-subtitle-design.md +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/docs/superpowers/specs/2026-04-03-phrase-shuffle-design.md +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/conftest.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_animator_shuffle.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_app_mcp_preload.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_app_preflight_gate.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_app_print_mode.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_app_shutdown.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_app_usage_line.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_cli_project_root.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_compact_command_semantics.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_completion_context_activation.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_completion_status_panel.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_context_command.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_custom_slash_commands.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_discover_tab.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_errors_tab.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_format_error.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_handle_error.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_history_printer.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_history_sync.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_input_behavior.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_input_history.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_installed_tab.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_interrupt_exit_semantics.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_layout_coordinator.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_logo.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_main_args.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_marketplaces_tab.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_mcp_cli.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_mcp_slash_command.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_mention_completer.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_path_context_hint.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_plugin_slash_commands.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_plugin_tui_components.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_preflight.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_preflight_copilot.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_question_key_bindings.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_question_view.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_resume_selector.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_rewind_command_semantics.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_rewind_store.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_rpc_protocol.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_rpc_stdio_bridge.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_selection_menu.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_skills_slash_command.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_slash_argument_hint.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_slash_completer.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_slash_registry.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_status_bar.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_status_bar_transient.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_task_panel_format.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_task_panel_key_bindings.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_task_panel_rendering.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_task_poll.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_tool_view.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_tui_elapsed_status.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_tui_mcp_init_gate.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_tui_paste_placeholder.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_tui_split_invariance.py +0 -0
- {comate_cli-0.4.8 → comate_cli-0.5.1}/tests/test_update_check.py +0 -0
|
@@ -43,6 +43,7 @@ _DEFAULT_TOOL_PANEL_MAX_LINES = 4
|
|
|
43
43
|
_DEFAULT_TASK_PANEL_MAX_LINES = 6
|
|
44
44
|
_RECENT_TEAM_EVENT_CACHE_SIZE = 128
|
|
45
45
|
_FILE_REF_MAX_COUNT_BYTES = 10 * 1024 * 1024 # 10 MB
|
|
46
|
+
_SYSTEM_MESSAGE_DEDUPE_WINDOW_SECONDS = 0.5
|
|
46
47
|
|
|
47
48
|
|
|
48
49
|
def _truncate(content: str, max_len: int = 120) -> str:
|
|
@@ -207,6 +208,37 @@ class EventRenderer:
|
|
|
207
208
|
self._recent_team_event_keys: deque[tuple[str, str, str, str, str, str]] = deque(
|
|
208
209
|
maxlen=_RECENT_TEAM_EVENT_CACHE_SIZE
|
|
209
210
|
)
|
|
211
|
+
self._last_history_append_at: float = 0.0
|
|
212
|
+
|
|
213
|
+
def _append_history_entry(self, entry: HistoryEntry) -> None:
|
|
214
|
+
self._history.append(entry)
|
|
215
|
+
self._last_history_append_at = time.monotonic()
|
|
216
|
+
|
|
217
|
+
def _should_drop_duplicate_system_message(
|
|
218
|
+
self,
|
|
219
|
+
*,
|
|
220
|
+
content: str,
|
|
221
|
+
severity: Literal["info", "warning", "error"],
|
|
222
|
+
) -> bool:
|
|
223
|
+
if severity not in {"warning", "error"}:
|
|
224
|
+
return False
|
|
225
|
+
if not self._history:
|
|
226
|
+
return False
|
|
227
|
+
last_entry = self._history[-1]
|
|
228
|
+
if last_entry.entry_type != "system":
|
|
229
|
+
return False
|
|
230
|
+
if last_entry.severity != severity:
|
|
231
|
+
return False
|
|
232
|
+
if str(last_entry.text).strip() != content:
|
|
233
|
+
return False
|
|
234
|
+
if time.monotonic() - self._last_history_append_at > _SYSTEM_MESSAGE_DEDUPE_WINDOW_SECONDS:
|
|
235
|
+
return False
|
|
236
|
+
logger.debug(
|
|
237
|
+
"Skip duplicate system message in scrollback: severity=%s content=%r",
|
|
238
|
+
severity,
|
|
239
|
+
content,
|
|
240
|
+
)
|
|
241
|
+
return True
|
|
210
242
|
|
|
211
243
|
def start_turn(self) -> None:
|
|
212
244
|
self._flush_assistant_segment()
|
|
@@ -218,7 +250,7 @@ class EventRenderer:
|
|
|
218
250
|
if not normalized:
|
|
219
251
|
return
|
|
220
252
|
self._flush_assistant_segment()
|
|
221
|
-
self.
|
|
253
|
+
self._append_history_entry(HistoryEntry(entry_type="user", text=normalized))
|
|
222
254
|
self._maybe_append_file_ref_hint(normalized)
|
|
223
255
|
|
|
224
256
|
def _maybe_append_file_ref_hint(self, text: str) -> None:
|
|
@@ -230,7 +262,9 @@ class EventRenderer:
|
|
|
230
262
|
candidate = self._project_root / raw_path
|
|
231
263
|
try:
|
|
232
264
|
if candidate.is_dir():
|
|
233
|
-
self.
|
|
265
|
+
self._append_history_entry(
|
|
266
|
+
HistoryEntry(entry_type="file_ref", text=f"Listed directory {raw_path}")
|
|
267
|
+
)
|
|
234
268
|
return
|
|
235
269
|
if candidate.is_file():
|
|
236
270
|
hint = f"Read {raw_path}"
|
|
@@ -241,7 +275,7 @@ class EventRenderer:
|
|
|
241
275
|
hint += f" ({line_count} lines)"
|
|
242
276
|
except OSError:
|
|
243
277
|
pass
|
|
244
|
-
self.
|
|
278
|
+
self._append_history_entry(HistoryEntry(entry_type="file_ref", text=hint))
|
|
245
279
|
return
|
|
246
280
|
except OSError:
|
|
247
281
|
continue
|
|
@@ -261,7 +295,7 @@ class EventRenderer:
|
|
|
261
295
|
|
|
262
296
|
def interrupt_turn(self) -> None:
|
|
263
297
|
if self._running_tools:
|
|
264
|
-
self.
|
|
298
|
+
self._append_history_entry(
|
|
265
299
|
HistoryEntry(
|
|
266
300
|
entry_type="system",
|
|
267
301
|
text=f"Current task interrupted ({len(self._running_tools)} running tools)",
|
|
@@ -343,7 +377,7 @@ class EventRenderer:
|
|
|
343
377
|
normalized = text.strip()
|
|
344
378
|
if not normalized:
|
|
345
379
|
return
|
|
346
|
-
self.
|
|
380
|
+
self._append_history_entry(HistoryEntry(entry_type="file_ref", text=normalized))
|
|
347
381
|
|
|
348
382
|
def append_system_message(
|
|
349
383
|
self,
|
|
@@ -354,8 +388,13 @@ class EventRenderer:
|
|
|
354
388
|
normalized = content.strip()
|
|
355
389
|
if not normalized:
|
|
356
390
|
return
|
|
391
|
+
if self._should_drop_duplicate_system_message(
|
|
392
|
+
content=normalized,
|
|
393
|
+
severity=severity,
|
|
394
|
+
):
|
|
395
|
+
return
|
|
357
396
|
self._flush_assistant_segment()
|
|
358
|
-
self.
|
|
397
|
+
self._append_history_entry(
|
|
359
398
|
HistoryEntry(entry_type="system", text=normalized, severity=severity)
|
|
360
399
|
)
|
|
361
400
|
|
|
@@ -433,14 +472,14 @@ class EventRenderer:
|
|
|
433
472
|
if not normalized:
|
|
434
473
|
return
|
|
435
474
|
self._flush_assistant_segment()
|
|
436
|
-
self.
|
|
475
|
+
self._append_history_entry(HistoryEntry(entry_type="elapsed", text=normalized))
|
|
437
476
|
|
|
438
477
|
def append_assistant_message(self, content: str) -> None:
|
|
439
478
|
normalized = content.strip()
|
|
440
479
|
if not normalized:
|
|
441
480
|
return
|
|
442
481
|
self._flush_assistant_segment()
|
|
443
|
-
self.
|
|
482
|
+
self._append_history_entry(HistoryEntry(entry_type="assistant", text=normalized))
|
|
444
483
|
|
|
445
484
|
def tool_panel_entries(
|
|
446
485
|
self, *, max_lines: int | None = None
|
|
@@ -554,7 +593,9 @@ class EventRenderer:
|
|
|
554
593
|
def _flush_assistant_segment(self) -> None:
|
|
555
594
|
if not self._assistant_buffer:
|
|
556
595
|
return
|
|
557
|
-
self.
|
|
596
|
+
self._append_history_entry(
|
|
597
|
+
HistoryEntry(entry_type="assistant", text=self._assistant_buffer)
|
|
598
|
+
)
|
|
558
599
|
self._assistant_buffer = ""
|
|
559
600
|
|
|
560
601
|
def _append_assistant_text(self, text: str) -> None:
|
|
@@ -700,18 +741,18 @@ class EventRenderer:
|
|
|
700
741
|
text_obj.append(f" · {model_name}", style="dim")
|
|
701
742
|
text_obj.append("\n")
|
|
702
743
|
text_obj.append(self._render_diff_text(diff_lines, max_lines=self._diff_max_lines))
|
|
703
|
-
self.
|
|
744
|
+
self._append_history_entry(
|
|
704
745
|
HistoryEntry(entry_type="tool_result", text=text_obj, severity="info", subtitle=subtitle)
|
|
705
746
|
)
|
|
706
747
|
return
|
|
707
748
|
if model_name:
|
|
708
749
|
text_obj = Text(signature)
|
|
709
750
|
text_obj.append(f" · {model_name}", style="dim")
|
|
710
|
-
self.
|
|
751
|
+
self._append_history_entry(
|
|
711
752
|
HistoryEntry(entry_type="tool_result", text=text_obj, severity=sev, subtitle=subtitle)
|
|
712
753
|
)
|
|
713
754
|
return
|
|
714
|
-
self.
|
|
755
|
+
self._append_history_entry(
|
|
715
756
|
HistoryEntry(
|
|
716
757
|
entry_type="tool_result",
|
|
717
758
|
text=signature,
|
|
@@ -764,7 +805,9 @@ class EventRenderer:
|
|
|
764
805
|
if state is None:
|
|
765
806
|
display_name = resolve_display_tool_name(tool_name, {})
|
|
766
807
|
signature = _tool_signature(display_name, "")
|
|
767
|
-
self.
|
|
808
|
+
self._append_history_entry(
|
|
809
|
+
HistoryEntry(entry_type="tool_result", text=signature, severity=sev, subtitle=subtitle)
|
|
810
|
+
)
|
|
768
811
|
return
|
|
769
812
|
|
|
770
813
|
if state.is_task:
|
|
@@ -811,15 +854,19 @@ class EventRenderer:
|
|
|
811
854
|
text_obj = Text(base)
|
|
812
855
|
text_obj.append("\n")
|
|
813
856
|
text_obj.append(self._render_diff_text(diff_lines, max_lines=self._diff_max_lines))
|
|
814
|
-
self.
|
|
857
|
+
self._append_history_entry(
|
|
815
858
|
HistoryEntry(entry_type="tool_result", text=text_obj, severity="info", subtitle=subtitle)
|
|
816
859
|
)
|
|
817
860
|
return
|
|
818
861
|
|
|
819
862
|
if isinstance(base, Text):
|
|
820
|
-
self.
|
|
863
|
+
self._append_history_entry(
|
|
864
|
+
HistoryEntry(entry_type="tool_result", text=base, severity=sev, subtitle=subtitle)
|
|
865
|
+
)
|
|
821
866
|
else:
|
|
822
|
-
self.
|
|
867
|
+
self._append_history_entry(
|
|
868
|
+
HistoryEntry(entry_type="tool_result", text=base, severity=sev, subtitle=subtitle)
|
|
869
|
+
)
|
|
823
870
|
|
|
824
871
|
@staticmethod
|
|
825
872
|
def _render_diff_text(diff_lines: list[str], max_lines: int = 50) -> Text:
|
|
@@ -900,7 +947,7 @@ class EventRenderer:
|
|
|
900
947
|
def _append_questions(self, questions: list[dict[str, Any]]) -> None:
|
|
901
948
|
if not questions:
|
|
902
949
|
return
|
|
903
|
-
self.
|
|
950
|
+
self._append_history_entry(
|
|
904
951
|
HistoryEntry(
|
|
905
952
|
entry_type="tool_result",
|
|
906
953
|
text=f"Input required: {len(questions)} question(s) pending.",
|
|
@@ -919,7 +966,7 @@ class EventRenderer:
|
|
|
919
966
|
if label:
|
|
920
967
|
labels.append(label)
|
|
921
968
|
choice_preview = f" (Options: {' / '.join(labels)})" if labels else ""
|
|
922
|
-
self.
|
|
969
|
+
self._append_history_entry(
|
|
923
970
|
HistoryEntry(
|
|
924
971
|
entry_type="tool_result",
|
|
925
972
|
text=f" {idx}. {header}: {question_text} {choice_preview}".strip(),
|
|
@@ -962,7 +1009,7 @@ class EventRenderer:
|
|
|
962
1009
|
if started is not None:
|
|
963
1010
|
elapsed_suffix = f" · {_format_duration(time.monotonic() - started)}"
|
|
964
1011
|
total = len(normalized)
|
|
965
|
-
self.
|
|
1012
|
+
self._append_history_entry(
|
|
966
1013
|
HistoryEntry(
|
|
967
1014
|
entry_type="tool_result",
|
|
968
1015
|
text=f"tasks {total}/{total} completed{elapsed_suffix}",
|
|
@@ -1042,7 +1089,7 @@ class EventRenderer:
|
|
|
1042
1089
|
pass
|
|
1043
1090
|
case ThinkingEvent(content=thinking):
|
|
1044
1091
|
self._thinking_content = thinking
|
|
1045
|
-
self.
|
|
1092
|
+
self._append_history_entry(HistoryEntry(entry_type="thinking", text=thinking))
|
|
1046
1093
|
case CompactionResultEvent(
|
|
1047
1094
|
current_tokens=tokens,
|
|
1048
1095
|
threshold=threshold,
|
|
@@ -1207,7 +1254,7 @@ class EventRenderer:
|
|
|
1207
1254
|
if reason == "waiting_for_plan_approval":
|
|
1208
1255
|
return (False, None)
|
|
1209
1256
|
if reason == "interrupted":
|
|
1210
|
-
self.
|
|
1257
|
+
self._append_history_entry(
|
|
1211
1258
|
HistoryEntry(entry_type="system", text="Current task interrupted.", severity="warning")
|
|
1212
1259
|
)
|
|
1213
1260
|
case PlanApprovalRequiredEvent(
|
|
@@ -1225,18 +1272,18 @@ class EventRenderer:
|
|
|
1225
1272
|
except Exception:
|
|
1226
1273
|
logger.warning("ExitPlanMode: Failed to read plan file %s", plan_path)
|
|
1227
1274
|
if plan_content:
|
|
1228
|
-
self.
|
|
1275
|
+
self._append_history_entry(
|
|
1229
1276
|
HistoryEntry(
|
|
1230
1277
|
entry_type="system",
|
|
1231
1278
|
text="─── Here is the plan, please review and approve or reject ───",
|
|
1232
1279
|
)
|
|
1233
1280
|
)
|
|
1234
|
-
self.
|
|
1281
|
+
self._append_history_entry(HistoryEntry(entry_type="assistant", text=plan_content))
|
|
1235
1282
|
|
|
1236
1283
|
text = f"Plan ready for review: {plan_path}"
|
|
1237
1284
|
if summary:
|
|
1238
1285
|
text = f"{text} | {summary}"
|
|
1239
|
-
self.
|
|
1286
|
+
self._append_history_entry(
|
|
1240
1287
|
HistoryEntry(entry_type="system", text=text)
|
|
1241
1288
|
)
|
|
1242
1289
|
case TeamMessageEvent(
|
|
@@ -4,6 +4,8 @@ from __future__ import annotations
|
|
|
4
4
|
import logging
|
|
5
5
|
import os
|
|
6
6
|
import sys
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
7
9
|
from typing import TYPE_CHECKING
|
|
8
10
|
|
|
9
11
|
from prompt_toolkit.application import run_in_terminal
|
|
@@ -12,6 +14,30 @@ if TYPE_CHECKING:
|
|
|
12
14
|
from comate_cli.terminal_agent.event_renderer import EventRenderer
|
|
13
15
|
|
|
14
16
|
|
|
17
|
+
_COMATE_LOGGING_SESSION_MARKER = "_comate_tui_logging_session"
|
|
18
|
+
_RECENT_DUPLICATE_LOG_WINDOW_SECONDS = 0.5
|
|
19
|
+
_recent_log_message_times: dict[str, float] = {}
|
|
20
|
+
_recent_log_message_lock = threading.Lock()
|
|
21
|
+
_active_tui_logging_session: "TUILoggingSession | None" = None
|
|
22
|
+
_active_tui_logging_session_lock = threading.Lock()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _should_drop_recent_duplicate_log(msg_key: str) -> bool:
|
|
26
|
+
now = time.monotonic()
|
|
27
|
+
with _recent_log_message_lock:
|
|
28
|
+
expired_keys = [
|
|
29
|
+
key
|
|
30
|
+
for key, ts in _recent_log_message_times.items()
|
|
31
|
+
if now - ts > _RECENT_DUPLICATE_LOG_WINDOW_SECONDS
|
|
32
|
+
]
|
|
33
|
+
for key in expired_keys:
|
|
34
|
+
_recent_log_message_times.pop(key, None)
|
|
35
|
+
|
|
36
|
+
previous = _recent_log_message_times.get(msg_key)
|
|
37
|
+
_recent_log_message_times[msg_key] = now
|
|
38
|
+
return previous is not None
|
|
39
|
+
|
|
40
|
+
|
|
15
41
|
class TUILoggingHandler(logging.Handler):
|
|
16
42
|
"""自定义 logging handler,将日志友好地显示在 TUI 中
|
|
17
43
|
|
|
@@ -40,6 +66,8 @@ class TUILoggingHandler(logging.Handler):
|
|
|
40
66
|
|
|
41
67
|
# 首次显示检查
|
|
42
68
|
msg_key = self._get_message_key(record)
|
|
69
|
+
if _should_drop_recent_duplicate_log(msg_key):
|
|
70
|
+
return
|
|
43
71
|
if msg_key in self._shown_messages:
|
|
44
72
|
return
|
|
45
73
|
self._shown_messages.add(msg_key)
|
|
@@ -108,9 +136,9 @@ class TUILoggingHandler(logging.Handler):
|
|
|
108
136
|
# DummyApplication,直接调用
|
|
109
137
|
_append()
|
|
110
138
|
else:
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
)
|
|
139
|
+
# run_in_terminal() 自身已经会调度 Future;这里不能再交给
|
|
140
|
+
# create_background_task() 二次包装,否则会触发重复执行竞态。
|
|
141
|
+
run_in_terminal(_append, in_executor=False)
|
|
114
142
|
except Exception:
|
|
115
143
|
# 没有 app 或导入失败,直接调用
|
|
116
144
|
_append()
|
|
@@ -159,6 +187,10 @@ class TUILoggingSession:
|
|
|
159
187
|
continue
|
|
160
188
|
|
|
161
189
|
self._root_logger.setLevel(self._previous_level)
|
|
190
|
+
with _active_tui_logging_session_lock:
|
|
191
|
+
global _active_tui_logging_session
|
|
192
|
+
if _active_tui_logging_session is self:
|
|
193
|
+
_active_tui_logging_session = None
|
|
162
194
|
|
|
163
195
|
|
|
164
196
|
def _build_file_handler() -> logging.Handler:
|
|
@@ -197,14 +229,40 @@ def setup_tui_logging(renderer: EventRenderer) -> TUILoggingSession:
|
|
|
197
229
|
root = logging.getLogger()
|
|
198
230
|
previous_level = root.level
|
|
199
231
|
|
|
232
|
+
with _active_tui_logging_session_lock:
|
|
233
|
+
global _active_tui_logging_session
|
|
234
|
+
active_session = _active_tui_logging_session
|
|
235
|
+
if active_session is not None:
|
|
236
|
+
active_session.close()
|
|
237
|
+
with _recent_log_message_lock:
|
|
238
|
+
_recent_log_message_times.clear()
|
|
239
|
+
|
|
240
|
+
stale_handlers: list[logging.Handler] = []
|
|
241
|
+
for handler in list(root.handlers):
|
|
242
|
+
if not getattr(handler, _COMATE_LOGGING_SESSION_MARKER, False):
|
|
243
|
+
continue
|
|
244
|
+
try:
|
|
245
|
+
root.removeHandler(handler)
|
|
246
|
+
except Exception:
|
|
247
|
+
pass
|
|
248
|
+
stale_handlers.append(handler)
|
|
249
|
+
|
|
250
|
+
for handler in stale_handlers:
|
|
251
|
+
try:
|
|
252
|
+
handler.close()
|
|
253
|
+
except Exception:
|
|
254
|
+
continue
|
|
255
|
+
|
|
200
256
|
# 1. 日志文件 handler(完整调试信息,含 traceback)
|
|
201
257
|
file_handler = _build_file_handler()
|
|
258
|
+
setattr(file_handler, _COMATE_LOGGING_SESSION_MARKER, True)
|
|
202
259
|
root.addHandler(file_handler)
|
|
203
260
|
root.setLevel(logging.DEBUG)
|
|
204
261
|
|
|
205
262
|
# 2. TUI handler 挂到 root(覆盖所有命名空间的 WARNING/ERROR)
|
|
206
263
|
tui_handler = TUILoggingHandler(renderer)
|
|
207
264
|
tui_handler.setLevel(logging.WARNING)
|
|
265
|
+
setattr(tui_handler, _COMATE_LOGGING_SESSION_MARKER, True)
|
|
208
266
|
root.addHandler(tui_handler)
|
|
209
267
|
|
|
210
268
|
# 3. 临时静默直写终端的 handler,避免污染 prompt_toolkit UI
|
|
@@ -218,9 +276,12 @@ def setup_tui_logging(renderer: EventRenderer) -> TUILoggingSession:
|
|
|
218
276
|
handler.addFilter(log_filter)
|
|
219
277
|
muted_handlers.append((handler, log_filter))
|
|
220
278
|
|
|
221
|
-
|
|
279
|
+
session = TUILoggingSession(
|
|
222
280
|
root_logger=root,
|
|
223
281
|
added_handlers=[file_handler, tui_handler],
|
|
224
282
|
muted_handlers=muted_handlers,
|
|
225
283
|
previous_level=previous_level,
|
|
226
284
|
)
|
|
285
|
+
with _active_tui_logging_session_lock:
|
|
286
|
+
_active_tui_logging_session = session
|
|
287
|
+
return session
|
|
@@ -135,6 +135,7 @@ class TerminalAgentTUI(
|
|
|
135
135
|
pass
|
|
136
136
|
self._renderer = renderer
|
|
137
137
|
self._task_poll_next_at = time.monotonic() + _TASK_POLL_INTERVAL_S
|
|
138
|
+
self._task_poll_last_list_id: str | None = None
|
|
138
139
|
self._rewind_store = RewindStore(session=self._session, project_root=Path.cwd())
|
|
139
140
|
|
|
140
141
|
# Team inbox 消息直连 scrollback(绕开 session_event_queue,实现实时显示)
|
|
@@ -467,6 +468,23 @@ class TerminalAgentTUI(
|
|
|
467
468
|
filter=Condition(lambda: self._ui_mode == UIMode.MCP_CONNECTING),
|
|
468
469
|
)
|
|
469
470
|
|
|
471
|
+
# idle 时 todo/diff/queue 全部隐藏,它们之间的分隔空行也应该一并消失,
|
|
472
|
+
# 否则多余的固定高度会导致 prompt_toolkit 非全屏渲染时高度波动 → scrollback 污染。
|
|
473
|
+
_has_todo_or_diff = Condition(
|
|
474
|
+
lambda: self._renderer.has_active_todos()
|
|
475
|
+
or (
|
|
476
|
+
self._diff_panel_visible
|
|
477
|
+
and self._renderer.latest_diff_lines is not None
|
|
478
|
+
)
|
|
479
|
+
)
|
|
480
|
+
_has_diff_or_queue = Condition(
|
|
481
|
+
lambda: (
|
|
482
|
+
self._diff_panel_visible
|
|
483
|
+
and self._renderer.latest_diff_lines is not None
|
|
484
|
+
)
|
|
485
|
+
or self._should_show_queue_panel()
|
|
486
|
+
)
|
|
487
|
+
|
|
470
488
|
self._main_container = HSplit(
|
|
471
489
|
[
|
|
472
490
|
self._loading_container,
|
|
@@ -478,10 +496,17 @@ class TerminalAgentTUI(
|
|
|
478
496
|
),
|
|
479
497
|
),
|
|
480
498
|
self._todo_container,
|
|
481
|
-
|
|
499
|
+
ConditionalContainer(
|
|
500
|
+
content=Window(height=1, style="class:loading"),
|
|
501
|
+
filter=_has_todo_or_diff,
|
|
502
|
+
),
|
|
482
503
|
self._diff_panel_container,
|
|
483
|
-
|
|
504
|
+
ConditionalContainer(
|
|
505
|
+
content=Window(height=1, style="class:input.separator"),
|
|
506
|
+
filter=_has_diff_or_queue,
|
|
507
|
+
),
|
|
484
508
|
self._queue_container,
|
|
509
|
+
Window(height=1, style="class:loading"),
|
|
485
510
|
Window(height=1, char="─", style="class:input.separator"),
|
|
486
511
|
self._input_container,
|
|
487
512
|
self._question_container,
|
|
@@ -1255,8 +1280,10 @@ class TerminalAgentTUI(
|
|
|
1255
1280
|
poll_result = await asyncio.to_thread(self._fetch_tasks_from_store)
|
|
1256
1281
|
if poll_result is not None:
|
|
1257
1282
|
task_dicts, list_id = poll_result
|
|
1258
|
-
self.
|
|
1259
|
-
|
|
1283
|
+
if list_id != self._task_poll_last_list_id:
|
|
1284
|
+
self._task_poll_last_list_id = list_id
|
|
1285
|
+
self._renderer._update_tasks(task_dicts, list_id=list_id)
|
|
1286
|
+
self._render_dirty = True
|
|
1260
1287
|
|
|
1261
1288
|
await asyncio.sleep(sleep_s)
|
|
1262
1289
|
except asyncio.CancelledError:
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import tempfile
|
|
4
4
|
import unittest
|
|
5
5
|
from pathlib import Path
|
|
6
|
+
from unittest.mock import patch
|
|
6
7
|
|
|
7
8
|
from comate_agent_sdk.agent.events import (
|
|
8
9
|
CompactionResultEvent,
|
|
@@ -36,6 +37,41 @@ def _task_payload(size: int) -> list[dict[str, str]]:
|
|
|
36
37
|
|
|
37
38
|
|
|
38
39
|
class TestEventRenderer(unittest.TestCase):
|
|
40
|
+
def test_append_system_message_deduplicates_adjacent_warning_within_window(self) -> None:
|
|
41
|
+
renderer = EventRenderer()
|
|
42
|
+
|
|
43
|
+
with patch(
|
|
44
|
+
"comate_cli.terminal_agent.event_renderer.time.monotonic",
|
|
45
|
+
side_effect=[100.0, 100.2, 100.2],
|
|
46
|
+
):
|
|
47
|
+
renderer.append_system_message("boom", severity="warning")
|
|
48
|
+
renderer.append_system_message("boom", severity="warning")
|
|
49
|
+
|
|
50
|
+
system_entries = [e for e in renderer.history_entries() if e.entry_type == "system"]
|
|
51
|
+
self.assertEqual(len(system_entries), 1)
|
|
52
|
+
self.assertEqual(system_entries[0].text, "boom")
|
|
53
|
+
|
|
54
|
+
def test_append_system_message_keeps_same_warning_after_window(self) -> None:
|
|
55
|
+
renderer = EventRenderer()
|
|
56
|
+
|
|
57
|
+
with patch(
|
|
58
|
+
"comate_cli.terminal_agent.event_renderer.time.monotonic",
|
|
59
|
+
side_effect=[100.0, 100.7, 100.7],
|
|
60
|
+
):
|
|
61
|
+
renderer.append_system_message("boom", severity="warning")
|
|
62
|
+
renderer.append_system_message("boom", severity="warning")
|
|
63
|
+
|
|
64
|
+
system_entries = [e for e in renderer.history_entries() if e.entry_type == "system"]
|
|
65
|
+
self.assertEqual(len(system_entries), 2)
|
|
66
|
+
|
|
67
|
+
def test_append_system_message_does_not_deduplicate_info(self) -> None:
|
|
68
|
+
renderer = EventRenderer()
|
|
69
|
+
renderer.append_system_message("same-info", severity="info")
|
|
70
|
+
renderer.append_system_message("same-info", severity="info")
|
|
71
|
+
|
|
72
|
+
system_entries = [e for e in renderer.history_entries() if e.entry_type == "system"]
|
|
73
|
+
self.assertEqual(len(system_entries), 2)
|
|
74
|
+
|
|
39
75
|
def test_team_message_event_deduplicates_identical_event(self) -> None:
|
|
40
76
|
renderer = EventRenderer()
|
|
41
77
|
event = TeamMessageEvent(
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
import logging
|
|
5
|
+
import sys
|
|
6
|
+
from unittest.mock import patch
|
|
7
|
+
|
|
8
|
+
from comate_cli.terminal_agent.logging_adapter import (
|
|
9
|
+
TUILoggingHandler,
|
|
10
|
+
TUILoggingSession,
|
|
11
|
+
setup_tui_logging,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class _FakeRenderer:
|
|
16
|
+
def __init__(self) -> None:
|
|
17
|
+
self.messages: list[tuple[str, str]] = []
|
|
18
|
+
|
|
19
|
+
def append_system_message(self, message: str, *, severity: str) -> None:
|
|
20
|
+
self.messages.append((message, severity))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_setup_tui_logging_preserves_existing_root_handlers_and_restores_them() -> None:
|
|
24
|
+
root = logging.getLogger()
|
|
25
|
+
original_handlers = list(root.handlers)
|
|
26
|
+
original_level = root.level
|
|
27
|
+
stderr_handler = logging.StreamHandler(sys.stderr)
|
|
28
|
+
root.addHandler(stderr_handler)
|
|
29
|
+
|
|
30
|
+
session: TUILoggingSession | None = None
|
|
31
|
+
try:
|
|
32
|
+
session = setup_tui_logging(_FakeRenderer())
|
|
33
|
+
|
|
34
|
+
assert stderr_handler in root.handlers
|
|
35
|
+
assert any(isinstance(handler, TUILoggingHandler) for handler in root.handlers)
|
|
36
|
+
assert root.level == logging.DEBUG
|
|
37
|
+
assert len(stderr_handler.filters) == 1
|
|
38
|
+
finally:
|
|
39
|
+
if session is not None:
|
|
40
|
+
session.close()
|
|
41
|
+
if stderr_handler in root.handlers:
|
|
42
|
+
root.removeHandler(stderr_handler)
|
|
43
|
+
root.handlers[:] = original_handlers
|
|
44
|
+
root.setLevel(original_level)
|
|
45
|
+
|
|
46
|
+
assert stderr_handler.filters == []
|
|
47
|
+
assert root.handlers == original_handlers
|
|
48
|
+
assert root.level == original_level
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_setup_tui_logging_does_not_mute_non_terminal_stream_handlers() -> None:
|
|
52
|
+
root = logging.getLogger()
|
|
53
|
+
original_handlers = list(root.handlers)
|
|
54
|
+
original_level = root.level
|
|
55
|
+
buffer_handler = logging.StreamHandler(io.StringIO())
|
|
56
|
+
root.addHandler(buffer_handler)
|
|
57
|
+
|
|
58
|
+
session: TUILoggingSession | None = None
|
|
59
|
+
try:
|
|
60
|
+
session = setup_tui_logging(_FakeRenderer())
|
|
61
|
+
assert buffer_handler in root.handlers
|
|
62
|
+
assert buffer_handler.filters == []
|
|
63
|
+
finally:
|
|
64
|
+
if session is not None:
|
|
65
|
+
session.close()
|
|
66
|
+
if buffer_handler in root.handlers:
|
|
67
|
+
root.removeHandler(buffer_handler)
|
|
68
|
+
root.handlers[:] = original_handlers
|
|
69
|
+
root.setLevel(original_level)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_setup_tui_logging_replaces_stale_managed_handlers_before_installing_new_ones() -> None:
|
|
73
|
+
root = logging.getLogger()
|
|
74
|
+
original_handlers = list(root.handlers)
|
|
75
|
+
original_level = root.level
|
|
76
|
+
renderer1 = _FakeRenderer()
|
|
77
|
+
renderer2 = _FakeRenderer()
|
|
78
|
+
|
|
79
|
+
session1: TUILoggingSession | None = None
|
|
80
|
+
session2: TUILoggingSession | None = None
|
|
81
|
+
try:
|
|
82
|
+
session1 = setup_tui_logging(renderer1)
|
|
83
|
+
session2 = setup_tui_logging(renderer2)
|
|
84
|
+
|
|
85
|
+
tui_handlers = [h for h in root.handlers if isinstance(h, TUILoggingHandler)]
|
|
86
|
+
managed_handlers = [
|
|
87
|
+
h for h in root.handlers if getattr(h, "_comate_tui_logging_session", False)
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
assert len(tui_handlers) == 1
|
|
91
|
+
assert len(managed_handlers) == 2
|
|
92
|
+
|
|
93
|
+
logging.getLogger("test.logging").warning("duplicate-warning-check")
|
|
94
|
+
|
|
95
|
+
assert renderer1.messages == []
|
|
96
|
+
assert renderer2.messages == [("duplicate-warning-check", "warning")]
|
|
97
|
+
finally:
|
|
98
|
+
if session2 is not None:
|
|
99
|
+
session2.close()
|
|
100
|
+
if session1 is not None:
|
|
101
|
+
session1.close()
|
|
102
|
+
root.handlers[:] = original_handlers
|
|
103
|
+
root.setLevel(original_level)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_tui_logging_handler_deduplicates_recent_duplicate_across_handlers() -> None:
|
|
107
|
+
renderer = _FakeRenderer()
|
|
108
|
+
handler1 = TUILoggingHandler(renderer)
|
|
109
|
+
handler2 = TUILoggingHandler(renderer)
|
|
110
|
+
record = logging.LogRecord(
|
|
111
|
+
name="test.logging",
|
|
112
|
+
level=logging.WARNING,
|
|
113
|
+
pathname=__file__,
|
|
114
|
+
lineno=1,
|
|
115
|
+
msg="duplicate-warning-check",
|
|
116
|
+
args=(),
|
|
117
|
+
exc_info=None,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
with patch(
|
|
121
|
+
"comate_cli.terminal_agent.logging_adapter._recent_log_message_times",
|
|
122
|
+
{},
|
|
123
|
+
):
|
|
124
|
+
handler1.emit(record)
|
|
125
|
+
handler2.emit(record)
|
|
126
|
+
|
|
127
|
+
assert renderer.messages == [("duplicate-warning-check", "warning")]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_tui_logging_handler_does_not_wrap_run_in_terminal_with_background_task() -> None:
|
|
131
|
+
renderer = _FakeRenderer()
|
|
132
|
+
handler = TUILoggingHandler(renderer)
|
|
133
|
+
record = logging.LogRecord(
|
|
134
|
+
name="test.logging",
|
|
135
|
+
level=logging.WARNING,
|
|
136
|
+
pathname=__file__,
|
|
137
|
+
lineno=1,
|
|
138
|
+
msg="background-task-race-check",
|
|
139
|
+
args=(),
|
|
140
|
+
exc_info=None,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
class _FakeApp:
|
|
144
|
+
def create_background_task(self, _coroutine) -> None:
|
|
145
|
+
raise AssertionError("create_background_task should not be called")
|
|
146
|
+
|
|
147
|
+
def _run_in_terminal_now(func, render_cli_done=False, in_executor=False): # type: ignore[no-untyped-def]
|
|
148
|
+
assert render_cli_done is False
|
|
149
|
+
assert in_executor is False
|
|
150
|
+
func()
|
|
151
|
+
return object()
|
|
152
|
+
|
|
153
|
+
with (
|
|
154
|
+
patch("prompt_toolkit.application.get_app", return_value=_FakeApp()),
|
|
155
|
+
patch(
|
|
156
|
+
"comate_cli.terminal_agent.logging_adapter.run_in_terminal",
|
|
157
|
+
side_effect=_run_in_terminal_now,
|
|
158
|
+
),
|
|
159
|
+
patch(
|
|
160
|
+
"comate_cli.terminal_agent.logging_adapter._recent_log_message_times",
|
|
161
|
+
{},
|
|
162
|
+
),
|
|
163
|
+
):
|
|
164
|
+
handler.emit(record)
|
|
165
|
+
|
|
166
|
+
assert renderer.messages == [("background-task-race-check", "warning")]
|