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 (