fix(tui): surface verbose tool details (#30225)

* fix(tui): surface verbose tool details

Emit redacted structured verbose args/results to the TUI so /verbose verbose can show full tool detail without reopening stdout, and fail closed if redaction is unavailable.

Salvages #29011.

Co-authored-by: helix4u <4317663+helix4u@users.noreply.github.com>

* fix(tui): address verbose detail review

Label verbose tool failures as errors, cover forced verbose reasoning, and avoid new diff type warnings from the redaction regression tests.

* fix(tui): bound verbose tool payloads

Cap verbose tool detail text before emitting JSON-RPC events and preserve verbose results on inline diff completions.

* fix(tui): align termux argv test with gc flag

Update the stale TUI launch expectation so the Termux freshness path matches the current direct Node argv.

---------

Co-authored-by: helix4u <4317663+helix4u@users.noreply.github.com>
This commit is contained in:
brooklyn!
2026-05-22 00:16:52 -05:00
committed by GitHub
parent 4e2c66a098
commit 1264fab156
11 changed files with 306 additions and 38 deletions

View File

@@ -1061,6 +1061,10 @@ def _session_tool_progress_mode(sid: str) -> str:
return str(_sessions.get(sid, {}).get("tool_progress_mode", "all") or "all")
def _session_verbose(sid: str) -> bool:
return _session_tool_progress_mode(sid) == "verbose"
def _tool_progress_enabled(sid: str) -> bool:
return _session_tool_progress_mode(sid) != "off"
@@ -1492,6 +1496,74 @@ def _tool_ctx(name: str, args: dict) -> str:
return ""
_TUI_VERBOSE_TEXT_MAX_CHARS = 16_000
_TUI_VERBOSE_TEXT_MAX_LINES = 240
def _cap_tui_verbose_text(text: str) -> str:
if (
len(text) <= _TUI_VERBOSE_TEXT_MAX_CHARS
and text.count("\n") < _TUI_VERBOSE_TEXT_MAX_LINES
):
return text
idx = len(text)
start = 0
for _ in range(_TUI_VERBOSE_TEXT_MAX_LINES):
idx = text.rfind("\n", 0, idx)
if idx < 0:
start = 0
break
start = idx + 1
line_start = start
start = max(line_start, len(text) - _TUI_VERBOSE_TEXT_MAX_CHARS)
if start > line_start:
next_break = text.find("\n", start)
if 0 <= next_break < len(text) - 1:
start = next_break + 1
tail = text[start:].lstrip()
omitted_chars = max(0, len(text) - len(tail))
omitted_lines = text[:start].count("\n")
if omitted_lines:
label = (
"[showing verbose tail; omitted "
f"{omitted_lines} lines / {omitted_chars} chars]\n"
)
else:
label = f"[showing verbose tail; omitted {omitted_chars} chars]\n"
return f"{label}{tail}"
def _redact_tui_verbose_text(text: str) -> str:
try:
from agent.redact import redact_sensitive_text
redacted = redact_sensitive_text(str(text), force=True)
except Exception:
return ""
return _cap_tui_verbose_text(redacted)
def _tool_args_text(args: dict) -> str:
try:
raw = json.dumps(args or {}, indent=2, ensure_ascii=False, default=str)
except Exception:
raw = str(args or {})
return _redact_tui_verbose_text(raw)
def _tool_result_text(result: object) -> str:
try:
from agent.tool_dispatch_helpers import _multimodal_text_summary
raw = _multimodal_text_summary(result)
except Exception:
raw = str(result)
return _redact_tui_verbose_text(raw)
def _fmt_tool_duration(seconds: float | None) -> str:
if seconds is None:
return ""
@@ -1553,13 +1625,18 @@ def _on_tool_start(sid: str, tool_call_id: str, name: str, args: dict):
pass
session.setdefault("tool_started_at", {})[tool_call_id] = time.time()
if _tool_progress_enabled(sid):
payload = {
"tool_id": tool_call_id,
"name": name,
"context": _tool_ctx(name, args),
}
if _session_verbose(sid):
args_text = _tool_args_text(args)
if args_text:
payload["args_text"] = args_text
# tool.complete is the source of truth for todos (full list from the
# tool result). args.todos here may be a partial merge update.
_emit(
"tool.start",
sid,
{"tool_id": tool_call_id, "name": name, "context": _tool_ctx(name, args)},
)
_emit("tool.start", sid, payload)
def _on_tool_complete(sid: str, tool_call_id: str, name: str, args: dict, result: str):
@@ -1576,6 +1653,10 @@ def _on_tool_complete(sid: str, tool_call_id: str, name: str, args: dict, result
summary = _tool_summary(name, result, duration_s)
if summary:
payload["summary"] = summary
if _session_verbose(sid):
result_text = _tool_result_text(result)
if result_text:
payload["result_text"] = result_text
if name == "todo":
try:
data = json.loads(result)
@@ -1615,7 +1696,10 @@ def _on_tool_progress(
_emit("tool.progress", sid, {"name": name, "preview": preview or ""})
return
if event_type == "reasoning.available" and preview:
_emit("reasoning.available", sid, {"text": str(preview)})
payload: dict[str, object] = {"text": str(preview)}
if _session_verbose(sid):
payload["verbose"] = True
_emit("reasoning.available", sid, payload)
return
if event_type.startswith("subagent."):
payload = {
@@ -1691,7 +1775,11 @@ def _agent_cbs(sid: str) -> dict:
"tool_gen_callback": lambda name: _tool_progress_enabled(sid)
and _emit("tool.generating", sid, {"name": name}),
"thinking_callback": lambda text: _emit("thinking.delta", sid, {"text": text}),
"reasoning_callback": lambda text: _emit("reasoning.delta", sid, {"text": text}),
"reasoning_callback": lambda text: _emit(
"reasoning.delta",
sid,
{"text": text, **({"verbose": True} if _session_verbose(sid) else {})},
),
"status_callback": lambda kind, text=None: _status_update(
sid, str(kind), None if text is None else str(text)
),