Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion e2e/tools.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,9 +317,19 @@ test.describe("Tools page", () => {
body: JSON.stringify({
target: { kind: "local" },
resolved_arguments: { query: "cloudflare", limit: 5 },
content: [
{ type: "text", text: "Found 2 matching issues", mimeType: "text/plain" },
{ type: "text", text: '{"total":2}', mimeType: "application/json" },
{
type: "image",
text: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"></svg>',
mimeType: "image/svg+xml",
},
],
structured_output: { total: 2, query: "cloudflare" },
annotations: { readOnlyHint: true },
pre_hooks_run: [],
warnings: [],
warnings: [{ code: "elicitation_skipped", hooks: ["approval_hook"] }],
}),
});
});
Expand All @@ -346,6 +356,14 @@ test.describe("Tools page", () => {
await previewButton.click();

await expect(panel.getByText("Preview 200")).toBeVisible();
await expect(panel.getByText("Warnings", { exact: true }).first()).toBeVisible();
await expect(
panel.getByText("Live invocation may request user input; preview skipped approval_hook."),
).toBeVisible();
await expect(panel.getByText("Tool result")).toBeVisible();
await expect(panel.getByText("Found 2 matching issues").first()).toBeVisible();
await expect(panel.getByRole("img", { name: "Tool result image 3" })).toBeVisible();
await expect(panel.getByText("Structured output")).toBeVisible();
await expect(panel.getByText("Resolved arguments")).toBeVisible();
expect(previewBody).toEqual({ arguments: { query: "cloudflare", limit: 5 } });
expect(previewHeaders["x-tenant-id"]).toBe("team-a");
Expand Down
27 changes: 27 additions & 0 deletions src/api/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,35 @@ export interface ToolPreviewTarget {
[key: string]: unknown;
}

export interface ToolResultResource {
uri?: string;
mimeType?: string;
mime_type?: string;
text?: string;
blob?: string;
data?: string;
[key: string]: unknown;
}

export interface ToolResultContentBlock {
type?: string;
text?: string;
data?: string;
blob?: string;
mimeType?: string;
mime_type?: string;
uri?: string;
resource?: ToolResultResource;
[key: string]: unknown;
}

export interface ToolPreviewResponse {
content?: ToolResultContentBlock[];
resolved_arguments?: Record<string, unknown>;
structured_output?: unknown;
structuredOutput?: unknown;
isError?: boolean;
is_error?: boolean;
target?: "local" | "federated" | ToolPreviewTarget | null;
annotations?: Record<string, unknown> | null;
pre_hooks_run?: unknown[] | number | null;
Expand Down
104 changes: 103 additions & 1 deletion src/components/tools/ToolPreviewResult.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { describe, expect, it } from "vitest";
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

import { renderWithProviders as render } from "@/test/test-utils";
import { ToolPreviewResult } from "./ToolPreviewResult";
import type { ToolPreviewState } from "@/hooks/useToolPreview";
import type { ToolPreviewResponse } from "@/api/tools";
import { TOOL_RESULT_STRUCTURED_OUTPUT_SIZE_LIMIT_BYTES } from "./toolResultContent";

function previewProps(
overrides: Partial<Pick<ToolPreviewState, "result" | "error" | "hasRun">>,
Expand All @@ -23,7 +26,7 @@ describe("ToolPreviewResult", () => {
});

it("renders status, warnings, resolved arguments, and raw response for success", () => {
render(
const { container } = render(
<ToolPreviewResult
preview={previewProps({
hasRun: true,
Expand All @@ -33,6 +36,7 @@ describe("ToolPreviewResult", () => {
preview: {
target: { kind: "federated", gateway_name: "github" },
resolved_arguments: { query: "cloudflare" },
content: [{ type: "text", text: "found issue", mimeType: "text/plain" }],
warnings: [{ code: "elicitation_skipped", message: "approval skipped" }],
},
},
Expand All @@ -45,8 +49,12 @@ describe("ToolPreviewResult", () => {
expect(screen.getByText("federated: github")).toBeInTheDocument();
expect(screen.getByText("Warnings")).toBeInTheDocument();
expect(screen.getByText("approval skipped")).toBeInTheDocument();
expect(screen.getByText("Tool result")).toBeInTheDocument();
expect(screen.getByText("found issue")).toBeInTheDocument();
expect(screen.getByText("Resolved arguments")).toBeInTheDocument();
expect(screen.getByText("Raw preview response")).toBeInTheDocument();
expect(screen.getByLabelText("Copy raw preview response")).toBeVisible();
expect(container.textContent).toContain('"resolved_arguments"');
});

it("renders API failures", () => {
Expand Down Expand Up @@ -86,6 +94,100 @@ describe("ToolPreviewResult", () => {
expect(screen.queryByText("Resolved arguments")).not.toBeInTheDocument();
});

it("renders elicitation skipped warnings without backend messages", () => {
render(
<ToolPreviewResult
preview={previewProps({
hasRun: true,
result: {
status: 200,
renderTimeMs: 0,
preview: {
target: "local",
warnings: [{ code: "elicitation_skipped", hooks: ["approval_hook"] }],
},
},
})}
/>,
);

expect(
screen.getByText("Live invocation may request user input; preview skipped approval_hook."),
).toBeInTheDocument();
});

it("renders localized fallback hook labels for elicitation warnings", () => {
render(
<ToolPreviewResult
preview={previewProps({
hasRun: true,
result: {
status: 200,
renderTimeMs: 0,
preview: {
warnings: [{ code: "elicitation_skipped" }],
},
},
})}
/>,
);

expect(
screen.getByText(
"Live invocation may request user input; preview skipped one or more hooks.",
),
).toBeInTheDocument();
});

it("keeps large raw responses collapsed until requested", async () => {
const user = userEvent.setup();
const marker = "hidden raw marker";
const preview = {
debug: `${"x".repeat(TOOL_RESULT_STRUCTURED_OUTPUT_SIZE_LIMIT_BYTES)} ${marker}`,
} as unknown as ToolPreviewResponse;
const { container } = render(
<ToolPreviewResult
preview={previewProps({
hasRun: true,
result: {
status: 200,
renderTimeMs: 0,
preview,
},
})}
/>,
);

expect(screen.getByText(/Large content hidden/)).toBeInTheDocument();
expect(container.textContent).not.toContain(marker);

await user.click(screen.getByRole("button", { name: "View all" }));

expect(container.textContent).toContain(marker);
});

it("renders tool error results without treating the HTTP request as failed", () => {
render(
<ToolPreviewResult
preview={previewProps({
hasRun: true,
result: {
status: 200,
renderTimeMs: 5,
preview: {
content: [{ type: "text", text: "tool failed", mimeType: "text/plain" }],
isError: true,
},
},
})}
/>,
);

expect(screen.getByText("Preview 200")).toBeInTheDocument();
expect(screen.getByText("Error response")).toBeInTheDocument();
expect(screen.getByText("tool failed")).toBeInTheDocument();
});

it("renders generic failures without an HTTP status", () => {
render(
<ToolPreviewResult
Expand Down
Loading
Loading