diff --git a/clients/web/src/components/groups/ProtocolEntry/ProtocolEntry.test.tsx b/clients/web/src/components/groups/ProtocolEntry/ProtocolEntry.test.tsx index 0408f0b5..45d658c4 100644 --- a/clients/web/src/components/groups/ProtocolEntry/ProtocolEntry.test.tsx +++ b/clients/web/src/components/groups/ProtocolEntry/ProtocolEntry.test.tsx @@ -756,6 +756,41 @@ describe("ProtocolEntry — client-rejected response", () => { expect(screen.getByText(REASON)).toBeInTheDocument(); }); + // messageLogState falls back to the standalone response frame when no request + // entry was there to fold into (a trimmed log, or a reconnect boundary). That + // entry is not `direction: "request"`, so the request-only status lifecycle + // would have rendered it without any badge at all. + const rejectedStandaloneResponse: MessageEntry = { + id: "rej-standalone", + timestamp: new Date("2026-07-28T10:00:00Z"), + direction: "response", + origin: "server", + message: { + jsonrpc: "2.0", + id: 11, + result: { resultType: "complete", tools: [] }, + }, + clientError: REASON, + }; + + it("renders Error on a rejected standalone response entry", () => { + renderWithMantine( + , + ); + expect(screen.getByText("Error")).toBeInTheDocument(); + }); + + it("still renders no status badge on an unrejected standalone response", () => { + const clean: MessageEntry = { + ...rejectedStandaloneResponse, + clientError: undefined, + }; + renderWithMantine(); + expect(screen.queryByText("Error")).not.toBeInTheDocument(); + expect(screen.queryByText("OK")).not.toBeInTheDocument(); + expect(screen.queryByText("Pending")).not.toBeInTheDocument(); + }); + it("shows no rejection alert on an ordinary success", () => { renderWithMantine( , diff --git a/clients/web/src/components/groups/ProtocolEntry/ProtocolEntry.tsx b/clients/web/src/components/groups/ProtocolEntry/ProtocolEntry.tsx index d5819d3d..73edda6c 100644 --- a/clients/web/src/components/groups/ProtocolEntry/ProtocolEntry.tsx +++ b/clients/web/src/components/groups/ProtocolEntry/ProtocolEntry.tsx @@ -220,12 +220,16 @@ function extractResourceUri(entry: MessageEntry): string | undefined { function extractStatus( entry: MessageEntry, ): "success" | "error" | "pending" | "none" { + // A response the CLIENT refused is an error whichever entry carries it, so + // this is checked BEFORE the request-only lifecycle below (#1953). + // messageLogState annotates the request entry when the response was folded + // into one, but falls back to the standalone response frame when there was + // no matching request (a trimmed log, or a reconnect boundary) — and that + // entry would otherwise fall straight through to "none" and render no badge. + if (entry.clientError) return "error"; if (entry.direction !== "request") return "none"; if (!entry.response) return "pending"; if ("error" in entry.response) return "error"; - // The server answered with a valid result the CLIENT then refused (#1953). - // The call failed, so the entry must not read as a success. - if (entry.clientError) return "error"; return "success"; }