import type { ReactNode } from "react"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { SystemNotice } from "@/components/SystemNotice"; import { systemNoticeFixtures } from "@/fixtures/systemNoticeFixtures"; import { cn } from "@/lib/utils"; import { CircleDashed, FlaskConical, Layers, ListChecks, Sparkles, } from "lucide-react"; function LabSection({ id, eyebrow, title, description, accentClassName, children, }: { id?: string; eyebrow: string; title: string; description: string; accentClassName?: string; children: ReactNode; }) { return (
{eyebrow}

{title}

{description}

{children}
); } function FixtureFrame({ caption, children }: { caption: string; children: ReactNode }) { return (
{caption}
{children}
); } function MockUserBubble({ authorName, body, alignEnd, }: { authorName: string; body: string; alignEnd?: boolean; }) { return (
{!alignEnd ? ( {authorName.slice(0, 2).toUpperCase()} ) : null}
{authorName}
{body}
{alignEnd ? ( {authorName.slice(0, 2).toUpperCase()} ) : null}
); } function MockAgentBubble({ agentName, body }: { agentName: string; body: string }) { return (
{agentName.slice(0, 2).toUpperCase()}
{agentName}
{body}
); } const checklist = [ "One container per system notice — no nested chat bubble", "Tone communicated by icon + label, never color alone", "Operational evidence hidden behind Details, expanded only on demand", "Issue, agent, and run metadata render as typed link rows, not raw markdown", "Hierarchy visibly distinct from user (right-aligned) and agent (left-aligned) bubbles", ]; export function SystemNoticeUxLab() { const fixtureById = new Map(systemNoticeFixtures.map((f) => [f.id, f] as const)); const warningCollapsed = fixtureById.get("warning-collapsed")!; const warningExpanded = fixtureById.get("warning-expanded")!; const dangerCollapsed = fixtureById.get("danger-collapsed")!; const dangerExpanded = fixtureById.get("danger-expanded")!; const neutralCollapsed = fixtureById.get("neutral-collapsed")!; const neutralExpanded = fixtureById.get("neutral-expanded")!; const warningNoDetails = fixtureById.get("warning-no-details")!; return (
System Notice Lab

First-class system notice treatment

Replaces the current pattern where a Paperclip-authored warning renders inside a user-style chat bubble. The notice is one container, system-styled, with hidden-by-default operational metadata. Tone is conveyed by icon, label, and color together so it stays accessible.

PAP-3525 plan phase 1 — UX tones: warning · danger · neutral
YO
You

Successful run handoff missing

  • Source issue: PAP-3440
  • Source run: 9cdba892-c7ca-4d93-8604-4843873b127c
  • Recovery run: 61fdb79b-8012-4676-ac71-2971830e126a
  • Status before: in_progress
  • Normalized cause: Run completed without disposition
  • Recovery owner: CTO
  • Suggested action: Reassign to recovery agent

Author reads as You even though the author is the Paperclip system. Two containers stack the warning inside a user-style bubble, and operational evidence is always visible.

Same content. The visible body is one short system sentence; reviewers expand{" "} Details only when they need run evidence. Tone is reinforced by the octagon icon and the "System alert" label, not just red.

Implementation notes
Handoff to engineering What the Phase 4 UI implementation should preserve from this design.
Component
Use {``}{" "} from @/components/SystemNotice. It accepts tone,{" "} label,{" "} body,{" "} metadata, and{" "} detailsDefaultOpen.
Routing in IssueChatThread
Comments where{" "} authorType === "system"{" "} or{" "} presentation.kind === "system_notice"{" "} should render as a SystemNotice row at full content width — never inside an{" "} IssueChatUserMessage{" "} or assistant bubble.
Accessibility
The Details button has{" "} aria-expanded{" "} and{" "} aria-controls{" "} wired to the panel id. The container exposes{" "} role="status"{" "} and an{" "} aria-label{" "} equal to the visible tone label so screen readers announce tone with text.
Legacy fallback
Existing comments without{" "} presentation{" "} keep rendering through the current{" "} SuccessfulRunHandoffCommentCallout{" "} string-detector. The new contract is opt-in for the system generators in Phase 5.
); } export default SystemNoticeUxLab;