"use client";
import { ChevronRight } from "lucide-react";
import { motion, useReducedMotion } from "motion/react";
import { type ReactNode, useLayoutEffect, useRef, useState } from "react";
import { EASE_OUT } from "@/lib/ease";
import { cn } from "@/lib/utils";
import { ThreadShimmerText } from "./cards";
import { ThreadStreamingCaret } from "./status";
export {
ThreadCard,
ThreadCardButton,
ThreadCommandRow,
ThreadDiffCard,
ThreadDiffRow,
ThreadFileCard,
ThreadShimmerText,
} from "./cards";
export {
ThreadApprovalCard,
ThreadBranchSwitcher,
ThreadCheckpoint,
ThreadElicitation,
ThreadErrorState,
ThreadScrollPill,
ThreadStreamingCaret,
ThreadSuggestions,
ThreadSystemBanner,
ThreadTask,
ThreadTaskList,
ThreadThinking,
ThreadToolCall,
ThreadUsage,
} from "./status";
export interface ThreadProps {
className?: string;
children?: ReactNode;
}
/**
* Conversation column — centers the message stream at a fixed reading
* width. Renders `children` directly; spacing between items (user messages,
* turns) comes from each item's own margins rather than a gap here, so a
* lone item still looks right regardless of what precedes it.
*/
export function Thread({ className, children }: ThreadProps) {
return (
{children}
);
}
export interface ThreadItemProps {
className?: string;
children?: ReactNode;
}
/**
* Entrance wrapper for a stream item (a user message or an agent turn) as it
* streams in — a short opacity + upward slide, reduced to an opacity-only
* fade under `useReducedMotion()`. Purely presentational: callers decide
* whether to wrap a given item at all (e.g. history rendered on first paint
* may skip it to avoid replaying the entrance).
*/
export function ThreadItem({ className, children }: ThreadItemProps) {
const reduce = useReducedMotion() ?? false;
return (
{children}
);
}
export interface ThreadUserMessageProps {
className?: string;
children?: ReactNode;
}
/** Right-aligned user message — a rounded gray pill capped at 77% of the column width. */
export function ThreadUserMessage({ className, children }: ThreadUserMessageProps) {
return (
{children}
);
}
export interface ThreadTurnHeaderProps {
/** Controlled open state — purely a chevron-rotation signal (see below). */
open?: boolean;
onOpenChange?: (next: boolean) => void;
/** While the turn is still executing: shimmers the label. The header stays fully interactive — a running turn's live work log can be expanded and collapsed too. */
working?: boolean;
className?: string;
children?: ReactNode;
}
/**
* Turn-header button, e.g. "Worked for 2m 4s ›". Renders only the header
* itself — label plus a chevron that rotates 90° to signal open/closed —
* and does not fold or animate any content region below it: pair it with
* `ThreadCollapse` (wired to the same `open`) to collapse the turn's work
* log, or render your own region. Works controlled (`open`/`onOpenChange`)
* or uncontrolled. `working` (the turn is still executing, e.g. a live
* elapsed-seconds label) only swaps the label into `ThreadShimmerText`;
* chevron and toggling stay live, since a running turn's work log can be
* inspected mid-flight.
*/
export function ThreadTurnHeader({
open: openProp,
onOpenChange,
working = false,
className,
children,
}: ThreadTurnHeaderProps) {
const reduce = useReducedMotion() ?? false;
const [openState, setOpenState] = useState(false);
const open = openProp ?? openState;
const toggle = () => {
const next = !open;
setOpenState(next);
onOpenChange?.(next);
};
return (
);
}
export interface ThreadCollapseProps {
open: boolean;
className?: string;
children?: ReactNode;
}
/**
* Measured-height collapse region — the pairing for `ThreadTurnHeader`: put
* the turn's work log (thinking row, tool calls, interim notes) inside and
* wire `open` to the header's state so clicking the header collapses and
* expands it. Height comes from a `ResizeObserver`, so content that changes
* while open (e.g. rows streaming in) is tracked; animated 0 ↔ measured
* with `EASE_OUT` (0.25s), switched instantly under `useReducedMotion()`.
*/
export function ThreadCollapse({ open, className, children }: ThreadCollapseProps) {
const reduce = useReducedMotion() ?? false;
const contentRef = useRef(null);
const [contentHeight, setContentHeight] = useState(0);
useLayoutEffect(() => {
const node = contentRef.current;
if (!node) return;
const update = () => setContentHeight(node.offsetHeight);
update();
const observer = new ResizeObserver(update);
observer.observe(node);
return () => observer.disconnect();
}, []);
return (
{children}
);
}
export interface ThreadMessageProps {
/** Appends a blinking `ThreadStreamingCaret` after `children` while the response is still streaming in. */
streaming?: boolean;
className?: string;
children?: ReactNode;
}
/**
* Typography container for agent-authored prose. A pure styling shell — the
* Markdown → JSX rendering engine is left to the caller (this component
* doesn't depend on any particular one); descendant selectors give
* paragraphs, lists and inline marks consistent spacing regardless of which
* renderer produced them. `streaming` appends a `ThreadStreamingCaret` at
* the container tail — after a block-level last paragraph that lands on its
* own line; to embed the caret inside the last line of text, place
* `ThreadStreamingCaret` directly in your own JSX instead.
*/
export function ThreadMessage({ streaming = false, className, children }: ThreadMessageProps) {
return (
{children}
{streaming ? : null}
);
}
export interface ThreadInlineCodeProps {
className?: string;
children?: ReactNode;
}
/** Inline code chip for use inside `ThreadMessage` prose. */
export function ThreadInlineCode({ className, children }: ThreadInlineCodeProps) {
return (
{children}
);
}
export interface ThreadCodeBlockProps {
/** Small label in the top-right corner, e.g. a language name like "bash". */
label?: ReactNode;
className?: string;
children?: ReactNode;
}
/** Fenced code block for `ThreadMessage` prose — a `pre > code` structure with an optional corner label. */
export function ThreadCodeBlock({ label, className, children }: ThreadCodeBlockProps) {
return (
{label ? (
{label}
) : null}
{children}
);
}
export interface ThreadActionBarProps {
/** Rendered after the buttons, e.g. a relative send time. */
timestamp?: ReactNode;
className?: string;
children?: ReactNode;
}
/**
* Turn-tail action row — hidden until the turn is hovered or a child gains
* focus. Pair it with a parent that carries the `group/turn` class (see the
* preview) so `group-hover/turn:opacity-100` has something to key off; a
* bare hover on the bar itself would only reveal it once the pointer is
* already over these 20px-tall icons.
*/
export function ThreadActionBar({ timestamp, className, children }: ThreadActionBarProps) {
return (