fix: render a tool result's structuredContent in the Tools screen (#1908)

A tool declaring an `outputSchema` returns its real payload in
`structuredContent`; the `content[]` blocks usually only summarize it. v1
rendered that payload as its own inspectable JSON section — v2's
ToolResultPanel only ever walked `result.content`, so the payload was
dropped with no label, section, or hint that it existed.

Adds a `StructuredOutputPanel` group: a collapsible "Structured Output"
box (bordered like the existing "Resource Links" box) rendering the
payload as pretty-printed, syntax-highlighted, copyable JSON, bounded so
a large payload scrolls within itself. ToolResultPanel renders it below
the content blocks — and also when `content` is empty (where "No results
yet" was simply wrong) and below the alert on an error result, so it is
never silently dropped.

Adds a `list_items` test-server preset (nested payload, the issue's repro
shape) and a `structured-output-http.json` showcase config, documented in
the README.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SW1p8E2uiyyLx4RKwrwSrt
This commit is contained in:
cliffhall 2026-08-11 09:23:43 -04:00
commit 6f47eaa143

View file

@ -109,6 +109,78 @@ describe("ToolResultPanel", () => {
expect(screen.getByText("divider")).toBeInTheDocument();
});
describe("structuredContent (#1908)", () => {
const structured = { items: [{ id: 1, name: "Item A" }], total: 1 };
it("renders a Structured Output section alongside the content blocks", () => {
renderWithMantine(
<ToolResultPanel
result={{ ...okResult, structuredContent: structured }}
onClear={() => {}}
/>,
);
expect(screen.getByText("ok")).toBeInTheDocument();
expect(
screen.getByRole("heading", { name: "Structured Output" }),
).toBeInTheDocument();
});
it("omits the section when the result has no structuredContent", () => {
renderWithMantine(
<ToolResultPanel result={okResult} onClear={() => {}} />,
);
expect(
screen.queryByRole("heading", { name: "Structured Output" }),
).not.toBeInTheDocument();
});
it("renders the section instead of the empty state when content is empty", () => {
renderWithMantine(
<ToolResultPanel
result={{ content: [], structuredContent: structured }}
onClear={() => {}}
/>,
);
expect(screen.queryByText("No results yet")).not.toBeInTheDocument();
expect(
screen.getByRole("heading", { name: "Structured Output" }),
).toBeInTheDocument();
});
it("renders the section below the alert on an error result", () => {
renderWithMantine(
<ToolResultPanel
result={{ ...errorResult, structuredContent: structured }}
onClear={() => {}}
/>,
);
expect(screen.getByText("Tool Error")).toBeInTheDocument();
expect(
screen.getByRole("heading", { name: "Structured Output" }),
).toBeInTheDocument();
});
it("renders the section alongside a Resource Links box", () => {
renderWithMantine(
<ToolResultPanel
result={{
content: [
{ type: "resource_link", uri: "demo://r/1", name: "One" },
],
structuredContent: structured,
}}
onClear={() => {}}
/>,
);
expect(
screen.getByRole("heading", { name: "Resource Links" }),
).toBeInTheDocument();
expect(
screen.getByRole("heading", { name: "Structured Output" }),
).toBeInTheDocument();
});
});
it("invokes onClear when the close button is clicked", async () => {
const user = userEvent.setup();
const onClear = vi.fn();