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 (
{lang ?? "unknown"} {streaming ? ( still streaming, so no controls yet ) : (
{onRun && ( )} {apply && ( )}
)}
{/* Two representations. Numbers help discussion and hurt copying, so they sit outside the selection and outside the accessibility tree. */}
        
        
          {code}
          {streaming && 
      
); } ──────────────────────────────────────────────────────────────────────── // demo.tsx ──────────────────────────────────────────────────────────────────────── import { useEffect, useState } from "react"; import { CodeBlock } from "./CodeBlock"; const CLOSED = 'for f in dist/*.js; do\n gzip -k "$f"\ndone'; const OPEN_HEAD = "npm run build && npm run"; const OPEN_TAIL = " deploy -- --prod"; /** A closed block with its controls, and one still arriving without them. */ export default function Demo() { const [tail, setTail] = useState(OPEN_HEAD); const [streaming, setStreaming] = useState(true); const [applied, setApplied] = useState(null); useEffect(() => { const words = OPEN_TAIL.split(/(?<=\s)/); let i = 0; const id = setInterval(() => { if (i >= words.length) { setStreaming(false); clearInterval(id); return; } setTail((t) => t + words[i++]); }, 700); return () => clearInterval(id); }, []); return (
{}} apply={{ target: "deploy.sh", onApply: (code) => setApplied(code) }} /> {applied && (

apply handed {applied.split("\n").length} lines to the diff view for deploy.sh

)}
); }