From cdd7b77248b17f43b4a54c65dbc9770f7883bd8a Mon Sep 17 00:00:00 2001 From: Pratik Gandhi Date: Thu, 20 Aug 2026 15:52:41 +0100 Subject: [PATCH 1/4] feat: render tool preview results Signed-off-by: Pratik Gandhi --- e2e/tools.spec.ts | 14 +- src/api/tools.ts | 27 ++ .../tools/ToolPreviewResult.test.tsx | 47 ++++ src/components/tools/ToolPreviewResult.tsx | 77 ++++-- .../tools/ToolResultRenderer.test.tsx | 85 ++++++ src/components/tools/ToolResultRenderer.tsx | 253 ++++++++++++++++++ .../tools/toolResultContent.test.ts | 72 +++++ src/components/tools/toolResultContent.ts | 206 ++++++++++++++ src/i18n/locales/en-US/tools.json | 14 + src/i18n/locales/es-ES/tools.json | 14 + src/i18n/locales/pt-BR/tools.json | 14 + 11 files changed, 805 insertions(+), 18 deletions(-) create mode 100644 src/components/tools/ToolResultRenderer.test.tsx create mode 100644 src/components/tools/ToolResultRenderer.tsx create mode 100644 src/components/tools/toolResultContent.test.ts create mode 100644 src/components/tools/toolResultContent.ts diff --git a/e2e/tools.spec.ts b/e2e/tools.spec.ts index 727fe5a..faeb10e 100644 --- a/e2e/tools.spec.ts +++ b/e2e/tools.spec.ts @@ -317,9 +317,14 @@ 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" }, + ], + structured_output: { total: 2, query: "cloudflare" }, annotations: { readOnlyHint: true }, pre_hooks_run: [], - warnings: [], + warnings: [{ code: "elicitation_skipped", hooks: ["approval_hook"] }], }), }); }); @@ -346,6 +351,13 @@ test.describe("Tools page", () => { await previewButton.click(); await expect(panel.getByText("Preview 200")).toBeVisible(); + await expect(panel.getByText("Warnings")).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")).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"); diff --git a/src/api/tools.ts b/src/api/tools.ts index 5beb407..d8ac29a 100644 --- a/src/api/tools.ts +++ b/src/api/tools.ts @@ -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; + structured_output?: unknown; + structuredOutput?: unknown; + isError?: boolean; + is_error?: boolean; target?: "local" | "federated" | ToolPreviewTarget | null; annotations?: Record | null; pre_hooks_run?: unknown[] | number | null; diff --git a/src/components/tools/ToolPreviewResult.test.tsx b/src/components/tools/ToolPreviewResult.test.tsx index 6ca9e87..c7976d0 100644 --- a/src/components/tools/ToolPreviewResult.test.tsx +++ b/src/components/tools/ToolPreviewResult.test.tsx @@ -33,6 +33,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" }], }, }, @@ -45,6 +46,8 @@ 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(); }); @@ -86,6 +89,50 @@ describe("ToolPreviewResult", () => { expect(screen.queryByText("Resolved arguments")).not.toBeInTheDocument(); }); + it("renders elicitation skipped warnings without backend messages", () => { + render( + , + ); + + expect( + screen.getByText("Live invocation may request user input; preview skipped approval_hook."), + ).toBeInTheDocument(); + }); + + it("renders tool error results without treating the HTTP request as failed", () => { + render( + , + ); + + 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( ; @@ -18,7 +26,10 @@ export function ToolPreviewResult({ preview }: ToolPreviewResultProps) { if (!hasRun) return null; const renderTimeMs = result?.renderTimeMs ?? error?.renderTimeMs ?? 0; + const response = result?.preview; + const toolResultIsError = response ? getToolResultIsError(response) : false; const succeeded = result !== null; + const statusOk = succeeded && !toolResultIsError; const statusCode = result?.status ?? error?.status ?? null; const statusLabel = succeeded ? intl.formatMessage({ id: "tools.details.preview.statusOk" }, { status: statusCode ?? 200 }) @@ -29,7 +40,6 @@ export function ToolPreviewResult({ preview }: ToolPreviewResultProps) { ) : intl.formatMessage({ id: "tools.details.preview.statusError" }); - const response = result?.preview; const target = response ? formatTarget(response.target) : null; const warnings = response?.warnings ?? []; @@ -40,12 +50,12 @@ export function ToolPreviewResult({ preview }: ToolPreviewResultProps) { aria-live="polite" className="flex flex-wrap items-center gap-x-2 gap-y-1 text-[13px]" > - {succeeded ? ( + {statusOk ? ( ) : ( )} - + {statusLabel}