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.
Files changed (131) hide show
  1. {comate_cli-0.4.7 → comate_cli-0.5.0}/.gitignore +1 -0
  2. {comate_cli-0.4.7 → comate_cli-0.5.0}/PKG-INFO +1 -1
  3. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/history_printer.py +2 -2
  4. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/tool_view.py +64 -20
  5. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/tui.py +31 -4
  6. {comate_cli-0.4.7 → comate_cli-0.5.0}/pyproject.toml +1 -1
  7. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_history_printer.py +24 -1
  8. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_tool_view.py +142 -1
  9. comate_cli-0.5.0/uv.lock +2280 -0
  10. comate_cli-0.4.7/uv.lock +0 -2241
  11. {comate_cli-0.4.7 → comate_cli-0.5.0}/README.md +0 -0
  12. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/__init__.py +0 -0
  13. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/__main__.py +0 -0
  14. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/main.py +0 -0
  15. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/mcp_cli.py +0 -0
  16. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/__init__.py +0 -0
  17. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/animations.py +0 -0
  18. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/app.py +0 -0
  19. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/assistant_render.py +0 -0
  20. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/codenames.py +0 -0
  21. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/custom_slash_commands.py +0 -0
  22. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/env_utils.py +0 -0
  23. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/error_display.py +0 -0
  24. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/event_renderer.py +0 -0
  25. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/fragment_utils.py +0 -0
  26. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/input_geometry.py +0 -0
  27. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/layout_coordinator.py +0 -0
  28. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/logging_adapter.py +0 -0
  29. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/logo.py +0 -0
  30. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/markdown_render.py +0 -0
  31. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/mention_completer.py +0 -0
  32. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/message_style.py +0 -0
  33. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/models.py +0 -0
  34. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/path_context_hint.py +0 -0
  35. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/plugins/__init__.py +0 -0
  36. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/plugins/components/__init__.py +0 -0
  37. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/plugins/components/detail_view.py +0 -0
  38. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/plugins/components/plugin_list.py +0 -0
  39. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/plugins/components/search_box.py +0 -0
  40. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/plugins/components/tab_bar.py +0 -0
  41. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/plugins/marketplace_install_view.py +0 -0
  42. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/plugins/plugin_picker.py +0 -0
  43. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/plugins/tabs/__init__.py +0 -0
  44. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/plugins/tabs/discover_tab.py +0 -0
  45. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/plugins/tabs/errors_tab.py +0 -0
  46. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/plugins/tabs/installed_tab.py +0 -0
  47. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/plugins/tabs/marketplaces_tab.py +0 -0
  48. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/preflight.py +0 -0
  49. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/question_view.py +0 -0
  50. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/resume_selector.py +0 -0
  51. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/rewind_store.py +0 -0
  52. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/rpc_protocol.py +0 -0
  53. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/rpc_stdio.py +0 -0
  54. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/selection_menu.py +0 -0
  55. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/session_view.py +0 -0
  56. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/slash_commands.py +0 -0
  57. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/startup.py +0 -0
  58. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/status_bar.py +0 -0
  59. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/text_effects.py +0 -0
  60. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/tips.py +0 -0
  61. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/tui_parts/__init__.py +0 -0
  62. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/tui_parts/commands.py +0 -0
  63. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/tui_parts/history_sync.py +0 -0
  64. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/tui_parts/input_behavior.py +0 -0
  65. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/tui_parts/key_bindings.py +0 -0
  66. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/tui_parts/mcp_connecting_view.py +0 -0
  67. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/tui_parts/render_panels.py +0 -0
  68. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/tui_parts/slash_command_registry.py +0 -0
  69. {comate_cli-0.4.7 → comate_cli-0.5.0}/comate_cli/terminal_agent/tui_parts/ui_mode.py +0 -0
  70. {comate_cli-0.4.7 → comate_cli-0.5.0}/docs/superpowers/plans/2026-04-03-phrase-shuffle.md +0 -0
  71. {comate_cli-0.4.7 → comate_cli-0.5.0}/docs/superpowers/specs/2026-04-01-conditional-diff-subtitle-design.md +0 -0
  72. {comate_cli-0.4.7 → comate_cli-0.5.0}/docs/superpowers/specs/2026-04-03-phrase-shuffle-design.md +0 -0
  73. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/conftest.py +0 -0
  74. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_animator_shuffle.py +0 -0
  75. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_app_mcp_preload.py +0 -0
  76. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_app_preflight_gate.py +0 -0
  77. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_app_print_mode.py +0 -0
  78. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_app_shutdown.py +0 -0
  79. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_app_usage_line.py +0 -0
  80. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_cli_project_root.py +0 -0
  81. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_compact_command_semantics.py +0 -0
  82. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_completion_context_activation.py +0 -0
  83. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_completion_status_panel.py +0 -0
  84. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_context_command.py +0 -0
  85. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_custom_slash_commands.py +0 -0
  86. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_discover_tab.py +0 -0
  87. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_errors_tab.py +0 -0
  88. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_event_renderer.py +0 -0
  89. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_format_error.py +0 -0
  90. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_handle_error.py +0 -0
  91. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_history_sync.py +0 -0
  92. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_input_behavior.py +0 -0
  93. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_input_history.py +0 -0
  94. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_installed_tab.py +0 -0
  95. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_interrupt_exit_semantics.py +0 -0
  96. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_layout_coordinator.py +0 -0
  97. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_logging_adapter.py +0 -0
  98. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_logo.py +0 -0
  99. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_main_args.py +0 -0
  100. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_marketplaces_tab.py +0 -0
  101. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_mcp_cli.py +0 -0
  102. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_mcp_slash_command.py +0 -0
  103. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_mention_completer.py +0 -0
  104. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_path_context_hint.py +0 -0
  105. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_plugin_slash_commands.py +0 -0
  106. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_plugin_tui_components.py +0 -0
  107. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_preflight.py +0 -0
  108. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_preflight_copilot.py +0 -0
  109. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_question_key_bindings.py +0 -0
  110. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_question_view.py +0 -0
  111. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_resume_selector.py +0 -0
  112. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_rewind_command_semantics.py +0 -0
  113. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_rewind_store.py +0 -0
  114. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_rpc_protocol.py +0 -0
  115. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_rpc_stdio_bridge.py +0 -0
  116. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_selection_menu.py +0 -0
  117. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_skills_slash_command.py +0 -0
  118. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_slash_argument_hint.py +0 -0
  119. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_slash_completer.py +0 -0
  120. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_slash_registry.py +0 -0
  121. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_status_bar.py +0 -0
  122. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_status_bar_transient.py +0 -0
  123. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_task_panel_format.py +0 -0
  124. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_task_panel_key_bindings.py +0 -0
  125. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_task_panel_rendering.py +0 -0
  126. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_task_poll.py +0 -0
  127. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_tui_elapsed_status.py +0 -0
  128. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_tui_mcp_init_gate.py +0 -0
  129. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_tui_paste_placeholder.py +0 -0
  130. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_tui_split_invariance.py +0 -0
  131. {comate_cli-0.4.7 → comate_cli-0.5.0}/tests/test_update_check.py +0 -0
@@ -8,6 +8,7 @@ set_env.sh
8
8
  .worktrees/
9
9
  # C extensions
10
10
  *.so
11
+ codebase_claude/
11
12
  claude_code_codebase/
12
13
  .claude/
13
14
  # Distribution / packaging
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: comate-cli
3
- Version: 0.4.7
3
+ Version: 0.5.0
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
@@ -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 = _normalize_path_for_display(path, project_root)
339
- return f"path={path_display}" if path_display else _compact_json(args)
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 = _normalize_path_for_display(path, project_root)
343
- return path_display or _compact_json(args)
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 = _normalize_path_for_display(path, project_root)
347
- return path_display or _compact_json(args)
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 = _normalize_path_for_display(path, project_root)
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
- return f"path={path_display} offset={offset} limit={limit}" if path_display else _compact_json(args)
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 = _normalize_path_for_display(path, project_root)
388
- return f"path={path_display} pattern={pattern}" if (path_display or pattern) else _compact_json(args)
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, 180)
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), 180)}" if command else _compact_json(args)
405
- return f"command={_truncate(str(command), 180)}" if command else _compact_json(args)
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
- Window(height=1, style="class:loading"),
499
+ ConditionalContainer(
500
+ content=Window(height=1, style="class:loading"),
501
+ filter=_has_todo_or_diff,
502
+ ),
482
503
  self._diff_panel_container,
483
- Window(height=1, style="class:input.separator"),
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._renderer._update_tasks(task_dicts, list_id=list_id)
1259
- self._render_dirty = True
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:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "comate-cli"
7
- version = "0.4.7"
7
+ version = "0.5.0"
8
8
  description = "Comate terminal CLI built on comate-agent-sdk"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -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 ToolEventView, should_show_tool_in_scrollback
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)