← ui

codex

ui

OpenAI's Codex CLI terminal UI, drawn from an agent session

npx shadcn add @ag/codex
Already installed? Preview an update
npx shadcn add @ag/codex --dry-run

Your installed source stays unchanged until you update it. How updates work

Latest changes · 2026-09-30
  • New: OpenAI Codex CLI's terminal UI drawn from an agent-session: the header box, › prompts, • replies and Ran commands with their └ output, the working line, the ─ Worked for 2h 14m rule a finished turn leaves, the composer. Same terminal reading as claude-code.
Updated 2026-09-30 · release notes

shelf devices & demos

uses agent-session

the demo above, verbatim

codex.demo.tsx
"use client"

import { Sample } from "@/app/samples"
import type { AgentEntry } from "@/registry/base-nova/lib/agent-session"
import { Codex } from "@/registry/base-nova/ui/codex"

// #region session
const ENTRIES: AgentEntry[] = [
  {
    kind: "prompt",
    text: "Refactor the invoice pipeline to stream, then run the tests.",
  },
  { kind: "say", text: "Reading the pipeline first." },
  {
    kind: "tool",
    name: "Bash",
    arg: "rg -n 'buildInvoice' src",
    result: ["src/invoice/pipeline.ts:42: export function buildInvoice("],
  },
]
// #endregion

export default function CodexDemo() {
  return (
    <Sample
      name="working"
      glance="large"
      title="at work"
      description="a transcript and the working line"
      with="session"
    >
      <div className="h-[420px] rounded-xl bg-[#161617] p-5 font-mono text-[13px] leading-[1.45]">
        <Codex
          entries={ENTRIES}
          work={{ seconds: 64, request: 12, tokens: 1800, ms: 0 }}
          draft=""
          cwd="~/invoices"
        />
      </div>
    </Sample>
  )
}

the source, every file an install writes

codex.tsx
// OpenAI Codex CLI's terminal UI, drawn from an agent-session: the header box, the
// transcript (› prompts, • replies, • Ran commands with their └ output), the working
// line, and the composer. Presentational: it fills its box with the terminal's own
// monospace and text size, and reads like a terminal: from the top while it fits,
// the oldest lines scrolled away once it does not (a column-reverse box anchors its
// one child at the bottom, and the child's auto bottom margin takes any free space:
// at the top while it fits, anchored at the bottom with its top cut once it
// overflows). A page or a story supplies the entries.

import { cn } from "@/lib/utils"
import {
  type AgentEntry,
  type AgentViewProps,
  formatElapsed,
} from "@/registry/base-nova/lib/agent-session"

export function Codex({
  entries,
  work,
  finished,
  draft,
  cwd = "~",
  className,
}: AgentViewProps) {
  return (
    <div
      data-slot="codex"
      className={cn(
        "flex h-full min-h-0 flex-col-reverse overflow-hidden whitespace-pre-wrap text-[#e6e6e6]",
        className,
      )}
    >
      <div className="mb-auto flex flex-col gap-[0.9em]">
        <div className="self-start rounded-[6px] border border-[#4a4a4a] px-[1.2ch] py-[0.5em]">
          <div>
            <span className="text-[#8a8a8a]">{">_ "}</span>
            <span className="font-semibold">OpenAI Codex</span>
          </div>
          <div className="text-[#8a8a8a]">
            {"\nmodel:     gpt-5-codex\ndirectory: "}
            {cwd}
          </div>
        </div>
        {entries.map((e, i) => (
          // biome-ignore lint/suspicious/noArrayIndexKey: a transcript is positional
          <Entry key={i} entry={e} />
        ))}
        {work && (
          <div>
            <span className="text-[#8a8a8a]">◦ </span>
            <span className="font-semibold">Working</span>{" "}
            <span className="text-[#8a8a8a]">
              ({formatElapsed(work.seconds)} • esc to interrupt)
            </span>
          </div>
        )}
        {finished && (
          <div className="flex items-center gap-[1ch] text-[#6f6f6f]">
            <span>─ Worked for {formatElapsed(finished.seconds)}</span>
            <span className="h-px flex-1 bg-[#3a3a3a]" />
          </div>
        )}
        <div>
          <div className="bg-white/[0.06] px-[1ch] py-[0.3em]">
            <span className="text-[#8a8a8a]">{"› "}</span>
            {draft || (
              <span className="text-[#6f6f6f]">Ask Codex to do anything</span>
            )}
          </div>
          <div className="px-[1ch] pt-[0.2em] text-[#6f6f6f]">
            {"⏎ send   ⌃J newline   ⌃T transcript   ⌃C quit"}
          </div>
        </div>
      </div>
    </div>
  )
}

function Entry({ entry: e }: { entry: AgentEntry }) {
  if (e.kind === "prompt")
    return (
      <div className="-mx-[1ch] bg-white/[0.06] px-[1ch] py-[0.2em]">
        <span className="text-[#8a8a8a]">{"› "}</span>
        {e.text}
      </div>
    )
  if (e.kind === "say")
    return (
      <div className="flex gap-[1ch]">
        <span className="text-[#8a8a8a]">•</span>
        <span>{e.text}</span>
      </div>
    )
  return (
    <div>
      <div className="flex gap-[1ch]">
        <span className="text-[#8a8a8a]">•</span>
        <span>
          <span className="font-semibold">
            {e.result.length ? "Ran" : "Running"}
          </span>{" "}
          {e.arg}
        </span>
      </div>
      {e.result.map((line, i) => (
        // biome-ignore lint/suspicious/noArrayIndexKey: output lines are positional
        <div key={i} className="text-[#9a9a9a]">
          {i === 0 ? "  └ " : "    "}
          {line.trimStart()}
        </div>
      ))}
    </div>
  )
}