'use client'; /** * Shared tool-call card for the AgentBar, in the AgentSidebar design board's * `.ae-tool` language: a bordered row with a leading glyph, truncating title, an * optional `@scene` pill, an optional inline bar-action (always visible on the * row — e.g. a Restore button), an icon-only status mark (running = violet * spinner, done = emerald check ✓, failed = amber cross ✗; the text label is a * hover tooltip). Tool cards are intentionally NOT expandable — they show a * single status row with no detail disclosure. Every tool card (regenerate / * read / future) renders through this shell so they stay visually uniform. */ import type { ReactNode } from 'react'; import { AtSign, Check, CircleStop, Loader2, X, type LucideIcon } from 'lucide-react'; import { cn } from '@/lib/utils/cn'; import { useStageStore } from '@/lib/store/stage'; export type ToolStatus = 'running' | 'done' | 'failed' | 'stopped'; /** * True when a tool result is the synthetic "stopped" marker the client runtime * writes for tool calls that never produced a result because the user cancelled * the turn (see use-agent-runtime). Shared so every tool UI reports a stopped * card the same way. */ export function isStoppedResult(result: unknown): boolean { return ( typeof result === 'object' && result !== null && (result as { __stopped?: boolean }).__stopped === true ); } /** * The page (scene) a tool acted on, shown as an `@title` chip — the chat is a * single global thread (Cursor-style), so each tool card carries its own scene * reference instead of splitting history per page. */ export function ScenePill({ sceneId }: { sceneId?: string }) { const title = useStageStore((s) => sceneId ? (s.scenes.find((x) => x.id === sceneId)?.title ?? null) : null, ); if (!title) return null; return ( {title} ); } const STATUS_ICON: Record = { running: Loader2, done: Check, failed: X, stopped: CircleStop, }; const STATUS_TONE: Record = { running: 'text-[#5b1fa8] dark:text-violet-300', done: 'text-emerald-600 dark:text-emerald-400', failed: 'text-amber-600 dark:text-amber-400', // Stopped: a deliberately loud rose stop sign so an interrupted run reads as // "you stopped this", clearly distinct from a green done or amber failure. stopped: 'text-rose-600 dark:text-rose-400', }; export function ToolCard({ title, icon: Icon, sceneId, status, statusLabel, barAction, }: { title: string; icon: LucideIcon; sceneId?: string; status: ToolStatus; /** Shown as a hover tooltip on the status mark (the mark itself is icon-only). */ statusLabel: string; /** Inline action rendered on the always-visible row (e.g. Restore). */ barAction?: ReactNode; /** Accepted for source compatibility but IGNORED — cards are non-expandable. */ children?: ReactNode; }) { const running = status === 'running'; const StatusIcon = STATUS_ICON[status]; // Non-expandable by design: a single status row, no detail disclosure. Failure // surfaces only through the status mark (icon + tooltip) — never an inline body. return (
{/* Title takes the free space and truncates; the @scene pill rides in the right cluster next to the status mark, so pills stay right-aligned across cards regardless of how long each tool name is. */} {title} {barAction ? {barAction} : null}
); }