fix(tui): keep card-tool lifecycle under the answer-only chrome gate

The gateway's _tool_lifecycle_required_for_ui listed only clarify /
manage_connections / setup_mcp, while Desktop's answer-only gate keeps
card tools via isCardTool (delegate_task, image_generate, manage_catalog,
plus clarify / manage_connections). With show_reasoning off, a successful
image_generate / manage_catalog / delegate_task was dropped at the gateway
(no tool.start, no tool.complete) so Desktop rendered nothing at all — and a
failed one emitted an orphaned tool.complete. Extend the UI-required set to
match isCardTool so the two layers agree.
This commit is contained in:
Hermes Agent
2026-09-25 18:09:07 -05:00
committed by brooklyn!
parent e9e93b5f57
commit c0f1ed114c
2 changed files with 39 additions and 2 deletions

View File

@@ -175,6 +175,33 @@ def test_shown_reasoning_keeps_subagent_thinking_text(monkeypatch):
assert events[0][2]["text"] == "the child's visible thought"
def test_hidden_reasoning_keeps_card_tool_lifecycle(monkeypatch):
"""Card tools are the turn's deliverable, not chrome.
Desktop's answer-only gate (`message-parts.tsx`) keeps card tools visible via
`isCardTool` (tool-render-class.ts): clarify, delegate_task, image_generate,
manage_catalog, manage_connections. The gateway must emit their full
lifecycle under answer-only mode too — no event means no part means nothing
for Desktop to keep. A suppressed `tool.start` would also orphan a failed
call's `tool.complete`.
"""
events = _capture(monkeypatch)
_session(monkeypatch, "hide-cards", show_reasoning=False, effort="high")
cases = {
"tool-image": ("image_generate", {"prompt": "a cat"}),
"tool-catalog": ("manage_catalog", {"action": "list"}),
"tool-delegate": ("delegate_task", {"goal": "do it"}),
}
for tool_id, (name, args) in cases.items():
server._on_tool_start("hide-cards", tool_id, name, args)
server._on_tool_complete("hide-cards", tool_id, name, args, json.dumps({"success": True}))
for tool_id in cases:
lifecycle = [event[0] for event in events if event[2].get("tool_id") == tool_id]
assert lifecycle == ["tool.start", "tool.complete"], tool_id
def test_hidden_reasoning_shows_failed_terminal_exit_code(monkeypatch):
events = _capture(monkeypatch)
_session(monkeypatch, "hide-exit", show_reasoning=False, effort="high")

View File

@@ -2024,8 +2024,18 @@ def _tool_progress_enabled(sid: str) -> bool:
def _tool_lifecycle_required_for_ui(name: str) -> bool:
"""Interactive UI, not optional chrome: Desktop renders clarify / connection cards from the tool-call part."""
return name in ("clarify", "manage_connections", "setup_mcp")
"""Interactive UI / card surfaces, not optional chrome.
Desktop renders these from the tool-call part itself, so suppressing the
lifecycle hides the turn's deliverable entirely (`isCardTool` in
apps/desktop/src/lib/tool-render-class.ts must stay in sync with this set):
clarify / connection cards are consent surfaces, and image_generate /
manage_catalog / delegate_task draw the thing the user asked for.
"""
return name in (
"clarify", "manage_connections", "setup_mcp",
"image_generate", "manage_catalog", "delegate_task",
)
def _restart_slash_worker(sid: str, session: dict):