'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}