diff --git a/tests/tui_gateway/test_todo_state_events.py b/tests/tui_gateway/test_todo_state_events.py index 8ac433c95c..2f38226e37 100644 --- a/tests/tui_gateway/test_todo_state_events.py +++ b/tests/tui_gateway/test_todo_state_events.py @@ -98,3 +98,18 @@ def test_empty_list_at_nonzero_revision_is_a_real_clear(): state = server._normalize_todo_state({"todos": [], "revision": 2}) assert state == {"todos": [], "revision": 2} + + +def test_subagent_lifecycle_bypasses_tool_progress_off(monkeypatch): + """Subagent rows feed the Desktop status stack / TUI spawn tree — application state, not + tool-progress chrome — so display.tool_progress=off must not swallow them.""" + sid = "subagent-progress-off" + events = [] + monkeypatch.setitem(server._sessions, sid, {"agent": None, "tool_progress_mode": "off"}) + monkeypatch.setattr(server, "_tool_progress_enabled", lambda _sid: False) + monkeypatch.setattr(server, "_emit", lambda event, event_sid, payload=None: events.append(event)) + + server._on_tool_progress(sid, "subagent.start", "delegate_task", "goal", None, goal="goal", subagent_id="s1") + server._on_tool_progress(sid, "reasoning.available", "_thinking", "hmm", None) + + assert events == ["subagent.start"] diff --git a/tui_gateway/tool_progress.py b/tui_gateway/tool_progress.py index e7f362a882..9130b47834 100644 --- a/tui_gateway/tool_progress.py +++ b/tui_gateway/tool_progress.py @@ -358,10 +358,14 @@ def _on_tool_progress( sid: str, event_type: str, name: str | None = None, preview: str | None = None, _args: dict | None = None, **_kwargs, ): - if not _tool_progress_enabled(sid) or (event_type == "tool.started" and name): + if event_type == "tool.started" and name: return + # Subagent lifecycle is application state (Desktop status stack, TUI spawn tree), not + # tool-progress chrome: it must survive display.tool_progress=off like todo.updated does. if event_type.startswith("subagent."): return _progress_subagent(sid, name, preview, _kwargs, event_type) + if not _tool_progress_enabled(sid): + return handler, requires = _PROGRESS_HANDLERS.get(event_type, (None, None)) if handler is not None and (requires is None or {"name": name, "preview": preview}[requires]): handler(sid, name, preview, _kwargs) diff --git a/website/docs/user-guide/configuration.md b/website/docs/user-guide/configuration.md index d0f20e555f..f7e851950e 100644 --- a/website/docs/user-guide/configuration.md +++ b/website/docs/user-guide/configuration.md @@ -2076,6 +2076,8 @@ In the CLI, cycle through these modes with `/verbose`. To use `/verbose` in mess Tool progress requires a gateway adapter that can display progress updates safely. Platforms without message editing support, including Signal, suppress tool-progress bubbles even if `/verbose` saves a non-`off` mode. +`off` hides tool-call *chrome* only. Application state that has its own surface in the Desktop app and TUI — the task list (`todo_list`), subagent progress, clarify questions, and MCP consent cards — keeps flowing regardless of this setting. + ### Focus view (`/focus`, CLI + TUI) `display.focus_view: true` enables **focus view** — a reduced-output display mode for when you want the answer, not the play-by-play. It is a thin layer over the same `tool_progress` machinery rather than a second suppression path: