Add RunBlock, RunAgent, and ViewAgentOutput tools to ChatMessagesContainer for expanded functionality

This commit is contained in:
abhi1992002
2026-02-03 13:57:30 +05:30
parent b06868f453
commit d3018cc8ea
8 changed files with 1044 additions and 0 deletions

View File

@@ -14,6 +14,9 @@ import { UIMessage, UIDataTypes, UITools, ToolUIPart } from "ai";
import { FindBlocksTool } from "../../tools/FindBlocks/FindBlocks";
import { FindAgentsTool } from "../../tools/FindAgents/FindAgents";
import { SearchDocsTool } from "../../tools/SearchDocs/SearchDocs";
import { RunBlockTool } from "../../tools/RunBlock/RunBlock";
import { RunAgentTool } from "../../tools/RunAgent/RunAgent";
import { ViewAgentOutputTool } from "../../tools/ViewAgentOutput/ViewAgentOutput";
interface ChatMessagesContainerProps {
messages: UIMessage<unknown, UIDataTypes, UITools>[];
@@ -79,6 +82,27 @@ export const ChatMessagesContainer = ({
part={part as ToolUIPart}
/>
);
case "tool-run_block":
return (
<RunBlockTool
key={`${message.id}-${i}`}
part={part as ToolUIPart}
/>
);
case "tool-run_agent":
return (
<RunAgentTool
key={`${message.id}-${i}`}
part={part as ToolUIPart}
/>
);
case "tool-view_agent_output":
return (
<ViewAgentOutputTool
key={`${message.id}-${i}`}
part={part as ToolUIPart}
/>
);
default:
return null;
}

View File

@@ -40,6 +40,7 @@ export function ToolAccordion({
className={cn(
"mt-2 rounded-2xl border bg-background px-3 py-2",
className,
)}
>
<button

View File

@@ -0,0 +1,185 @@
"use client";
import type { ToolUIPart } from "ai";
import Link from "next/link";
import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation";
import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion";
import {
formatMaybeJson,
getAnimationText,
getRunAgentToolOutput,
StateIcon,
type RunAgentToolOutput,
} from "./helpers";
export interface RunAgentToolPart {
type: string;
toolCallId: string;
state: ToolUIPart["state"];
input?: unknown;
output?: unknown;
}
interface Props {
part: RunAgentToolPart;
}
function getAccordionMeta(output: RunAgentToolOutput): {
badgeText: string;
title: string;
description?: string;
} {
if (output.type === "execution_started") {
return {
badgeText: "Run agent",
title: output.graph_name,
description: `Status: ${output.status}`,
};
}
if (output.type === "setup_requirements") {
const missingCredsCount = Object.keys(
output.setup_info.user_readiness.missing_credentials ?? {},
).length;
return {
badgeText: "Run agent",
title: output.setup_info.agent_name,
description:
missingCredsCount > 0
? `Missing ${missingCredsCount} credential${missingCredsCount === 1 ? "" : "s"}`
: output.message,
};
}
if (output.type === "need_login") {
return { badgeText: "Run agent", title: "Sign in required" };
}
return { badgeText: "Run agent", title: "Error" };
}
export function RunAgentTool({ part }: Props) {
const text = getAnimationText(part);
const output = getRunAgentToolOutput(part);
const hasExpandableContent =
part.state === "output-available" &&
!!output &&
(output.type === "execution_started" ||
output.type === "setup_requirements" ||
output.type === "need_login" ||
output.type === "error");
return (
<div className="py-2">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<StateIcon state={part.state} />
<MorphingTextAnimation text={text} />
</div>
{hasExpandableContent && output && (
<ToolAccordion {...getAccordionMeta(output)}>
{output.type === "execution_started" && (
<div className="grid gap-2">
<div className="rounded-2xl border bg-background p-3">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<p className="text-sm font-medium text-foreground">
Execution started
</p>
<p className="mt-0.5 truncate text-xs text-muted-foreground">
{output.execution_id}
</p>
<p className="mt-2 text-xs text-muted-foreground">
{output.message}
</p>
</div>
{output.library_agent_link && (
<Link
href={output.library_agent_link}
className="shrink-0 text-xs font-medium text-purple-600 hover:text-purple-700"
>
Open
</Link>
)}
</div>
</div>
</div>
)}
{output.type === "setup_requirements" && (
<div className="grid gap-2">
<p className="text-sm text-foreground">{output.message}</p>
{Object.keys(
output.setup_info.user_readiness.missing_credentials ?? {},
).length > 0 && (
<div className="rounded-2xl border bg-background p-3">
<p className="text-xs font-medium text-foreground">
Missing credentials
</p>
<ul className="mt-2 list-disc space-y-1 pl-5 text-xs text-muted-foreground">
{Object.entries(
output.setup_info.user_readiness.missing_credentials ??
{},
).map(([field, cred]) => (
<li key={field}>
{cred.title} ({cred.provider})
</li>
))}
</ul>
</div>
)}
{output.setup_info.requirements.inputs?.length > 0 && (
<div className="rounded-2xl border bg-background p-3">
<p className="text-xs font-medium text-foreground">
Expected inputs
</p>
<div className="mt-2 grid gap-2">
{output.setup_info.requirements.inputs.map((input) => (
<div key={input.name} className="rounded-xl border p-2">
<div className="flex items-center justify-between gap-2">
<p className="truncate text-xs font-medium text-foreground">
{input.title}
</p>
<span className="shrink-0 rounded-full border bg-muted px-2 py-0.5 text-[11px] text-muted-foreground">
{input.required ? "Required" : "Optional"}
</span>
</div>
<p className="mt-1 text-xs text-muted-foreground">
{input.name} {input.type}
{input.description ? `${input.description}` : ""}
</p>
</div>
))}
</div>
</div>
)}
</div>
)}
{output.type === "need_login" && (
<p className="text-sm text-foreground">{output.message}</p>
)}
{output.type === "error" && (
<div className="grid gap-2">
<p className="text-sm text-foreground">{output.message}</p>
{output.error && (
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
{formatMaybeJson(output.error)}
</pre>
)}
{output.details && (
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
{formatMaybeJson(output.details)}
</pre>
)}
</div>
)}
</ToolAccordion>
)}
</div>
);
}

View File

@@ -0,0 +1,185 @@
import type { ToolUIPart } from "ai";
import {
CheckCircleIcon,
CircleNotchIcon,
XCircleIcon,
} from "@phosphor-icons/react";
export interface RunAgentInput {
username_agent_slug?: string;
library_agent_id?: string;
inputs?: Record<string, unknown>;
use_defaults?: boolean;
schedule_name?: string;
cron?: string;
timezone?: string;
}
export interface CredentialsMeta {
id: string;
provider: string;
type: string;
title: string;
}
export interface SetupInfo {
agent_id: string;
agent_name: string;
requirements: {
credentials: CredentialsMeta[];
inputs: Array<{
name: string;
title: string;
type: string;
description: string;
required: boolean;
}>;
execution_modes: string[];
};
user_readiness: {
has_all_credentials: boolean;
missing_credentials: Record<string, CredentialsMeta>;
ready_to_run: boolean;
};
}
export interface SetupRequirementsOutput {
type: "setup_requirements";
message: string;
session_id?: string;
setup_info: SetupInfo;
graph_id?: string | null;
graph_version?: number | null;
}
export interface ExecutionStartedOutput {
type: "execution_started";
message: string;
session_id?: string;
execution_id: string;
graph_id: string;
graph_name: string;
library_agent_id?: string | null;
library_agent_link?: string | null;
status: string;
}
export interface ErrorOutput {
type: "error";
message: string;
session_id?: string;
error?: string | null;
details?: Record<string, unknown> | null;
}
export interface NeedLoginOutput {
type: "need_login";
message: string;
session_id?: string;
}
export type RunAgentToolOutput =
| SetupRequirementsOutput
| ExecutionStartedOutput
| NeedLoginOutput
| ErrorOutput;
function parseOutput(output: unknown): RunAgentToolOutput | null {
if (!output) return null;
if (typeof output === "string") {
const trimmed = output.trim();
if (!trimmed) return null;
try {
return JSON.parse(trimmed) as RunAgentToolOutput;
} catch {
return null;
}
}
if (typeof output === "object") return output as RunAgentToolOutput;
return null;
}
export function getRunAgentToolOutput(
part: unknown,
): RunAgentToolOutput | null {
if (!part || typeof part !== "object") return null;
return parseOutput((part as { output?: unknown }).output);
}
function getAgentIdentifierText(
input: RunAgentInput | undefined,
): string | null {
if (!input) return null;
const slug = input.username_agent_slug?.trim();
if (slug) return slug;
const libraryId = input.library_agent_id?.trim();
if (libraryId) return `Library agent ${libraryId}`;
return null;
}
function getExecutionModeText(input: RunAgentInput | undefined): string | null {
if (!input) return null;
const isSchedule = Boolean(input.schedule_name?.trim() || input.cron?.trim());
return isSchedule ? "Scheduled run" : "Run";
}
export function getAnimationText(part: {
state: ToolUIPart["state"];
input?: unknown;
output?: unknown;
}): string {
const input = part.input as RunAgentInput | undefined;
const agentIdentifier = getAgentIdentifierText(input);
const mode = getExecutionModeText(input);
switch (part.state) {
case "input-streaming":
return "Preparing to run agent";
case "input-available":
return agentIdentifier ? `${mode}: ${agentIdentifier}` : "Running agent";
case "output-available": {
const output = parseOutput(part.output);
if (!output) return "Agent run updated";
if (output.type === "execution_started") {
return `Started: ${output.graph_name}`;
}
if (output.type === "setup_requirements") {
return `Needs setup: ${output.setup_info.agent_name}`;
}
if (output.type === "need_login") return "Sign in required to run agent";
return "Error running agent";
}
case "output-error":
return "Error running agent";
default:
return "Processing";
}
}
export function StateIcon({ state }: { state: ToolUIPart["state"] }) {
switch (state) {
case "input-streaming":
case "input-available":
return (
<CircleNotchIcon
className="h-4 w-4 animate-spin text-muted-foreground"
weight="bold"
/>
);
case "output-available":
return <CheckCircleIcon className="h-4 w-4 text-green-500" />;
case "output-error":
return <XCircleIcon className="h-4 w-4 text-red-500" />;
default:
return null;
}
}
export function formatMaybeJson(value: unknown): string {
if (typeof value === "string") return value;
try {
return JSON.stringify(value, null, 2);
} catch {
return String(value);
}
}

View File

@@ -0,0 +1,173 @@
"use client";
import type { ToolUIPart } from "ai";
import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation";
import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion";
import {
formatMaybeJson,
getAnimationText,
getRunBlockToolOutput,
StateIcon,
type RunBlockToolOutput,
} from "./helpers";
export interface RunBlockToolPart {
type: string;
toolCallId: string;
state: ToolUIPart["state"];
input?: unknown;
output?: unknown;
}
interface Props {
part: RunBlockToolPart;
}
function getAccordionMeta(output: RunBlockToolOutput): {
badgeText: string;
title: string;
description?: string;
} {
if (output.type === "block_output") {
const keys = Object.keys(output.outputs ?? {});
return {
badgeText: "Run block",
title: output.block_name,
description:
keys.length > 0
? `${keys.length} output key${keys.length === 1 ? "" : "s"}`
: output.message,
};
}
if (output.type === "setup_requirements") {
const missingCredsCount = Object.keys(
output.setup_info.user_readiness.missing_credentials ?? {},
).length;
return {
badgeText: "Run block",
title: output.setup_info.agent_name,
description:
missingCredsCount > 0
? `Missing ${missingCredsCount} credential${missingCredsCount === 1 ? "" : "s"}`
: output.message,
};
}
return { badgeText: "Run block", title: "Error" };
}
export function RunBlockTool({ part }: Props) {
const text = getAnimationText(part);
const output = getRunBlockToolOutput(part);
const hasExpandableContent =
part.state === "output-available" &&
!!output &&
(output.type === "block_output" ||
output.type === "setup_requirements" ||
output.type === "error");
return (
<div className="py-2">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<StateIcon state={part.state} />
<MorphingTextAnimation text={text} />
</div>
{hasExpandableContent && output && (
<ToolAccordion {...getAccordionMeta(output)}>
{output.type === "block_output" && (
<div className="grid gap-2">
<p className="text-sm text-foreground">{output.message}</p>
{Object.entries(output.outputs ?? {}).map(([key, items]) => (
<div key={key} className="rounded-2xl border bg-background p-3">
<div className="flex items-center justify-between gap-2">
<p className="truncate text-xs font-medium text-foreground">
{key}
</p>
<span className="shrink-0 rounded-full border bg-muted px-2 py-0.5 text-[11px] text-muted-foreground">
{items.length} item{items.length === 1 ? "" : "s"}
</span>
</div>
<pre className="mt-2 whitespace-pre-wrap text-xs text-muted-foreground">
{formatMaybeJson(items.slice(0, 3))}
</pre>
</div>
))}
</div>
)}
{output.type === "setup_requirements" && (
<div className="grid gap-2">
<p className="text-sm text-foreground">{output.message}</p>
{Object.keys(
output.setup_info.user_readiness.missing_credentials ?? {},
).length > 0 && (
<div className="rounded-2xl border bg-background p-3">
<p className="text-xs font-medium text-foreground">
Missing credentials
</p>
<ul className="mt-2 list-disc space-y-1 pl-5 text-xs text-muted-foreground">
{Object.entries(
output.setup_info.user_readiness.missing_credentials ??
{},
).map(([field, cred]) => (
<li key={field}>
{cred.title} ({cred.provider})
</li>
))}
</ul>
</div>
)}
{output.setup_info.requirements.inputs?.length > 0 && (
<div className="rounded-2xl border bg-background p-3">
<p className="text-xs font-medium text-foreground">
Expected inputs
</p>
<div className="mt-2 grid gap-2">
{output.setup_info.requirements.inputs.map((input) => (
<div key={input.name} className="rounded-xl border p-2">
<div className="flex items-center justify-between gap-2">
<p className="truncate text-xs font-medium text-foreground">
{input.title}
</p>
<span className="shrink-0 rounded-full border bg-muted px-2 py-0.5 text-[11px] text-muted-foreground">
{input.required ? "Required" : "Optional"}
</span>
</div>
<p className="mt-1 text-xs text-muted-foreground">
{input.name} {input.type}
{input.description ? `${input.description}` : ""}
</p>
</div>
))}
</div>
</div>
)}
</div>
)}
{output.type === "error" && (
<div className="grid gap-2">
<p className="text-sm text-foreground">{output.message}</p>
{output.error && (
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
{formatMaybeJson(output.error)}
</pre>
)}
{output.details && (
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
{formatMaybeJson(output.details)}
</pre>
)}
</div>
)}
</ToolAccordion>
)}
</div>
);
}

View File

@@ -0,0 +1,155 @@
import type { ToolUIPart } from "ai";
import {
CheckCircleIcon,
CircleNotchIcon,
XCircleIcon,
} from "@phosphor-icons/react";
export interface RunBlockInput {
block_id?: string;
input_data?: Record<string, unknown>;
}
export interface CredentialsMeta {
id: string;
provider: string;
type: string;
title: string;
}
export interface SetupInfo {
agent_id: string;
agent_name: string;
requirements: {
credentials: CredentialsMeta[];
inputs: Array<{
name: string;
title: string;
type: string;
description: string;
required: boolean;
}>;
execution_modes: string[];
};
user_readiness: {
has_all_credentials: boolean;
missing_credentials: Record<string, CredentialsMeta>;
ready_to_run: boolean;
};
}
export interface SetupRequirementsOutput {
type: "setup_requirements";
message: string;
session_id?: string;
setup_info: SetupInfo;
}
export interface BlockOutput {
type: "block_output";
message: string;
session_id?: string;
block_id: string;
block_name: string;
outputs: Record<string, unknown[]>;
success: boolean;
}
export interface ErrorOutput {
type: "error";
message: string;
session_id?: string;
error?: string | null;
details?: Record<string, unknown> | null;
}
export type RunBlockToolOutput =
| SetupRequirementsOutput
| BlockOutput
| ErrorOutput;
function parseOutput(output: unknown): RunBlockToolOutput | null {
if (!output) return null;
if (typeof output === "string") {
const trimmed = output.trim();
if (!trimmed) return null;
try {
return JSON.parse(trimmed) as RunBlockToolOutput;
} catch {
return null;
}
}
if (typeof output === "object") return output as RunBlockToolOutput;
return null;
}
export function getRunBlockToolOutput(
part: unknown,
): RunBlockToolOutput | null {
if (!part || typeof part !== "object") return null;
return parseOutput((part as { output?: unknown }).output);
}
function getBlockLabel(input: RunBlockInput | undefined): string | null {
const blockId = input?.block_id?.trim();
if (!blockId) return null;
return `Block ${blockId.slice(0, 8)}`;
}
export function getAnimationText(part: {
state: ToolUIPart["state"];
input?: unknown;
output?: unknown;
}): string {
const input = part.input as RunBlockInput | undefined;
const blockLabel = getBlockLabel(input);
switch (part.state) {
case "input-streaming":
return "Preparing to run block";
case "input-available":
return blockLabel ? `Running ${blockLabel}` : "Running block";
case "output-available": {
const output = parseOutput(part.output);
if (!output) return "Block run updated";
if (output.type === "block_output")
return `Block ran: ${output.block_name}`;
if (output.type === "setup_requirements") {
return `Needs setup: ${output.setup_info.agent_name}`;
}
return "Error running block";
}
case "output-error":
return "Error running block";
default:
return "Processing";
}
}
export function StateIcon({ state }: { state: ToolUIPart["state"] }) {
switch (state) {
case "input-streaming":
case "input-available":
return (
<CircleNotchIcon
className="h-4 w-4 animate-spin text-muted-foreground"
weight="bold"
/>
);
case "output-available":
return <CheckCircleIcon className="h-4 w-4 text-green-500" />;
case "output-error":
return <XCircleIcon className="h-4 w-4 text-red-500" />;
default:
return null;
}
}
export function formatMaybeJson(value: unknown): string {
if (typeof value === "string") return value;
try {
return JSON.stringify(value, null, 2);
} catch {
return String(value);
}
}

View File

@@ -0,0 +1,171 @@
"use client";
import type { ToolUIPart } from "ai";
import Link from "next/link";
import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation";
import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion";
import {
formatMaybeJson,
getAnimationText,
getViewAgentOutputToolOutput,
StateIcon,
type ViewAgentOutputToolOutput,
} from "./helpers";
export interface ViewAgentOutputToolPart {
type: string;
toolCallId: string;
state: ToolUIPart["state"];
input?: unknown;
output?: unknown;
}
interface Props {
part: ViewAgentOutputToolPart;
}
function getAccordionMeta(output: ViewAgentOutputToolOutput): {
badgeText: string;
title: string;
description?: string;
} {
if (output.type === "agent_output") {
const status = output.execution?.status;
return {
badgeText: "Agent output",
title: output.agent_name,
description: status ? `Status: ${status}` : output.message,
};
}
if (output.type === "no_results") {
return { badgeText: "Agent output", title: "No results" };
}
return { badgeText: "Agent output", title: "Error" };
}
export function ViewAgentOutputTool({ part }: Props) {
const text = getAnimationText(part);
const output = getViewAgentOutputToolOutput(part);
const hasExpandableContent =
part.state === "output-available" &&
!!output &&
(output.type === "agent_output" ||
output.type === "no_results" ||
output.type === "error");
return (
<div className="py-2">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<StateIcon state={part.state} />
<MorphingTextAnimation text={text} />
</div>
{hasExpandableContent && output && (
<ToolAccordion {...getAccordionMeta(output)}>
{output.type === "agent_output" && (
<div className="grid gap-2">
<div className="flex items-start justify-between gap-3">
<p className="text-sm text-foreground">{output.message}</p>
{output.library_agent_link && (
<Link
href={output.library_agent_link}
className="shrink-0 text-xs font-medium text-purple-600 hover:text-purple-700"
>
Open
</Link>
)}
</div>
{output.execution ? (
<div className="grid gap-2">
<div className="rounded-2xl border bg-background p-3">
<p className="text-xs font-medium text-foreground">
Execution
</p>
<p className="mt-1 truncate text-xs text-muted-foreground">
{output.execution.execution_id}
</p>
<p className="mt-1 text-xs text-muted-foreground">
Status: {output.execution.status}
</p>
</div>
{output.execution.inputs_summary && (
<div className="rounded-2xl border bg-background p-3">
<p className="text-xs font-medium text-foreground">
Inputs summary
</p>
<pre className="mt-2 whitespace-pre-wrap text-xs text-muted-foreground">
{formatMaybeJson(output.execution.inputs_summary)}
</pre>
</div>
)}
{Object.entries(output.execution.outputs ?? {}).map(
([key, items]) => (
<div
key={key}
className="rounded-2xl border bg-background p-3"
>
<div className="flex items-center justify-between gap-2">
<p className="truncate text-xs font-medium text-foreground">
{key}
</p>
<span className="shrink-0 rounded-full border bg-muted px-2 py-0.5 text-[11px] text-muted-foreground">
{items.length} item{items.length === 1 ? "" : "s"}
</span>
</div>
<pre className="mt-2 whitespace-pre-wrap text-xs text-muted-foreground">
{formatMaybeJson(items.slice(0, 3))}
</pre>
</div>
),
)}
</div>
) : (
<div className="rounded-2xl border bg-background p-3">
<p className="text-sm text-foreground">
No execution selected.
</p>
<p className="mt-1 text-xs text-muted-foreground">
Try asking for a specific run or execution_id.
</p>
</div>
)}
</div>
)}
{output.type === "no_results" && (
<div className="grid gap-2">
<p className="text-sm text-foreground">{output.message}</p>
{output.suggestions && output.suggestions.length > 0 && (
<ul className="mt-1 list-disc space-y-1 pl-5 text-xs text-muted-foreground">
{output.suggestions.slice(0, 5).map((s) => (
<li key={s}>{s}</li>
))}
</ul>
)}
</div>
)}
{output.type === "error" && (
<div className="grid gap-2">
<p className="text-sm text-foreground">{output.message}</p>
{output.error && (
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
{formatMaybeJson(output.error)}
</pre>
)}
{output.details && (
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
{formatMaybeJson(output.details)}
</pre>
)}
</div>
)}
</ToolAccordion>
)}
</div>
);
}

View File

@@ -0,0 +1,150 @@
import type { ToolUIPart } from "ai";
import {
CheckCircleIcon,
CircleNotchIcon,
XCircleIcon,
} from "@phosphor-icons/react";
export interface ViewAgentOutputInput {
agent_name?: string;
library_agent_id?: string;
store_slug?: string;
execution_id?: string;
run_time?: string;
}
export interface ExecutionOutputInfo {
execution_id: string;
status: string;
started_at?: string | null;
ended_at?: string | null;
outputs: Record<string, unknown[]>;
inputs_summary?: Record<string, unknown> | null;
}
export interface AgentOutputOutput {
type: "agent_output";
message: string;
session_id?: string;
agent_name: string;
agent_id: string;
library_agent_id?: string | null;
library_agent_link?: string | null;
execution?: ExecutionOutputInfo | null;
available_executions?: Array<Record<string, unknown>> | null;
total_executions: number;
}
export interface NoResultsOutput {
type: "no_results";
message: string;
session_id?: string;
suggestions?: string[];
}
export interface ErrorOutput {
type: "error";
message: string;
session_id?: string;
error?: string | null;
details?: Record<string, unknown> | null;
}
export type ViewAgentOutputToolOutput =
| AgentOutputOutput
| NoResultsOutput
| ErrorOutput;
function parseOutput(output: unknown): ViewAgentOutputToolOutput | null {
if (!output) return null;
if (typeof output === "string") {
const trimmed = output.trim();
if (!trimmed) return null;
try {
return JSON.parse(trimmed) as ViewAgentOutputToolOutput;
} catch {
return null;
}
}
if (typeof output === "object") return output as ViewAgentOutputToolOutput;
return null;
}
export function getViewAgentOutputToolOutput(
part: unknown,
): ViewAgentOutputToolOutput | null {
if (!part || typeof part !== "object") return null;
return parseOutput((part as { output?: unknown }).output);
}
function getAgentIdentifierText(
input: ViewAgentOutputInput | undefined,
): string | null {
if (!input) return null;
const libraryId = input.library_agent_id?.trim();
if (libraryId) return `Library agent ${libraryId}`;
const slug = input.store_slug?.trim();
if (slug) return slug;
const name = input.agent_name?.trim();
if (name) return name;
return null;
}
export function getAnimationText(part: {
state: ToolUIPart["state"];
input?: unknown;
output?: unknown;
}): string {
const input = part.input as ViewAgentOutputInput | undefined;
const agent = getAgentIdentifierText(input);
switch (part.state) {
case "input-streaming":
return "Looking up agent outputs";
case "input-available":
return agent ? `Loading outputs: ${agent}` : "Loading agent outputs";
case "output-available": {
const output = parseOutput(part.output);
if (!output) return "Loaded agent outputs";
if (output.type === "agent_output") {
if (output.execution)
return `Loaded output (${output.execution.status})`;
return "Loaded agent outputs";
}
if (output.type === "no_results") return "No outputs found";
return "Error loading agent output";
}
case "output-error":
return "Error loading agent output";
default:
return "Processing";
}
}
export function StateIcon({ state }: { state: ToolUIPart["state"] }) {
switch (state) {
case "input-streaming":
case "input-available":
return (
<CircleNotchIcon
className="h-4 w-4 animate-spin text-muted-foreground"
weight="bold"
/>
);
case "output-available":
return <CheckCircleIcon className="h-4 w-4 text-green-500" />;
case "output-error":
return <XCircleIcon className="h-4 w-4 text-red-500" />;
default:
return null;
}
}
export function formatMaybeJson(value: unknown): string {
if (typeof value === "string") return value;
try {
return JSON.stringify(value, null, 2);
} catch {
return String(value);
}
}