comate-cli 0.4.7__tar.gz → 0.5.0__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.7 → comate_cli-0.5.0}/.gitignore +1 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/PKG-INFO +1 -1
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/history_printer.py +2 -2
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/tool_view.py +64 -20
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/tui.py +31 -4
- {comate_cli-0.4.7 → comate_cli-0.5.0}/pyproject.toml +1 -1
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_history_printer.py +24 -1
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_tool_view.py +142 -1
- comate_cli-0.5.0/uv.lock +2280 -0
- comate_cli-0.4.7/uv.lock +0 -2241
- {comate_cli-0.4.7 → comate_cli-0.5.0}/README.md +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/__init__.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/__main__.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/main.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/mcp_cli.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/__init__.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/animations.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/app.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/assistant_render.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/codenames.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/custom_slash_commands.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/env_utils.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/error_display.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/event_renderer.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/fragment_utils.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/input_geometry.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/layout_coordinator.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/logging_adapter.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/logo.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/markdown_render.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/mention_completer.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/message_style.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/models.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/path_context_hint.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/plugins/__init__.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/plugins/components/__init__.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/plugins/components/detail_view.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/plugins/components/plugin_list.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/plugins/components/search_box.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/plugins/components/tab_bar.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/plugins/marketplace_install_view.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/plugins/plugin_picker.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/plugins/tabs/__init__.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/plugins/tabs/discover_tab.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/plugins/tabs/errors_tab.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/plugins/tabs/installed_tab.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/plugins/tabs/marketplaces_tab.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/preflight.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/question_view.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/resume_selector.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/rewind_store.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/rpc_protocol.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/rpc_stdio.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/selection_menu.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/session_view.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/slash_commands.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/startup.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/status_bar.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/text_effects.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/tips.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/tui_parts/__init__.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/tui_parts/commands.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/tui_parts/history_sync.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/tui_parts/input_behavior.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/tui_parts/key_bindings.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/tui_parts/mcp_connecting_view.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/tui_parts/render_panels.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/tui_parts/slash_command_registry.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/tui_parts/ui_mode.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/docs/superpowers/plans/2026-04-03-phrase-shuffle.md +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/docs/superpowers/specs/2026-04-01-conditional-diff-subtitle-design.md +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/docs/superpowers/specs/2026-04-03-phrase-shuffle-design.md +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/conftest.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_animator_shuffle.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_app_mcp_preload.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_app_preflight_gate.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_app_print_mode.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_app_shutdown.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_app_usage_line.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_cli_project_root.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_compact_command_semantics.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_completion_context_activation.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_completion_status_panel.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_context_command.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_custom_slash_commands.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_discover_tab.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_errors_tab.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_event_renderer.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_format_error.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_handle_error.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_history_sync.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_input_behavior.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_input_history.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_installed_tab.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_interrupt_exit_semantics.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_layout_coordinator.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_logging_adapter.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_logo.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_main_args.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_marketplaces_tab.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_mcp_cli.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_mcp_slash_command.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_mention_completer.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_path_context_hint.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_plugin_slash_commands.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_plugin_tui_components.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_preflight.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_preflight_copilot.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_question_key_bindings.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_question_view.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_resume_selector.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_rewind_command_semantics.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_rewind_store.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_rpc_protocol.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_rpc_stdio_bridge.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_selection_menu.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_skills_slash_command.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_slash_argument_hint.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_slash_completer.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_slash_registry.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_status_bar.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_status_bar_transient.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_task_panel_format.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_task_panel_key_bindings.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_task_panel_rendering.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_task_poll.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_tui_elapsed_status.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_tui_mcp_init_gate.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_tui_paste_placeholder.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_tui_split_invariance.py +0 -0
- {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_update_check.py +0 -0
|
@@ -173,8 +173,8 @@ def render_history_group(
|
|
|
173
173
|
|
|
174
174
|
|
|
175
175
|
async def print_history_group_async(console: Console, group: Group) -> None:
|
|
176
|
-
console.print(group)
|
|
176
|
+
console.print(group, soft_wrap=True)
|
|
177
177
|
|
|
178
178
|
|
|
179
179
|
def print_history_group_sync(console: Console, group: Group) -> None:
|
|
180
|
-
console.print(group)
|
|
180
|
+
console.print(group, soft_wrap=True)
|
|
@@ -152,7 +152,34 @@ def extract_todos(args: dict[str, Any]) -> list[TodoItemState] | None:
|
|
|
152
152
|
def _truncate(content: str, max_len: int = 280) -> str:
|
|
153
153
|
if len(content) <= max_len:
|
|
154
154
|
return content
|
|
155
|
-
return f"{content[:max_len]}..."
|
|
155
|
+
return f"{content[:max_len - 3]}..."
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
TOOL_SUMMARY_MAX_LENGTH = 50
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _truncate_path_middle(path: str | None, max_len: int) -> str:
|
|
162
|
+
"""Middle-truncate a path, preserving the filename.
|
|
163
|
+
|
|
164
|
+
Example: 'a/b/c/d/very/deep/file.ts' → '…/deep/file.ts'
|
|
165
|
+
"""
|
|
166
|
+
if not path:
|
|
167
|
+
return ""
|
|
168
|
+
if len(path) <= max_len:
|
|
169
|
+
return path
|
|
170
|
+
parts = path.replace("\\", "/").split("/")
|
|
171
|
+
filename = parts[-1]
|
|
172
|
+
if len(filename) + 2 >= max_len: # +2 for …/
|
|
173
|
+
return _truncate(filename, max_len)
|
|
174
|
+
result = filename
|
|
175
|
+
for part in reversed(parts[:-1]):
|
|
176
|
+
candidate = f"{part}/{result}"
|
|
177
|
+
if len(candidate) + 2 > max_len: # +2 for …/
|
|
178
|
+
break
|
|
179
|
+
result = candidate
|
|
180
|
+
if result == path:
|
|
181
|
+
return path
|
|
182
|
+
return f"…/{result}"
|
|
156
183
|
|
|
157
184
|
|
|
158
185
|
def _normalize_inline(content: str) -> str:
|
|
@@ -335,27 +362,36 @@ def summarize_tool_args(
|
|
|
335
362
|
lowered = tool_name.lower()
|
|
336
363
|
if lowered == "write":
|
|
337
364
|
path = _lookup_arg(args, "file_path", "path")
|
|
338
|
-
path_display =
|
|
339
|
-
|
|
365
|
+
path_display = _truncate_path_middle(
|
|
366
|
+
_normalize_path_for_display(path, project_root) or "", 45
|
|
367
|
+
)
|
|
368
|
+
return _truncate(f"path={path_display}" if path_display else _compact_json(args), TOOL_SUMMARY_MAX_LENGTH)
|
|
340
369
|
if lowered == "edit":
|
|
341
370
|
path = _lookup_arg(args, "file_path", "path")
|
|
342
|
-
path_display =
|
|
343
|
-
|
|
371
|
+
path_display = _truncate_path_middle(
|
|
372
|
+
_normalize_path_for_display(path, project_root) or "", 45
|
|
373
|
+
)
|
|
374
|
+
return _truncate(path_display or _compact_json(args), TOOL_SUMMARY_MAX_LENGTH)
|
|
344
375
|
if lowered == "multiedit":
|
|
345
376
|
path = _lookup_arg(args, "file_path", "path")
|
|
346
|
-
path_display =
|
|
347
|
-
|
|
377
|
+
path_display = _truncate_path_middle(
|
|
378
|
+
_normalize_path_for_display(path, project_root) or "", 45
|
|
379
|
+
)
|
|
380
|
+
return _truncate(path_display or _compact_json(args), TOOL_SUMMARY_MAX_LENGTH)
|
|
348
381
|
if lowered == "read":
|
|
349
382
|
path = _lookup_arg(args, "file_path", "path")
|
|
350
|
-
path_display =
|
|
383
|
+
path_display = _truncate_path_middle(
|
|
384
|
+
_normalize_path_for_display(path, project_root) or "", 40
|
|
385
|
+
)
|
|
351
386
|
offset = _lookup_arg(args, "offset_line")
|
|
352
387
|
limit = _lookup_arg(args, "limit_lines")
|
|
353
|
-
|
|
388
|
+
raw = f"path={path_display} offset={offset} limit={limit}" if path_display else _compact_json(args)
|
|
389
|
+
return _truncate(raw, 65)
|
|
354
390
|
if lowered == "agent":
|
|
355
391
|
subagent_name, description = _extract_task_identity(args)
|
|
356
392
|
if description and description != subagent_name:
|
|
357
|
-
return f"{subagent_name}({description})"
|
|
358
|
-
return subagent_name
|
|
393
|
+
return _truncate(f"{subagent_name}({description})", TOOL_SUMMARY_MAX_LENGTH)
|
|
394
|
+
return _truncate(subagent_name, TOOL_SUMMARY_MAX_LENGTH)
|
|
359
395
|
if lowered == "taskcreate":
|
|
360
396
|
subject = str(_lookup_arg(args, "subject") or "").strip()
|
|
361
397
|
blocked_by = _lookup_arg(args, "blocked_by")
|
|
@@ -384,31 +420,39 @@ def summarize_tool_args(
|
|
|
384
420
|
if lowered in {"grep", "glob", "ls"}:
|
|
385
421
|
pattern = _lookup_arg(args, "pattern")
|
|
386
422
|
path = _lookup_arg(args, "path")
|
|
387
|
-
path_display =
|
|
388
|
-
|
|
423
|
+
path_display = _truncate_path_middle(
|
|
424
|
+
_normalize_path_for_display(path, project_root) or "", 25
|
|
425
|
+
)
|
|
426
|
+
pattern_display = _truncate(str(pattern), 20) if pattern else ""
|
|
427
|
+
parts = []
|
|
428
|
+
if path_display:
|
|
429
|
+
parts.append(f"path={path_display}")
|
|
430
|
+
if pattern_display:
|
|
431
|
+
parts.append(f"pattern={pattern_display}")
|
|
432
|
+
return _truncate(" ".join(parts) if parts else _compact_json(args), TOOL_SUMMARY_MAX_LENGTH)
|
|
389
433
|
if lowered == "bash":
|
|
390
434
|
command_args = _lookup_arg(args, "args")
|
|
391
435
|
if isinstance(command_args, list):
|
|
392
436
|
cmd = " ".join(str(part) for part in command_args)
|
|
393
|
-
cmd_display = _truncate(cmd,
|
|
437
|
+
cmd_display = _truncate(cmd, 120)
|
|
394
438
|
cwd = _lookup_arg(args, "cwd")
|
|
395
439
|
cwd_display = _normalize_cwd_for_display(cwd, project_root)
|
|
396
440
|
if cwd_display:
|
|
397
|
-
return f"cwd={cwd_display} {cmd_display}"
|
|
441
|
+
return _truncate(f"cwd={cwd_display} {cmd_display}", 120)
|
|
398
442
|
return cmd_display
|
|
399
443
|
# 回退:兼容非标准格式(如 command 字段)
|
|
400
444
|
command = _lookup_arg(args, "command")
|
|
401
445
|
cwd = _lookup_arg(args, "cwd")
|
|
402
446
|
cwd_display = _normalize_cwd_for_display(cwd, project_root)
|
|
403
447
|
if cwd_display:
|
|
404
|
-
return f"cwd={cwd_display} command={_truncate(str(command),
|
|
405
|
-
return f"command={_truncate(str(command),
|
|
448
|
+
return _truncate(f"cwd={cwd_display} command={_truncate(str(command), 120)}", 120) if command else _compact_json(args)
|
|
449
|
+
return _truncate(f"command={_truncate(str(command), 120)}", 120) if command else _compact_json(args)
|
|
406
450
|
if lowered == "skill":
|
|
407
451
|
skill_name = _lookup_arg(args, "skill_name", "skill")
|
|
408
|
-
return str(skill_name).strip() if skill_name else ""
|
|
452
|
+
return _truncate(str(skill_name).strip(), TOOL_SUMMARY_MAX_LENGTH) if skill_name else ""
|
|
409
453
|
if lowered == "webfetch":
|
|
410
454
|
url = _lookup_arg(args, "url")
|
|
411
|
-
return f"url={url}" if url else _compact_json(args)
|
|
455
|
+
return _truncate(f"url={url}" if url else _compact_json(args), TOOL_SUMMARY_MAX_LENGTH)
|
|
412
456
|
if lowered == "todowrite":
|
|
413
457
|
todos = extract_todos(args)
|
|
414
458
|
if todos is None:
|
|
@@ -420,7 +464,7 @@ def summarize_tool_args(
|
|
|
420
464
|
f"todos={len(todos)} pending={pending} "
|
|
421
465
|
f"in_progress={in_progress} completed={completed}"
|
|
422
466
|
)
|
|
423
|
-
return _friendly_kv(args)
|
|
467
|
+
return _truncate(_friendly_kv(args), TOOL_SUMMARY_MAX_LENGTH)
|
|
424
468
|
|
|
425
469
|
|
|
426
470
|
class ToolEventView:
|
|
@@ -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:
|
|
@@ -5,7 +5,7 @@ import unittest
|
|
|
5
5
|
from rich.console import Console
|
|
6
6
|
from rich.text import Text
|
|
7
7
|
|
|
8
|
-
from comate_cli.terminal_agent.history_printer import render_history_group
|
|
8
|
+
from comate_cli.terminal_agent.history_printer import render_history_group, print_history_group_sync
|
|
9
9
|
from comate_cli.terminal_agent.models import HistoryEntry
|
|
10
10
|
|
|
11
11
|
|
|
@@ -78,3 +78,26 @@ class TestSubtitleRendering(unittest.TestCase):
|
|
|
78
78
|
self.assertIn("⎿", output)
|
|
79
79
|
self.assertIn("Listed directory src/components/", output)
|
|
80
80
|
self.assertNotIn("•", output)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class TestSoftWrapPrefixProtection(unittest.TestCase):
|
|
84
|
+
"""Verify ● prefix never gets separated from tool content via print_history_group_sync."""
|
|
85
|
+
|
|
86
|
+
def test_long_tool_result_prefix_on_same_line(self) -> None:
|
|
87
|
+
"""print_history_group_sync must keep ● on the same line as tool name."""
|
|
88
|
+
entry = HistoryEntry(
|
|
89
|
+
entry_type="tool_result",
|
|
90
|
+
text="Read(path=very/long/path/that/exceeds/terminal/width/file.ts offset=595 limit=35)",
|
|
91
|
+
)
|
|
92
|
+
console = Console(file=None, force_terminal=True, width=40)
|
|
93
|
+
group = render_history_group(
|
|
94
|
+
console, [entry], terminal_width=40, render_markdown_to_plain=_identity_md,
|
|
95
|
+
)
|
|
96
|
+
self.assertIsNotNone(group)
|
|
97
|
+
with console.capture() as capture:
|
|
98
|
+
print_history_group_sync(console, group)
|
|
99
|
+
output = capture.get()
|
|
100
|
+
lines = output.strip().split("\n")
|
|
101
|
+
first_content_line = lines[0]
|
|
102
|
+
self.assertIn("●", first_content_line)
|
|
103
|
+
self.assertIn("Read", first_content_line)
|
|
@@ -2,7 +2,13 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import unittest
|
|
4
4
|
|
|
5
|
-
from comate_cli.terminal_agent.tool_view import
|
|
5
|
+
from comate_cli.terminal_agent.tool_view import (
|
|
6
|
+
ToolEventView,
|
|
7
|
+
should_show_tool_in_scrollback,
|
|
8
|
+
_truncate,
|
|
9
|
+
_truncate_path_middle,
|
|
10
|
+
TOOL_SUMMARY_MAX_LENGTH,
|
|
11
|
+
)
|
|
6
12
|
|
|
7
13
|
|
|
8
14
|
class TestToolEventView(unittest.TestCase):
|
|
@@ -143,5 +149,140 @@ class TestShouldShowToolInScrollback(unittest.TestCase):
|
|
|
143
149
|
self.assertFalse(should_show_tool_in_scrollback("TASKCREATE", {"subject": "x"}))
|
|
144
150
|
|
|
145
151
|
|
|
152
|
+
class TestTruncateSemantics(unittest.TestCase):
|
|
153
|
+
"""Verify _truncate output is strictly <= max_len."""
|
|
154
|
+
|
|
155
|
+
def test_truncate_output_within_max_len(self) -> None:
|
|
156
|
+
result = _truncate("x" * 100, 50)
|
|
157
|
+
self.assertLessEqual(len(result), 50)
|
|
158
|
+
self.assertTrue(result.endswith("..."))
|
|
159
|
+
|
|
160
|
+
def test_truncate_short_input_unchanged(self) -> None:
|
|
161
|
+
self.assertEqual(_truncate("hello", 50), "hello")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class TestTruncatePathMiddle(unittest.TestCase):
|
|
165
|
+
def test_short_path_unchanged(self) -> None:
|
|
166
|
+
self.assertEqual(_truncate_path_middle("src/main.py", 50), "src/main.py")
|
|
167
|
+
|
|
168
|
+
def test_long_path_preserves_filename(self) -> None:
|
|
169
|
+
path = "a/b/c/d/very/deep/nested/file.ts"
|
|
170
|
+
result = _truncate_path_middle(path, 20)
|
|
171
|
+
self.assertTrue(result.endswith("file.ts"))
|
|
172
|
+
self.assertTrue(result.startswith("…/"))
|
|
173
|
+
self.assertLessEqual(len(result), 20)
|
|
174
|
+
|
|
175
|
+
def test_filename_only_no_separator(self) -> None:
|
|
176
|
+
result = _truncate_path_middle("file.ts", 50)
|
|
177
|
+
self.assertEqual(result, "file.ts")
|
|
178
|
+
|
|
179
|
+
def test_filename_exceeds_max_len_end_truncates(self) -> None:
|
|
180
|
+
result = _truncate_path_middle("very_long_filename_that_is_way_too_long.ts", 15)
|
|
181
|
+
self.assertLessEqual(len(result), 15)
|
|
182
|
+
self.assertTrue(result.endswith("..."))
|
|
183
|
+
|
|
184
|
+
def test_empty_path(self) -> None:
|
|
185
|
+
self.assertEqual(_truncate_path_middle("", 50), "")
|
|
186
|
+
|
|
187
|
+
def test_none_path_returns_empty(self) -> None:
|
|
188
|
+
self.assertEqual(_truncate_path_middle(None, 50), "")
|
|
189
|
+
|
|
190
|
+
def test_preserves_multiple_trailing_segments(self) -> None:
|
|
191
|
+
path = "a/b/c/deep/file.ts"
|
|
192
|
+
result = _truncate_path_middle(path, 16)
|
|
193
|
+
self.assertIn("deep/file.ts", result)
|
|
194
|
+
self.assertTrue(result.startswith("…/"))
|
|
195
|
+
self.assertLessEqual(len(result), 16)
|
|
196
|
+
|
|
197
|
+
def test_backslash_path_normalized(self) -> None:
|
|
198
|
+
path = "a\\b\\c\\deep\\file.ts"
|
|
199
|
+
result = _truncate_path_middle(path, 16)
|
|
200
|
+
self.assertIn("file.ts", result)
|
|
201
|
+
self.assertNotIn("\\", result)
|
|
202
|
+
|
|
203
|
+
def test_constant_value(self) -> None:
|
|
204
|
+
self.assertEqual(TOOL_SUMMARY_MAX_LENGTH, 50)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class TestSummarizeToolArgsTruncation(unittest.TestCase):
|
|
208
|
+
"""Verify all tool branches respect truncation limits."""
|
|
209
|
+
|
|
210
|
+
_long_path = "claude-code-source-code-3da94d5e5f2b99c9d82b0d8f09448b04775cd41f/src/utils/swarm/inProcessRunner.ts"
|
|
211
|
+
|
|
212
|
+
def test_read_within_budget(self) -> None:
|
|
213
|
+
from comate_cli.terminal_agent.tool_view import summarize_tool_args
|
|
214
|
+
args = {"file_path": self._long_path, "offset_line": 595, "limit_lines": 35}
|
|
215
|
+
result = summarize_tool_args("Read", args, None)
|
|
216
|
+
self.assertLessEqual(len(result), 65)
|
|
217
|
+
self.assertIn("inProcessRunner.ts", result)
|
|
218
|
+
|
|
219
|
+
def test_write_within_budget(self) -> None:
|
|
220
|
+
from comate_cli.terminal_agent.tool_view import summarize_tool_args
|
|
221
|
+
args = {"file_path": self._long_path}
|
|
222
|
+
result = summarize_tool_args("Write", args, None)
|
|
223
|
+
self.assertLessEqual(len(result), TOOL_SUMMARY_MAX_LENGTH)
|
|
224
|
+
self.assertIn("inProcessRunner.ts", result)
|
|
225
|
+
|
|
226
|
+
def test_edit_within_budget(self) -> None:
|
|
227
|
+
from comate_cli.terminal_agent.tool_view import summarize_tool_args
|
|
228
|
+
args = {"file_path": self._long_path}
|
|
229
|
+
result = summarize_tool_args("Edit", args, None)
|
|
230
|
+
self.assertLessEqual(len(result), TOOL_SUMMARY_MAX_LENGTH)
|
|
231
|
+
|
|
232
|
+
def test_grep_within_budget(self) -> None:
|
|
233
|
+
from comate_cli.terminal_agent.tool_view import summarize_tool_args
|
|
234
|
+
args = {"path": self._long_path, "pattern": "function findAvailableTask"}
|
|
235
|
+
result = summarize_tool_args("Grep", args, None)
|
|
236
|
+
self.assertLessEqual(len(result), TOOL_SUMMARY_MAX_LENGTH)
|
|
237
|
+
|
|
238
|
+
def test_bash_within_budget(self) -> None:
|
|
239
|
+
from comate_cli.terminal_agent.tool_view import summarize_tool_args
|
|
240
|
+
args = {"args": ["echo"] + ["x" * 200]}
|
|
241
|
+
result = summarize_tool_args("Bash", args, None)
|
|
242
|
+
self.assertLessEqual(len(result), 120)
|
|
243
|
+
|
|
244
|
+
def test_bash_command_within_budget(self) -> None:
|
|
245
|
+
from comate_cli.terminal_agent.tool_view import summarize_tool_args
|
|
246
|
+
args = {"command": "x" * 200}
|
|
247
|
+
result = summarize_tool_args("Bash", args, None)
|
|
248
|
+
self.assertLessEqual(len(result), 120)
|
|
249
|
+
|
|
250
|
+
def test_fallback_within_budget(self) -> None:
|
|
251
|
+
from comate_cli.terminal_agent.tool_view import summarize_tool_args
|
|
252
|
+
args = {"key": "x" * 300}
|
|
253
|
+
result = summarize_tool_args("UnknownTool", args, None)
|
|
254
|
+
self.assertLessEqual(len(result), TOOL_SUMMARY_MAX_LENGTH)
|
|
255
|
+
|
|
256
|
+
def test_short_args_unchanged(self) -> None:
|
|
257
|
+
from comate_cli.terminal_agent.tool_view import summarize_tool_args
|
|
258
|
+
args = {"file_path": "src/main.py"}
|
|
259
|
+
result = summarize_tool_args("Read", args, None)
|
|
260
|
+
self.assertIn("src/main.py", result)
|
|
261
|
+
|
|
262
|
+
def test_webfetch_within_budget(self) -> None:
|
|
263
|
+
from comate_cli.terminal_agent.tool_view import summarize_tool_args
|
|
264
|
+
args = {"url": "https://example.com/" + "x" * 200}
|
|
265
|
+
result = summarize_tool_args("WebFetch", args, None)
|
|
266
|
+
self.assertLessEqual(len(result), TOOL_SUMMARY_MAX_LENGTH)
|
|
267
|
+
|
|
268
|
+
def test_agent_within_budget(self) -> None:
|
|
269
|
+
from comate_cli.terminal_agent.tool_view import summarize_tool_args
|
|
270
|
+
args = {"subagent_type": "Explore", "description": "x" * 200}
|
|
271
|
+
result = summarize_tool_args("Agent", args, None)
|
|
272
|
+
self.assertLessEqual(len(result), TOOL_SUMMARY_MAX_LENGTH)
|
|
273
|
+
|
|
274
|
+
def test_skill_within_budget(self) -> None:
|
|
275
|
+
from comate_cli.terminal_agent.tool_view import summarize_tool_args
|
|
276
|
+
args = {"skill": "x" * 200}
|
|
277
|
+
result = summarize_tool_args("Skill", args, None)
|
|
278
|
+
self.assertLessEqual(len(result), TOOL_SUMMARY_MAX_LENGTH)
|
|
279
|
+
|
|
280
|
+
def test_grep_pattern_only_within_budget(self) -> None:
|
|
281
|
+
from comate_cli.terminal_agent.tool_view import summarize_tool_args
|
|
282
|
+
args = {"pattern": "x" * 200}
|
|
283
|
+
result = summarize_tool_args("Grep", args, None)
|
|
284
|
+
self.assertLessEqual(len(result), TOOL_SUMMARY_MAX_LENGTH)
|
|
285
|
+
|
|
286
|
+
|
|
146
287
|
if __name__ == "__main__":
|
|
147
288
|
unittest.main(verbosity=2)
|