This commit is contained in:
Brennan Butler 2026-10-03 10:42:33 -07:00 • committed by GitHub
commit f0e405e763
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 355 additions and 10 deletions

View file

@ -1,4 +1,4 @@
import { render, screen, within } from "@testing-library/react";
import { fireEvent, render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { GuardrailJumpLink, LogDetailContent } from "./LogDetailContent";
@ -34,6 +34,33 @@ const createLogEntry = (overrides: Partial<LogEntry> = {}): LogEntry =>
}) as LogEntry;
describe("LogDetailContent", () => {
it("resets WebSocket pagination for a different log but preserves it when the same log refreshes", () => {
const response = {
results: Array.from({ length: 101 }, (_, index) => ({
type: "response.completed",
response: { id: `resp_${index}`, output: [] },
})),
};
const log = createLogEntry({ request_id: "websocket-first", response });
const { rerender } = render(<LogDetailContent logEntry={log} />);
fireEvent.click(screen.getByRole("button", { name: "Next turns" }));
expect(screen.getByText("Turns 51–100 of 101")).toBeInTheDocument();
rerender(<LogDetailContent logEntry={{ ...log, response: { ...response } }} />);
expect(screen.getByText("Turns 51–100 of 101")).toBeInTheDocument();
rerender(<LogDetailContent logEntry={{ ...log, request_id: "websocket-second" }} />);
expect(screen.getByText("Turns 1–50 of 101")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Previous turns" })).toBeDisabled();
expect(screen.queryByText("Turn 51 · Completed")).not.toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "Next turns" }));
fireEvent.click(screen.getByRole("button", { name: "Next turns" }));
expect(screen.getByText("Turns 101–101 of 101")).toBeInTheDocument();
rerender(<LogDetailContent logEntry={log} />);
expect(screen.getByText("Turns 1–50 of 101")).toBeInTheDocument();
});
it("should render the component successfully", () => {
render(<LogDetailContent logEntry={createLogEntry()} />);

View file

@ -664,6 +664,7 @@ function RequestResponseSection({
<div>
<TabsContent value="pretty">
<PrettyMessagesView
key={logEntry.request_id}
request={getRawRequest()}
response={getFormattedResponse()}
metrics={{

View file

@ -1,9 +1,84 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { fireEvent, render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import { PrettyMessagesView } from "./PrettyMessagesView";
describe("PrettyMessagesView", () => {
it("bounds rendered WebSocket turns and lets operators navigate all pages", () => {
const response = Array.from({ length: 101 }, (_, index) => ({
type: "response.completed",
response: { id: `resp_${index}`, output: [] },
}));
render(<PrettyMessagesView request={{}} response={response} />);
expect(screen.getAllByRole("region", { name: /^Turn / })).toHaveLength(50);
expect(screen.getByText("Turns 1–50 of 101")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Previous turns" })).toBeDisabled();
fireEvent.click(screen.getByRole("button", { name: "Next turns" }));
expect(screen.queryByText("Turn 1 · Completed")).not.toBeInTheDocument();
expect(screen.getByText("Turn 51 · Completed")).toBeInTheDocument();
expect(screen.getAllByRole("region", { name: /^Turn / })).toHaveLength(50);
fireEvent.click(screen.getByRole("button", { name: "Next turns" }));
expect(screen.getByText("Turns 101–101 of 101")).toBeInTheDocument();
expect(screen.getAllByRole("region", { name: /^Turn / })).toHaveLength(1);
expect(screen.getByRole("button", { name: "Next turns" })).toBeDisabled();
fireEvent.click(screen.getByRole("button", { name: "Previous turns" }));
expect(screen.getByText("Turn 51 · Completed")).toBeInTheDocument();
});
it.each([false, true])("renders separate WebSocket turns (wrapped: %s)", (wrapped) => {
const events = [
{ type: "response.created", response: { id: "resp_1", output: [] } },
{ type: "response.output_text.delta", delta: "First answer" },
{
type: "response.completed",
response: {
id: "resp_1",
output: [{ type: "message", content: [{ type: "output_text", text: "First answer" }] }],
},
},
{
type: "response.incomplete",
response: {
id: "resp_2",
incomplete_details: { reason: "max_output_tokens" },
output: [{ type: "function_call", call_id: "call_1", name: "get_weather", arguments: '{"city":"Hanoi"}' }],
},
},
];
render(
<PrettyMessagesView
request={{ input: "Check the weather" }}
response={wrapped ? { results: events, usage: {} } : events}
metrics={{ completion_tokens: 42, output_cost: 0.001 }}
/>,
);
expect(screen.getByText("Check the weather")).toBeInTheDocument();
expect(screen.getAllByText("First answer")).toHaveLength(1);
expect(screen.getByText("Turn 1 · Completed")).toBeInTheDocument();
expect(screen.getByText("Turn 2 · Incomplete")).toBeInTheDocument();
expect(screen.getByText("max_output_tokens")).toBeInTheDocument();
expect(screen.getByText("get_weather")).toBeInTheDocument();
expect(screen.getAllByText("Tokens: 42")).toHaveLength(1);
expect(screen.getAllByText("Cost: $0.001000")).toHaveLength(1);
});
it("shows failed and unfinished WebSocket turns without inventing output", () => {
render(
<PrettyMessagesView
request={{}}
response={{
results: [
{ type: "response.failed", response: { id: "resp_failed", error: { message: "Provider unavailable" } } },
{ type: "response.created", response: { id: "resp_unfinished" } },
],
}}
/>,
);
expect(screen.getByText("Turn 1 · Failed")).toBeInTheDocument();
expect(screen.getByText("Provider unavailable")).toBeInTheDocument();
expect(screen.getByText("Turn 2 · No terminal event recorded")).toBeInTheDocument();
});
it("should render the component for standard chat completions", () => {
const request = {
messages: [{ role: "user", content: "Hello" }],

View file

@ -4,14 +4,15 @@
* Detects realtime API responses and renders a specialized view.
*/
import { parseMessages } from "./prettyMessagesUtils";
import { parseMessages, parseResponsesWebSocketTurns } from "./prettyMessagesUtils";
import { ResponsesWebSocketPrettyView } from "./ResponsesWebSocketPrettyView";
import { InputCard } from "./InputCard";
import { OutputCard } from "./OutputCard";
import { isRealtimeResponse, RealtimePrettyView } from "./RealtimePrettyView";
interface PrettyMessagesViewProps {
request: any;
response: any;
request: unknown;
response: unknown;
metrics?: {
prompt_tokens?: number;
completion_tokens?: number;
@ -26,6 +27,7 @@ export function PrettyMessagesView({ request, response, metrics }: PrettyMessage
}
const { requestMessages, responseMessage } = parseMessages(request, response);
const turns = parseResponsesWebSocketTurns(response);
return (
<div>
@ -33,11 +35,19 @@ export function PrettyMessagesView({ request, response, metrics }: PrettyMessage
<InputCard messages={requestMessages} promptTokens={metrics?.prompt_tokens} inputCost={metrics?.input_cost} />
{/* Output Card */}
<OutputCard
message={responseMessage}
completionTokens={metrics?.completion_tokens}
outputCost={metrics?.output_cost}
/>
{turns !== null ? (
<ResponsesWebSocketPrettyView
turns={turns}
completionTokens={metrics?.completion_tokens}
outputCost={metrics?.output_cost}
/>
) : (
<OutputCard
message={responseMessage}
completionTokens={metrics?.completion_tokens}
outputCost={metrics?.output_cost}
/>
)}
</div>
);
}

View file

@ -0,0 +1,87 @@
import { useState } from "react";
import { toast } from "@/lib/toast";
import { ResponsesWebSocketTurn } from "./prettyMessagesTypes";
import { SectionHeader } from "./SectionHeader";
import { SimpleMessageBlock } from "./SimpleMessageBlock";
import { Button } from "@/components/ui/button";
const TURNS_PER_PAGE = 50;
interface ResponsesWebSocketPrettyViewProps {
turns: readonly ResponsesWebSocketTurn[];
completionTokens?: number;
outputCost?: number;
}
export function ResponsesWebSocketPrettyView({
turns,
completionTokens,
outputCost,
}: ResponsesWebSocketPrettyViewProps) {
const [isCollapsed, setIsCollapsed] = useState(false);
const [page, setPage] = useState(0);
const lastPage = Math.max(0, Math.ceil(turns.length / TURNS_PER_PAGE) - 1);
const currentPage = Math.min(page, lastPage);
const start = currentPage * TURNS_PER_PAGE;
const end = Math.min(start + TURNS_PER_PAGE, turns.length);
const copyOutput = async () => {
await navigator.clipboard.writeText(JSON.stringify(turns, null, 2));
toast.success("Output copied");
};
return (
<div className="overflow-hidden rounded-md border border-border">
<SectionHeader
type="output"
tokens={completionTokens}
cost={outputCost}
onCopy={copyOutput}
isCollapsed={isCollapsed}
onToggleCollapse={() => setIsCollapsed(!isCollapsed)}
/>
{!isCollapsed && (
<div className="divide-y divide-border">
{turns.slice(start, end).map((turn, index) => (
<section
key={`${turn.id}-${start + index}`}
className="px-4 py-3"
aria-label={`Turn ${start + index + 1} · ${turn.status}`}
>
<h4 className="mb-2 text-sm font-medium">
Turn {start + index + 1} · {turn.status}
</h4>
{turn.id && <div className="mb-2 break-all text-xs text-muted-foreground">{turn.id}</div>}
{turn.detail && <p className="mb-2 whitespace-pre-wrap text-sm">{turn.detail}</p>}
{turn.message ? (
<SimpleMessageBlock
label="ASSISTANT"
content={turn.message.content}
toolCalls={turn.message.toolCalls}
/>
) : (
<span className="text-[13px] italic text-muted-foreground">No output recorded for this turn</span>
)}
</section>
))}
{turns.length > TURNS_PER_PAGE && (
<nav aria-label="Response turns" className="flex items-center justify-between gap-2 px-4 py-3">
<Button variant="outline" size="sm" disabled={currentPage === 0} onClick={() => setPage(currentPage - 1)}>
Previous turns
</Button>
<span className="text-sm" aria-live="polite">
Turns {start + 1}–{end} of {turns.length}
</span>
<Button
variant="outline"
size="sm"
disabled={currentPage === lastPage}
onClick={() => setPage(currentPage + 1)}
>
Next turns
</Button>
</nav>
)}
</div>
)}
</div>
);
}

View file

@ -31,3 +31,10 @@ export interface ParsedMessages {
requestMessages: ParsedMessage[];
responseMessage: ParsedMessage | null;
}
export interface ResponsesWebSocketTurn {
id: string;
status: "Completed" | "Incomplete" | "Failed" | "Error" | "No terminal event recorded";
message: ParsedMessage | null;
detail: string;
}

View file

@ -0,0 +1,86 @@
import { describe, expect, it } from "vitest";
import { parseResponsesWebSocketTurns } from "./prettyMessagesUtils";
describe("parseResponsesWebSocketTurns", () => {
it("reads event types a bounded number of times for a large session", () => {
const reads = { count: 0 };
const events = Array.from({ length: 2000 }, (_, index) =>
["response.created", "response.completed", "response.completed"].map((type) => ({
get type() {
reads.count += 1;
return type;
},
response: { id: `resp_${index}`, output: [] },
})),
).flat();
const turns = parseResponsesWebSocketTurns(events);
expect(turns).toHaveLength(2000);
expect(turns?.[1999].id).toBe("resp_1999");
expect(reads.count).toBeLessThan(events.length * 20);
});
it.each([
null,
{},
[],
{ results: null },
{ choices: [] },
{ output: [] },
{ results: [{ type: "response.done" }] },
{ results: [null, "invalid", 42] },
])("leaves non-Responses payloads to their existing renderer: %j", (payload) => {
expect(parseResponsesWebSocketTurns(payload)).toBeNull();
});
it("uses terminal output once, skipping deltas, created snapshots, and duplicate terminal IDs", () => {
const response = {
id: "resp_1",
output: [{ type: "message", content: [{ type: "output_text", text: "Final text" }] }],
};
expect(
parseResponsesWebSocketTurns([
null,
{ type: "response.created", response },
{ type: "response.output_text.delta", delta: "Final text" },
{ type: "response.completed", response },
{ type: "response.completed", response },
]),
).toEqual([
{
id: "resp_1",
status: "Completed",
detail: "",
message: { role: "assistant", content: "Final text", toolCalls: undefined },
},
]);
});
it("keeps failed output and errors inspectable alongside an unfinished turn", () => {
const turns = parseResponsesWebSocketTurns({
results: [
{
type: "response.failed",
response: {
id: "failed",
error: { code: "server_error" },
output: [{ type: "message", content: [{ type: "output_text", text: "Partial answer" }] }],
},
},
{ type: "response.created", response: { id: "pending" } },
{ type: "error", error: { message: "Connection error" } },
],
});
expect(turns?.map(({ status, detail }) => ({ status, detail }))).toEqual([
{ status: "Failed", detail: "server_error" },
{ status: "No terminal event recorded", detail: "" },
{ status: "Error", detail: "Connection error" },
]);
expect(turns?.[0].message?.content).toBe("Partial answer");
});
it("does not invent output for malformed terminal payloads", () => {
expect(parseResponsesWebSocketTurns([{ type: "response.completed", response: null }])).toEqual([
{ id: "", status: "Completed", message: null, detail: "" },
]);
});
});

View file

@ -8,6 +8,7 @@ import {
ParsedMessages,
RequestPayload,
ResponsePayload,
ResponsesWebSocketTurn,
ToolCall,
} from "./prettyMessagesTypes";
@ -52,6 +53,57 @@ export const parseMessages = (request: unknown, response: unknown): ParsedMessag
responseMessage: parseResponseMessage(classifyResponse(response)),
});
export const parseResponsesWebSocketTurns = (response: unknown): ResponsesWebSocketTurn[] | null => {
const results: unknown = isRecord(response) ? response.results : response;
if (!Array.isArray(results)) return null;
const events = results.filter(isRecord);
if (
!events.some((event) =>
["response.created", "response.completed", "response.incomplete", "response.failed"].includes(
asString(event.type),
),
)
)
return null;
const terminalTypes = ["response.completed", "response.incomplete", "response.failed"];
const terminalStatuses: Readonly<Record<string, ResponsesWebSocketTurn["status"]>> = {
"response.completed": "Completed",
"response.incomplete": "Incomplete",
"response.failed": "Failed",
};
const terminalIds = new Set(
events.flatMap((event) =>
terminalTypes.includes(asString(event.type)) && isRecord(event.response) ? [asString(event.response.id)] : [],
),
);
const seenTerminalIds = new Set<string>();
return events.flatMap((event): ResponsesWebSocketTurn[] => {
const body = isRecord(event.response) ? event.response : {};
const id = asString(body.id);
if (event.type === "response.created") {
const hasTerminal = terminalIds.has(id);
return hasTerminal ? [] : [{ id, status: "No terminal event recorded", message: null, detail: "" }];
}
if (event.type === "error") {
const error = isRecord(event.error) ? event.error : event;
return [{ id: "", status: "Error", message: null, detail: asString(error.message) || asString(error.code) }];
}
if (!terminalTypes.includes(asString(event.type))) return [];
if (id && seenTerminalIds.has(id)) return [];
if (id) seenTerminalIds.add(id);
const error = isRecord(body.error) ? body.error : {};
const incomplete = isRecord(body.incomplete_details) ? body.incomplete_details : {};
return [
{
id,
status: terminalStatuses[asString(event.type)],
message: parseResponseMessage(classifyResponse(body)),
detail: asString(error.message) || asString(error.code) || asString(incomplete.reason),
},
];
});
};
const parseRequestMessages = (payload: RequestPayload): ParsedMessage[] => {
switch (payload.kind) {
case "chat":