import type React from 'react';
import { defineStory } from '@djangocfg/playground';
import { MarkdownMessage } from './MarkdownMessage';
import { ActionRow } from './ActionRow';
import { ChatMessageRow } from './ChatMessageRow';
import type { LinkRule } from './types';

export default defineStory({
  title: 'Components/Markdown Message',
  component: MarkdownMessage,
  description:
    'Chat markdown renderer. Stories cover the declarative `linkRules` API for handling custom URL schemes (e.g. cmdop://) without hand-writing a custom `a` renderer.',
});

// Canonical mention shape: `[label](cmdop://machine/<uuid>)`. The
// `@` prefix you sometimes see in composers (`@[label](href)`) is
// decorative; the chip itself reads as the mention indicator. The
// rule below strips the leading `@` in `preprocess` so the page
// doesn't show "@<chip>".
const CMDOP_CONTENT = `Talk to @[Vps-audi](cmdop://machine/abc-123) about deployment.

Inline reference to [Server-2](cmdop://machine/def-456) inside a sentence.

Plain links survive: [docs](https://example.com).
Local file: [file](cmdop://local?path=/tmp/x.md).`;

// Chip stand-in. Two variants — `subtle` for chips on neutral cards,
// `onPrimary` for chips inside a saturated `bg-primary` bubble.
function MentionChip({
  id,
  label,
  variant = 'subtle',
}: {
  id: string;
  label: string;
  variant?: 'subtle' | 'onPrimary';
}) {
  const onClick = () => {
    // eslint-disable-next-line no-console
    console.log('[story] MentionChip clicked', { id, label });
    if (typeof window !== 'undefined') alert(`Chip clicked: ${label} (${id})`);
  };
  const palette =
    variant === 'onPrimary'
      ? 'bg-primary-foreground/15 text-primary-foreground hover:bg-primary-foreground/25'
      : 'bg-primary/10 text-primary hover:bg-primary/20';
  return (
    <button
      type="button"
      onClick={onClick}
      className={`inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-xs font-sans font-medium align-baseline transition-colors ${palette}`}
    >
      <span
        className={
          variant === 'onPrimary'
            ? 'h-1.5 w-1.5 rounded-full bg-primary-foreground/70'
            : 'h-1.5 w-1.5 rounded-full bg-emerald-500'
        }
        aria-hidden
      />
      <span>{label}</span>
    </button>
  );
}

// Helper used by the chip rule: pull a clean string label from
// react-markdown's children (string, number, array, or React element).
function extractLabel(node: React.ReactNode): string {
  if (typeof node === 'string') return node;
  if (typeof node === 'number') return String(node);
  if (Array.isArray(node)) return node.map(extractLabel).join('');
  return '';
}

function makeRules(chipVariant: 'subtle' | 'onPrimary'): LinkRule[] {
  return [
    {
      name: 'cmdop-machine-mention',
      protocols: ['cmdop'],
      // Strip the decorative leading `@` from `@[label](cmdop://machine/<id>)`
      // so the chip alone reads as the mention indicator.
      preprocess: (s) =>
        s.replace(/(^|[^A-Za-z0-9_])@(\[[^\]]+\]\(cmdop:\/\/machine\/[^)\s]+\))/g, '$1$2'),
      match: (href) => href.startsWith('cmdop://machine/'),
      render: ({ href, children }) => {
        const id = href.slice('cmdop://machine/'.length).trim();
        const label = extractLabel(children) || id;
        return <MentionChip id={id} label={label} variant={chipVariant} />;
      },
    },
    {
      name: 'cmdop-local-file',
      protocols: ['cmdop'],
      match: (href) => href.startsWith('cmdop://local'),
      render: ({ href, children }) => (
        <a
          href={href}
          onClick={(e) => {
            e.preventDefault();
            // eslint-disable-next-line no-console
            console.log('[story] local file click', { href });
            if (typeof window !== 'undefined') alert(`Local link: ${href}`);
          }}
          className={
            chipVariant === 'onPrimary'
              ? 'text-primary-foreground/90 underline hover:text-primary-foreground'
              : 'text-primary underline hover:text-primary/80'
          }
        >
          {children}
        </a>
      ),
    },
  ];
}

const subtleRules = makeRules('subtle');
const onPrimaryRules = makeRules('onPrimary');

export const LinkRules = () => (
  <div className="mx-auto max-w-2xl space-y-6 p-6">
    <div>
      <h3 className="mb-2 text-sm font-semibold">
        Declarative rules — preferred API
      </h3>
      <p className="mb-3 text-xs text-muted-foreground">
        Each rule declares its protocol, an optional preprocess pass,
        a match predicate, and a render. <code>MarkdownMessage</code>
        composes them: per-rule sanitize whitelist, custom <code>a</code>
        renderer, and source rewrites all happen behind one prop.
      </p>
      <div className="rounded-lg border border-border bg-card p-4 text-card-foreground">
        <MarkdownMessage
          content={CMDOP_CONTENT}
          isUser={false}
          linkRules={subtleRules}
        />
      </div>
    </div>

    <div>
      <h3 className="mb-2 text-sm font-semibold">
        Inside a saturated bubble (<code>bg-primary</code>)
      </h3>
      <p className="mb-3 text-xs text-muted-foreground">
        Same content, rules carrying the on-primary palette. Theme
        tokens (<code>primary-foreground</code>) keep contrast in light
        and dark themes.
      </p>
      <div className="rounded-xl bg-primary p-4 text-primary-foreground">
        <MarkdownMessage
          content={CMDOP_CONTENT}
          isUser
          linkRules={onPrimaryRules}
        />
      </div>
    </div>

    <div>
      <h3 className="mb-2 text-sm font-semibold">
        Without rules (control)
      </h3>
      <p className="mb-3 text-xs text-muted-foreground">
        No rules → react-markdown's default urlTransform strips
        <code> cmdop://</code> hrefs before render. Plain text falls
        through; cmdop links degrade to label-only anchors.
      </p>
      <div className="rounded-lg border border-border bg-card p-4 text-card-foreground">
        <MarkdownMessage content={CMDOP_CONTENT} isUser={false} />
      </div>
    </div>
  </div>
);

// Chat-style stories — exercise the plain-text fast path so short
// one-liners and prose paragraphs don't get prose vertical margins
// inside a chat bubble. Compare against `LongAssistantReply` below
// to see when the heavier ReactMarkdown pipeline kicks in.
const SHORT_USER_LINES = [
  'Hi',
  'А?',
  'How do I install cmdop?',
  'Can you ssh into vps-audi and check uptime?',
];

const SHORT_ASSISTANT_LINES = [
  'Sure — what do you need?',
  'Готово, запросил статус.',
  'Done.',
];

// Crafted to make the plain-vs-markdown contrast obvious: contains
// `*pair*` (italic in markdown), a `#hash` token (lone hash with no
// space — NOT a heading even in markdown, kept here to show the
// asymmetry), and embedded newlines that should survive in plain mode.
const CONTRAST_CONTENT = `Multi
line
user message
with *asterisks*
and #hash that should NOT format`;

const LONG_ASSISTANT_REPLY = `## Установка CMDOP

Запусти этот скрипт:

\`\`\`bash
curl -sSL cmdop.com/install.sh | bash
\`\`\`

Дальше:

1. **Войти:** \`cmdop login\`
2. **Подключить машину:** \`cmdop connect\`
3. **Проверить:** \`cmdop doctor\`

Если что-то не работает — скажи.`;

// Mermaid payload — exercises the `mermaid` fence branch in
// components.tsx (lines 101–106). MarkdownMessage hands the code body
// to <Mermaid> instead of PrettyCode when the fence language is
// `mermaid`. We pick a small flowchart + a sequence diagram so the
// story reflects what an LLM typically emits.
const MERMAID_CONTENT = `Here's the chat session lifecycle:

\`\`\`mermaid
flowchart LR
  A[User types] --> B[ChatProvider]
  B --> C{streaming?}
  C -->|yes| D[CancelMessage]
  C -->|no| E[SendMessage]
  D --> E
  E --> F[Agent stream]
  F --> G[Tokens arrive]
  G --> H[Reducer dispatch]
  H --> I[Bubble re-renders]
\`\`\`

And the cancel flow as a sequence:

\`\`\`mermaid
sequenceDiagram
  participant U as User
  participant C as ChatInput
  participant P as ChatProvider
  participant S as ChatService
  U->>C: Press Enter (mid-stream)
  C->>P: sendMessage(text)
  P->>S: CancelMessage
  S-->>P: STREAM_CANCELLED
  P->>S: SendMessage(text)
  S-->>P: STREAM_START
\`\`\`

The renderer routes \`mermaid\` fences to the diagram component;
non-mermaid fences (above) keep going through PrettyCode.`;

// Kitchen-sink: every block element the renderer can produce, in one
// payload, so any spacing regression is visible at a glance. Pairs
// neighbouring elements that are easy to break — `p → hr → p`,
// `code → list`, `list → table → list`, blockquote-with-paragraphs.
const KITCHEN_SINK = `# Top-level heading

Short intro paragraph that sits right under the heading. The next
paragraph should have a clear gap from this one, not be glued to it.

Second paragraph — verifies \`p + p\` spacing.

---

After the divider, the next paragraph should still breathe. The
divider itself is a soft hairline, not a heavy 1px gray bar.

## Subheading h2

Plain *italic*, **bold**, and \`inline code\` mid-sentence. Inline
code should sit on the line, not push it taller.

### Subheading h3

A short list:

- Bullet one with a [link](https://example.com)
- Bullet two with \`inline code\`
- Bullet three — short

A numbered list:

1. First step
2. Second step with **bold** inside
3. Third step

A loose list (paragraph-wrapped \`<li>\`):

- First item

  with a follow-up paragraph that should NOT explode the list.

- Second item, also loose.

#### Heading h4

> Blockquote without italic, with a left border. Lighter colour for
> de-emphasis. Multi-line quotes wrap cleanly.
>
> Second paragraph inside the same blockquote.

A code fence:

\`\`\`go
func add(a, b int) int {
    // verifies code block spacing, rounded corners, dark bg
    return a + b
}
\`\`\`

A table — wraps inside a horizontally scrollable container:

| Field | Type | Notes |
|---|---|---|
| id | string | UUID |
| name | string | display name |
| online | bool | last-heartbeat ≤ 60s |

---

Closing paragraph after a second divider, to verify spacing holds at
the very end too.`;

function ChatBubble({
  role,
  content,
  rules,
  plainText,
  actions,
}: {
  role: 'user' | 'assistant';
  content: string;
  rules?: LinkRule[];
  plainText?: boolean;
  /** Show the copy-on-hover ActionRow under the bubble. */
  actions?: 'copy';
}) {
  const isUser = role === 'user';
  return (
    <ChatMessageRow
      isUser={isUser}
      actions={
        actions === 'copy'
          ? (visible) => <ActionRow value={content} isUser={isUser} visible={visible} />
          : undefined
      }
    >
      <div
        className={`max-w-[85%] rounded-xl px-3 py-2 ${
          isUser
            ? 'bg-primary text-primary-foreground'
            : 'bg-card text-card-foreground border border-border'
        }`}
      >
        <MarkdownMessage
          content={content}
          isUser={isUser}
          linkRules={rules}
          plainText={plainText}
        />
      </div>
    </ChatMessageRow>
  );
}

export const ChatBubbles = () => (
  <div className="mx-auto max-w-2xl space-y-6 p-6">
    <div>
      <h3 className="mb-2 text-sm font-semibold">
        User messages — explicit <code>plainText</code>
      </h3>
      <p className="mb-3 text-xs text-muted-foreground">
        ChatGPT/Claude.ai split: user-typed content stays as-typed.
        <code> *stars* </code> aren't bold; <code># headers </code>
        aren't headings. Newlines preserved via{' '}
        <code>whitespace-pre-wrap</code>.
      </p>
      <div className="rounded-lg border border-border bg-card p-4 space-y-2">
        {SHORT_USER_LINES.map((line, i) => (
          <ChatBubble
            key={`u-${i}`}
            role="user"
            content={line}
            plainText
            rules={onPrimaryRules}
          />
        ))}
        <ChatBubble
          role="user"
          content={'Multi\nline\nuser message\nwith *asterisks*\nand #hash that should NOT format'}
          plainText
          rules={onPrimaryRules}
        />
      </div>
    </div>

    <div>
      <h3 className="mb-2 text-sm font-semibold">
        Heuristic auto-detect (no <code>plainText</code> prop)
      </h3>
      <p className="mb-3 text-xs text-muted-foreground">
        Without <code>plainText</code>, <code>looksLikePlainProse</code>
        decides: short / single-paragraph / no markers → flat render;
        long / multi-paragraph / structural → full markdown. Same
        renderer for both branches; only margins differ.
      </p>
      <div className="rounded-lg border border-border bg-card p-4 space-y-2">
        {SHORT_ASSISTANT_LINES.map((line, i) => (
          <ChatBubble
            key={`a-${i}`}
            role="assistant"
            content={line}
            rules={subtleRules}
          />
        ))}
        <ChatBubble
          role="assistant"
          content="Sure — let me check uptime and disk on the machine and report back. Should only take a couple of seconds."
          rules={subtleRules}
        />
      </div>
    </div>

    <div>
      <h3 className="mb-2 text-sm font-semibold">
        Bubble with mention chips
      </h3>
      <p className="mb-3 text-xs text-muted-foreground">
        Content with <code>[label](cmdop://...)</code> always goes
        through ReactMarkdown — that's how MentionChip renders.
      </p>
      <div className="rounded-lg border border-border bg-card p-4 space-y-2">
        <ChatBubble
          role="user"
          content="Спроси у @[Vps-audi](cmdop://machine/abc-123) сколько свободного места."
          rules={onPrimaryRules}
        />
        <ChatBubble
          role="assistant"
          content="Готово — спросил [Vps-audi](cmdop://machine/abc-123)."
          rules={subtleRules}
        />
      </div>
    </div>

    <div>
      <h3 className="mb-2 text-sm font-semibold">
        Same content, two modes — side-by-side contrast
      </h3>
      <p className="mb-3 text-xs text-muted-foreground">
        Identical input string. Left bubble forces{' '}
        <code>plainText</code> (user-side default in cmdop):{' '}
        <code>*asterisks*</code> stay literal, <code>#hash</code> stays
        literal, newlines preserved verbatim. Right bubble omits{' '}
        <code>plainText</code> so the heuristic kicks in — short
        content but with <code>*pair*</code> markers, so it routes
        through ReactMarkdown and renders <em>italic</em>.
      </p>
      <div className="grid grid-cols-2 gap-3">
        <div className="rounded-lg border border-border bg-card p-4">
          <div className="mb-2 text-[10px] uppercase tracking-wide text-muted-foreground">
            plainText = true
          </div>
          <ChatBubble
            role="user"
            content={CONTRAST_CONTENT}
            plainText
            rules={onPrimaryRules}
          />
        </div>
        <div className="rounded-lg border border-border bg-card p-4">
          <div className="mb-2 text-[10px] uppercase tracking-wide text-muted-foreground">
            plainText omitted → heuristic
          </div>
          <ChatBubble
            role="assistant"
            content={CONTRAST_CONTENT}
            rules={subtleRules}
          />
        </div>
      </div>
    </div>

    <div>
      <h3 className="mb-2 text-sm font-semibold">
        Long assistant reply (multi-paragraph + code + headings)
      </h3>
      <p className="mb-3 text-xs text-muted-foreground">
        Full markdown pipeline. Tight CSS overrides
        (<code>p+p mt-2</code>, <code>p my-0</code>) keep the bubble
        from looking airy — defaults from Tailwind's prose are too
        loose for chat density.
      </p>
      <div className="rounded-lg border border-border bg-card p-4">
        <ChatBubble
          role="assistant"
          content={LONG_ASSISTANT_REPLY}
          rules={subtleRules}
        />
      </div>
    </div>
  </div>
);

// Kitchen-sink: stress-test every element the renderer can emit in a
// single bubble. If something regresses (e.g. paragraphs collapse, hr
// shows a heavy gray bar, table loses borders, list items glue to
// each other), it's visible at a glance.
//
// Numbers in MarkdownMessage.tsx come from research on ChatGPT /
// Claude.ai / Cursor 2025 — see KitchenSink output as the visual
// contract. Don't tune values by feel; pair-check against this view.
export const KitchenSink = () => (
  <div className="mx-auto max-w-2xl space-y-6 p-6">
    <div>
      <h3 className="mb-2 text-sm font-semibold">
        Every block element in one bubble
      </h3>
      <p className="mb-3 text-xs text-muted-foreground">
        Visual contract for the renderer. Heading sizes downscaled
        chat-style (h1 ≈ body), <code>p</code>/<code>ul</code>/
        <code>ol</code>/<code>pre</code>/<code>blockquote</code>/
        <code>hr</code>/<code>table</code> all share a coherent
        rhythm. Loose lists keep their density — no exploded gap on
        paragraph-wrapped <code>&lt;li&gt;</code>.
      </p>
      <div className="rounded-lg border border-border bg-card p-4">
        <ChatBubble
          role="assistant"
          content={KITCHEN_SINK}
          rules={subtleRules}
        />
      </div>
    </div>

    <div>
      <h3 className="mb-2 text-sm font-semibold">
        Same payload on the user-side palette
      </h3>
      <p className="mb-3 text-xs text-muted-foreground">
        Verifies that <code>prose-invert</code> doesn't break any of
        the new rules — saturated bubbles need the same spacing.
      </p>
      <div className="rounded-lg border border-border bg-card p-4">
        <ChatBubble
          role="user"
          content={KITCHEN_SINK}
          rules={onPrimaryRules}
        />
      </div>
    </div>
  </div>
);

// Soft line breaks — `remark-breaks` enabled. LLMs (Claude / GPT) and
// chat users routinely write "joke punchline / poem stanza / dialogue"
// blocks separated by a single `\n` rather than a blank line. CommonMark
// would collapse those into one run-on paragraph; the chat convention
// (ChatGPT, Slack, Discord, Linear) preserves them as `<br>`. This
// story is the regression case for that.
const SOFT_BREAK_JOKE = `Приходит программист к врачу:
— Доктор, я со всеми общаюсь только через мессенджер. Даже с женой.
— И что жена говорит?
— Подождите, сейчас напишу и спрошу… 😄`;

const SOFT_BREAK_POEM = `Roses are red,
Violets are blue,
This line is short,
And so are you.`;

const SOFT_BREAK_DIALOGUE = `Customer: Why is my code not working?
Support: Have you tried turning it off and on again?
Customer: I'm a software engineer.
Support: Have you tried turning it off and on again?`;

export const SoftLineBreaks = () => (
  <div className="mx-auto max-w-2xl space-y-6 p-6">
    <div>
      <h3 className="mb-2 text-sm font-semibold">Joke (assistant bubble)</h3>
      <p className="mb-3 text-xs text-muted-foreground">
        Single <code>\n</code> between lines should render as line
        breaks, not collapse onto one row. This was the user-reported
        regression — LLM punchlines surfaced as a wall of text.
      </p>
      <div className="rounded-lg border border-border bg-card p-4">
        <ChatBubble role="assistant" content={SOFT_BREAK_JOKE} rules={subtleRules} />
      </div>
    </div>

    <div>
      <h3 className="mb-2 text-sm font-semibold">Poem (user bubble)</h3>
      <p className="mb-3 text-xs text-muted-foreground">
        Same payload on the saturated user palette. Each line stays on
        its own row.
      </p>
      <div className="rounded-lg border border-border bg-card p-4">
        <ChatBubble role="user" content={SOFT_BREAK_POEM} rules={onPrimaryRules} />
      </div>
    </div>

    <div>
      <h3 className="mb-2 text-sm font-semibold">Dialogue (assistant bubble)</h3>
      <p className="mb-3 text-xs text-muted-foreground">
        Speaker turns separated by single newlines.
      </p>
      <div className="rounded-lg border border-border bg-card p-4">
        <ChatBubble role="assistant" content={SOFT_BREAK_DIALOGUE} rules={subtleRules} />
      </div>
    </div>
  </div>
);

// Plugin sampler — emoji shortcodes, smart typography, external link
// hardening. Verifies the three "lightweight chat" plugins added next
// to remark-breaks: remark-emoji, remark-smartypants, rehype-
// external-links. Each plugin contributes one transform; this story
// makes them visible in isolation.
const PLUGIN_SAMPLER = `### Plugin sampler

Emoji shortcodes work inline: :rocket: :tada: :+1: — alongside Unicode 🎉.

Smart typography: "double quotes", 'single quotes', em-dash --, ellipsis...

External link should open in a new tab: [Anthropic](https://anthropic.com).
Internal-style link stays in place: [#section](#section).

A line above —
soft break here — should still wrap (one \\n only).
And then a normal paragraph below.`;

export const ChatPlugins = () => (
  <div className="mx-auto max-w-2xl space-y-6 p-6">
    <div>
      <h3 className="mb-2 text-sm font-semibold">Assistant bubble</h3>
      <p className="mb-3 text-xs text-muted-foreground">
        Three plugins firing at once: <code>remark-emoji</code>,
        <code>remark-smartypants</code>, <code>rehype-external-links</code>.
        Hover the Anthropic link — it should carry <code>target=_blank</code>
        and <code>rel=noopener noreferrer</code> automatically.
      </p>
      <div className="rounded-lg border border-border bg-card p-4">
        <ChatBubble role="assistant" content={PLUGIN_SAMPLER} rules={subtleRules} />
      </div>
    </div>

    <div>
      <h3 className="mb-2 text-sm font-semibold">User bubble</h3>
      <p className="mb-3 text-xs text-muted-foreground">
        Same payload on the saturated palette.
      </p>
      <div className="rounded-lg border border-border bg-card p-4">
        <ChatBubble role="user" content={PLUGIN_SAMPLER} rules={onPrimaryRules} />
      </div>
    </div>
  </div>
);

// Mermaid diagrams. Live-renders the fence body via the <Mermaid>
// tool (lazy-loaded). Useful as a manual smoke test for: (1) the
// `language === 'mermaid'` branch in components.tsx, (2) the SVG
// scaling inside a chat-density bubble, (3) interaction with the
// surrounding markdown spacing rules (paragraph above/below the
// diagram should keep the same rhythm as around a code fence).
export const MermaidDiagrams = () => (
  <div className="mx-auto max-w-2xl space-y-6 p-6">
    <div>
      <h3 className="mb-2 text-sm font-semibold">
        Mermaid in an assistant bubble
      </h3>
      <p className="mb-3 text-xs text-muted-foreground">
        Two diagrams (flowchart + sequence) wrapped in surrounding
        prose. Verifies that <code>```mermaid</code> fences route to{' '}
        <code>&lt;Mermaid&gt;</code> and that the SVG fits the
        bubble's max-width without breaking the layout.
      </p>
      <div className="rounded-lg border border-border bg-card p-4">
        <ChatBubble
          role="assistant"
          content={MERMAID_CONTENT}
          rules={subtleRules}
        />
      </div>
    </div>

    <div>
      <h3 className="mb-2 text-sm font-semibold">
        Same payload on the user-side palette
      </h3>
      <p className="mb-3 text-xs text-muted-foreground">
        On a saturated <code>bg-primary</code> bubble the diagram
        keeps its own surface — Mermaid renders SVG with its own
        background, independent of the bubble palette.
      </p>
      <div className="rounded-lg border border-border bg-card p-4">
        <ChatBubble
          role="user"
          content={MERMAID_CONTENT}
          rules={onPrimaryRules}
        />
      </div>
    </div>
  </div>
);

// CopyActions — opt-in action row under the bubble. The `<ActionRow>`
// component lives outside the bubble (so it doesn't fight the
// saturated `bg-primary` palette on user messages); the `group` class
// on the parent flex container makes it appear on hover. On touch
// devices it stays at 50% opacity so the affordance is discoverable
// without hover.
export const CopyActions = () => (
  <div className="mx-auto max-w-2xl space-y-6 p-6">
    <div>
      <h3 className="mb-2 text-sm font-semibold">
        Hover any message to reveal the copy button
      </h3>
      <p className="mb-3 text-xs text-muted-foreground">
        <code>ActionRow</code> is exported separately from{' '}
        <code>MarkdownMessage</code> on purpose — placement is a
        layout concern of the bubble, not of the renderer. Wrap your
        bubble + <code>&lt;ActionRow&gt;</code> in a{' '}
        <code>group</code> div, and the row toggles via{' '}
        <code>group-hover</code>. On <code>@media (hover: none)</code>{' '}
        it stays at 50% opacity instead of hiding.
      </p>
      <div className="rounded-lg border border-border bg-card p-4 space-y-8">
        <ChatBubble
          role="user"
          content="How do I install cmdop?"
          plainText
          actions="copy"
        />
        <ChatBubble
          role="assistant"
          content={`Run this from your terminal:

\`\`\`bash
curl -sSL cmdop.com/install.sh | bash
\`\`\`

It auto-detects your shell and adds \`cmdop\` to PATH.`}
          actions="copy"
        />
        <ChatBubble
          role="user"
          content={'Multi\nline\nuser message\nto verify copy preserves all newlines'}
          plainText
          actions="copy"
        />
      </div>
    </div>

    <div>
      <h3 className="mb-2 text-sm font-semibold">
        Without <code>actions</code> — no extra row, no overhead
      </h3>
      <p className="mb-3 text-xs text-muted-foreground">
        Default behaviour. Existing consumers see no visual change.
      </p>
      <div className="rounded-lg border border-border bg-card p-4 space-y-3">
        <ChatBubble role="user" content="Plain user bubble." plainText />
        <ChatBubble role="assistant" content="Plain assistant bubble." />
      </div>
    </div>
  </div>
);
