comate-cli 0.5.1__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.1 → comate_cli-0.5.2}/PKG-INFO +2 -1
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/mcp_cli.py +20 -4
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/app.py +1 -1
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/event_renderer.py +36 -4
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/logging_adapter.py +91 -8
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/rewind_store.py +3 -1
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/tool_view.py +40 -17
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/tui.py +22 -7
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/tui_parts/commands.py +41 -10
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/tui_parts/history_sync.py +10 -2
- {comate_cli-0.5.1 → 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.1 → comate_cli-0.5.2}/pyproject.toml +6 -1
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_event_renderer.py +39 -10
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_history_sync.py +107 -0
- {comate_cli-0.5.1 → 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.1 → comate_cli-0.5.2}/tests/test_mcp_slash_command.py +111 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_rewind_store.py +125 -11
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_tool_view.py +4 -4
- {comate_cli-0.5.1 → 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.1/tests/test_logging_adapter.py +0 -166
- {comate_cli-0.5.1 → comate_cli-0.5.2}/.gitignore +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/README.md +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/__init__.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/__main__.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/main.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/__init__.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/animations.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/assistant_render.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/codenames.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/custom_slash_commands.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/env_utils.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/error_display.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/fragment_utils.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/history_printer.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/input_geometry.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/layout_coordinator.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/logo.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/markdown_render.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/mention_completer.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/message_style.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/models.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/path_context_hint.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/plugins/__init__.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/plugins/components/__init__.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/plugins/components/detail_view.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/plugins/components/plugin_list.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/plugins/components/search_box.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/plugins/components/tab_bar.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/plugins/marketplace_install_view.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/plugins/plugin_picker.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/plugins/tabs/__init__.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/plugins/tabs/discover_tab.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/plugins/tabs/errors_tab.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/plugins/tabs/installed_tab.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/plugins/tabs/marketplaces_tab.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/preflight.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/question_view.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/resume_selector.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/rpc_protocol.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/rpc_stdio.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/selection_menu.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/session_view.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/slash_commands.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/startup.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/status_bar.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/text_effects.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/tips.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/tui_parts/__init__.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/tui_parts/input_behavior.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/tui_parts/key_bindings.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/tui_parts/render_panels.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/tui_parts/slash_command_registry.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/comate_cli/terminal_agent/tui_parts/ui_mode.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/docs/superpowers/plans/2026-04-03-phrase-shuffle.md +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/docs/superpowers/specs/2026-04-01-conditional-diff-subtitle-design.md +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/docs/superpowers/specs/2026-04-03-phrase-shuffle-design.md +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/conftest.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_animator_shuffle.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_app_mcp_preload.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_app_preflight_gate.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_app_print_mode.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_app_shutdown.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_app_usage_line.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_cli_project_root.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_compact_command_semantics.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_completion_context_activation.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_completion_status_panel.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_context_command.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_custom_slash_commands.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_discover_tab.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_errors_tab.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_format_error.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_handle_error.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_history_printer.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_input_behavior.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_installed_tab.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_interrupt_exit_semantics.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_layout_coordinator.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_logo.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_main_args.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_marketplaces_tab.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_mcp_cli.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_mention_completer.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_path_context_hint.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_plugin_slash_commands.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_plugin_tui_components.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_preflight.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_preflight_copilot.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_question_key_bindings.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_question_view.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_resume_selector.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_rewind_command_semantics.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_rpc_protocol.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_rpc_stdio_bridge.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_selection_menu.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_skills_slash_command.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_slash_argument_hint.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_slash_completer.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_slash_registry.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_status_bar.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_status_bar_transient.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_task_panel_format.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_task_panel_key_bindings.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_task_panel_rendering.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_task_poll.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_tui_elapsed_status.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_tui_mcp_init_gate.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_tui_paste_placeholder.py +0 -0
- {comate_cli-0.5.1 → comate_cli-0.5.2}/tests/test_tui_split_invariance.py +0 -0
- {comate_cli-0.5.1 → 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
|
|
|
@@ -712,7 +712,39 @@ class EventRenderer:
|
|
|
712
712
|
|
|
713
713
|
if lowered == "grep":
|
|
714
714
|
lines = [line for line in (result or "").splitlines() if line.strip()]
|
|
715
|
-
|
|
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"
|
|
716
748
|
|
|
717
749
|
return None
|
|
718
750
|
|
|
@@ -729,7 +761,7 @@ class EventRenderer:
|
|
|
729
761
|
Args:
|
|
730
762
|
signature: 工具签名,例如 "Read(path=xxx)"
|
|
731
763
|
is_error: 是否为错误结果
|
|
732
|
-
diff_lines: optional diff lines for Edit
|
|
764
|
+
diff_lines: optional diff lines for Edit
|
|
733
765
|
model_name: model name for Agent tools (rendered dim)
|
|
734
766
|
subtitle: optional subtitle rendered as `⎿ ...`
|
|
735
767
|
"""
|
|
@@ -833,7 +865,7 @@ class EventRenderer:
|
|
|
833
865
|
else:
|
|
834
866
|
display_name = state.display_tool_name or state.tool_name
|
|
835
867
|
summary = state.args_summary
|
|
836
|
-
# Append line range for Edit
|
|
868
|
+
# Append line range for Edit
|
|
837
869
|
if display_name == "Update" and metadata:
|
|
838
870
|
sl = metadata.get("start_line")
|
|
839
871
|
el = metadata.get("end_line")
|
|
@@ -843,7 +875,7 @@ class EventRenderer:
|
|
|
843
875
|
signature = _tool_signature(display_name, summary)
|
|
844
876
|
base = f"{signature}"
|
|
845
877
|
|
|
846
|
-
# Render diff for Edit
|
|
878
|
+
# Render diff for Edit if metadata contains diff lines
|
|
847
879
|
if not is_error and metadata:
|
|
848
880
|
diff_lines = metadata.get("diff")
|
|
849
881
|
if isinstance(diff_lines, list) and len(diff_lines) > 0:
|
|
@@ -6,6 +6,7 @@ import os
|
|
|
6
6
|
import sys
|
|
7
7
|
import threading
|
|
8
8
|
import time
|
|
9
|
+
from pathlib import Path
|
|
9
10
|
from typing import TYPE_CHECKING
|
|
10
11
|
|
|
11
12
|
from prompt_toolkit.application import run_in_terminal
|
|
@@ -14,6 +15,8 @@ if TYPE_CHECKING:
|
|
|
14
15
|
from comate_cli.terminal_agent.event_renderer import EventRenderer
|
|
15
16
|
|
|
16
17
|
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
17
20
|
_COMATE_LOGGING_SESSION_MARKER = "_comate_tui_logging_session"
|
|
18
21
|
_RECENT_DUPLICATE_LOG_WINDOW_SECONDS = 0.5
|
|
19
22
|
_recent_log_message_times: dict[str, float] = {}
|
|
@@ -21,6 +24,11 @@ _recent_log_message_lock = threading.Lock()
|
|
|
21
24
|
_active_tui_logging_session: "TUILoggingSession | None" = None
|
|
22
25
|
_active_tui_logging_session_lock = threading.Lock()
|
|
23
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
|
+
|
|
24
32
|
|
|
25
33
|
def _should_drop_recent_duplicate_log(msg_key: str) -> bool:
|
|
26
34
|
now = time.monotonic()
|
|
@@ -193,19 +201,80 @@ class TUILoggingSession:
|
|
|
193
201
|
_active_tui_logging_session = None
|
|
194
202
|
|
|
195
203
|
|
|
196
|
-
def
|
|
197
|
-
|
|
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
|
+
|
|
233
|
+
|
|
234
|
+
def _resolve_log_level(project_root: Path | None = None) -> int:
|
|
235
|
+
"""解析生效日志级别。
|
|
236
|
+
|
|
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
|
|
198
267
|
|
|
199
268
|
log_dir = os.path.join(os.path.expanduser("~"), ".comate", "logs")
|
|
200
269
|
os.makedirs(log_dir, exist_ok=True)
|
|
201
270
|
log_path = os.path.join(log_dir, "agent.log")
|
|
202
|
-
file_handler =
|
|
271
|
+
file_handler = ConcurrentRotatingFileHandler(
|
|
203
272
|
log_path,
|
|
204
273
|
maxBytes=10 * 1024 * 1024,
|
|
205
274
|
backupCount=3,
|
|
206
275
|
encoding="utf-8",
|
|
207
276
|
)
|
|
208
|
-
file_handler.setLevel(
|
|
277
|
+
file_handler.setLevel(level)
|
|
209
278
|
file_handler.setFormatter(
|
|
210
279
|
logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s")
|
|
211
280
|
)
|
|
@@ -219,15 +288,26 @@ def _should_mute_terminal_stream(handler: logging.Handler) -> bool:
|
|
|
219
288
|
return stream in {sys.stdout, sys.stderr, sys.__stdout__, sys.__stderr__}
|
|
220
289
|
|
|
221
290
|
|
|
222
|
-
def setup_tui_logging(
|
|
291
|
+
def setup_tui_logging(
|
|
292
|
+
renderer: EventRenderer,
|
|
293
|
+
*,
|
|
294
|
+
project_root: Path | None = None,
|
|
295
|
+
) -> TUILoggingSession:
|
|
223
296
|
"""统一日志初始化:文件 + TUI 双通道。
|
|
224
297
|
|
|
225
|
-
- 所有日志(含 traceback)写入 ~/.comate/logs/agent.log
|
|
298
|
+
- 所有日志(含 traceback)写入 ~/.comate/logs/agent.log
|
|
299
|
+
(ConcurrentRotatingFileHandler:跨进程文件锁 + Windows rename 重试)
|
|
226
300
|
- WARNING/ERROR 以用户友好格式显示在 TUI scrollback
|
|
227
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 刷屏。
|
|
228
307
|
"""
|
|
229
308
|
root = logging.getLogger()
|
|
230
309
|
previous_level = root.level
|
|
310
|
+
level = _resolve_log_level(project_root)
|
|
231
311
|
|
|
232
312
|
with _active_tui_logging_session_lock:
|
|
233
313
|
global _active_tui_logging_session
|
|
@@ -254,10 +334,13 @@ def setup_tui_logging(renderer: EventRenderer) -> TUILoggingSession:
|
|
|
254
334
|
continue
|
|
255
335
|
|
|
256
336
|
# 1. 日志文件 handler(完整调试信息,含 traceback)
|
|
257
|
-
file_handler = _build_file_handler()
|
|
337
|
+
file_handler = _build_file_handler(level)
|
|
258
338
|
setattr(file_handler, _COMATE_LOGGING_SESSION_MARKER, True)
|
|
259
339
|
root.addHandler(file_handler)
|
|
260
|
-
root
|
|
340
|
+
# root 级别与 file handler 保持一致——这是单 knob 控第三方 DEBUG 噪音的关键:
|
|
341
|
+
# 第三方 logger level=NOTSET 会继承 root 的 effective level,DEBUG 记录
|
|
342
|
+
# 在 isEnabledFor 阶段就被丢弃,不会走到任何 handler。
|
|
343
|
+
root.setLevel(level)
|
|
261
344
|
|
|
262
345
|
# 2. TUI handler 挂到 root(覆盖所有命名空间的 WARNING/ERROR)
|
|
263
346
|
tui_handler = TUILoggingHandler(renderer)
|
|
@@ -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
|
|
@@ -63,7 +63,7 @@ _ALLOWED_PRIORITY = {"high", "medium", "low"}
|
|
|
63
63
|
# Tools that are always hidden from scrollback (have dedicated event channels).
|
|
64
64
|
_ALWAYS_HIDDEN_TOOLS: frozenset[str] = frozenset({"askuserquestion", "exitplanmode"})
|
|
65
65
|
|
|
66
|
-
# Task coordination tools hidden from scrollback unless
|
|
66
|
+
# Task coordination tools hidden from scrollback unless explicitly surfaced.
|
|
67
67
|
_SILENT_TASK_TOOLS: frozenset[str] = frozenset({
|
|
68
68
|
"taskcreate", "taskupdate", "tasklist", "taskget",
|
|
69
69
|
})
|
|
@@ -149,13 +149,13 @@ def extract_todos(args: dict[str, Any]) -> list[TodoItemState] | None:
|
|
|
149
149
|
return todos
|
|
150
150
|
|
|
151
151
|
|
|
152
|
-
def _truncate(content: str, max_len: int =
|
|
152
|
+
def _truncate(content: str, max_len: int = 400) -> str:
|
|
153
153
|
if len(content) <= max_len:
|
|
154
154
|
return content
|
|
155
155
|
return f"{content[:max_len - 3]}..."
|
|
156
156
|
|
|
157
157
|
|
|
158
|
-
TOOL_SUMMARY_MAX_LENGTH =
|
|
158
|
+
TOOL_SUMMARY_MAX_LENGTH = 160
|
|
159
159
|
|
|
160
160
|
|
|
161
161
|
def _truncate_path_middle(path: str | None, max_len: int) -> str:
|
|
@@ -206,12 +206,22 @@ def _compact_json(value: Any, max_len: int = 220) -> str:
|
|
|
206
206
|
return _truncate(content, max_len=max_len)
|
|
207
207
|
|
|
208
208
|
|
|
209
|
+
def _summarize_env_vars(value: Any, max_len: int = 120) -> str:
|
|
210
|
+
if not isinstance(value, dict) or not value:
|
|
211
|
+
return ""
|
|
212
|
+
parts: list[str] = []
|
|
213
|
+
for key in sorted(value):
|
|
214
|
+
rendered = f"{key}={value[key]}"
|
|
215
|
+
parts.append(_truncate(str(rendered), 48))
|
|
216
|
+
return _truncate(",".join(parts), max_len=max_len)
|
|
217
|
+
|
|
218
|
+
|
|
209
219
|
def _friendly_kv(args: dict[str, Any], max_len: int = 220) -> str:
|
|
210
220
|
"""将 dict 格式化为 key: "value" 的友好显示格式。"""
|
|
211
221
|
parts: list[str] = []
|
|
212
222
|
for k, v in args.items():
|
|
213
223
|
if isinstance(v, str):
|
|
214
|
-
parts.append(f'{k}: "{_truncate(v,
|
|
224
|
+
parts.append(f'{k}: "{_truncate(v, 160)}"')
|
|
215
225
|
else:
|
|
216
226
|
parts.append(f"{k}: {v}")
|
|
217
227
|
result = ", ".join(parts)
|
|
@@ -363,30 +373,30 @@ def summarize_tool_args(
|
|
|
363
373
|
if lowered == "write":
|
|
364
374
|
path = _lookup_arg(args, "file_path", "path")
|
|
365
375
|
path_display = _truncate_path_middle(
|
|
366
|
-
_normalize_path_for_display(path, project_root) or "",
|
|
376
|
+
_normalize_path_for_display(path, project_root) or "", 120
|
|
367
377
|
)
|
|
368
378
|
return _truncate(f"path={path_display}" if path_display else _compact_json(args), TOOL_SUMMARY_MAX_LENGTH)
|
|
369
379
|
if lowered == "edit":
|
|
370
380
|
path = _lookup_arg(args, "file_path", "path")
|
|
371
381
|
path_display = _truncate_path_middle(
|
|
372
|
-
_normalize_path_for_display(path, project_root) or "",
|
|
382
|
+
_normalize_path_for_display(path, project_root) or "", 120
|
|
373
383
|
)
|
|
374
384
|
return _truncate(path_display or _compact_json(args), TOOL_SUMMARY_MAX_LENGTH)
|
|
375
385
|
if lowered == "multiedit":
|
|
376
386
|
path = _lookup_arg(args, "file_path", "path")
|
|
377
387
|
path_display = _truncate_path_middle(
|
|
378
|
-
_normalize_path_for_display(path, project_root) or "",
|
|
388
|
+
_normalize_path_for_display(path, project_root) or "", 120
|
|
379
389
|
)
|
|
380
390
|
return _truncate(path_display or _compact_json(args), TOOL_SUMMARY_MAX_LENGTH)
|
|
381
391
|
if lowered == "read":
|
|
382
392
|
path = _lookup_arg(args, "file_path", "path")
|
|
383
393
|
path_display = _truncate_path_middle(
|
|
384
|
-
_normalize_path_for_display(path, project_root) or "",
|
|
394
|
+
_normalize_path_for_display(path, project_root) or "", 120
|
|
385
395
|
)
|
|
386
|
-
offset = _lookup_arg(args, "offset_line")
|
|
387
|
-
limit = _lookup_arg(args, "limit_lines")
|
|
396
|
+
offset = _lookup_arg(args, "offset", "offset_line")
|
|
397
|
+
limit = _lookup_arg(args, "limit", "limit_lines")
|
|
388
398
|
raw = f"path={path_display} offset={offset} limit={limit}" if path_display else _compact_json(args)
|
|
389
|
-
return _truncate(raw,
|
|
399
|
+
return _truncate(raw, 160)
|
|
390
400
|
if lowered == "agent":
|
|
391
401
|
subagent_name, description = _extract_task_identity(args)
|
|
392
402
|
if description and description != subagent_name:
|
|
@@ -421,9 +431,9 @@ def summarize_tool_args(
|
|
|
421
431
|
pattern = _lookup_arg(args, "pattern")
|
|
422
432
|
path = _lookup_arg(args, "path")
|
|
423
433
|
path_display = _truncate_path_middle(
|
|
424
|
-
_normalize_path_for_display(path, project_root) or "",
|
|
434
|
+
_normalize_path_for_display(path, project_root) or "", 80
|
|
425
435
|
)
|
|
426
|
-
pattern_display = _truncate(str(pattern),
|
|
436
|
+
pattern_display = _truncate(str(pattern), 60) if pattern else ""
|
|
427
437
|
parts = []
|
|
428
438
|
if path_display:
|
|
429
439
|
parts.append(f"path={path_display}")
|
|
@@ -432,21 +442,34 @@ def summarize_tool_args(
|
|
|
432
442
|
return _truncate(" ".join(parts) if parts else _compact_json(args), TOOL_SUMMARY_MAX_LENGTH)
|
|
433
443
|
if lowered == "bash":
|
|
434
444
|
command_args = _lookup_arg(args, "args")
|
|
445
|
+
env_summary = _summarize_env_vars(_lookup_arg(args, "env"))
|
|
435
446
|
if isinstance(command_args, list):
|
|
436
447
|
cmd = " ".join(str(part) for part in command_args)
|
|
437
|
-
cmd_display = _truncate(cmd,
|
|
448
|
+
cmd_display = _truncate(cmd, 200)
|
|
438
449
|
cwd = _lookup_arg(args, "cwd")
|
|
439
450
|
cwd_display = _normalize_cwd_for_display(cwd, project_root)
|
|
451
|
+
prefix_parts = []
|
|
440
452
|
if cwd_display:
|
|
441
|
-
|
|
453
|
+
prefix_parts.append(f"cwd={cwd_display}")
|
|
454
|
+
if env_summary:
|
|
455
|
+
prefix_parts.append(f"env={env_summary}")
|
|
456
|
+
if prefix_parts:
|
|
457
|
+
return _truncate(f"{' '.join(prefix_parts)} {cmd_display}", 200)
|
|
442
458
|
return cmd_display
|
|
443
459
|
# 回退:兼容非标准格式(如 command 字段)
|
|
444
460
|
command = _lookup_arg(args, "command")
|
|
445
461
|
cwd = _lookup_arg(args, "cwd")
|
|
446
462
|
cwd_display = _normalize_cwd_for_display(cwd, project_root)
|
|
463
|
+
prefix_parts = []
|
|
447
464
|
if cwd_display:
|
|
448
|
-
|
|
449
|
-
|
|
465
|
+
prefix_parts.append(f"cwd={cwd_display}")
|
|
466
|
+
if env_summary:
|
|
467
|
+
prefix_parts.append(f"env={env_summary}")
|
|
468
|
+
if prefix_parts and command:
|
|
469
|
+
return _truncate(f"{' '.join(prefix_parts)} command={_truncate(str(command), 200)}", 200)
|
|
470
|
+
if prefix_parts:
|
|
471
|
+
return _truncate(" ".join(prefix_parts), 200)
|
|
472
|
+
return _truncate(f"command={_truncate(str(command), 200)}", 200) if command else _compact_json(args)
|
|
450
473
|
if lowered == "skill":
|
|
451
474
|
skill_name = _lookup_arg(args, "skill_name", "skill")
|
|
452
475
|
return _truncate(str(skill_name).strip(), TOOL_SUMMARY_MAX_LENGTH) if skill_name else ""
|
|
@@ -101,17 +101,25 @@ def _truncate_file_history(path: Path, max_entries: int = 200) -> None:
|
|
|
101
101
|
"""Truncate a prompt_toolkit FileHistory file to at most max_entries."""
|
|
102
102
|
if not path.exists():
|
|
103
103
|
return
|
|
104
|
-
|
|
105
|
-
if not
|
|
104
|
+
raw = path.read_bytes()
|
|
105
|
+
if not raw.strip():
|
|
106
106
|
return
|
|
107
|
+
content = raw.decode("utf-8")
|
|
108
|
+
normalized = content.replace("\r\n", "\n").replace("\r", "\n")
|
|
107
109
|
# FileHistory format: entries are +prefixed line blocks separated by blank lines
|
|
108
|
-
blocks =
|
|
110
|
+
blocks = normalized.split("\n\n")
|
|
109
111
|
# Filter out empty blocks
|
|
110
112
|
blocks = [b for b in blocks if b.strip()]
|
|
111
|
-
if
|
|
113
|
+
if not blocks:
|
|
112
114
|
return
|
|
113
115
|
kept = blocks[-max_entries:]
|
|
114
|
-
|
|
116
|
+
rewritten = "\n\n".join(kept) + "\n\n"
|
|
117
|
+
rewritten_bytes = rewritten.encode("utf-8")
|
|
118
|
+
if rewritten_bytes == raw:
|
|
119
|
+
return
|
|
120
|
+
# Canonicalize to UTF-8 + LF so prompt_toolkit can reload history consistently
|
|
121
|
+
# across platforms, including Windows.
|
|
122
|
+
path.write_bytes(rewritten_bytes)
|
|
115
123
|
|
|
116
124
|
|
|
117
125
|
class TerminalAgentTUI(
|
|
@@ -739,11 +747,18 @@ class TerminalAgentTUI(
|
|
|
739
747
|
session_cwd = getattr(self._session, "_cwd", None)
|
|
740
748
|
cwd = str(Path(session_cwd).expanduser().resolve()) if session_cwd else str(Path.cwd().resolve())
|
|
741
749
|
history_path = _get_input_history_path(cwd)
|
|
742
|
-
_truncate_file_history(history_path, _INPUT_HISTORY_MAX_ENTRIES)
|
|
743
|
-
return FileHistory(str(history_path))
|
|
744
750
|
except Exception:
|
|
745
751
|
logger.debug("Failed to create FileHistory, falling back to InMemoryHistory", exc_info=True)
|
|
746
752
|
return InMemoryHistory()
|
|
753
|
+
try:
|
|
754
|
+
_truncate_file_history(history_path, _INPUT_HISTORY_MAX_ENTRIES)
|
|
755
|
+
except Exception:
|
|
756
|
+
logger.warning(
|
|
757
|
+
"Failed to normalize input history file %s; continuing with FileHistory",
|
|
758
|
+
history_path,
|
|
759
|
+
exc_info=True,
|
|
760
|
+
)
|
|
761
|
+
return FileHistory(str(history_path))
|
|
747
762
|
|
|
748
763
|
def _input_placeholder_hint(self) -> str | None:
|
|
749
764
|
if self._ui_mode != UIMode.NORMAL:
|
|
@@ -231,6 +231,7 @@ class CommandsMixin:
|
|
|
231
231
|
manager = getattr(self._session._agent, "_mcp_manager", None)
|
|
232
232
|
info_rows = list(getattr(manager, "tool_infos", []) or []) if manager is not None else []
|
|
233
233
|
failed_rows = list(getattr(manager, "failed_servers", []) or []) if manager is not None else []
|
|
234
|
+
state_rows = dict(getattr(manager, "server_states", {}) or {}) if manager is not None else {}
|
|
234
235
|
|
|
235
236
|
tools_by_alias: dict[str, list[dict[str, str]]] = {}
|
|
236
237
|
tool_count_by_alias: dict[str, int] = {}
|
|
@@ -262,13 +263,32 @@ class CommandsMixin:
|
|
|
262
263
|
tool_count = int(tool_count_by_alias.get(alias, 0))
|
|
263
264
|
tools = list(tools_by_alias.get(alias, []))
|
|
264
265
|
tools.sort(key=lambda item: item["remote_name"].lower())
|
|
266
|
+
state = state_rows.get(alias)
|
|
267
|
+
raw_status = str(getattr(state, "status", "") or "").strip().lower()
|
|
268
|
+
state_reason = str(getattr(state, "reason", "") or "").strip() or None
|
|
269
|
+
state_tool_count = int(getattr(state, "tool_count", tool_count) or 0)
|
|
270
|
+
if raw_status == "connected":
|
|
271
|
+
tool_count = state_tool_count
|
|
265
272
|
|
|
266
273
|
if alias in failed_reason_by_alias:
|
|
267
274
|
status = f"✗ {failed_reason_by_alias[alias]}"
|
|
275
|
+
raw_status = "failed"
|
|
268
276
|
elif manager is None:
|
|
269
277
|
status = "… Not initialized"
|
|
270
|
-
|
|
278
|
+
raw_status = "idle"
|
|
279
|
+
elif raw_status == "connecting":
|
|
280
|
+
status = "… Connecting"
|
|
281
|
+
elif raw_status == "failed":
|
|
282
|
+
status = f"✗ {state_reason}" if state_reason else "✗ Not connected"
|
|
283
|
+
tool_count = 0
|
|
284
|
+
elif raw_status == "connected":
|
|
271
285
|
status = "✓ Connected"
|
|
286
|
+
elif tool_count > 0:
|
|
287
|
+
status = "✓ Connected"
|
|
288
|
+
raw_status = "connected"
|
|
289
|
+
else:
|
|
290
|
+
status = "… Not initialized"
|
|
291
|
+
raw_status = "idle"
|
|
272
292
|
|
|
273
293
|
if alias in project_servers:
|
|
274
294
|
config_location = str(project_path)
|
|
@@ -283,6 +303,7 @@ class CommandsMixin:
|
|
|
283
303
|
"server_type": server_type,
|
|
284
304
|
"endpoint": endpoint or "(empty)",
|
|
285
305
|
"status": status,
|
|
306
|
+
"raw_status": raw_status,
|
|
286
307
|
"tool_count": tool_count,
|
|
287
308
|
"tools": tools,
|
|
288
309
|
"config_location": config_location,
|
|
@@ -376,7 +397,8 @@ class CommandsMixin:
|
|
|
376
397
|
|
|
377
398
|
tool_count = int(row["tool_count"])
|
|
378
399
|
options: list[dict[str, str]] = []
|
|
379
|
-
|
|
400
|
+
raw_status = str(row.get("raw_status", "")).strip().lower()
|
|
401
|
+
if raw_status == "connected" and tool_count > 0:
|
|
380
402
|
options.append(
|
|
381
403
|
{
|
|
382
404
|
"value": "view_tools",
|
|
@@ -384,13 +406,14 @@ class CommandsMixin:
|
|
|
384
406
|
"description": f"{tool_count} tools",
|
|
385
407
|
}
|
|
386
408
|
)
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
409
|
+
if raw_status != "connecting":
|
|
410
|
+
options.append(
|
|
411
|
+
{
|
|
412
|
+
"value": "reconnect",
|
|
413
|
+
"label": "Reconnect",
|
|
414
|
+
"description": "Reconnect to this server",
|
|
415
|
+
}
|
|
416
|
+
)
|
|
394
417
|
options.append(
|
|
395
418
|
{
|
|
396
419
|
"value": "back",
|
|
@@ -454,6 +477,9 @@ class CommandsMixin:
|
|
|
454
477
|
def _start_mcp_reconnect(self, alias: str, rows_by_alias: dict[str, dict[str, Any]]) -> None:
|
|
455
478
|
"""启动 MCP server 重连流程。"""
|
|
456
479
|
agent_runtime = self._session._agent
|
|
480
|
+
row = rows_by_alias.get(alias, {})
|
|
481
|
+
server_type = str(row.get("server_type", "")).strip().upper()
|
|
482
|
+
connect_timeout_s = 30.0 if server_type in {"STDIO", "SDK"} else 10.0
|
|
457
483
|
|
|
458
484
|
def on_done(success: bool, error_message: str) -> None:
|
|
459
485
|
if success:
|
|
@@ -492,7 +518,12 @@ class CommandsMixin:
|
|
|
492
518
|
)
|
|
493
519
|
|
|
494
520
|
retry_coro = agent_runtime.retry_mcp_server(alias)
|
|
495
|
-
self._mcp_connecting_view.enter(
|
|
521
|
+
self._mcp_connecting_view.enter(
|
|
522
|
+
alias,
|
|
523
|
+
retry_coro,
|
|
524
|
+
on_done,
|
|
525
|
+
timeout_s=connect_timeout_s,
|
|
526
|
+
)
|
|
496
527
|
self._ui_mode = UIMode.MCP_CONNECTING
|
|
497
528
|
self._sync_focus_for_mode()
|
|
498
529
|
self._invalidate()
|
|
@@ -9,6 +9,7 @@ from prompt_toolkit.application import run_in_terminal
|
|
|
9
9
|
from rich.console import Console
|
|
10
10
|
|
|
11
11
|
from comate_agent_sdk.context.items import ItemType
|
|
12
|
+
from comate_agent_sdk.context.items import TOOL_OBSERVABLE_INPUTS_METADATA_KEY
|
|
12
13
|
from comate_agent_sdk.llm.messages import AssistantMessage, UserMessage
|
|
13
14
|
|
|
14
15
|
from comate_cli.terminal_agent.history_printer import (
|
|
@@ -102,6 +103,13 @@ class HistorySyncMixin:
|
|
|
102
103
|
args_dict = json.loads(args_str) if args_str else {}
|
|
103
104
|
except json.JSONDecodeError:
|
|
104
105
|
args_dict = {"_raw": args_str}
|
|
106
|
+
observable_inputs = (getattr(item, "metadata", {}) or {}).get(
|
|
107
|
+
TOOL_OBSERVABLE_INPUTS_METADATA_KEY
|
|
108
|
+
)
|
|
109
|
+
if isinstance(observable_inputs, dict):
|
|
110
|
+
observable_input = observable_inputs.get(tc.id)
|
|
111
|
+
if isinstance(observable_input, dict):
|
|
112
|
+
args_dict = observable_input
|
|
105
113
|
signature = self._build_resume_tool_signature(tool_name, args_dict)
|
|
106
114
|
# 存储映射,不调用 restore_tool_call(避免加入 _running_tools)
|
|
107
115
|
tool_call_info[tc.id] = (tool_name, signature)
|
|
@@ -138,9 +146,9 @@ class HistorySyncMixin:
|
|
|
138
146
|
# Extract metadata from ContextItem
|
|
139
147
|
item_metadata = getattr(item, "metadata", {}) or {}
|
|
140
148
|
|
|
141
|
-
# Extract diff from raw_envelope for Edit
|
|
149
|
+
# Extract diff from raw_envelope for Edit
|
|
142
150
|
diff_lines: list[str] | None = None
|
|
143
|
-
if tool_name
|
|
151
|
+
if tool_name == "Edit" and not is_error:
|
|
144
152
|
envelope = item_metadata.get("tool_raw_envelope")
|
|
145
153
|
if isinstance(envelope, dict):
|
|
146
154
|
data = envelope.get("data", {})
|