import { useState, type ReactNode } from "react"; import * as Collapsible from "@radix-ui/react-collapsible"; import { Loader2, ChevronRight } from "lucide-react"; import { cn } from "../lib/utils"; import { formatDuration } from "../utils/format"; import { LiveDuration } from "./run-item-primitives"; export type RunRowStatus = "running" | "success" | "error" | "idle"; /** * Trailing status indicator shared by every run row: a spinner while running, * a green/red dot on terminal states, nothing when idle. Status is shown here * — it never replaces the row's semantic lead icon. */ export function RunRowStatusDot({ status }: { status: RunRowStatus }) { if (status === "running") { return ; } if (status === "success") { return ( ); } if (status === "error") { return ( ); } return null; } export interface RunRowShellProps { /** Semantic lead glyph — always visible; never replaced by status. */ icon: ReactNode; /** Row title. A string in the common case; a node when the title carries its * own treatment (e.g. an active-state shimmer sweep) — it renders inline in * the shell's title slot either way. */ title: ReactNode; /** Secondary inline text (tool path/command, or a reasoning preview). */ description?: string; /** Render the description in mono (tools) vs prose (reasoning). */ descriptionMono?: boolean; status?: RunRowStatus; /** Live-ticks a duration while running. */ startTime?: number; /** Static duration shown once the row is no longer running. */ durationMs?: number; /** Radius shaping when rows are stacked into a group. */ groupPosition?: "single" | "first" | "middle" | "last"; /** Message shown below the header while collapsed (tool errors). */ collapsedError?: string; actions?: ReactNode; /** Controlled open state. Omit for internal (uncontrolled) state. */ open?: boolean; onOpenChange?: (open: boolean) => void; defaultOpen?: boolean; className?: string; contentClassName?: string; /** Expanded body. When absent the row is not expandable. */ children?: ReactNode; } const SHAPE_CLASS: Record< NonNullable, string > = { single: "rounded-[var(--radius-lg)]", first: "rounded-t-[var(--radius-lg)] rounded-b-[var(--radius-sm)]", middle: "rounded-[var(--radius-sm)]", last: "rounded-t-[var(--radius-sm)] rounded-b-[var(--radius-lg)]", }; /** * Shared shell for the agent activity rows (reasoning + tool). Owns the * bordered container, the lead icon badge, title/description, the trailing * meta cluster (duration + status + chevron) and the collapsible body, so * both row kinds read as one family instead of two bespoke layouts. */ export function RunRowShell({ icon, title, description, descriptionMono = false, status = "idle", startTime, durationMs, groupPosition = "single", collapsedError, actions, open: openProp, onOpenChange, defaultOpen = false, className, contentClassName, children, }: RunRowShellProps) { const [openState, setOpenState] = useState(defaultOpen); const isControlled = openProp !== undefined; const open = isControlled ? openProp : openState; const setOpen = (next: boolean) => { if (!isControlled) setOpenState(next); onOpenChange?.(next); }; const isRunning = status === "running"; const expandable = children != null; return (
{collapsedError && !open ? (
{collapsedError}
) : null} {expandable ? (
{children}
) : null}
{actions ? (
event.stopPropagation()} > {actions}
) : null}
); }