# Codex

> A full recreation of an OpenAI Codex CLI session built from the Codex 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/codex).

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

## Install

```bash
npx shadcn@latest add https://ui.usenotra.com/r/codex.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/codex/components/codex-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 { CodexTerminalProps } from "../types/codex";

export const CodexTerminal = ({
  children,
  className,
  footer,
  title,
  ...props
}: CodexTerminalProps) => (
  <Card
    className={cn(
      "bg-codex-bg font-codex text-codex-fg ring-codex-ring w-full gap-0 overflow-clip rounded-xl py-0 text-[0.8125rem] leading-[1.3] antialiased shadow-[0_1.5rem_3.5rem_-1rem_var(--codex-shadow)] ring-1",
      className
    )}
    data-slot="codex-terminal"
    {...props}
  >
    <CardHeader className="bg-codex-titlebar relative flex shrink-0 items-center justify-center gap-3 rounded-none px-3 py-2">
      <div
        aria-hidden="true"
        className="absolute start-3 flex items-center gap-2"
      >
        <span className="bg-codex-dot size-3 shrink-0 rounded-full" />
        <span className="bg-codex-dot size-3 shrink-0 rounded-full" />
        <span className="bg-codex-dot size-3 shrink-0 rounded-full" />
      </div>
      {title && (
        <CardTitle className="text-codex-title truncate px-16 text-xs leading-4 font-normal">
          {title}
        </CardTitle>
      )}
    </CardHeader>
    <ScrollArea className="min-h-0 flex-1">
      <CardContent className="flex flex-col gap-[1.3em] px-0 py-[1.3em]">
        {children}
      </CardContent>
    </ScrollArea>
    {footer && <div className="shrink-0 pb-[0.65em]">{footer}</div>}
  </Card>
);
```

### `registry/notra/codex/components/codex-header.tsx`

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

import {
  CODEX_DEFAULT_CWD,
  CODEX_DEFAULT_GREETING,
  CODEX_DEFAULT_VERSION,
  CODEX_ROW_CLASS,
} from "../constants/codex";
import type { CodexHeaderProps } from "../types/codex";

export const CodexHeader = ({
  className,
  cwd = CODEX_DEFAULT_CWD,
  greeting = CODEX_DEFAULT_GREETING,
  version = CODEX_DEFAULT_VERSION,
  ...props
}: CodexHeaderProps) => (
  <div
    className={cn(CODEX_ROW_CLASS, "text-codex-fg", className)}
    data-slot="codex-header"
    {...props}
  >
    <p className="col-start-2 min-w-0 wrap-break-word">
      <span className="text-codex-blue">&gt;_ </span>
      <span className="font-bold">OpenAI Codex</span>
      <span className="text-codex-muted"> (v{version})</span>
    </p>
    <p className="text-codex-muted col-start-2 min-w-0 truncate ps-[3ch]">
      {cwd}
    </p>
    {greeting && (
      <p className="text-codex-blue col-start-2 mt-[1.3em] min-w-0 wrap-break-word">
        {greeting}
      </p>
    )}
  </div>
);
```

### `registry/notra/codex/components/codex-message.tsx`

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

import { CODEX_ROW_CLASS } from "../constants/codex";
import type { CodexMessageProps } from "../types/codex";

export const CodexMessage = ({
  children,
  className,
  from = "assistant",
  ...props
}: CodexMessageProps) => {
  if (from === "user") {
    return (
      <div
        className={cn(
          CODEX_ROW_CLASS,
          "bg-codex-band text-codex-fg w-full py-[1.3em]",
          className
        )}
        data-from="user"
        data-slot="codex-message"
        {...props}
      >
        <span aria-hidden="true" className="text-codex-dim">
          ›
        </span>
        <div className="min-w-0 wrap-break-word whitespace-pre-wrap">
          {children}
        </div>
      </div>
    );
  }

  return (
    <div
      className={cn(CODEX_ROW_CLASS, "text-codex-fg w-full", className)}
      data-from="assistant"
      data-slot="codex-message"
      {...props}
    >
      <span aria-hidden="true" className="text-codex-muted">
        •
      </span>
      <div className="flex min-w-0 flex-col gap-[1.3em] wrap-break-word">
        {children}
      </div>
    </div>
  );
};
```

### `registry/notra/codex/components/codex-reasoning.tsx`

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

import { CODEX_ROW_CLASS } from "../constants/codex";
import type { CodexReasoningProps } from "../types/codex";

export const CodexReasoning = ({
  children,
  className,
  ...props
}: CodexReasoningProps) => (
  <div
    className={cn(CODEX_ROW_CLASS, "text-codex-muted", className)}
    data-slot="codex-reasoning"
    {...props}
  >
    <span aria-hidden="true">•</span>
    <p className="min-w-0 font-bold wrap-break-word italic">{children}</p>
  </div>
);
```

### `registry/notra/codex/components/codex-exec.tsx`

```tsx
"use client";

import { cn } from "cn";

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

import {
  CODEX_EXEC_PREVIEW_LINES,
  CODEX_EXEC_STATUS_CLASS,
  CODEX_EXEC_STATUS_LABEL,
  CODEX_ROW_CLASS,
  CODEX_SHELL_TOKEN_CLASS,
} from "../constants/codex";
import { tokenizeShellCommand } from "../lib/codex-shell";
import type { CodexExecProps } from "../types/codex";

export const CodexShellCommand = ({ command }: { command: string }) =>
  tokenizeShellCommand(command).map((token, index) => (
    <span className={CODEX_SHELL_TOKEN_CLASS[token.kind]} key={index}>
      {token.text}
    </span>
  ));

export const CodexExec = ({
  className,
  command,
  output,
  previewLines = CODEX_EXEC_PREVIEW_LINES,
  status = "ran",
  ...props
}: CodexExecProps) => {
  const lines = output ? output.split("\n") : [];
  const preview = lines.slice(0, previewLines).join("\n");
  const rest = lines.slice(previewLines);

  return (
    <Collapsible
      className={cn(CODEX_ROW_CLASS, "text-codex-fg", className)}
      data-slot="codex-exec"
      data-status={status}
      {...props}
    >
      <span aria-hidden="true" className={CODEX_EXEC_STATUS_CLASS[status]}>
        •
      </span>
      <p className="min-w-0 truncate">
        <span className="font-bold">{CODEX_EXEC_STATUS_LABEL[status]}</span>{" "}
        <CodexShellCommand command={command} />
      </p>
      {lines.length > 0 && (
        <div className="text-codex-muted col-start-2 grid min-w-0 grid-cols-[2ch_minmax(0,1fr)]">
          <span aria-hidden="true">└</span>
          <pre className="min-w-0 font-[inherit] whitespace-pre-wrap">
            {preview}
          </pre>
          {rest.length > 0 && (
            <>
              <CollapsibleContent className="col-start-2 min-w-0">
                <pre className="font-[inherit] whitespace-pre-wrap">
                  {rest.join("\n")}
                </pre>
              </CollapsibleContent>
              <CollapsibleTrigger
                className="col-start-2 justify-self-start"
                render={
                  <Button
                    className="text-codex-muted hover:text-codex-fg aria-expanded:text-codex-fg focus-visible:ring-codex-blue/60 h-auto rounded-xs p-0 text-start font-[inherit] text-[length:inherit] leading-[inherit] font-normal whitespace-normal hover:bg-transparent focus-visible:border-transparent focus-visible:ring-1 active:not-aria-[haspopup]:translate-y-0 aria-expanded:bg-transparent dark:hover:bg-transparent"
                    variant="ghost"
                  />
                }
              >
                <span className="in-data-panel-open:hidden">
                  {`+ ${rest.length} lines (click to expand)`}
                </span>
                <span className="hidden in-data-panel-open:inline">
                  − collapse
                </span>
              </CollapsibleTrigger>
            </>
          )}
        </div>
      )}
    </Collapsible>
  );
};
```

### `registry/notra/codex/components/codex-explored.tsx`

```tsx
"use client";

import { cn } from "cn";

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

import { CODEX_ROW_CLASS } from "../constants/codex";
import type { CodexExploredItem, CodexExploredProps } from "../types/codex";

const CodexExploredLine = ({
  first,
  item,
}: {
  first: boolean;
  item: CodexExploredItem;
}) => (
  <li className="contents">
    <span aria-hidden="true" className="text-codex-muted">
      {first ? "└" : ""}
    </span>
    <span className="min-w-0 truncate">
      <span className="text-codex-blue">{item.verb}</span> {item.target}
      {item.scope && (
        <>
          <span className="text-codex-muted"> in </span>
          {item.scope}
        </>
      )}
    </span>
  </li>
);

export const CodexExplored = ({
  active = false,
  className,
  details = [],
  items,
  ...props
}: CodexExploredProps) => (
  <Collapsible
    className={cn(CODEX_ROW_CLASS, "text-codex-fg", className)}
    data-slot="codex-explored"
    {...props}
  >
    <span
      aria-hidden="true"
      className={cn(
        "text-codex-muted",
        active && "animate-pulse motion-reduce:animate-none"
      )}
    >
      •
    </span>
    <p className="font-bold">{active ? "Exploring" : "Explored"}</p>
    <ul className="col-start-2 grid min-w-0 grid-cols-[2ch_minmax(0,1fr)]">
      {items.map((item, index) => (
        <CodexExploredLine first={index === 0} item={item} key={item.id} />
      ))}
    </ul>
    {details.length > 0 && (
      <>
        <CollapsibleContent className="col-start-2 min-w-0">
          <ul className="grid grid-cols-[2ch_minmax(0,1fr)]">
            {details.map((item) => (
              <CodexExploredLine first={false} item={item} key={item.id} />
            ))}
          </ul>
        </CollapsibleContent>
        <CollapsibleTrigger
          className="col-start-2 ms-[2ch] justify-self-start"
          render={
            <Button
              className="text-codex-muted hover:text-codex-fg aria-expanded:text-codex-fg focus-visible:ring-codex-blue/60 h-auto rounded-xs p-0 text-start font-[inherit] text-[length:inherit] leading-[inherit] font-normal whitespace-normal hover:bg-transparent focus-visible:border-transparent focus-visible:ring-1 active:not-aria-[haspopup]:translate-y-0 aria-expanded:bg-transparent dark:hover:bg-transparent"
              variant="ghost"
            />
          }
        >
          <span className="in-data-panel-open:hidden">+ Show details</span>
          <span className="hidden in-data-panel-open:inline">
            − Hide details
          </span>
        </CollapsibleTrigger>
      </>
    )}
  </Collapsible>
);
```

### `registry/notra/codex/components/codex-working.tsx`

```tsx
"use client";

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

import {
  CODEX_BULLET_BLINK_MS,
  CODEX_ROW_CLASS,
  CODEX_SHIMMER_MS,
} from "../constants/codex";
import type { CodexInterruptedProps, CodexWorkingProps } from "../types/codex";

export const CodexWorking = ({
  className,
  elapsed = "0s",
  label = "Working",
  ...props
}: CodexWorkingProps) => {
  const labelRef = useRef<HTMLSpanElement>(null);
  const bulletRef = useRef<HTMLSpanElement>(null);

  useEffect(() => {
    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
      return;
    }
    const shimmer = labelRef.current?.animate(
      [{ backgroundPosition: "100% 0" }, { backgroundPosition: "0% 0" }],
      { duration: CODEX_SHIMMER_MS, iterations: Number.POSITIVE_INFINITY }
    );
    const blink = bulletRef.current?.animate(
      [{ opacity: 1 }, { opacity: 0.35 }, { opacity: 1 }],
      { duration: CODEX_BULLET_BLINK_MS, iterations: Number.POSITIVE_INFINITY }
    );
    return () => {
      shimmer?.cancel();
      blink?.cancel();
    };
  }, []);

  return (
    <div
      className={cn(CODEX_ROW_CLASS, "text-codex-dim", className)}
      data-slot="codex-working"
      role="status"
      {...props}
    >
      <span aria-hidden="true" className="text-codex-fg" ref={bulletRef}>
        •
      </span>
      <p className="min-w-0 truncate">
        <span
          className="bg-[linear-gradient(90deg,var(--codex-dim)_35%,var(--codex-fg)_50%,var(--codex-dim)_65%)] bg-size-[300%_100%] bg-clip-text bg-position-[100%_0] text-transparent"
          ref={labelRef}
        >
          {label}
        </span>{" "}
        ({elapsed} • <span className="text-codex-fg font-bold">esc</span> to
        interrupt)
      </p>
    </div>
  );
};

export const CodexInterrupted = ({
  children = "Conversation interrupted - tell the model what to do differently.",
  className,
  ...props
}: CodexInterruptedProps) => (
  <div
    className={cn(CODEX_ROW_CLASS, "text-codex-red", className)}
    data-slot="codex-interrupted"
    role="status"
    {...props}
  >
    <span aria-hidden="true">■</span>
    <p className="min-w-0 wrap-break-word">{children}</p>
  </div>
);
```

### `registry/notra/codex/components/codex-markdown.tsx`

```tsx
import { cn } from "cn";
import type { ComponentProps, ReactNode } from "react";

import type { CodexListProps, CodexTableProps } from "../types/codex";

/** Keys items by their text, counting repeats so duplicate rows stay unique. */
const withTextKeys = <T,>(items: readonly T[], toText: (item: T) => string) => {
  const seen = new Map<string, number>();
  return items.map((item) => {
    const text = toText(item);
    const count = (seen.get(text) ?? 0) + 1;
    seen.set(text, count);
    return { item, key: `${text}#${count}` };
  });
};

const nodeText = (node: ReactNode): string =>
  typeof node === "string" || typeof node === "number" ? String(node) : "";

export const CodexCode = ({ className, ...props }: ComponentProps<"code">) => (
  <code
    className={cn("text-codex-green font-[inherit]", className)}
    data-slot="codex-code"
    {...props}
  />
);

export const CodexList = ({ className, items, ...props }: CodexListProps) => (
  <ul
    className={cn("grid grid-cols-[2ch_minmax(0,1fr)]", className)}
    data-slot="codex-list"
    {...props}
  >
    {withTextKeys(items, nodeText).map(({ item, key }) => (
      <li className="contents" key={key}>
        <span aria-hidden="true">•</span>
        <span className="min-w-0">{item}</span>
      </li>
    ))}
  </ul>
);

export const CodexTable = ({
  className,
  codeColumns = [],
  headers,
  rows,
  ...props
}: CodexTableProps) => {
  const codeColumnSet = new Set(codeColumns);

  // The block mirrors the Codex terminal, so it renders a bare table rather
  // than the framed shadcn one.
  return (
    <div className="-ms-[2ch] max-w-[calc(100%+2ch)] overflow-x-auto">
      <table
        className={cn(
          "w-auto caption-bottom border-separate border-spacing-x-[2ch] border-spacing-y-0 text-start text-[length:inherit]",
          className
        )}
        data-slot="codex-table"
        {...props}
      >
        <thead>
          <tr>
            {headers.map((header) => (
              <th
                className="border-codex-rule text-codex-yellow border-b-2 px-[1ch] pb-[0.65em] text-start align-middle font-bold"
                key={header}
                scope="col"
              >
                {header}
              </th>
            ))}
          </tr>
        </thead>
        <tbody>
          {withTextKeys(rows, (row) => row.map(nodeText).join("|")).map(
            ({ item: row, key }) => (
              <tr className="group/codex-row" key={key}>
                {row.map((cell, columnIndex) => (
                  <td
                    className={cn(
                      "border-codex-rule border-b px-[1ch] py-[0.65em] align-top group-last/codex-row:border-b-0",
                      codeColumnSet.has(columnIndex) && "text-codex-green"
                    )}
                    key={headers[columnIndex] ?? `extra-${columnIndex}`}
                  >
                    {cell}
                  </td>
                ))}
              </tr>
            )
          )}
        </tbody>
      </table>
    </div>
  );
};
```

### `registry/notra/codex/components/codex-composer.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 {
  CODEX_DEFAULT_CWD,
  CODEX_DEFAULT_MODEL,
  CODEX_DEFAULT_PLACEHOLDER,
  CODEX_ROW_CLASS,
} from "../constants/codex";
import type { CodexComposerProps } from "../types/codex";

const CodexDot = () => <span className="text-codex-dim"> · </span>;

const CodexKey = ({ children }: { children: string }) => (
  <Kbd className="text-codex-fg font-codex inline h-auto min-w-0 rounded-none bg-transparent p-0 text-[length:inherit] leading-[inherit] font-bold">
    {children}
  </Kbd>
);

export const CodexComposer = ({
  "aria-label": ariaLabel,
  busy = false,
  className,
  context,
  cwd = CODEX_DEFAULT_CWD,
  defaultValue,
  effort,
  inputClassName,
  model = CODEX_DEFAULT_MODEL,
  onChange,
  onKeyDown,
  onSend,
  onStop,
  placeholder = CODEX_DEFAULT_PLACEHOLDER,
  task,
  value,
  warnings,
  ...props
}: CodexComposerProps) => {
  const [draft, setDraft] = useState(String(defaultValue ?? ""));
  const isControlled = value !== undefined;
  const text = isControlled ? String(value) : draft;
  const isEmpty = text.length === 0;
  const [cursorChar = " ", ...restChars] = Array.from(placeholder);

  const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
    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-codex text-codex-dim min-w-0 text-[0.8125rem] leading-[1.3]",
        className
      )}
      data-slot="codex-composer"
      onSubmit={handleSubmit}
    >
      <InputGroup
        className={cn(
          CODEX_ROW_CLASS,
          "bg-codex-composer dark:bg-codex-composer h-auto rounded-none border-0 py-[1.3em] has-[[data-slot=input-group-control]:focus-visible]:ring-0"
        )}
      >
        <InputGroupAddon className="text-codex-fg order-none cursor-text justify-start p-0 text-[length:inherit] leading-[inherit] font-bold">
          <span aria-hidden="true">›</span>
        </InputGroupAddon>
        <div className="relative min-w-0">
          {isEmpty && (
            <span
              aria-hidden="true"
              className="text-codex-placeholder pointer-events-none absolute inset-0 truncate"
            >
              <span className="bg-codex-cursor text-codex-fg">
                {cursorChar}
              </span>
              {restChars.join("")}
            </span>
          )}
          <InputGroupInput
            aria-label={ariaLabel ?? placeholder}
            className={cn(
              "text-codex-fg caret-codex-cursor h-auto p-0 font-[inherit] text-[0.8125rem] leading-[1.3] md:text-[0.8125rem]",
              isEmpty && "caret-transparent",
              inputClassName
            )}
            autoComplete="off"
            enterKeyHint="send"
            onChange={handleChange}
            onKeyDown={handleKeyDown}
            type="text"
            value={text}
            {...props}
          />
        </div>
      </InputGroup>
      <div className={cn(CODEX_ROW_CLASS, "mt-[0.5em]")}>
        <p className="col-start-2 min-w-0 truncate">
          <span className="text-codex-yellow">
            {effort ? `${model} ${effort}` : model}
          </span>
          <CodexDot />
          <span className="text-codex-green">{cwd}</span>
          {task && (
            <>
              <CodexDot />
              <span className="text-codex-yellow">{task}</span>
            </>
          )}
        </p>
        <div className="col-start-2 flex min-w-0 flex-wrap justify-between gap-x-[2ch]">
          <p>
            <CodexKey>←</CodexKey> for agents
            <CodexDot />
            <CodexKey>?</CodexKey> for shortcuts
          </p>
          {warnings ? (
            <p>
              <span className="text-codex-amber">
                <span aria-hidden="true" className="inline-block w-[1ch]">
                  ⚠
                </span>
                {` ${warnings} ${warnings === 1 ? "warning" : "warnings"}`}
              </span>
              <CodexDot />
              <CodexKey>f2</CodexKey> to view
            </p>
          ) : (
            context && <p>{context}</p>
          )}
        </div>
      </div>
    </form>
  );
};
```

### `registry/notra/codex/lib/codex-shell.ts`

```ts
import type { CodexShellToken } from "../types/codex";

const SHELL_TOKEN_PATTERN =
  /(\s+)|('[^']*'?|"[^"]*"?)|(&&|\|\||[|;<>])|([^\s'"|;&<>]+|&)/g;

export const tokenizeShellCommand = (command: string): CodexShellToken[] => {
  const tokens: CodexShellToken[] = [];
  let expectsCommand = true;

  for (const match of command.matchAll(SHELL_TOKEN_PATTERN)) {
    const [text, space, quoted, operator] = match;
    if (space) {
      tokens.push({ kind: "space", text });
    } else if (quoted) {
      tokens.push({ kind: "string", text });
      expectsCommand = false;
    } else if (operator) {
      tokens.push({ kind: "operator", text });
      expectsCommand = true;
    } else if (expectsCommand) {
      tokens.push({ kind: "command", text });
      expectsCommand = false;
    } else {
      tokens.push({ kind: text.startsWith("-") ? "flag" : "argument", text });
    }
  }

  return tokens;
};
```

### `registry/notra/codex/constants/codex.ts`

```ts
import type { CodexExecStatus } from "../types/codex";

export const CODEX_DEFAULT_VERSION = "0.159.2";

export const CODEX_DEFAULT_MODEL = "gpt-6.1-codex";

export const CODEX_DEFAULT_CWD = "~/acme/web";

export const CODEX_DEFAULT_PLACEHOLDER = "Ask Codex to do anything";

export const CODEX_DEFAULT_GREETING = "Anything interesting on the docket?";

export const CODEX_EXEC_PREVIEW_LINES = 3;

export const CODEX_SHIMMER_MS = 2000;

export const CODEX_BULLET_BLINK_MS = 1200;

/** Pacing of a played-back turn in the demo chat. */
export const CODEX_CHAT_TIMING = {
  /** Working time before the first command. */
  thinkMs: 1100,
  /** How long each command shows as running. */
  execMs: 1000,
  /** Delay between streamed words. */
  wordMs: 26,
  /** Cap on every delay when the user prefers reduced motion. */
  reducedMaxMs: 60,
  /** How often the elapsed seconds tick. */
  tickMs: 1000,
} as const;

export const CODEX_EXEC_STATUS_LABEL: Record<CodexExecStatus, string> = {
  failed: "Ran",
  ran: "Ran",
  running: "Running",
};

export const CODEX_EXEC_STATUS_CLASS: Record<CodexExecStatus, string> = {
  failed: "text-codex-red",
  ran: "text-codex-success",
  running: "text-codex-muted animate-pulse motion-reduce:animate-none",
};

export const CODEX_SHELL_TOKEN_CLASS = {
  argument: "text-codex-arg",
  command: "text-codex-command",
  flag: "text-codex-flag",
  operator: "text-codex-operator",
  space: "",
  string: "text-codex-green",
} as const;

export const CODEX_ROW_CLASS =
  "grid grid-cols-[2ch_minmax(0,1fr)] px-[1ch] font-codex text-[0.8125rem] leading-[1.3]";
```

### `registry/notra/codex/constants/codex-demo.ts`

```ts
import type {
  CodexDemoExec,
  CodexDemoSession,
  CodexReply,
} from "../types/codex";

export const CODEX_DEMO_SESSION: CodexDemoSession = {
  composer: {
    effort: "low",
    model: "gpt-6.1-codex",
    placeholder: "Ask Codex to do anything",
    task: "Draft changelog",
    warnings: 2,
  },
  exec: {
    command:
      "git log --oneline --since='1 week' | rg -i 'feat|fix' && cat CHANGELOG.md",
    output: [
      "a41c9e2 feat(geo): track citations per model",
      "7d02b18 fix(scheduler): retry failed publish jobs",
      "c9e5f11 feat(studio): brand voice presets",
      "4be0a7d fix(api): paginate /v1/posts past 100 items",
      "91f3c20 fix(geo): dedupe prompts across projects",
      "e03d6b4 feat(api): add /v1/citations endpoint",
      "2c7a9f8 fix(ui): focus ring on the composer",
      "## 2026-09-23",
      "- Scheduler v2 with 40% faster builds",
    ].join("\n"),
  },
  explored: [
    {
      id: "search-changelog",
      scope: "CHANGELOG.md",
      target: "changelog|release",
      verb: "Search",
    },
    { id: "read-package", target: "package.json", verb: "Read" },
  ],
  exploredDetails: [
    { id: "list-apps", target: "apps", verb: "List" },
    { id: "read-readme", target: "apps/dashboard/README.md", verb: "Read" },
  ],
  followUp: "Want me to publish it to acme.com/changelog?",
  header: {
    cwd: "~/acme/web",
    version: "0.159.2",
  },
  highlights: [
    {
      id: "citations",
      label: "GEO citations:",
      text: "See which sources each AI model cites for your tracked prompts.",
    },
    {
      id: "voice",
      label: "Brand voice presets:",
      text: "Save a voice once and reuse it for every changelog and post.",
    },
    {
      id: "scheduler",
      label: "Scheduler:",
      text: "Failed publish jobs now retry on their own.",
    },
  ],
  intro:
    "I'll check this week's merged commits and the current changelog, then draft the new entry.",
  reasoning: "Grouping commits into features and fixes",
  summary: "This week shipped 3 features and 4 fixes. Here is the draft:",
  table: {
    headers: ["Folder", "Change"],
    rows: [
      ["apps/dashboard", "Citations view on the GEO page"],
      ["packages/geo-core", "Citation tracking per model"],
      ["apps/api", "New /v1/citations endpoint"],
      ["packages/ui", "Brand voice picker"],
    ],
  },
  tableIntro: "The changes touch these parts of the repo:",
  title: "codex — ~/acme/web",
  userMessage: "what shipped this week? draft the changelog",
};

export const CODEX_DEMO_EXEC_STATES: CodexDemoExec[] = [
  {
    command: "notra list_events --range week --json | jq '.[].title'",
    id: "ran",
    output: [
      '"Scheduler v2"',
      '"GEO citations"',
      '"Brand voice presets"',
      '"Paginate /v1/posts"',
      '"Dedupe prompts"',
    ].join("\n"),
    status: "ran",
  },
  {
    command: "notra create_post --type changelog --voice 'acme'",
    id: "running",
    status: "running",
  },
  {
    command: "notra publish_post changelog",
    id: "failed",
    output: "error: missing NOTRA_API_KEY",
    status: "failed",
  },
];

/** Answers the demo plays back, one per prompt you send, in order. */
export const CODEX_REPLIES: CodexReply[] = [
  {
    execs: [
      {
        command: "notra publish_post changelog --site acme.com",
        id: "publish",
        output: [
          "published changelog-2026-09-30",
          "https://acme.com/changelog/2026-09-30",
        ].join("\n"),
        status: "ran",
      },
    ],
    text: "Published. The entry is live at `acme.com/changelog/2026-09-30` and the RSS feed picked it up.\n\nWant a short post for LinkedIn as well?",
  },
  {
    execs: [
      {
        command: "rg -n 'retry' packages/api/src/jobs",
        id: "grep-retry",
        output: [
          "publish.ts:41:  const retry = createRetry({ attempts: 3 });",
          "publish.ts:58:  await retry(() => sendToSite(post));",
          "publish.test.ts:12:it('retries a failed publish', async () => {",
        ].join("\n"),
        status: "ran",
      },
    ],
    text: "The retry lives in `packages/api/src/jobs/publish.ts`. Failed publish jobs retry three times with backoff, and `publish.test.ts` covers it.",
  },
  {
    execs: [
      {
        command: "bun run test --filter api",
        id: "tests",
        output: "212 pass\n0 fail",
        status: "ran",
      },
    ],
    text: "All 212 tests pass. Nothing is blocking the release.",
  },
];
```

### `registry/notra/codex/types/codex.ts`

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

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

export type CodexExecStatus = "ran" | "running" | "failed";

export type CodexMessageAuthor = "user" | "assistant";

export type CodexShellTokenKind =
  | "command"
  | "flag"
  | "string"
  | "operator"
  | "argument"
  | "space";

export interface CodexShellToken {
  kind: CodexShellTokenKind;
  text: string;
}

export type CodexExploredVerb = "Read" | "Search" | "List";

export interface CodexExploredItem {
  id: string;
  scope?: string;
  target: string;
  verb: CodexExploredVerb;
}

export interface CodexTerminalProps extends Omit<
  ComponentProps<typeof Card>,
  "title"
> {
  footer?: ReactNode;
  title?: ReactNode;
}

export interface CodexHeaderProps extends ComponentProps<"div"> {
  cwd?: string;
  greeting?: ReactNode;
  version?: string;
}

export interface CodexMessageProps extends ComponentProps<"div"> {
  from?: CodexMessageAuthor;
}

export interface CodexReasoningProps extends ComponentProps<"div"> {
  children?: ReactNode;
}

export interface CodexExecProps extends Omit<
  ComponentProps<typeof Collapsible>,
  "children"
> {
  command: string;
  output?: string;
  previewLines?: number;
  status?: CodexExecStatus;
}

export interface CodexExploredProps extends Omit<
  ComponentProps<typeof Collapsible>,
  "children"
> {
  active?: boolean;
  details?: CodexExploredItem[];
  items: CodexExploredItem[];
}

export interface CodexWorkingProps extends ComponentProps<"div"> {
  elapsed?: string;
  label?: string;
}

export interface CodexInterruptedProps extends ComponentProps<"div"> {
  children?: ReactNode;
}

export interface CodexTableProps extends ComponentProps<"table"> {
  codeColumns?: number[];
  headers: string[];
  rows: ReactNode[][];
}

export interface CodexListProps extends ComponentProps<"ul"> {
  items: ReactNode[];
}

export interface CodexComposerProps extends Omit<
  ComponentProps<"input">,
  "className" | "type"
> {
  /** A turn is running: Enter does not send and Escape calls `onStop`. */
  busy?: boolean;
  className?: string;
  context?: string;
  cwd?: string;
  effort?: string;
  inputClassName?: string;
  model?: string;
  /**
   * Called with the trimmed text on Enter. An uncontrolled composer clears
   * afterwards; with `value`, clear it yourself here.
   */
  onSend?: (text: string) => void;
  /** Called on Escape while `busy`, to interrupt the running turn. */
  onStop?: () => void;
  task?: string;
  warnings?: number;
}

export interface CodexDemoExec {
  command: string;
  id: string;
  output?: string;
  status: CodexExecStatus;
}

export interface CodexDemoHighlight {
  id: string;
  label: string;
  text: string;
}

export interface CodexDemoSession {
  composer: {
    effort: string;
    model: string;
    placeholder: string;
    task: string;
    warnings: number;
  };
  exec: {
    command: string;
    output: string;
  };
  explored: CodexExploredItem[];
  exploredDetails: CodexExploredItem[];
  followUp: string;
  header: {
    cwd: string;
    version: string;
  };
  highlights: CodexDemoHighlight[];
  intro: string;
  reasoning: string;
  summary: string;
  table: {
    headers: string[];
    rows: string[][];
  };
  tableIntro: string;
  title: string;
  userMessage: string;
}

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

/** A scripted answer the demo plays back when you send a prompt. */
export interface CodexReply {
  /** Commands Codex runs before it answers, with their final status. */
  execs: CodexDemoExec[];
  /** The answer. Blank lines split paragraphs, backticks mark code. */
  text: string;
}

export interface CodexChatTurn {
  execs: CodexDemoExec[];
  id: string;
  prompt: string;
  reply: string;
  status: CodexTurnStatus;
}

export interface CodexChat {
  busy: boolean;
  /** Whole seconds since the running turn started. */
  elapsed: number;
  send: (text: string) => Promise<void>;
  stop: () => void;
  turns: CodexChatTurn[];
}
```

### `registry/notra/codex/hooks/use-codex-chat.ts`

```ts
"use client";

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

import { CODEX_CHAT_TIMING } from "../constants/codex";
import type { CodexChat, CodexChatTurn, CodexReply } from "../types/codex";

const WORD_PATTERN = /(\s+)/;

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

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

/**
 * Runs the demo conversation: `send` appends your prompt, works, runs the
 * reply's commands one by one and streams the answer. `stop` interrupts the
 * running turn like Escape does.
 */
export const useCodexChat = (replies: readonly CodexReply[]): CodexChat => {
  const [turns, setTurns] = useState<CodexChatTurn[]>([]);
  const [activeId, setActiveId] = useState<string | null>(null);
  const [startedAt, setStartedAt] = useState(0);
  const [now, setNow] = 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());
    }, CODEX_CHAT_TIMING.tickMs);
    return () => window.clearInterval(timer);
  }, [activeId]);

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

  const patch = useCallback(
    (id: string, next: (turn: CodexChatTurn) => Partial<CodexChatTurn>) => {
      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) => ({
      execs: turn.execs.filter((exec) => exec.status !== "running"),
      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, CODEX_CHAT_TIMING.reducedMaxMs) : ms);
      const start = Date.now();
      const id = `turn-${start}`;

      activeRef.current = id;
      setActiveId(id);
      setStartedAt(start);
      setNow(start);
      setTurns((current) => [
        ...current,
        { execs: [], id, prompt: text, reply: "", status: "working" },
      ]);

      await delay(CODEX_CHAT_TIMING.thinkMs);

      for (const exec of reply.execs) {
        if (!alive()) {
          return;
        }
        const execId = `${id}-${exec.id}`;
        patch(id, (turn) => ({
          execs: [
            ...turn.execs,
            { command: exec.command, id: execId, status: "running" },
          ],
        }));
        // react-doctor-disable-next-line react-doctor/async-await-in-loop -- commands run one after another
        await delay(CODEX_CHAT_TIMING.execMs);
        if (!alive()) {
          return;
        }
        patch(id, (turn) => ({
          execs: turn.execs.map((item) =>
            item.id === execId ? { ...exec, id: execId } : item
          ),
        }));
      }

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

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

      if (!alive()) {
        return;
      }
      patch(id, () => ({ status: "done" }));
      finish();
    },
    [finish, patch, replies]
  );

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

### `registry/notra/codex/examples/codex-demo.tsx`

```tsx
"use client";

import { Fragment, useEffect, useRef } from "react";

import { CodexComposer } from "../components/codex-composer";
import { CodexExec } from "../components/codex-exec";
import { CodexExplored } from "../components/codex-explored";
import { CodexHeader } from "../components/codex-header";
import { CodexCode, CodexList, CodexTable } from "../components/codex-markdown";
import { CodexMessage } from "../components/codex-message";
import { CodexTerminal } from "../components/codex-terminal";
import { CodexInterrupted, CodexWorking } from "../components/codex-working";
import { CODEX_DEMO_SESSION, CODEX_REPLIES } from "../constants/codex-demo";
import { useCodexChat } from "../hooks/use-codex-chat";
import type { CodexChatTurn } from "../types/codex";

const PARAGRAPH_SEPARATOR = "\n\n";

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

const CODE_PATTERN = /(`[^`]+`)/;

/** Renders one paragraph, turning `backticks` into code. */
const CodexInline = ({ text }: { text: string }) => {
  let offset = 0;
  return text.split(CODE_PATTERN).map((part) => {
    const key = `${offset}-${part}`;
    offset += part.length;
    return part.length > 2 && part.startsWith("`") && part.endsWith("`") ? (
      <CodexCode key={key}>{part.slice(1, -1)}</CodexCode>
    ) : (
      <Fragment key={key}>{part}</Fragment>
    );
  });
};

const CodexTurn = ({ turn }: { turn: CodexChatTurn }) => {
  let offset = 0;
  const paragraphs = turn.reply.split(PARAGRAPH_SEPARATOR).map((text) => {
    const key = offset;
    offset += text.length + PARAGRAPH_SEPARATOR.length;
    return { key, text };
  });

  return (
    <>
      <CodexMessage from="user">{turn.prompt}</CodexMessage>
      {turn.execs.map(({ id, ...exec }) => (
        <CodexExec key={id} {...exec} />
      ))}
      {turn.reply && (
        <CodexMessage>
          {paragraphs.map((paragraph) => (
            <p key={paragraph.key}>
              <CodexInline text={paragraph.text} />
            </p>
          ))}
        </CodexMessage>
      )}
      {turn.status === "interrupted" && <CodexInterrupted />}
    </>
  );
};

export default function CodexDemo() {
  const session = CODEX_DEMO_SESSION;
  const chat = useCodexChat(CODEX_REPLIES);
  const terminalRef = useRef<HTMLDivElement>(null);
  const followRef = useRef(false);

  // 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);
  };

  return (
    <div className="w-full min-w-0">
      <CodexTerminal
        className="h-170"
        footer={
          <>
            {chat.busy && (
              <CodexWorking
                className="pb-[0.65em]"
                elapsed={`${chat.elapsed}s`}
              />
            )}
            <CodexComposer
              busy={chat.busy}
              cwd={session.header.cwd}
              effort={session.composer.effort}
              model={session.composer.model}
              onSend={handleSend}
              onStop={chat.stop}
              placeholder={session.composer.placeholder}
              task={session.composer.task}
              warnings={session.composer.warnings}
            />
          </>
        }
        ref={terminalRef}
        title={session.title}
      >
        <CodexHeader {...session.header} />
        <CodexMessage from="user">{session.userMessage}</CodexMessage>
        <CodexMessage>{session.intro}</CodexMessage>
        <CodexExec {...session.exec} />
        <CodexExplored
          details={session.exploredDetails}
          items={session.explored}
        />
        <CodexMessage>
          <p>{session.summary}</p>
          <CodexList
            items={session.highlights.map((highlight) => (
              <Fragment key={highlight.id}>
                <strong>{highlight.label}</strong> {highlight.text}
              </Fragment>
            ))}
          />
          <p>{session.tableIntro}</p>
          <CodexTable codeColumns={[0]} {...session.table} />
          <p>{session.followUp}</p>
        </CodexMessage>
        {chat.turns.map((turn) => (
          <CodexTurn key={turn.id} turn={turn} />
        ))}
      </CodexTerminal>
    </div>
  );
}
```
