# Claude Code

> A Claude Code terminal session with the compact header, tool calls, a running turn and the prompt, built from the Claude Code components on shadcn Base UI.

A shadcn/ui block from the [@notra](https://ui.usenotra.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/notra/claude-code).

- Page, with a live preview: https://shadcn.exchange/r/notra/claude-code
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/notra/claude-code-light-03799c439e3dfcde.png
- Registry item: https://ui.usenotra.com/r/claude-code.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://ui.usenotra.com/r/claude-code.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `cn`
- Registry items: `button`, `card`, `collapsible`, `input-group`, `kbd`, `scroll-area`

## Files

### `registry/notra/claude-code/components/claude-code-terminal.tsx`

```tsx
import { cn } from "cn";

import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { ScrollArea } from "@/components/ui/scroll-area";

import type { ClaudeCodeTerminalProps } from "../types/claude-code";

export const ClaudeCodeTerminal = ({
  children,
  className,
  contentClassName,
  footer,
  title,
  ...props
}: ClaudeCodeTerminalProps) => (
  <Card
    className={cn(
      "bg-claude-code-bg font-claude-code text-claude-code-fg ring-claude-code-ring w-full min-w-0 gap-0 overflow-clip rounded-xl py-0 shadow-[0_1.5rem_3.5rem_-1rem_var(--claude-code-shadow)] ring-1",
      className
    )}
    data-slot="claude-code-terminal"
    {...props}
  >
    <CardHeader className="bg-claude-code-chrome relative flex h-8 shrink-0 items-center justify-center rounded-none px-3 py-0">
      <div
        aria-hidden="true"
        className="absolute inset-y-0 left-3 flex items-center gap-2"
      >
        <span className="bg-claude-code-traffic size-3 shrink-0 rounded-full" />
        <span className="bg-claude-code-traffic size-3 shrink-0 rounded-full" />
        <span className="bg-claude-code-traffic size-3 shrink-0 rounded-full" />
      </div>
      {title && (
        <CardTitle className="font-claude-code text-claude-code-title max-w-[60%] min-w-0 truncate text-xs leading-4 font-normal">
          {title}
        </CardTitle>
      )}
    </CardHeader>
    <ScrollArea className="min-h-0 flex-1">
      <CardContent
        className={cn(
          "flex flex-col gap-5 px-3 py-4 text-[0.8125rem] leading-5 sm:px-4",
          contentClassName
        )}
        data-slot="claude-code-terminal-content"
      >
        {children}
      </CardContent>
    </ScrollArea>
    {footer && (
      <div className="shrink-0 px-3 pb-3 text-[0.8125rem] leading-5 sm:px-4">
        {footer}
      </div>
    )}
  </Card>
);
```

### `registry/notra/claude-code/components/claude-code-header.tsx`

```tsx
import { cn } from "cn";

import { CLAUDE_CODE_RESULT_GLYPH } from "../constants/claude-code";
import { renderClaudeCodeInline } from "../lib/claude-code-inline";
import type { ClaudeCodeHeaderProps } from "../types/claude-code";
import { ClaudeCodeLogo } from "./claude-code-logo";

export const ClaudeCodeHeader = ({
  className,
  cwd = "~/",
  model,
  org,
  tips = [],
  version = "v2.1.0",
  whatsNew = [],
  ...props
}: ClaudeCodeHeaderProps) => {
  const plan = [model, org].filter(Boolean).join(" · ");
  const hasNotices = whatsNew.length > 0 || tips.length > 0;

  return (
    <div
      className={cn(
        "font-claude-code text-claude-code-fg flex min-w-0 flex-col gap-5 text-[0.8125rem] leading-5",
        className
      )}
      data-slot="claude-code-header"
      {...props}
    >
      <div className="flex min-w-0 items-center gap-[2ch]">
        <ClaudeCodeLogo className="text-claude-code-accent shrink-0" />
        <div className="flex min-w-0 flex-col">
          <p className="truncate">
            <span className="text-claude-code-strong font-bold">
              Claude Code
            </span>{" "}
            <span className="text-claude-code-muted">{version}</span>
          </p>
          {plan && <p className="text-claude-code-muted truncate">{plan}</p>}
          {cwd && <p className="text-claude-code-muted truncate">{cwd}</p>}
        </div>
      </div>
      {hasNotices && (
        <div className="flex min-w-0 flex-col pl-[2ch]">
          {whatsNew.map((line) => (
            <p key={line}>{renderClaudeCodeInline(line)}</p>
          ))}
          {tips.map((line) => (
            <p className="text-claude-code-muted flex gap-[2ch]" key={line}>
              <span aria-hidden="true" className="shrink-0">
                {CLAUDE_CODE_RESULT_GLYPH}
              </span>
              <span className="min-w-0">{renderClaudeCodeInline(line)}</span>
            </p>
          ))}
        </div>
      )}
    </div>
  );
};
```

### `registry/notra/claude-code/components/claude-code-logo.tsx`

```tsx
import { useId } from "react";

import {
  CLAUDE_CODE_MASCOT_BODY,
  CLAUDE_CODE_MASCOT_EYES,
  CLAUDE_CODE_MASCOT_SIZE,
} from "../constants/claude-code";
import type { ClaudeCodeLogoProps } from "../types/claude-code";

const { height, width } = CLAUDE_CODE_MASCOT_SIZE;

export const ClaudeCodeLogo = ({
  size = 76,
  ...props
}: ClaudeCodeLogoProps) => {
  const maskId = useId();

  return (
    <svg
      aria-hidden="true"
      fill="currentColor"
      height={(size * height) / width}
      shapeRendering="crispEdges"
      viewBox={`0 0 ${width} ${height}`}
      width={size}
      xmlns="http://www.w3.org/2000/svg"
      {...props}
    >
      <mask id={maskId}>
        {CLAUDE_CODE_MASCOT_BODY.map(([x, y, w, h]) => (
          <rect
            fill="white"
            height={h}
            key={`${x}-${y}`}
            width={w}
            x={x}
            y={y}
          />
        ))}
        {CLAUDE_CODE_MASCOT_EYES.map(([x, y, w, h]) => (
          <rect
            fill="black"
            height={h}
            key={`${x}-${y}`}
            width={w}
            x={x}
            y={y}
          />
        ))}
      </mask>
      <rect height={height} mask={`url(#${maskId})`} width={width} />
    </svg>
  );
};
```

### `registry/notra/claude-code/components/claude-code-message.tsx`

```tsx
import { cn } from "cn";

import { renderClaudeCodeChildren } from "../lib/claude-code-inline";
import type { ClaudeCodeMessageProps } from "../types/claude-code";

export const ClaudeCodeMessage = ({
  children,
  className,
  from = "assistant",
  ...props
}: ClaudeCodeMessageProps) => {
  const isUser = from === "user";

  return (
    <div
      className={cn(
        "font-claude-code grid min-w-0 grid-cols-[2ch_minmax(0,1fr)] text-[0.8125rem] leading-5",
        isUser
          ? "bg-claude-code-user-bg text-claude-code-strong"
          : "text-claude-code-fg",
        className
      )}
      data-from={from}
      data-slot="claude-code-message"
      {...props}
    >
      <span
        aria-hidden="true"
        className={cn(
          "select-none",
          isUser ? "text-claude-code-user-prompt" : "text-claude-code-strong"
        )}
      >
        {isUser ? "❯" : "●"}
      </span>
      <div className="min-w-0 break-words whitespace-pre-wrap">
        {renderClaudeCodeChildren(children)}
      </div>
    </div>
  );
};
```

### `registry/notra/claude-code/components/claude-code-thinking.tsx`

```tsx
"use client";

import { cn } from "cn";

import { Button } from "@/components/ui/button";
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from "@/components/ui/collapsible";

import type { ClaudeCodeThinkingProps } from "../types/claude-code";

export const ClaudeCodeThinking = ({
  children,
  className,
  label = "Thinking…",
  ...props
}: ClaudeCodeThinkingProps) => (
  <Collapsible
    className={cn(
      "group/thinking font-claude-code text-claude-code-muted flex min-w-0 flex-col text-[0.8125rem] leading-5",
      className
    )}
    data-slot="claude-code-thinking"
    {...props}
  >
    <CollapsibleTrigger
      render={
        <Button
          className="text-claude-code-muted hover:text-claude-code-muted aria-expanded:text-claude-code-muted focus-visible:ring-claude-code-muted/60 grid h-auto w-fit grid-cols-[2ch_auto] justify-start rounded-sm border-0 p-0 text-start text-[length:inherit] leading-[inherit] font-normal whitespace-normal italic hover:bg-transparent focus-visible:border-transparent focus-visible:ring-2 active:not-aria-[haspopup]:translate-y-0 aria-expanded:bg-transparent dark:hover:bg-transparent"
          variant="ghost"
        />
      }
    >
      <span aria-hidden="true" className="not-italic">
        ∴
      </span>
      <span>
        {label}
        <span className="not-italic group-data-[open]/thinking:hidden">
          {" "}
          (click to expand)
        </span>
      </span>
    </CollapsibleTrigger>
    <CollapsibleContent className="pl-[2ch] break-words whitespace-pre-wrap italic">
      {children}
    </CollapsibleContent>
  </Collapsible>
);
```

### `registry/notra/claude-code/components/claude-code-todo-list.tsx`

```tsx
import { cn } from "cn";

import {
  CLAUDE_CODE_RESULT_GLYPH,
  CLAUDE_CODE_TODOS,
} from "../constants/claude-code";
import type { ClaudeCodeTodoListProps } from "../types/claude-code";

export const ClaudeCodeTodoList = ({
  className,
  heading = "Update Todos",
  todos,
  ...props
}: ClaudeCodeTodoListProps) => (
  <div
    className={cn(
      "font-claude-code text-claude-code-fg flex min-w-0 flex-col text-[0.8125rem] leading-5",
      className
    )}
    data-slot="claude-code-todo-list"
    {...props}
  >
    {heading !== null && (
      <p className="grid grid-cols-[2ch_minmax(0,1fr)]">
        <span aria-hidden="true" className="text-claude-code-success">
          ●
        </span>
        <span className="text-claude-code-strong font-bold">{heading}</span>
      </p>
    )}
    <ul className="flex min-w-0 flex-col pl-[2ch]">
      {todos.map((todo, index) => {
        const config = CLAUDE_CODE_TODOS[todo.status];

        return (
          <li
            className="grid grid-cols-[3ch_2ch_minmax(0,1fr)]"
            data-status={todo.status}
            key={todo.label}
          >
            <span aria-hidden="true" className="text-claude-code-muted">
              {index === 0 && heading !== null ? CLAUDE_CODE_RESULT_GLYPH : ""}
            </span>
            <span
              aria-hidden="true"
              className={cn(
                "text-center",
                todo.status === "done"
                  ? "text-claude-code-muted"
                  : "text-claude-code-fg"
              )}
            >
              {config.glyph}
            </span>
            <span className={cn("min-w-0 pl-[1ch]", config.labelClassName)}>
              <span className="sr-only">{config.srLabel}: </span>
              {todo.label}
            </span>
          </li>
        );
      })}
    </ul>
  </div>
);
```

### `registry/notra/claude-code/components/claude-code-tool-call.tsx`

```tsx
"use client";

import { cn } from "cn";

import { Button } from "@/components/ui/button";
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from "@/components/ui/collapsible";

import {
  CLAUDE_CODE_RESULT_GLYPH,
  CLAUDE_CODE_TOOL_CALLS,
} from "../constants/claude-code";
import type {
  ClaudeCodeToolCallProps,
  ClaudeCodeToolSummaryProps,
} from "../types/claude-code";

const ROW = "grid min-w-0 grid-cols-[2ch_minmax(0,1fr)]";
const RESULT_ROW = "grid min-w-0 grid-cols-[3ch_minmax(0,1fr)] pl-[2ch]";

/** Strips the shadcn Button chrome so a trigger reads as terminal text. */
const TRIGGER_CLASS =
  "text-claude-code-muted hover:text-claude-code-fg aria-expanded:text-claude-code-muted focus-visible:ring-claude-code-muted/60 h-auto justify-start rounded-sm border-0 p-0 text-start text-[length:inherit] leading-[inherit] font-normal whitespace-normal hover:bg-transparent focus-visible:border-transparent focus-visible:ring-2 active:not-aria-[haspopup]:translate-y-0 aria-expanded:bg-transparent dark:hover:bg-transparent";

export const ClaudeCodeToolCall = ({
  arg,
  children,
  className,
  result,
  status = "success",
  tool,
  ...props
}: ClaudeCodeToolCallProps) => {
  const config = CLAUDE_CODE_TOOL_CALLS[status];
  const hasDetail = Boolean(children);
  const resultLine = (
    <>
      <span aria-hidden="true">{CLAUDE_CODE_RESULT_GLYPH}</span>
      <span className="min-w-0 break-words whitespace-pre-wrap">
        {result}
        {hasDetail && (
          <span className="group-data-[open]/tool:hidden">
            {" "}
            (click to expand)
          </span>
        )}
      </span>
    </>
  );

  return (
    <Collapsible
      className={cn(
        "group/tool font-claude-code text-claude-code-fg flex min-w-0 flex-col text-[0.8125rem] leading-5",
        className
      )}
      data-slot="claude-code-tool-call"
      data-status={status}
      {...props}
    >
      <p className={ROW}>
        <span aria-hidden="true" className={config.className}>
          ●
        </span>
        <span className="min-w-0 break-words">
          <span className="sr-only">{config.srLabel}: </span>
          {arg === undefined ? (
            tool
          ) : (
            <>
              <span className="text-claude-code-strong font-bold">{tool}</span>
              <span>({arg})</span>
            </>
          )}
        </span>
      </p>
      {hasDetail ? (
        <CollapsibleTrigger
          render={
            <Button
              className={cn(TRIGGER_CLASS, RESULT_ROW, "w-full")}
              variant="ghost"
            />
          }
        >
          {resultLine}
        </CollapsibleTrigger>
      ) : (
        <p className={cn(RESULT_ROW, "text-claude-code-muted")}>{resultLine}</p>
      )}
      {hasDetail && (
        <CollapsibleContent className="text-claude-code-muted pl-[5ch] break-words whitespace-pre-wrap">
          {children}
        </CollapsibleContent>
      )}
    </Collapsible>
  );
};

export const ClaudeCodeToolSummary = ({
  children,
  className,
  count,
  noun = "shell command",
  verb = "Ran",
  ...props
}: ClaudeCodeToolSummaryProps) => {
  const label = count === 1 ? noun : `${noun}s`;
  const summary = (
    <>
      {verb} <span className="font-bold">{count}</span> {label}
    </>
  );

  return (
    <Collapsible
      className={cn(
        "group/summary font-claude-code text-claude-code-muted flex min-w-0 flex-col gap-5 pl-[2ch] text-[0.8125rem] leading-5",
        className
      )}
      data-slot="claude-code-tool-summary"
      {...props}
    >
      {children ? (
        <CollapsibleTrigger
          render={
            <Button className={cn(TRIGGER_CLASS, "w-fit")} variant="ghost" />
          }
        >
          {summary}
          <span className="opacity-0 transition-opacity group-focus-within/summary:opacity-100 group-hover/summary:opacity-100 group-data-[open]/summary:hidden">
            {" "}
            (click to expand)
          </span>
        </CollapsibleTrigger>
      ) : (
        <p>{summary}</p>
      )}
      {children && (
        <CollapsibleContent className="-ml-[2ch] flex flex-col gap-5">
          {children}
        </CollapsibleContent>
      )}
    </Collapsible>
  );
};
```

### `registry/notra/claude-code/components/claude-code-status.tsx`

```tsx
"use client";

import { cn } from "cn";
import { useEffect, useState } from "react";

import {
  CLAUDE_CODE_RESULT_GLYPH,
  CLAUDE_CODE_SPINNER_FRAMES,
  CLAUDE_CODE_SPINNER_INTERVAL_MS,
} from "../constants/claude-code";
import { renderClaudeCodeInline } from "../lib/claude-code-inline";
import type {
  ClaudeCodeInterruptedProps,
  ClaudeCodeSpinnerProps,
  ClaudeCodeTurnSummaryProps,
} from "../types/claude-code";

const FRAME_CYCLE = [
  ...CLAUDE_CODE_SPINNER_FRAMES,
  ...CLAUDE_CODE_SPINNER_FRAMES.slice(1, -1).reverse(),
];

const REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";

const useSpinnerFrame = (paused: boolean) => {
  const [frame, setFrame] = useState(0);

  useEffect(() => {
    if (paused || window.matchMedia(REDUCED_MOTION_QUERY).matches) {
      return;
    }
    const id = window.setInterval(() => {
      setFrame((current) => (current + 1) % FRAME_CYCLE.length);
    }, CLAUDE_CODE_SPINNER_INTERVAL_MS);
    return () => window.clearInterval(id);
  }, [paused]);

  return FRAME_CYCLE[frame];
};

const formatTokens = (tokens: number) =>
  `↓ ${tokens.toLocaleString("en-US")} tokens`;

export const ClaudeCodeSpinner = ({
  className,
  details = [],
  elapsed,
  paused = false,
  tip,
  tokens,
  verb = "Thinking",
  ...props
}: ClaudeCodeSpinnerProps) => {
  const glyph = useSpinnerFrame(paused);
  const meta = [
    elapsed,
    tokens === undefined ? undefined : formatTokens(tokens),
    ...details,
  ].filter(Boolean);

  return (
    <div
      className={cn(
        "font-claude-code text-claude-code-muted flex min-w-0 flex-col text-[0.8125rem] leading-5",
        className
      )}
      data-slot="claude-code-spinner"
      role="status"
      {...props}
    >
      <p className="grid grid-cols-[2ch_minmax(0,1fr)]">
        <span aria-hidden="true" className="text-claude-code-accent">
          {glyph}
        </span>
        <span className="min-w-0">
          <span className="text-claude-code-accent">{verb}…</span>
          {meta.length > 0 && <> ({meta.join(" · ")})</>}
        </span>
      </p>
      {tip && (
        <p className="grid grid-cols-[3ch_minmax(0,1fr)] pl-[2ch]">
          <span aria-hidden="true">{CLAUDE_CODE_RESULT_GLYPH}</span>
          <span className="min-w-0">{renderClaudeCodeInline(tip)}</span>
        </p>
      )}
    </div>
  );
};

export const ClaudeCodeTurnSummary = ({
  className,
  doneAt,
  duration,
  verb = "Worked",
  ...props
}: ClaudeCodeTurnSummaryProps) => (
  <p
    className={cn(
      "font-claude-code text-claude-code-muted grid min-w-0 grid-cols-[2ch_minmax(0,1fr)] text-[0.8125rem] leading-5",
      className
    )}
    data-slot="claude-code-turn-summary"
    {...props}
  >
    <span aria-hidden="true">✻</span>
    <span className="min-w-0">
      {verb} for {duration}
      {doneAt && <> · done {doneAt}</>}
    </span>
  </p>
);

export const ClaudeCodeInterrupted = ({
  className,
  hint = "What should Claude do instead?",
  ...props
}: ClaudeCodeInterruptedProps) => (
  <p
    className={cn(
      "font-claude-code text-claude-code-muted grid min-w-0 grid-cols-[3ch_minmax(0,1fr)] pl-[2ch] text-[0.8125rem] leading-5",
      className
    )}
    data-slot="claude-code-interrupted"
    role="status"
    {...props}
  >
    <span aria-hidden="true">{CLAUDE_CODE_RESULT_GLYPH}</span>
    <span className="min-w-0">
      <span className="text-claude-code-error">Interrupted</span>
      {hint && <> · {hint}</>}
    </span>
  </p>
);
```

### `registry/notra/claude-code/components/claude-code-prompt.tsx`

```tsx
"use client";

import { cn } from "cn";
import { useState } from "react";
import type { ChangeEvent, FormEvent, KeyboardEvent } from "react";

import {
  InputGroup,
  InputGroupAddon,
  InputGroupInput,
} from "@/components/ui/input-group";
import { Kbd } from "@/components/ui/kbd";

import {
  CLAUDE_CODE_EFFORTS,
  CLAUDE_CODE_MODES,
} from "../constants/claude-code";
import type { ClaudeCodePromptProps } from "../types/claude-code";

const KBD_CLASS =
  "font-claude-code text-claude-code-muted inline h-auto min-w-0 rounded-none bg-transparent p-0 text-[length:inherit] leading-[inherit] font-normal select-auto";

export const ClaudeCodePrompt = ({
  busy = false,
  className,
  defaultValue = "",
  effort = false,
  inputClassName,
  inputRef,
  mode = "manual",
  onChange,
  onKeyDown,
  onSend,
  onStop,
  placeholder,
  pullRequest,
  value,
  ...props
}: ClaudeCodePromptProps) => {
  const [draft, setDraft] = useState(defaultValue);
  const isControlled = value !== undefined;
  const text = isControlled ? value : draft;
  const isEmpty = text === "";
  const modeConfig = CLAUDE_CODE_MODES[mode];
  const effortConfig = effort ? CLAUDE_CODE_EFFORTS[effort] : undefined;

  const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
    if (!isControlled) {
      setDraft(event.target.value);
    }
    onChange?.(event);
  };

  const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
    if (event.key === "Escape" && busy && onStop) {
      event.preventDefault();
      onStop();
    }
    onKeyDown?.(event);
  };

  const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    const message = text.trim();
    if (!message || busy || !onSend) {
      return;
    }
    onSend(message);
    if (!isControlled) {
      setDraft("");
    }
  };

  return (
    <form
      className={cn(
        "font-claude-code text-claude-code-fg flex min-w-0 flex-col gap-5 text-[0.8125rem] leading-5",
        className
      )}
      data-slot="claude-code-prompt"
      onSubmit={handleSubmit}
      {...props}
    >
      <InputGroup className="border-claude-code-rule has-[[data-slot=input-group-control]:focus-visible]:border-claude-code-rule grid h-auto min-w-0 grid-cols-[2ch_minmax(0,1fr)] items-stretch rounded-none border-0 border-y bg-transparent py-1.5 has-[[data-slot=input-group-control]:focus-visible]:ring-0 dark:bg-transparent">
        <InputGroupAddon className="text-claude-code-strong order-none cursor-text justify-start p-0 text-[length:inherit] leading-[inherit] font-normal">
          <span aria-hidden="true">❯</span>
        </InputGroupAddon>
        <span className="relative flex min-w-0">
          <InputGroupInput
            aria-label="Message Claude Code"
            autoComplete="off"
            className={cn(
              "font-claude-code text-claude-code-strong placeholder:text-claude-code-muted caret-claude-code-strong h-auto p-0 text-[length:inherit] leading-[inherit] transition-none md:text-[length:inherit]",
              isEmpty && "caret-transparent",
              inputClassName
            )}
            enterKeyHint="send"
            onChange={handleChange}
            onKeyDown={handleKeyDown}
            placeholder={placeholder}
            ref={inputRef}
            spellCheck={false}
            type="text"
            value={text}
          />
          {isEmpty && (
            <span
              aria-hidden="true"
              className="pointer-events-none absolute inset-y-0 left-0 w-[1ch] bg-white mix-blend-difference"
            />
          )}
        </span>
      </InputGroup>
      <div className="flex min-w-0 flex-wrap items-baseline justify-between gap-x-[2ch] pl-[2ch]">
        <p className="text-claude-code-muted min-w-0">
          <span className={modeConfig.className}>{modeConfig.label}</span>
          {modeConfig.cycles && (
            <>
              {" ("}
              <Kbd className={KBD_CLASS}>shift+tab</Kbd>
              {" to cycle)"}
            </>
          )}
          {pullRequest && (
            <>
              {" · PR "}
              <a
                className="text-claude-code-pr underline underline-offset-2"
                href={pullRequest.href ?? "#"}
              >
                #{pullRequest.number}
              </a>
            </>
          )}
        </p>
        {effortConfig && (
          <p className={cn("shrink-0", effortConfig.className)}>
            {effortConfig.glyph} {effort} · /effort
          </p>
        )}
      </div>
    </form>
  );
};
```

### `registry/notra/claude-code/types/claude-code.ts`

```ts
import type {
  ChangeEventHandler,
  ComponentProps,
  KeyboardEventHandler,
  ReactNode,
  Ref,
} from "react";

import type { Card } from "@/components/ui/card";
import type { Collapsible } from "@/components/ui/collapsible";

export type ClaudeCodeMode =
  | "auto"
  | "manual"
  | "accept-edits"
  | "plan"
  | "bypass";

export type ClaudeCodeEffort =
  | "low"
  | "medium"
  | "high"
  | "xhigh"
  | "max"
  | "ultracode";

export type ClaudeCodeTodoStatus = "done" | "active" | "todo";

export type ClaudeCodeToolCallStatus = "success" | "error" | "pending";

export interface ClaudeCodeModeConfig {
  className: string;
  /** Whether the status line shows the shift+tab hint. */
  cycles: boolean;
  label: string;
}

export interface ClaudeCodeEffortConfig {
  className: string;
  glyph: string;
}

export interface ClaudeCodeTodoConfig {
  glyph: string;
  labelClassName: string;
  srLabel: string;
}

export interface ClaudeCodeToolCallConfig {
  className: string;
  srLabel: string;
}

export interface ClaudeCodeTodo {
  label: string;
  status: ClaudeCodeTodoStatus;
}

export interface ClaudeCodePullRequest {
  href?: string;
  number: number;
}

export interface ClaudeCodeTerminalProps extends Omit<
  ComponentProps<typeof Card>,
  "title"
> {
  contentClassName?: string;
  /** Pinned under the scrolling transcript, like Claude Code's prompt. */
  footer?: ReactNode;
  title?: ReactNode;
}

export interface ClaudeCodeLogoProps extends ComponentProps<"svg"> {
  size?: number;
}

export interface ClaudeCodeHeaderProps extends ComponentProps<"div"> {
  cwd?: string;
  model?: string;
  org?: string;
  /** Dim `⎿` lines under the notices, like hook output or tips. */
  tips?: string[];
  version?: string;
  /** Notice lines printed under the header. Supports `code` and **bold**. */
  whatsNew?: string[];
}

export interface ClaudeCodeMessageProps extends ComponentProps<"div"> {
  from?: "user" | "assistant";
}

export interface ClaudeCodeThinkingProps extends Omit<
  ComponentProps<typeof Collapsible>,
  "children"
> {
  children?: ReactNode;
  label?: ReactNode;
}

export interface ClaudeCodeSpinnerProps extends ComponentProps<"div"> {
  /** Extra fields in the parentheses, like `thinking`. */
  details?: string[];
  elapsed?: string;
  /** Freeze the glyph, for screenshots and reduced motion. */
  paused?: boolean;
  tip?: string;
  tokens?: number;
  verb?: string;
}

export interface ClaudeCodeTurnSummaryProps extends ComponentProps<"div"> {
  doneAt?: string;
  duration: string;
  verb?: string;
}

export interface ClaudeCodeInterruptedProps extends ComponentProps<"p"> {
  /** Shown after `Interrupted ·`. */
  hint?: ReactNode;
}

export interface ClaudeCodeTodoListProps extends ComponentProps<"div"> {
  /** The tool line above the list. Pass `null` to leave it out. */
  heading?: string | null;
  todos: ClaudeCodeTodo[];
}

export interface ClaudeCodeToolCallProps extends Omit<
  ComponentProps<typeof Collapsible>,
  "children"
> {
  arg?: string;
  children?: ReactNode;
  result: ReactNode;
  status?: ClaudeCodeToolCallStatus;
  tool: string;
}

export interface ClaudeCodeToolSummaryProps extends Omit<
  ComponentProps<typeof Collapsible>,
  "children"
> {
  children?: ReactNode;
  count: number;
  noun?: string;
  verb?: string;
}

export interface ClaudeCodePromptProps extends Omit<
  ComponentProps<"form">,
  "defaultValue" | "onChange" | "onKeyDown" | "onSubmit"
> {
  /** A turn is running: Enter does not send and Escape calls `onStop`. */
  busy?: boolean;
  defaultValue?: string;
  effort?: ClaudeCodeEffort | false;
  inputClassName?: string;
  inputRef?: Ref<HTMLInputElement>;
  mode?: ClaudeCodeMode;
  onChange?: ChangeEventHandler<HTMLInputElement>;
  onKeyDown?: KeyboardEventHandler<HTMLInputElement>;
  /**
   * Called with the trimmed text on Enter. An uncontrolled prompt clears
   * afterwards; with `value`, clear it yourself here.
   */
  onSend?: (text: string) => void;
  /** Called on Escape while `busy`, to interrupt the running turn. */
  onStop?: () => void;
  placeholder?: string;
  pullRequest?: ClaudeCodePullRequest;
  value?: string;
}

export interface ClaudeCodeSessionHeader {
  cwd: string;
  model: string;
  org: string;
  tips: string[];
  version: string;
  whatsNew: string[];
}

export interface ClaudeCodeSessionToolCall {
  arg?: string;
  detail?: string;
  id: string;
  result: string;
  status?: ClaudeCodeToolCallStatus;
  tool: string;
}

export interface ClaudeCodeSessionSpinner {
  details: string[];
  elapsed: string;
  tip: string;
  tokens: number;
  verb: string;
}

export interface ClaudeCodeSessionTurn {
  answer: string;
  commands: ClaudeCodeSessionToolCall[];
  id: string;
  prompt: string;
  summary: ClaudeCodeSessionSummary;
  thinking?: string;
  todos?: ClaudeCodeTodo[];
}

export interface ClaudeCodeSessionSummary {
  doneAt: string;
  duration: string;
  verb: string;
}

export type ClaudeCodeTurnStatus =
  | "working"
  | "streaming"
  | "done"
  | "interrupted";

/** A scripted answer the demo plays back when you send a prompt. */
export interface ClaudeCodeReply {
  answer: string;
  commands: ClaudeCodeSessionToolCall[];
  /** The spinner verb while the turn runs, like `Sketching`. */
  spinnerVerb: string;
  /** The verb on the summary line once the turn is done, like `Cooked`. */
  summaryVerb: string;
}

export interface ClaudeCodeChatTurn extends Omit<
  ClaudeCodeSessionTurn,
  "summary"
> {
  status: ClaudeCodeTurnStatus;
  summary?: ClaudeCodeSessionSummary;
}

export interface ClaudeCodeChat {
  busy: boolean;
  /** Whole seconds since the running turn started. */
  elapsed: number;
  send: (text: string) => Promise<void>;
  spinnerVerb: string;
  stop: () => void;
  /** Tokens streamed in the running turn so far. */
  tokens: number;
  turns: ClaudeCodeChatTurn[];
}

export interface ClaudeCodeSessionPendingTurn {
  prompt: string;
  spinner: ClaudeCodeSessionSpinner;
  todos: ClaudeCodeTodo[];
  toolCall: ClaudeCodeSessionToolCall;
}

export interface ClaudeCodeSession {
  header: ClaudeCodeSessionHeader;
  pending: ClaudeCodeSessionPendingTurn;
  promptPlaceholder: string;
  pullRequest: ClaudeCodePullRequest;
  title: string;
  turns: ClaudeCodeSessionTurn[];
}
```

### `registry/notra/claude-code/constants/claude-code.ts`

```ts
import type {
  ClaudeCodeEffort,
  ClaudeCodeEffortConfig,
  ClaudeCodeMode,
  ClaudeCodeModeConfig,
  ClaudeCodeTodoConfig,
  ClaudeCodeTodoStatus,
  ClaudeCodeToolCallConfig,
  ClaudeCodeToolCallStatus,
} from "../types/claude-code";

/** The order Shift+Tab walks through. */
export const CLAUDE_CODE_MODE_ORDER: ClaudeCodeMode[] = [
  "manual",
  "accept-edits",
  "plan",
  "auto",
  "bypass",
];

export const CLAUDE_CODE_EFFORT_ORDER: ClaudeCodeEffort[] = [
  "low",
  "medium",
  "high",
  "xhigh",
  "max",
  "ultracode",
];

export const CLAUDE_CODE_MODES: Record<ClaudeCodeMode, ClaudeCodeModeConfig> = {
  "accept-edits": {
    className: "text-claude-code-mode-accept-edits",
    cycles: true,
    label: "⏵⏵ accept edits on",
  },
  auto: {
    className: "text-claude-code-mode-auto",
    cycles: true,
    label: "⏵⏵ auto mode on",
  },
  bypass: {
    className: "text-claude-code-mode-bypass",
    cycles: true,
    label: "⏵⏵ bypass permissions on",
  },
  manual: {
    className: "text-claude-code-muted",
    cycles: false,
    label: "? for shortcuts",
  },
  plan: {
    className: "text-claude-code-mode-plan",
    cycles: true,
    label: "⏸ plan mode on",
  },
};

export const CLAUDE_CODE_EFFORTS: Record<
  ClaudeCodeEffort,
  ClaudeCodeEffortConfig
> = {
  high: { className: "text-claude-code-muted", glyph: "◕" },
  low: { className: "text-claude-code-muted", glyph: "○" },
  max: { className: "text-claude-code-muted", glyph: "●" },
  medium: { className: "text-claude-code-muted", glyph: "◑" },
  ultracode: { className: "text-claude-code-accent", glyph: "✦" },
  xhigh: { className: "text-claude-code-muted", glyph: "◉" },
};

export const CLAUDE_CODE_TODOS: Record<
  ClaudeCodeTodoStatus,
  ClaudeCodeTodoConfig
> = {
  active: {
    glyph: "☐",
    labelClassName: "text-claude-code-strong font-bold",
    srLabel: "In progress",
  },
  done: {
    glyph: "☒",
    labelClassName: "text-claude-code-muted line-through",
    srLabel: "Done",
  },
  todo: {
    glyph: "☐",
    labelClassName: "text-claude-code-fg",
    srLabel: "To do",
  },
};

export const CLAUDE_CODE_TOOL_CALLS: Record<
  ClaudeCodeToolCallStatus,
  ClaudeCodeToolCallConfig
> = {
  error: { className: "text-claude-code-error", srLabel: "Failed" },
  pending: {
    className:
      "text-claude-code-muted animate-pulse motion-reduce:animate-none",
    srLabel: "Running",
  },
  success: { className: "text-claude-code-success", srLabel: "Done" },
};

/** The spinner glyphs, played forward then back. */
export const CLAUDE_CODE_SPINNER_FRAMES = ["·", "✢", "✳", "✶", "✻", "✽"];

export const CLAUDE_CODE_SPINNER_INTERVAL_MS = 120;

export const CLAUDE_CODE_RESULT_GLYPH = "⎿";

/** Pacing of a played-back turn in the demo chat. */
export const CLAUDE_CODE_CHAT_TIMING = {
  /** Spinner time before the first tool call. */
  thinkMs: 1200,
  /** How long each tool call stays pending. */
  toolMs: 900,
  /** Delay between streamed words. */
  wordMs: 28,
  /** Cap on every delay when the user prefers reduced motion. */
  reducedMaxMs: 60,
  /** How often the elapsed seconds tick. */
  tickMs: 1000,
} as const;

/** The mascot grid, in cells. */
export const CLAUDE_CODE_MASCOT_SIZE = { height: 14, width: 22 };

/**
 * The mascot's filled cells as `[x, y, width, height]` rectangles.
 * Eyes are cut out with a mask so the terminal background shows through.
 */
export const CLAUDE_CODE_MASCOT_BODY: [number, number, number, number][] = [
  [2, 0, 18, 10],
  [0, 4, 22, 3],
  [3, 10, 2, 4],
  [7, 10, 2, 4],
  [13, 10, 2, 4],
  [17, 10, 2, 4],
];

export const CLAUDE_CODE_MASCOT_EYES: [number, number, number, number][] = [
  [6, 2, 2, 3],
  [14, 2, 2, 3],
];
```

### `registry/notra/claude-code/constants/claude-code-session.ts`

```ts
import type { ClaudeCodeReply, ClaudeCodeSession } from "../types/claude-code";

export const CLAUDE_CODE_SESSION: ClaudeCodeSession = {
  header: {
    cwd: "~/acme/web",
    model: "Opus 5.5 (1M context)",
    org: "Claude Max",
    tips: [
      "SessionStart:startup hook succeeded",
      "notra MCP server connected · 24 tools",
    ],
    version: "v2.1.285",
    whatsNew: [
      "Get to finished work sooner with Opus 5.5. Switch anytime with `/model`.",
    ],
  },
  pending: {
    prompt: "draft the changelog for v1.9 and queue it in notra",
    spinner: {
      details: ["thinking"],
      elapsed: "14s",
      tip: "Tip: Run `/resume` to pick up an earlier conversation",
      tokens: 688,
      verb: "Sketching",
    },
    todos: [
      { label: "Group the 14 merged PRs by area", status: "done" },
      { label: "Draft the changelog in brand voice", status: "active" },
      { label: "Queue the post in notra", status: "todo" },
    ],
    toolCall: {
      id: "brand-voice",
      result:
        "$ notra brand voice --json && notra posts list --type changelog --limit 3",
      status: "pending",
      tool: "Checking brand voice and the last changelogs",
    },
  },
  promptPlaceholder: 'Try "write a LinkedIn post about the release"',
  pullRequest: { number: 1317 },
  title: "claude — ~/acme/web",
  turns: [
    {
      answer:
        "Since **v1.8.0** there are 14 merged PRs on `main`, split across three areas.\n\n`apps/dashboard` got the new **GEO scan** view with per-engine citations, `packages/api` added `POST /v1/posts/:id/schedule`, and `apps/web` moved the changelog to `/changelog/[slug]`. Two PRs are fixes from review.\n\nWant me to draft the changelog for v1.9 from these?",
      commands: [
        {
          arg: "git log --oneline v1.8.0..HEAD",
          id: "git-log",
          result: "14 commits",
          tool: "Bash",
        },
      ],
      id: "what-changed",
      prompt: "what changed since the last release?",
      summary: { doneAt: "9:41 AM", duration: "7s", verb: "Cogitated" },
    },
    {
      answer:
        "Drafted **v1.9** in your brand voice and queued it in Notra for Thursday 9:00 AM. The post groups the 14 PRs into features and fixes and links each one.\n\nOpen it with `notra posts open changelog-v1-9` to review before it goes out.",
      commands: [
        {
          arg: "type: changelog, voice: acme",
          id: "brand-voice",
          result: "Loaded brand voice and the last 3 changelogs",
          tool: "notra - get_brand_voice (MCP)",
        },
        {
          arg: "title: v1.9, schedule: thu 09:00",
          id: "queue-post",
          result: "Queued changelog-v1-9",
          tool: "notra - create_post (MCP)",
        },
      ],
      id: "draft-changelog",
      prompt: "draft the changelog for v1.9 and queue it in notra",
      summary: { doneAt: "9:43 AM", duration: "38s", verb: "Brewed" },
      todos: [
        { label: "Group the 14 merged PRs by area", status: "done" },
        { label: "Draft the changelog in brand voice", status: "done" },
        { label: "Queue the post in notra", status: "done" },
      ],
    },
  ],
};

/** Answers the demo plays back, one per prompt you send, in order. */
export const CLAUDE_CODE_REPLIES: ClaudeCodeReply[] = [
  {
    answer:
      "Here's a LinkedIn post for the release:\n\n**Acme v1.9 is out.** Track which AI engines cite you with the new GEO scan view, schedule posts straight from the API, and find every release at `/changelog`.\n\nSaved it as a draft next to the changelog. Want a shorter version for X?",
    commands: [
      {
        arg: "id: changelog-v1-9",
        id: "get-post",
        result: "Read changelog-v1-9 (412 words)",
        tool: "notra - get_post (MCP)",
      },
      {
        arg: "type: linkedin, source: changelog-v1-9",
        id: "create-linkedin",
        result: "Created linkedin-v1-9 as a draft",
        tool: "notra - create_post (MCP)",
      },
    ],
    spinnerVerb: "Crafting",
    summaryVerb: "Crafted",
  },
  {
    answer:
      "The scheduler fix is in `packages/api/src/jobs/publish.ts`. Failed publish jobs now retry three times with backoff before they surface in the dashboard.\n\nThe tests in `publish.test.ts` cover both the retry and the final failure.",
    commands: [
      {
        arg: 'pattern: "retry", path: packages/api',
        id: "grep-retry",
        result: "Found 4 files",
        tool: "Search",
      },
      {
        arg: "packages/api/src/jobs/publish.ts",
        id: "read-publish",
        result: "Read 86 lines",
        tool: "Read",
      },
    ],
    spinnerVerb: "Spelunking",
    summaryVerb: "Worked",
  },
  {
    answer:
      "All 212 tests pass and the build is clean. Nothing else is blocking **v1.9**.",
    commands: [
      {
        arg: "bun run test",
        id: "run-tests",
        result: "212 passed",
        tool: "Bash",
      },
    ],
    spinnerVerb: "Verifying",
    summaryVerb: "Cogitated",
  },
];
```

### `registry/notra/claude-code/lib/claude-code-inline.tsx`

```tsx
import type { ReactNode } from "react";

const INLINE_TOKEN = /(`[^`]+`|\*\*[^*]+\*\*)/g;

/**
 * Renders the two bits of markdown Claude Code styles inline:
 * `code` in the link color and **bold** in bright white.
 */
export const renderClaudeCodeInline = (text: string): ReactNode[] => {
  let offset = 0;

  return text.split(INLINE_TOKEN).map((part) => {
    const key = `${offset}-${part}`;
    offset += part.length;

    if (part.length > 2 && part.startsWith("`") && part.endsWith("`")) {
      return (
        <code className="text-claude-code-link font-claude-code" key={key}>
          {part.slice(1, -1)}
        </code>
      );
    }

    if (part.length > 4 && part.startsWith("**") && part.endsWith("**")) {
      return (
        <strong className="text-claude-code-strong font-bold" key={key}>
          {part.slice(2, -2)}
        </strong>
      );
    }

    return part;
  });
};

export const renderClaudeCodeChildren = (children: ReactNode): ReactNode =>
  typeof children === "string" ? renderClaudeCodeInline(children) : children;
```

### `registry/notra/claude-code/hooks/use-claude-code-chat.ts`

```ts
"use client";

import { useCallback, useEffect, useRef, useState } from "react";

import { CLAUDE_CODE_CHAT_TIMING } from "../constants/claude-code";
import type {
  ClaudeCodeChat,
  ClaudeCodeChatTurn,
  ClaudeCodeReply,
  ClaudeCodeSessionTurn,
} from "../types/claude-code";

const WORD_PATTERN = /(\s+)/;

const REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";

/** Rough English average, close enough for the spinner's token count. */
const CHARS_PER_TOKEN = 4;

const wait = (ms: number) =>
  new Promise<void>((resolve) => {
    window.setTimeout(resolve, ms);
  });

const formatClock = (date: Date) =>
  date.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" });

const settle = (turn: ClaudeCodeSessionTurn): ClaudeCodeChatTurn => ({
  ...turn,
  status: "done",
});

/**
 * Runs the demo conversation: `send` appends your prompt, spins, runs the
 * reply's tool calls one by one, streams the answer and closes the turn with
 * a summary line. `stop` interrupts the running turn like Escape does.
 */
export const useClaudeCodeChat = (
  history: readonly ClaudeCodeSessionTurn[],
  replies: readonly ClaudeCodeReply[]
): ClaudeCodeChat => {
  const [turns, setTurns] = useState<ClaudeCodeChatTurn[]>(() =>
    history.map(settle)
  );
  const [activeId, setActiveId] = useState<string | null>(null);
  const [spinnerVerb, setSpinnerVerb] = useState("");
  const [startedAt, setStartedAt] = useState(0);
  const [now, setNow] = useState(0);
  const [streamed, setStreamed] = useState(0);
  const runRef = useRef(0);
  const activeRef = useRef<string | null>(null);
  const replyRef = useRef(0);

  useEffect(() => {
    if (!activeId) {
      return;
    }
    const timer = window.setInterval(() => {
      setNow(Date.now());
    }, CLAUDE_CODE_CHAT_TIMING.tickMs);
    return () => window.clearInterval(timer);
  }, [activeId]);

  useEffect(
    () => () => {
      runRef.current += 1;
    },
    []
  );

  const patch = useCallback(
    (
      id: string,
      next: (turn: ClaudeCodeChatTurn) => Partial<ClaudeCodeChatTurn>
    ) => {
      setTurns((current) =>
        current.map((turn) =>
          turn.id === id ? { ...turn, ...next(turn) } : turn
        )
      );
    },
    []
  );

  const finish = useCallback(() => {
    activeRef.current = null;
    setActiveId(null);
  }, []);

  const stop = useCallback(() => {
    const id = activeRef.current;
    if (!id) {
      return;
    }
    runRef.current += 1;
    patch(id, (turn) => ({
      commands: turn.commands.filter((command) => command.status !== "pending"),
      status: "interrupted",
    }));
    finish();
  }, [finish, patch]);

  const send = useCallback(
    async (text: string) => {
      const reply = replies[replyRef.current % replies.length];
      if (activeRef.current || !reply) {
        return;
      }
      replyRef.current += 1;
      const run = runRef.current + 1;
      runRef.current = run;
      const alive = () => runRef.current === run;
      const reduced = window.matchMedia(REDUCED_MOTION_QUERY).matches;
      const delay = (ms: number) =>
        wait(reduced ? Math.min(ms, CLAUDE_CODE_CHAT_TIMING.reducedMaxMs) : ms);
      const start = Date.now();
      const id = `turn-${start}`;

      activeRef.current = id;
      setActiveId(id);
      setStartedAt(start);
      setNow(start);
      setStreamed(0);
      setSpinnerVerb(reply.spinnerVerb);
      setTurns((current) => [
        ...current,
        { answer: "", commands: [], id, prompt: text, status: "working" },
      ]);

      await delay(CLAUDE_CODE_CHAT_TIMING.thinkMs);

      for (const command of reply.commands) {
        if (!alive()) {
          return;
        }
        const commandId = `${id}-${command.id}`;
        patch(id, (turn) => ({
          commands: [
            ...turn.commands,
            { ...command, id: commandId, status: "pending" },
          ],
        }));
        // react-doctor-disable-next-line react-doctor/async-await-in-loop -- tool calls run one after another
        await delay(CLAUDE_CODE_CHAT_TIMING.toolMs);
        if (!alive()) {
          return;
        }
        patch(id, (turn) => ({
          commands: turn.commands.map((item) =>
            item.id === commandId
              ? { ...item, status: command.status ?? "success" }
              : item
          ),
        }));
      }

      if (!alive()) {
        return;
      }
      patch(id, () => ({ status: "streaming" }));

      let answer = "";
      const words = reduced ? [reply.answer] : reply.answer.split(WORD_PATTERN);
      for (const word of words) {
        if (!alive()) {
          return;
        }
        answer += word;
        const snapshot = answer;
        patch(id, () => ({ answer: snapshot }));
        setStreamed(snapshot.length);
        // react-doctor-disable-next-line react-doctor/async-await-in-loop -- words stream in order
        await delay(CLAUDE_CODE_CHAT_TIMING.wordMs);
      }

      if (!alive()) {
        return;
      }
      const seconds = Math.max(1, Math.round((Date.now() - start) / 1000));
      patch(id, () => ({
        status: "done",
        summary: {
          doneAt: formatClock(new Date()),
          duration: `${seconds}s`,
          verb: reply.summaryVerb,
        },
      }));
      finish();
    },
    [finish, patch, replies]
  );

  return {
    busy: activeId !== null,
    elapsed: Math.max(0, Math.floor((now - startedAt) / 1000)),
    send,
    spinnerVerb,
    stop,
    tokens: Math.round(streamed / CHARS_PER_TOKEN),
    turns,
  };
};
```

### `registry/notra/claude-code/examples/claude-code-demo.tsx`

```tsx
"use client";

import { useEffect, useRef, useState } from "react";
import type { KeyboardEvent } from "react";

import { ClaudeCodeHeader } from "../components/claude-code-header";
import { ClaudeCodeMessage } from "../components/claude-code-message";
import { ClaudeCodePrompt } from "../components/claude-code-prompt";
import {
  ClaudeCodeInterrupted,
  ClaudeCodeSpinner,
  ClaudeCodeTurnSummary,
} from "../components/claude-code-status";
import { ClaudeCodeTerminal } from "../components/claude-code-terminal";
import { ClaudeCodeTodoList } from "../components/claude-code-todo-list";
import {
  ClaudeCodeToolCall,
  ClaudeCodeToolSummary,
} from "../components/claude-code-tool-call";
import { CLAUDE_CODE_MODE_ORDER } from "../constants/claude-code";
import {
  CLAUDE_CODE_REPLIES,
  CLAUDE_CODE_SESSION,
} from "../constants/claude-code-session";
import { useClaudeCodeChat } from "../hooks/use-claude-code-chat";
import type { ClaudeCodeChatTurn, ClaudeCodeMode } from "../types/claude-code";

/** How close to the bottom still counts as following the transcript. */
const FOLLOW_THRESHOLD_PX = 48;

const nextMode = (current: ClaudeCodeMode) => {
  const index = CLAUDE_CODE_MODE_ORDER.indexOf(current);
  return CLAUDE_CODE_MODE_ORDER[(index + 1) % CLAUDE_CODE_MODE_ORDER.length];
};

const ClaudeCodeTurn = ({ turn }: { turn: ClaudeCodeChatTurn }) => {
  const toolCalls = turn.commands.map((command) => (
    <ClaudeCodeToolCall
      arg={command.arg}
      key={command.id}
      result={command.result}
      status={command.status}
      tool={command.tool}
    >
      {command.detail}
    </ClaudeCodeToolCall>
  ));
  // Claude Code folds finished shell commands into one "Ran N" line.
  const foldsShell =
    turn.status === "done" &&
    turn.commands.length > 0 &&
    turn.commands.every((command) => command.tool === "Bash");

  return (
    <div className="flex flex-col gap-5">
      <ClaudeCodeMessage from="user">{turn.prompt}</ClaudeCodeMessage>
      {turn.todos && <ClaudeCodeTodoList todos={turn.todos} />}
      {foldsShell ? (
        <ClaudeCodeToolSummary count={turn.commands.length}>
          {toolCalls}
        </ClaudeCodeToolSummary>
      ) : (
        toolCalls
      )}
      {turn.answer && <ClaudeCodeMessage>{turn.answer}</ClaudeCodeMessage>}
      {turn.status === "interrupted" && <ClaudeCodeInterrupted />}
      {turn.summary && <ClaudeCodeTurnSummary {...turn.summary} />}
    </div>
  );
};

export default function ClaudeCodeDemo() {
  const { header, pending, promptPlaceholder, pullRequest, title, turns } =
    CLAUDE_CODE_SESSION;
  const chat = useClaudeCodeChat(turns, CLAUDE_CODE_REPLIES);
  const [mode, setMode] = useState<ClaudeCodeMode>("bypass");
  const terminalRef = useRef<HTMLDivElement>(null);
  const followRef = useRef(false);
  const activeTurn = chat.busy ? chat.turns.at(-1) : undefined;

  // Follow new output only while the reader sits at the bottom, so
  // scrolling up to reread a turn is not undone by the next streamed word.
  // Sending always jumps back down. Scrolling the viewport directly (not
  // scrollIntoView) keeps the host page still.
  const viewportOf = () =>
    terminalRef.current?.querySelector<HTMLElement>(
      '[data-slot="scroll-area-viewport"]'
    );

  useEffect(() => {
    const viewport = viewportOf();
    if (!viewport) {
      return;
    }
    const handleScroll = () => {
      followRef.current =
        viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight <
        FOLLOW_THRESHOLD_PX;
    };
    handleScroll();
    viewport.addEventListener("scroll", handleScroll, { passive: true });
    return () => viewport.removeEventListener("scroll", handleScroll);
  }, []);

  useEffect(() => {
    if (!followRef.current) {
      return;
    }
    const viewport = viewportOf();
    viewport?.scrollTo({ top: viewport.scrollHeight });
  }, [chat.turns, chat.busy]);

  const handleSend = (text: string) => {
    followRef.current = true;
    return chat.send(text);
  };

  const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
    if (event.key === "Tab" && event.shiftKey) {
      event.preventDefault();
      setMode(nextMode);
    }
  };

  return (
    <ClaudeCodeTerminal
      className="h-170"
      footer={
        <ClaudeCodePrompt
          busy={chat.busy}
          mode={mode}
          onKeyDown={handleKeyDown}
          onSend={handleSend}
          onStop={chat.stop}
          placeholder={promptPlaceholder}
          pullRequest={pullRequest}
        />
      }
      ref={terminalRef}
      title={title}
    >
      <ClaudeCodeHeader {...header} />
      {chat.turns.map((turn) => (
        <ClaudeCodeTurn key={turn.id} turn={turn} />
      ))}
      {activeTurn && (
        <ClaudeCodeSpinner
          details={
            activeTurn.status === "working"
              ? ["thinking", "esc to interrupt"]
              : ["esc to interrupt"]
          }
          elapsed={`${chat.elapsed}s`}
          tip={pending.spinner.tip}
          tokens={chat.tokens > 0 ? chat.tokens : undefined}
          verb={chat.spinnerVerb}
        />
      )}
    </ClaudeCodeTerminal>
  );
}
```
