comate-cli 0.5.0__tar.gz → 0.5.2__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.0 → comate_cli-0.5.2}/PKG-INFO +2 -1
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/mcp_cli.py +20 -4
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/app.py +1 -1
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/event_renderer.py +107 -28
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/logging_adapter.py +156 -12
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/rewind_store.py +3 -1
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/tool_view.py +40 -17
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/tui.py +22 -7
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/tui_parts/commands.py +41 -10
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/tui_parts/history_sync.py +10 -2
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/tui_parts/mcp_connecting_view.py +15 -9
- comate_cli-0.5.2/memory_system_tengu_moth_copse_report.md +2366 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/pyproject.toml +6 -1
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_event_renderer.py +75 -10
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_history_sync.py +107 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_input_history.py +46 -5
- comate_cli-0.5.2/tests/test_logging_adapter.py +356 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_mcp_slash_command.py +111 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_rewind_store.py +125 -11
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_tool_view.py +4 -4
- {comate_cli-0.5.0 → comate_cli-0.5.2}/uv.lock +82 -2
- comate_cli-0.5.2//346/267/261/345/272/246Agent/345/217/257/350/247/206/345/214/226UI/350/256/276/350/256/241/346/200/235/350/267/257.md +3173 -0
- comate_cli-0.5.0/tests/test_logging_adapter.py +0 -68
- {comate_cli-0.5.0 → comate_cli-0.5.2}/.gitignore +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/README.md +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/__init__.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/__main__.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/main.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/__init__.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/animations.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/assistant_render.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/codenames.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/custom_slash_commands.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/env_utils.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/error_display.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/fragment_utils.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/history_printer.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/input_geometry.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/layout_coordinator.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/logo.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/markdown_render.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/mention_completer.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/message_style.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/models.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/path_context_hint.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/plugins/__init__.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/plugins/components/__init__.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/plugins/components/detail_view.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/plugins/components/plugin_list.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/plugins/components/search_box.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/plugins/components/tab_bar.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/plugins/marketplace_install_view.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/plugins/plugin_picker.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/plugins/tabs/__init__.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/plugins/tabs/discover_tab.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/plugins/tabs/errors_tab.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/plugins/tabs/installed_tab.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/plugins/tabs/marketplaces_tab.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/preflight.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/question_view.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/resume_selector.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/rpc_protocol.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/rpc_stdio.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/selection_menu.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/session_view.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/slash_commands.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/startup.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/status_bar.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/text_effects.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/tips.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/tui_parts/__init__.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/tui_parts/input_behavior.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/tui_parts/key_bindings.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/tui_parts/render_panels.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/tui_parts/slash_command_registry.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/comate_cli/terminal_agent/tui_parts/ui_mode.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/docs/superpowers/plans/2026-04-03-phrase-shuffle.md +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/docs/superpowers/specs/2026-04-01-conditional-diff-subtitle-design.md +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/docs/superpowers/specs/2026-04-03-phrase-shuffle-design.md +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/conftest.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_animator_shuffle.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_app_mcp_preload.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_app_preflight_gate.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_app_print_mode.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_app_shutdown.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_app_usage_line.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_cli_project_root.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_compact_command_semantics.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_completion_context_activation.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_completion_status_panel.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_context_command.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_custom_slash_commands.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_discover_tab.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_errors_tab.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_format_error.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_handle_error.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_history_printer.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_input_behavior.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_installed_tab.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_interrupt_exit_semantics.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_layout_coordinator.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_logo.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_main_args.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_marketplaces_tab.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_mcp_cli.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_mention_completer.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_path_context_hint.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_plugin_slash_commands.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_plugin_tui_components.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_preflight.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_preflight_copilot.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_question_key_bindings.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_question_view.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_resume_selector.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_rewind_command_semantics.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_rpc_protocol.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_rpc_stdio_bridge.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_selection_menu.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_skills_slash_command.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_slash_argument_hint.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_slash_completer.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_slash_registry.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_status_bar.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_status_bar_transient.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_task_panel_format.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_task_panel_key_bindings.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_task_panel_rendering.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_task_poll.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_tui_elapsed_status.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_tui_mcp_init_gate.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_tui_paste_placeholder.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_tui_split_invariance.py +0 -0
- {comate_cli-0.5.0 → comate_cli-0.5.2}/tests/test_update_check.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: comate-cli
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.2
|
|
4
4
|
Summary: Comate terminal CLI built on comate-agent-sdk
|
|
5
5
|
Project-URL: Homepage, https://github.com/AndyLee1024/agent-sdk
|
|
6
6
|
Project-URL: Repository, https://github.com/AndyLee1024/agent-sdk
|
|
@@ -16,6 +16,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
16
16
|
Requires-Python: >=3.11
|
|
17
17
|
Requires-Dist: charset-normalizer==3.4.7
|
|
18
18
|
Requires-Dist: comate-agent-sdk<1.0.0,>=0.0.2
|
|
19
|
+
Requires-Dist: concurrent-log-handler>=0.9.25
|
|
19
20
|
Requires-Dist: curl-cffi==0.13.0
|
|
20
21
|
Requires-Dist: packaging>=21.0
|
|
21
22
|
Requires-Dist: pillow==12.2.0
|
|
@@ -217,9 +217,17 @@ def _read_effective_server_with_source(
|
|
|
217
217
|
return None
|
|
218
218
|
|
|
219
219
|
|
|
220
|
-
def _render_health_status(
|
|
221
|
-
|
|
220
|
+
def _render_health_status(
|
|
221
|
+
*,
|
|
222
|
+
status: str | None,
|
|
223
|
+
connected: bool,
|
|
224
|
+
reason: str | None,
|
|
225
|
+
) -> str:
|
|
226
|
+
normalized = str(status or "").strip().lower()
|
|
227
|
+
if normalized == "connected" or (normalized in {"", "idle"} and connected):
|
|
222
228
|
return "✓ Connected"
|
|
229
|
+
if normalized == "connecting":
|
|
230
|
+
return "… Connecting"
|
|
223
231
|
if reason:
|
|
224
232
|
return f"✗ {reason}"
|
|
225
233
|
return "✗ Not connected"
|
|
@@ -294,7 +302,11 @@ def _cmd_list(args: argparse.Namespace, *, project_root: PathInput | None) -> No
|
|
|
294
302
|
endpoint = _format_server_endpoint(cfg)
|
|
295
303
|
health = health_by_alias.get(alias)
|
|
296
304
|
status = (
|
|
297
|
-
_render_health_status(
|
|
305
|
+
_render_health_status(
|
|
306
|
+
status=getattr(health, "status", None),
|
|
307
|
+
connected=health.connected,
|
|
308
|
+
reason=health.reason,
|
|
309
|
+
)
|
|
298
310
|
if health is not None
|
|
299
311
|
else "✗ Unknown"
|
|
300
312
|
)
|
|
@@ -330,7 +342,11 @@ def _cmd_get(args: argparse.Namespace, *, project_root: PathInput | None) -> Non
|
|
|
330
342
|
status = "✗ Not connected"
|
|
331
343
|
if health_rows:
|
|
332
344
|
row = health_rows[0]
|
|
333
|
-
status = _render_health_status(
|
|
345
|
+
status = _render_health_status(
|
|
346
|
+
status=getattr(row, "status", None),
|
|
347
|
+
connected=row.connected,
|
|
348
|
+
reason=row.reason,
|
|
349
|
+
)
|
|
334
350
|
|
|
335
351
|
server_type = str(cfg.get("type", "stdio")).strip().lower() # type: ignore[attr-defined]
|
|
336
352
|
lines = [
|
|
@@ -370,7 +370,7 @@ async def run(
|
|
|
370
370
|
# 而非 fallthrough 到 Python lastResort StreamHandler 以原始文本输出到 stderr。
|
|
371
371
|
renderer = EventRenderer(project_root=project_root)
|
|
372
372
|
from comate_cli.terminal_agent.logging_adapter import setup_tui_logging
|
|
373
|
-
logging_session = setup_tui_logging(renderer)
|
|
373
|
+
logging_session = setup_tui_logging(renderer, project_root=project_root)
|
|
374
374
|
|
|
375
375
|
session, mode = _resolve_session(agent, resume_session_id, cwd=project_root)
|
|
376
376
|
|
|
@@ -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:
|
|
@@ -671,7 +712,39 @@ class EventRenderer:
|
|
|
671
712
|
|
|
672
713
|
if lowered == "grep":
|
|
673
714
|
lines = [line for line in (result or "").splitlines() if line.strip()]
|
|
674
|
-
|
|
715
|
+
if not lines:
|
|
716
|
+
return "Found matches"
|
|
717
|
+
|
|
718
|
+
count_summary = re.search(
|
|
719
|
+
r"^Found\s+(at least\s+)?(\d+)\s+total occurrences across\s+(\d+)\s+(file|files)\.",
|
|
720
|
+
result or "",
|
|
721
|
+
re.MULTILINE,
|
|
722
|
+
)
|
|
723
|
+
if count_summary:
|
|
724
|
+
qualifier = count_summary.group(1) or ""
|
|
725
|
+
occurrences = count_summary.group(2)
|
|
726
|
+
file_count = count_summary.group(3)
|
|
727
|
+
file_word = count_summary.group(4)
|
|
728
|
+
return f"Found {qualifier}{occurrences} matches across {file_count} {file_word}"
|
|
729
|
+
|
|
730
|
+
if lines[0] == "No matches found":
|
|
731
|
+
return "Found 0 matches"
|
|
732
|
+
|
|
733
|
+
if lines[0] == "No files found":
|
|
734
|
+
return "Found matches in 0 files"
|
|
735
|
+
|
|
736
|
+
file_summary = re.match(r"^Found\s+(at least\s+)?(\d+)\s+files?\b", lines[0])
|
|
737
|
+
if file_summary:
|
|
738
|
+
qualifier = file_summary.group(1) or ""
|
|
739
|
+
return f"Found matches in {qualifier}{file_summary.group(2)} files"
|
|
740
|
+
|
|
741
|
+
if all(
|
|
742
|
+
not re.search(r":\d+(?::|$)", line)
|
|
743
|
+
for line in lines
|
|
744
|
+
):
|
|
745
|
+
return f"Found matches in {len(lines)} files"
|
|
746
|
+
|
|
747
|
+
return "Found matches"
|
|
675
748
|
|
|
676
749
|
return None
|
|
677
750
|
|
|
@@ -688,7 +761,7 @@ class EventRenderer:
|
|
|
688
761
|
Args:
|
|
689
762
|
signature: 工具签名,例如 "Read(path=xxx)"
|
|
690
763
|
is_error: 是否为错误结果
|
|
691
|
-
diff_lines: optional diff lines for Edit
|
|
764
|
+
diff_lines: optional diff lines for Edit
|
|
692
765
|
model_name: model name for Agent tools (rendered dim)
|
|
693
766
|
subtitle: optional subtitle rendered as `⎿ ...`
|
|
694
767
|
"""
|
|
@@ -700,18 +773,18 @@ class EventRenderer:
|
|
|
700
773
|
text_obj.append(f" · {model_name}", style="dim")
|
|
701
774
|
text_obj.append("\n")
|
|
702
775
|
text_obj.append(self._render_diff_text(diff_lines, max_lines=self._diff_max_lines))
|
|
703
|
-
self.
|
|
776
|
+
self._append_history_entry(
|
|
704
777
|
HistoryEntry(entry_type="tool_result", text=text_obj, severity="info", subtitle=subtitle)
|
|
705
778
|
)
|
|
706
779
|
return
|
|
707
780
|
if model_name:
|
|
708
781
|
text_obj = Text(signature)
|
|
709
782
|
text_obj.append(f" · {model_name}", style="dim")
|
|
710
|
-
self.
|
|
783
|
+
self._append_history_entry(
|
|
711
784
|
HistoryEntry(entry_type="tool_result", text=text_obj, severity=sev, subtitle=subtitle)
|
|
712
785
|
)
|
|
713
786
|
return
|
|
714
|
-
self.
|
|
787
|
+
self._append_history_entry(
|
|
715
788
|
HistoryEntry(
|
|
716
789
|
entry_type="tool_result",
|
|
717
790
|
text=signature,
|
|
@@ -764,7 +837,9 @@ class EventRenderer:
|
|
|
764
837
|
if state is None:
|
|
765
838
|
display_name = resolve_display_tool_name(tool_name, {})
|
|
766
839
|
signature = _tool_signature(display_name, "")
|
|
767
|
-
self.
|
|
840
|
+
self._append_history_entry(
|
|
841
|
+
HistoryEntry(entry_type="tool_result", text=signature, severity=sev, subtitle=subtitle)
|
|
842
|
+
)
|
|
768
843
|
return
|
|
769
844
|
|
|
770
845
|
if state.is_task:
|
|
@@ -790,7 +865,7 @@ class EventRenderer:
|
|
|
790
865
|
else:
|
|
791
866
|
display_name = state.display_tool_name or state.tool_name
|
|
792
867
|
summary = state.args_summary
|
|
793
|
-
# Append line range for Edit
|
|
868
|
+
# Append line range for Edit
|
|
794
869
|
if display_name == "Update" and metadata:
|
|
795
870
|
sl = metadata.get("start_line")
|
|
796
871
|
el = metadata.get("end_line")
|
|
@@ -800,7 +875,7 @@ class EventRenderer:
|
|
|
800
875
|
signature = _tool_signature(display_name, summary)
|
|
801
876
|
base = f"{signature}"
|
|
802
877
|
|
|
803
|
-
# Render diff for Edit
|
|
878
|
+
# Render diff for Edit if metadata contains diff lines
|
|
804
879
|
if not is_error and metadata:
|
|
805
880
|
diff_lines = metadata.get("diff")
|
|
806
881
|
if isinstance(diff_lines, list) and len(diff_lines) > 0:
|
|
@@ -811,15 +886,19 @@ class EventRenderer:
|
|
|
811
886
|
text_obj = Text(base)
|
|
812
887
|
text_obj.append("\n")
|
|
813
888
|
text_obj.append(self._render_diff_text(diff_lines, max_lines=self._diff_max_lines))
|
|
814
|
-
self.
|
|
889
|
+
self._append_history_entry(
|
|
815
890
|
HistoryEntry(entry_type="tool_result", text=text_obj, severity="info", subtitle=subtitle)
|
|
816
891
|
)
|
|
817
892
|
return
|
|
818
893
|
|
|
819
894
|
if isinstance(base, Text):
|
|
820
|
-
self.
|
|
895
|
+
self._append_history_entry(
|
|
896
|
+
HistoryEntry(entry_type="tool_result", text=base, severity=sev, subtitle=subtitle)
|
|
897
|
+
)
|
|
821
898
|
else:
|
|
822
|
-
self.
|
|
899
|
+
self._append_history_entry(
|
|
900
|
+
HistoryEntry(entry_type="tool_result", text=base, severity=sev, subtitle=subtitle)
|
|
901
|
+
)
|
|
823
902
|
|
|
824
903
|
@staticmethod
|
|
825
904
|
def _render_diff_text(diff_lines: list[str], max_lines: int = 50) -> Text:
|
|
@@ -900,7 +979,7 @@ class EventRenderer:
|
|
|
900
979
|
def _append_questions(self, questions: list[dict[str, Any]]) -> None:
|
|
901
980
|
if not questions:
|
|
902
981
|
return
|
|
903
|
-
self.
|
|
982
|
+
self._append_history_entry(
|
|
904
983
|
HistoryEntry(
|
|
905
984
|
entry_type="tool_result",
|
|
906
985
|
text=f"Input required: {len(questions)} question(s) pending.",
|
|
@@ -919,7 +998,7 @@ class EventRenderer:
|
|
|
919
998
|
if label:
|
|
920
999
|
labels.append(label)
|
|
921
1000
|
choice_preview = f" (Options: {' / '.join(labels)})" if labels else ""
|
|
922
|
-
self.
|
|
1001
|
+
self._append_history_entry(
|
|
923
1002
|
HistoryEntry(
|
|
924
1003
|
entry_type="tool_result",
|
|
925
1004
|
text=f" {idx}. {header}: {question_text} {choice_preview}".strip(),
|
|
@@ -962,7 +1041,7 @@ class EventRenderer:
|
|
|
962
1041
|
if started is not None:
|
|
963
1042
|
elapsed_suffix = f" · {_format_duration(time.monotonic() - started)}"
|
|
964
1043
|
total = len(normalized)
|
|
965
|
-
self.
|
|
1044
|
+
self._append_history_entry(
|
|
966
1045
|
HistoryEntry(
|
|
967
1046
|
entry_type="tool_result",
|
|
968
1047
|
text=f"tasks {total}/{total} completed{elapsed_suffix}",
|
|
@@ -1042,7 +1121,7 @@ class EventRenderer:
|
|
|
1042
1121
|
pass
|
|
1043
1122
|
case ThinkingEvent(content=thinking):
|
|
1044
1123
|
self._thinking_content = thinking
|
|
1045
|
-
self.
|
|
1124
|
+
self._append_history_entry(HistoryEntry(entry_type="thinking", text=thinking))
|
|
1046
1125
|
case CompactionResultEvent(
|
|
1047
1126
|
current_tokens=tokens,
|
|
1048
1127
|
threshold=threshold,
|
|
@@ -1207,7 +1286,7 @@ class EventRenderer:
|
|
|
1207
1286
|
if reason == "waiting_for_plan_approval":
|
|
1208
1287
|
return (False, None)
|
|
1209
1288
|
if reason == "interrupted":
|
|
1210
|
-
self.
|
|
1289
|
+
self._append_history_entry(
|
|
1211
1290
|
HistoryEntry(entry_type="system", text="Current task interrupted.", severity="warning")
|
|
1212
1291
|
)
|
|
1213
1292
|
case PlanApprovalRequiredEvent(
|
|
@@ -1225,18 +1304,18 @@ class EventRenderer:
|
|
|
1225
1304
|
except Exception:
|
|
1226
1305
|
logger.warning("ExitPlanMode: Failed to read plan file %s", plan_path)
|
|
1227
1306
|
if plan_content:
|
|
1228
|
-
self.
|
|
1307
|
+
self._append_history_entry(
|
|
1229
1308
|
HistoryEntry(
|
|
1230
1309
|
entry_type="system",
|
|
1231
1310
|
text="─── Here is the plan, please review and approve or reject ───",
|
|
1232
1311
|
)
|
|
1233
1312
|
)
|
|
1234
|
-
self.
|
|
1313
|
+
self._append_history_entry(HistoryEntry(entry_type="assistant", text=plan_content))
|
|
1235
1314
|
|
|
1236
1315
|
text = f"Plan ready for review: {plan_path}"
|
|
1237
1316
|
if summary:
|
|
1238
1317
|
text = f"{text} | {summary}"
|
|
1239
|
-
self.
|
|
1318
|
+
self._append_history_entry(
|
|
1240
1319
|
HistoryEntry(entry_type="system", text=text)
|
|
1241
1320
|
)
|
|
1242
1321
|
case TeamMessageEvent(
|
|
@@ -4,6 +4,9 @@ from __future__ import annotations
|
|
|
4
4
|
import logging
|
|
5
5
|
import os
|
|
6
6
|
import sys
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
from pathlib import Path
|
|
7
10
|
from typing import TYPE_CHECKING
|
|
8
11
|
|
|
9
12
|
from prompt_toolkit.application import run_in_terminal
|
|
@@ -12,6 +15,37 @@ if TYPE_CHECKING:
|
|
|
12
15
|
from comate_cli.terminal_agent.event_renderer import EventRenderer
|
|
13
16
|
|
|
14
17
|
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
_COMATE_LOGGING_SESSION_MARKER = "_comate_tui_logging_session"
|
|
21
|
+
_RECENT_DUPLICATE_LOG_WINDOW_SECONDS = 0.5
|
|
22
|
+
_recent_log_message_times: dict[str, float] = {}
|
|
23
|
+
_recent_log_message_lock = threading.Lock()
|
|
24
|
+
_active_tui_logging_session: "TUILoggingSession | None" = None
|
|
25
|
+
_active_tui_logging_session_lock = threading.Lock()
|
|
26
|
+
|
|
27
|
+
_LOG_LEVEL_ENV_VAR = "COMATE_LOG_LEVEL"
|
|
28
|
+
_VALID_LOG_LEVELS: tuple[str, ...] = ("DEBUG", "INFO", "WARNING", "ERROR")
|
|
29
|
+
_DEFAULT_LOG_LEVEL = logging.INFO
|
|
30
|
+
_invalid_log_level_warned = False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _should_drop_recent_duplicate_log(msg_key: str) -> bool:
|
|
34
|
+
now = time.monotonic()
|
|
35
|
+
with _recent_log_message_lock:
|
|
36
|
+
expired_keys = [
|
|
37
|
+
key
|
|
38
|
+
for key, ts in _recent_log_message_times.items()
|
|
39
|
+
if now - ts > _RECENT_DUPLICATE_LOG_WINDOW_SECONDS
|
|
40
|
+
]
|
|
41
|
+
for key in expired_keys:
|
|
42
|
+
_recent_log_message_times.pop(key, None)
|
|
43
|
+
|
|
44
|
+
previous = _recent_log_message_times.get(msg_key)
|
|
45
|
+
_recent_log_message_times[msg_key] = now
|
|
46
|
+
return previous is not None
|
|
47
|
+
|
|
48
|
+
|
|
15
49
|
class TUILoggingHandler(logging.Handler):
|
|
16
50
|
"""自定义 logging handler,将日志友好地显示在 TUI 中
|
|
17
51
|
|
|
@@ -40,6 +74,8 @@ class TUILoggingHandler(logging.Handler):
|
|
|
40
74
|
|
|
41
75
|
# 首次显示检查
|
|
42
76
|
msg_key = self._get_message_key(record)
|
|
77
|
+
if _should_drop_recent_duplicate_log(msg_key):
|
|
78
|
+
return
|
|
43
79
|
if msg_key in self._shown_messages:
|
|
44
80
|
return
|
|
45
81
|
self._shown_messages.add(msg_key)
|
|
@@ -108,9 +144,9 @@ class TUILoggingHandler(logging.Handler):
|
|
|
108
144
|
# DummyApplication,直接调用
|
|
109
145
|
_append()
|
|
110
146
|
else:
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
)
|
|
147
|
+
# run_in_terminal() 自身已经会调度 Future;这里不能再交给
|
|
148
|
+
# create_background_task() 二次包装,否则会触发重复执行竞态。
|
|
149
|
+
run_in_terminal(_append, in_executor=False)
|
|
114
150
|
except Exception:
|
|
115
151
|
# 没有 app 或导入失败,直接调用
|
|
116
152
|
_append()
|
|
@@ -159,21 +195,86 @@ class TUILoggingSession:
|
|
|
159
195
|
continue
|
|
160
196
|
|
|
161
197
|
self._root_logger.setLevel(self._previous_level)
|
|
198
|
+
with _active_tui_logging_session_lock:
|
|
199
|
+
global _active_tui_logging_session
|
|
200
|
+
if _active_tui_logging_session is self:
|
|
201
|
+
_active_tui_logging_session = None
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _read_level_from_settings_env(project_root: Path | None) -> str:
|
|
205
|
+
"""从 settings.json 的 env 段读取 COMATE_LOG_LEVEL。
|
|
206
|
+
|
|
207
|
+
查找顺序:project settings(.agent/settings.json) → user settings(~/.agent/settings.json)。
|
|
208
|
+
复用 SDK 侧的 load_settings_file,不引入独立解析路径。
|
|
209
|
+
返回空串表示未配置或读取失败,由调用方决定如何降级。
|
|
210
|
+
"""
|
|
211
|
+
try:
|
|
212
|
+
from comate_agent_sdk.agent.settings import USER_SETTINGS_PATH, load_settings_file
|
|
213
|
+
except Exception:
|
|
214
|
+
return ""
|
|
215
|
+
|
|
216
|
+
def _lookup(path: Path) -> str:
|
|
217
|
+
try:
|
|
218
|
+
cfg = load_settings_file(path)
|
|
219
|
+
except Exception:
|
|
220
|
+
return ""
|
|
221
|
+
if cfg is None or cfg.env is None:
|
|
222
|
+
return ""
|
|
223
|
+
raw = cfg.env.get(_LOG_LEVEL_ENV_VAR, "")
|
|
224
|
+
return raw.strip() if isinstance(raw, str) else ""
|
|
225
|
+
|
|
226
|
+
if project_root is not None:
|
|
227
|
+
value = _lookup(Path(project_root) / ".agent" / "settings.json")
|
|
228
|
+
if value:
|
|
229
|
+
return value
|
|
230
|
+
|
|
231
|
+
return _lookup(USER_SETTINGS_PATH)
|
|
232
|
+
|
|
162
233
|
|
|
234
|
+
def _resolve_log_level(project_root: Path | None = None) -> int:
|
|
235
|
+
"""解析生效日志级别。
|
|
163
236
|
|
|
164
|
-
|
|
165
|
-
|
|
237
|
+
优先级:os.environ[COMATE_LOG_LEVEL]
|
|
238
|
+
> project .agent/settings.json env.COMATE_LOG_LEVEL
|
|
239
|
+
> user ~/.agent/settings.json env.COMATE_LOG_LEVEL
|
|
240
|
+
> INFO。
|
|
241
|
+
非法值降级 INFO,并在整个进程生命周期内最多 warn 一次。
|
|
242
|
+
"""
|
|
243
|
+
global _invalid_log_level_warned
|
|
244
|
+
|
|
245
|
+
raw = os.environ.get(_LOG_LEVEL_ENV_VAR, "").strip()
|
|
246
|
+
if not raw:
|
|
247
|
+
raw = _read_level_from_settings_env(project_root)
|
|
248
|
+
|
|
249
|
+
if not raw:
|
|
250
|
+
return _DEFAULT_LOG_LEVEL
|
|
251
|
+
|
|
252
|
+
normalized = raw.upper()
|
|
253
|
+
if normalized not in _VALID_LOG_LEVELS:
|
|
254
|
+
if not _invalid_log_level_warned:
|
|
255
|
+
_invalid_log_level_warned = True
|
|
256
|
+
logger.warning(
|
|
257
|
+
f"Invalid {_LOG_LEVEL_ENV_VAR}={raw!r}; falling back to INFO. "
|
|
258
|
+
f"Valid values: {list(_VALID_LOG_LEVELS)}"
|
|
259
|
+
)
|
|
260
|
+
return _DEFAULT_LOG_LEVEL
|
|
261
|
+
|
|
262
|
+
return getattr(logging, normalized)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _build_file_handler(level: int) -> logging.Handler:
|
|
266
|
+
from concurrent_log_handler import ConcurrentRotatingFileHandler
|
|
166
267
|
|
|
167
268
|
log_dir = os.path.join(os.path.expanduser("~"), ".comate", "logs")
|
|
168
269
|
os.makedirs(log_dir, exist_ok=True)
|
|
169
270
|
log_path = os.path.join(log_dir, "agent.log")
|
|
170
|
-
file_handler =
|
|
271
|
+
file_handler = ConcurrentRotatingFileHandler(
|
|
171
272
|
log_path,
|
|
172
273
|
maxBytes=10 * 1024 * 1024,
|
|
173
274
|
backupCount=3,
|
|
174
275
|
encoding="utf-8",
|
|
175
276
|
)
|
|
176
|
-
file_handler.setLevel(
|
|
277
|
+
file_handler.setLevel(level)
|
|
177
278
|
file_handler.setFormatter(
|
|
178
279
|
logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s")
|
|
179
280
|
)
|
|
@@ -187,24 +288,64 @@ def _should_mute_terminal_stream(handler: logging.Handler) -> bool:
|
|
|
187
288
|
return stream in {sys.stdout, sys.stderr, sys.__stdout__, sys.__stderr__}
|
|
188
289
|
|
|
189
290
|
|
|
190
|
-
def setup_tui_logging(
|
|
291
|
+
def setup_tui_logging(
|
|
292
|
+
renderer: EventRenderer,
|
|
293
|
+
*,
|
|
294
|
+
project_root: Path | None = None,
|
|
295
|
+
) -> TUILoggingSession:
|
|
191
296
|
"""统一日志初始化:文件 + TUI 双通道。
|
|
192
297
|
|
|
193
|
-
- 所有日志(含 traceback)写入 ~/.comate/logs/agent.log
|
|
298
|
+
- 所有日志(含 traceback)写入 ~/.comate/logs/agent.log
|
|
299
|
+
(ConcurrentRotatingFileHandler:跨进程文件锁 + Windows rename 重试)
|
|
194
300
|
- WARNING/ERROR 以用户友好格式显示在 TUI scrollback
|
|
195
301
|
- 临时静默 root logger 上写往 stdout/stderr 的 stream handler,避免污染 TUI
|
|
302
|
+
|
|
303
|
+
日志级别通过 COMATE_LOG_LEVEL 控制(默认 INFO,可设为 DEBUG/INFO/WARNING/ERROR)。
|
|
304
|
+
查找顺序:os.environ > project .agent/settings.json env > user ~/.agent/settings.json env。
|
|
305
|
+
root 与 file handler 同步应用该级别——第三方库(anthropic/httpx/...)的 DEBUG 噪音随之收敛。
|
|
306
|
+
TUI handler 始终固定 WARNING,不受此开关影响,避免 DEBUG 刷屏。
|
|
196
307
|
"""
|
|
197
308
|
root = logging.getLogger()
|
|
198
309
|
previous_level = root.level
|
|
310
|
+
level = _resolve_log_level(project_root)
|
|
311
|
+
|
|
312
|
+
with _active_tui_logging_session_lock:
|
|
313
|
+
global _active_tui_logging_session
|
|
314
|
+
active_session = _active_tui_logging_session
|
|
315
|
+
if active_session is not None:
|
|
316
|
+
active_session.close()
|
|
317
|
+
with _recent_log_message_lock:
|
|
318
|
+
_recent_log_message_times.clear()
|
|
319
|
+
|
|
320
|
+
stale_handlers: list[logging.Handler] = []
|
|
321
|
+
for handler in list(root.handlers):
|
|
322
|
+
if not getattr(handler, _COMATE_LOGGING_SESSION_MARKER, False):
|
|
323
|
+
continue
|
|
324
|
+
try:
|
|
325
|
+
root.removeHandler(handler)
|
|
326
|
+
except Exception:
|
|
327
|
+
pass
|
|
328
|
+
stale_handlers.append(handler)
|
|
329
|
+
|
|
330
|
+
for handler in stale_handlers:
|
|
331
|
+
try:
|
|
332
|
+
handler.close()
|
|
333
|
+
except Exception:
|
|
334
|
+
continue
|
|
199
335
|
|
|
200
336
|
# 1. 日志文件 handler(完整调试信息,含 traceback)
|
|
201
|
-
file_handler = _build_file_handler()
|
|
337
|
+
file_handler = _build_file_handler(level)
|
|
338
|
+
setattr(file_handler, _COMATE_LOGGING_SESSION_MARKER, True)
|
|
202
339
|
root.addHandler(file_handler)
|
|
203
|
-
root
|
|
340
|
+
# root 级别与 file handler 保持一致——这是单 knob 控第三方 DEBUG 噪音的关键:
|
|
341
|
+
# 第三方 logger level=NOTSET 会继承 root 的 effective level,DEBUG 记录
|
|
342
|
+
# 在 isEnabledFor 阶段就被丢弃,不会走到任何 handler。
|
|
343
|
+
root.setLevel(level)
|
|
204
344
|
|
|
205
345
|
# 2. TUI handler 挂到 root(覆盖所有命名空间的 WARNING/ERROR)
|
|
206
346
|
tui_handler = TUILoggingHandler(renderer)
|
|
207
347
|
tui_handler.setLevel(logging.WARNING)
|
|
348
|
+
setattr(tui_handler, _COMATE_LOGGING_SESSION_MARKER, True)
|
|
208
349
|
root.addHandler(tui_handler)
|
|
209
350
|
|
|
210
351
|
# 3. 临时静默直写终端的 handler,避免污染 prompt_toolkit UI
|
|
@@ -218,9 +359,12 @@ def setup_tui_logging(renderer: EventRenderer) -> TUILoggingSession:
|
|
|
218
359
|
handler.addFilter(log_filter)
|
|
219
360
|
muted_handlers.append((handler, log_filter))
|
|
220
361
|
|
|
221
|
-
|
|
362
|
+
session = TUILoggingSession(
|
|
222
363
|
root_logger=root,
|
|
223
364
|
added_handlers=[file_handler, tui_handler],
|
|
224
365
|
muted_handlers=muted_handlers,
|
|
225
366
|
previous_level=previous_level,
|
|
226
367
|
)
|
|
368
|
+
with _active_tui_logging_session_lock:
|
|
369
|
+
_active_tui_logging_session = session
|
|
370
|
+
return session
|
|
@@ -16,7 +16,7 @@ from comate_agent_sdk.context.items import ItemType
|
|
|
16
16
|
|
|
17
17
|
logger = logging.getLogger(__name__)
|
|
18
18
|
|
|
19
|
-
_TRACKED_TOOLS = {"Write", "Edit"
|
|
19
|
+
_TRACKED_TOOLS = {"Write", "Edit"}
|
|
20
20
|
_SCHEMA_VERSION = 1
|
|
21
21
|
|
|
22
22
|
|
|
@@ -561,6 +561,8 @@ class RewindStore:
|
|
|
561
561
|
touched.add(relpath)
|
|
562
562
|
|
|
563
563
|
created_val = data.get("created")
|
|
564
|
+
if not isinstance(created_val, bool):
|
|
565
|
+
created_val = meta.get("created")
|
|
564
566
|
created: bool | None
|
|
565
567
|
if isinstance(created_val, bool):
|
|
566
568
|
created = created_val
|