Code block actions What the clipboard receives is not what the page renders. Line numbers and the syntax wrapper stay behind; the indent comes with it. A block still streaming has no controls, because applying half a file is worse than waiting. npx shadcn@latest add button npm i lucide-react Tokens this needs: --foreground, --card, --accent, --accent-foreground, --muted, --muted-foreground, --border ──────────────────────────────────────────────────────────────────────── // CodeBlock.tsx ──────────────────────────────────────────────────────────────────────── import { useState } from "react"; import { Check, Copy, Play, FileDiff } from "lucide-react"; import { Button } from "@/components/ui/button"; type Props = { /** The source exactly as the model wrote it. This is what copy sends. */ code: string; lang?: string; /** True until the closing fence arrives. Controls on an unclosed block * hand over half a function as though it were whole. */ streaming?: boolean; onRun?: (code: string) => void; /** Applying writes into the viewer's work, so it names the file and the * caller shows a diff, confirms, and keeps an undo. */ apply?: { target: string; onApply: (code: string) => void }; }; export function CodeBlock({ code, lang, streaming = false, onRun, apply }: Props) { const [copied, setCopied] = useState(false); const lines = code.split("\n"); // The clipboard gets `code`, not the DOM: the indent comes with it, the // line numbers and the syntax wrapper stay behind. const copy = async () => { await navigator.clipboard.writeText(code); setCopied(true); setTimeout(() => setCopied(false), 1500); }; return (
{code}
{streaming && }
apply handed {applied.split("\n").length} lines to the diff view for deploy.sh
)}