Skip to content

fix(tui): show proxy tool errors instead of dropping them#837

Open
thejesh23 wants to merge 2 commits into
usestrix:mainfrom
thejesh23:fix/proxy-renderer-drops-error-messages
Open

fix(tui): show proxy tool errors instead of dropping them#837
thejesh23 wants to merge 2 commits into
usestrix:mainfrom
thejesh23:fix/proxy-renderer-drops-error-messages

Conversation

@thejesh23

Copy link
Copy Markdown
Contributor

Fixes #836

What

Widens the result-rendering gate in all six proxy renderers so a failed call shows its error, instead of rendering nothing.

Why

The gate was:

if status == "completed" and isinstance(result, dict):
    if "error" in result:
        ...

but live_view._tool_status_from_result maps {"success": false} to "failed", and every proxy-tool error payload sets that flag — _no_client (tools.py:85), _err (:94), "Request not found" (:275, :408), "No raw {part}" (:288), "Invalid regex" (:316), and the four scope_rules argument errors (:572, :585, :604, :623).

So the payloads that carry an error were exactly the ones the gate excluded, making the error branch in all six renderers dead code. A failed proxy call rendered as a bare label — indistinguishable from a successful call that returned no rows.

res = {"success": False, "error": "list_requests failed: HTTPQL parse error"}

# status='failed'    (what the pipeline actually produces)
'<~> listing requests'
# status='completed' (never happens for an error payload)
'<~> listing requests  error: list_requests failed: HTTPQL parse error'

How

def _has_result(status: str) -> bool:
    return status in {"completed", "failed", "error"}

used for all six gates. The fix belongs here rather than in live_view: "failed" is the correct status and is consumed elsewhere (get_css_classes, status_icon) — only the gate was too narrow.

Success rendering is unaffected: every one of the six blocks already branches on the error key first, so an error payload takes the error branch and a success payload takes the same path it always did.

One behaviour change worth flagging: _format_replay_tool_result (tools.py:420) sets "success": replay["status"] == "DONE", i.e. a replay that did not reach DONE is the only failure payload with no error key. It previously rendered nothing at all; it now renders its replay response (elapsed, status code, body). That seemed strictly better than a blank line, but say the word if you'd rather it render an explicit "replay did not complete" instead.

Tests

In tests/test_proxy_renderer.py:

  • test_error_is_rendered_for_failed_status — parametrised over all six renderers (ListRequests, ViewRequest, RepeatRequest, ListSitemap, ViewSitemapEntry, ScopeRules), deriving the status from the real _tool_status_from_result rather than hard-coding "failed", so the test stays honest if that mapping changes
  • test_tool_status_from_result_maps_failure_to_failed — pins the mapping the gate has to agree with
  • test_successful_result_still_renders — guards the success path

All six parametrised cases fail on main (6 failed, 5 passed), pass with this change (11 passed).

uv run pytest -q
2 failed, 322 passed

Both failures are pre-existing on main (tests/test_runner_root_prompt.py, stale settings stub) and unrelated — separate PR open.

ruff format --check, ruff check and mypy strix/interface/tui/renderers/proxy_renderer.py are clean.

All six proxy renderers gated result rendering on `status == "completed"`,
but `live_view._tool_status_from_result` maps any `{"success": false}`
payload to `"failed"` -- and every proxy-tool error payload sets that flag.
So the `if "error" in result` branch in each renderer was unreachable, and
a failed call rendered as a bare label indistinguishable from a call that
returned no rows.

Affected: Caido unreachable, malformed HTTPQL, an invalid regex in
view_request, a missing request id, and all four scope_rules argument
errors.

Add `_has_result(status)` (completed/failed/error) and use it for all six
gates. Each block already branches on the error key first, so success
rendering is unchanged.

One payload -- a repeat_request replay that did not reach DONE
(tools.py:420) -- sets success=false with no error key; it now renders its
replay response rather than nothing at all.

Adds a parametrised regression test over all six renderers plus a test
pinning the live_view status mapping the gate has to agree with; they fail
on main.
@greptile-apps

greptile-apps Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR makes proxy tool failures visible in the TUI. The main changes are:

  • Adds a shared gate for completed, failed, and error results.
  • Applies the gate to all six proxy renderers.
  • Adds tests for failed-status errors and successful results.

Confidence Score: 5/5

This looks safe to merge after a small cleanup to failed replay output.

  • Proxy error payloads now reach their existing error branches.
  • A failed replay without an error string can still render like an empty response.

strix/interface/tui/renderers/proxy_renderer.py

Important Files Changed

Filename Overview
strix/interface/tui/renderers/proxy_renderer.py Broadens six rendering gates so terminal failure payloads are shown, with one unclear failed-replay presentation.
tests/test_proxy_renderer.py Adds coverage for failed errors across all six renderers and preserves the successful list result path.
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
strix/interface/tui/renderers/proxy_renderer.py:272
**Failed Replay Looks Like Response**

When a replay ends with a non-`DONE` status but no error string, `_format_replay_tool_result` returns `success: false` without an `error` key. This widened gate now sends that payload through the normal response branch, so the TUI can show elapsed time and an empty response instead of explaining that the replay failed.

Reviews (1): Last reviewed commit: "fix(tui): show proxy tool errors instead..." | Re-trigger Greptile

@@ -261,7 +272,7 @@ def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: PLR0912, PLR0915
elif modifications and isinstance(modifications, str):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Failed Replay Looks Like Response

When a replay ends with a non-DONE status but no error string, _format_replay_tool_result returns success: false without an error key. This widened gate now sends that payload through the normal response branch, so the TUI can show elapsed time and an empty response instead of explaining that the replay failed.

Prompt To Fix With AI
This is a comment left during a code review.
Path: strix/interface/tui/renderers/proxy_renderer.py
Line: 272

Comment:
**Failed Replay Looks Like Response**

When a replay ends with a non-`DONE` status but no error string, `_format_replay_tool_result` returns `success: false` without an `error` key. This widened gate now sends that payload through the normal response branch, so the TUI can show elapsed time and an empty response instead of explaining that the replay failed.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, fixed in 02899c9. This was the one tradeoff I called out in the PR description, and rendering it as a response is the wrong call.

elif not result.get("success", True):
    replay_status = _sanitize(str(result.get("status") or "unknown"), 40)
    text.append(f"\n  replay did not complete: {replay_status}", style="#ef4444")

It reuses the status field _format_replay_tool_result already puts in the payload (tools.py:419), so the operator sees replay did not complete: TIMEDOUT rather than an empty response line. Error and success paths untouched.

Tests added for the incomplete-replay case (asserts no << response line is rendered) and for a DONE replay still showing its status code.

_format_replay_tool_result sets success=False with no `error` key when a
replay never reaches DONE, so the widened gate sent it down the normal
response branch and rendered elapsed time with an empty response, as if
the request had come back.

Report the replay status instead (TIMEDOUT, ERROR, ...). Both the error
and success paths are unchanged.

Adds tests for the incomplete-replay and successful-replay cases.
@thejesh23

thejesh23 commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Agreed — that's the tradeoff I flagged in the PR description, and you're right that rendering it as a response is the wrong call. Fixed in 02899c9.

A replay that never reaches DONE now reports its replay status instead of an empty response line:

  replay did not complete: TIMEDOUT

It uses the status field _format_replay_tool_result already puts in the payload (tools.py:419), so the operator gets the actual reason. The error path and the success path are untouched.

Two tests added: one asserting the incomplete replay says so and renders no << response line, one asserting a DONE replay still renders its status code.

(Edited: this comment originally cited a wrong commit hash. The correct one is 02899c9.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Proxy tool errors are never shown in the TUI: the renderers' error branches are unreachable

1 participant