feat(frontend): use new output renderers on new run page (#10876)

## Changes 🏗️

Integrating the great work @ntindle did on the rich agent output
renderers into the new Agent run page in the library 💜

- Implemented enhanced output rendering in `<RunDetails />` using the
shared output-renderers
- Added `<RunOutputs />` sub-component at
`RunDetails/components/RunOutputs.tsx` that:
- [x] builds items from `run.outputs`, extracts metadata, picks a
renderer via `globalRegistry`, and falls back to `TextRenderer`
- [x] renders `<OutputActions />` for copy/download and a list of
`<OutputItems />`.

## Checklist 📋

### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  - [x] Run an agent on the view which outputs rich content
- [x] See the output use the new renderers, for example code is
higlighted

### For configuration changes:

None
This commit is contained in:
Ubbe
2025-09-09 14:44:37 +09:00
committed by GitHub
parent e8cf3edbf4
commit 925f249ce1
15 changed files with 117 additions and 23 deletions

View File

@@ -14,6 +14,7 @@ import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
import { Skeleton } from "@/components/ui/skeleton";
import { AgentInputsReadOnly } from "../AgentInputsReadOnly/AgentInputsReadOnly";
import { RunDetailCard } from "../RunDetailCard/RunDetailCard";
import { RunOutputs } from "./components/RunOutputs";
interface RunDetailsProps {
agent: LibraryAgent;
@@ -74,28 +75,10 @@ export function RunDetails({
<RunDetailCard>
{isLoading ? (
<div className="text-neutral-500">Loading</div>
) : !run ||
!("outputs" in run) ||
Object.keys(run.outputs || {}).length === 0 ? (
<div className="text-neutral-600">No output from this run.</div>
) : run && "outputs" in run ? (
<RunOutputs outputs={run.outputs as any} />
) : (
<div className="flex flex-col gap-4">
{Object.entries(run.outputs).map(([key, values]) => (
<div key={key} className="flex flex-col gap-1.5">
<label className="text-sm font-medium">{key}</label>
{values.map((value, i) => (
<p
key={i}
className="whitespace-pre-wrap break-words text-sm text-neutral-700"
>
{typeof value === "object"
? JSON.stringify(value, undefined, 2)
: String(value)}
</p>
))}
</div>
))}
</div>
<div className="text-neutral-600">No output from this run.</div>
)}
</RunDetailCard>
</TabsLineContent>

View File

@@ -0,0 +1,107 @@
"use client";
import React, { useMemo } from "react";
import {
globalRegistry,
OutputItem,
OutputActions,
} from "@/app/(platform)/library/agents/[id]/components/AgentRunsView/components/OutputRenderers";
import type {
OutputMetadata,
OutputRenderer,
} from "@/app/(platform)/library/agents/[id]/components/AgentRunsView/components/OutputRenderers";
type OutputsRecord = Record<string, Array<unknown>>;
interface RunOutputsProps {
outputs: OutputsRecord;
}
export function RunOutputs({ outputs }: RunOutputsProps) {
const items = useMemo(() => {
const list: Array<{
key: string;
label: string;
value: unknown;
metadata?: OutputMetadata;
renderer: OutputRenderer;
}> = [];
Object.entries(outputs || {}).forEach(([key, values]) => {
(values || []).forEach((value, index) => {
const metadata: OutputMetadata = {};
if (
typeof value === "object" &&
value !== null &&
!React.isValidElement(value)
) {
const obj = value as Record<string, unknown>;
if (typeof obj["type"] === "string")
metadata.type = obj["type"] as string;
if (typeof obj["mimeType"] === "string")
metadata.mimeType = obj["mimeType"] as string;
if (typeof obj["filename"] === "string")
metadata.filename = obj["filename"] as string;
if (typeof obj["language"] === "string")
metadata.language = obj["language"] as string;
}
const renderer = globalRegistry.getRenderer(value, metadata);
if (renderer) {
list.push({
key: `${key}-${index}`,
label: index === 0 ? key : "",
value,
metadata,
renderer,
});
} else {
const textRenderer = globalRegistry
.getAllRenderers()
.find((r) => r.name === "TextRenderer");
if (textRenderer) {
list.push({
key: `${key}-${index}`,
label: index === 0 ? key : "",
value:
typeof value === "string"
? value
: JSON.stringify(value, null, 2),
metadata,
renderer: textRenderer,
});
}
}
});
});
return list;
}, [outputs]);
if (!items.length) {
return <div className="text-neutral-600">No output from this run.</div>;
}
return (
<div className="flex flex-col gap-4">
<div className="flex items-center justify-end">
<OutputActions
items={items.map((item) => ({
value: item.value,
metadata: item.metadata,
renderer: item.renderer,
}))}
/>
</div>
{items.map((item) => (
<OutputItem
key={item.key}
value={item.value}
metadata={item.metadata}
renderer={item.renderer}
label={item.label}
/>
))}
</div>
);
}

View File

@@ -6,8 +6,12 @@ import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import LoadingBox from "@/components/ui/loading";
import { globalRegistry, OutputItem, OutputActions } from "./output-renderers";
import type { OutputMetadata } from "./output-renderers";
import {
globalRegistry,
OutputItem,
OutputActions,
} from "../../AgentRunsView/components/OutputRenderers";
import type { OutputMetadata } from "../../AgentRunsView/components/OutputRenderers";
export function AgentRunOutputView({
agentRunOutputs,