Refusal Cannot, will not, and does not know are three different sentences with three different offers. Length is the tell for the moralising version, and people who hit a wrong refusal stop asking, so it never shows up in the logs. npx shadcn@latest add button Tokens this needs: --card, --card-foreground, --muted-foreground, --border, --primary ──────────────────────────────────────────────────────────────────────── // Refusal.tsx ──────────────────────────────────────────────────────────────────────── import { Button } from "@/components/ui/button"; /** * Three declines with three causes, and the cause is the first thing the * viewer needs. A capability limit, a policy decision and a knowledge gap * call for different next moves, so the set is closed and the label is * derived from it rather than typed each time. */ export type RefusalKind = "cannot" | "will-not" | "does-not-know"; const KIND_LABEL: Record = { "cannot": "Cannot", "will-not": "Will not", "does-not-know": "Does not know", }; /** The nearest thing the assistant can do instead. Paste it in, search for it. */ export type Offer = { label: string; onSelect: () => void }; /** * Length is the tell. A decline that runs past a sentence or two is almost * always a defensive one, so anything longer than this is flagged in * development rather than shipped as a paragraph. */ const MAX_REASON = 120; type Props = { /** One plain sentence. No apology, no restating the request as sinister. */ reason: string; /** Wrong refusals never show up in the logs, because people stop asking. * A report control a human reads is the only cheap way to see them. */ onReport: () => void; } & ( /** A capability or knowledge limit always has a nearest thing to offer. */ | { kind: "cannot" | "does-not-know"; offer: Offer } /** A policy decline may have nothing to offer, and says so in one line. */ | { kind: "will-not"; offer?: Offer } ); export function Refusal({ kind, reason, offer, onReport }: Props) { if (reason.length > MAX_REASON) { console.warn(`Refusal: "${reason.slice(0, 40)}…" is ${reason.length} characters. Cut it to one sentence.`); } return (

{KIND_LABEL[kind]}

{reason} {offer && ( <> {" "} )}

); } ──────────────────────────────────────────────────────────────────────── // demo.tsx ──────────────────────────────────────────────────────────────────────── import { Refusal } from "./Refusal"; /** * The closed set, one of each. Cannot and does-not-know carry an offer; * the policy decline is one line and stops there. Every one has the report * control, because the wrong ones are the ones nobody measures. */ export default function Demo() { const report = (kind: string) => () => console.log("reported a wrong refusal:", kind); return (
{} }} onReport={report("cannot")} /> {} }} onReport={report("does-not-know")} />
); }