import { useState } from "react"; import type { ApprovalDecision, Item } from "../types"; import { shortArgs } from "./ApprovalCard"; import { humanizeAsk, humanizeTool, type HumanLine } from "../humanize"; import { Markdown } from "./Markdown"; import { ConnectorMessageCard } from "./ConnectorMessageCard"; import { Icon } from "./Icon"; // Long user pastes swallow the transcript (owner ask 2026-07-30): clamp past a generous // threshold with a more…/less… toggle. Normal typed messages never see the control; the // full text still drives copy (BubbleMeta) and is what the model received. const USER_CLAMP_CHARS = 1200; function ClampedUserText({ text }: { text: string }) { const [open, setOpen] = useState(false); if (text.length <= USER_CLAMP_CHARS) return <>{text}>; return ( <> {open ? text : text.slice(0, USER_CLAMP_CHARS).trimEnd() + "…"} > ); } // Hover affordances for a message bubble (FB-005): copy the raw text + the message's time. // Lives in a ZERO-HEIGHT strip under the bubble (absolute, inside the transcript's 20px gap) // so revealing it on group-hover never shifts the layout. `ts` is unix seconds — canonical // messages carry it, pre-stamp history doesn't, so the time simply omits itself when absent. function BubbleMeta({ text, ts, align }: { text: string; ts?: number; align: "left" | "right" }) { const [copied, setCopied] = useState(false); const when = typeof ts === "number" ? new Date(ts * 1000) : null; const copy = () => { // "Copied" only after the write actually lands — WebKit can reject outside a // trusted gesture, and claiming success on a silent no-op would gaslight the user. navigator.clipboard ?.writeText(text) .then(() => { setCopied(true); window.setTimeout(() => setCopied(false), 1200); }) .catch(() => {}); }; return (
{`${tool.name} ${shortArgs(tool.args)}`}
{tool.preview ? `\n→ ${tool.preview.length > 1500 ? tool.preview.slice(0, 1500) + "\n…" : tool.preview}` : ""}
)}
{tool.status === "denied" && tool.reviewerReason && (