# Code View

> Show a file's code or a diff of it, with line numbers and a copy button.

Source: https://ai.honestui.com/docs/components/code-view

```tsx
import { CodeView } from "@/components/ai/code-view"

const sampleCode = `function exportTotal(rows: Row[]) {
  const total = rows.reduce((sum, row) => sum + row.amount, 0)
  return roundTotal(total)
}`

export function CodeViewDemo() {
  return (
    <div className="w-full max-w-md min-w-0">
      <CodeView filename="export.ts" code={sampleCode} />
    </div>
  )
}

```

## Overview [#overview]

Code View shows code an agent wrote or changed, in a compact panel. Pass `code` for a numbered listing, or `diff` for a unified diff where each changed line carries a sign and a tint.

It does not highlight syntax. Most products already have a highlighter, and a second one would disagree with it. Pass `renderLine` to color each line with yours.

## Anatomy [#anatomy]

<Anatomy title="CodeView">
  <AnatomyItem name="Header" description="The file name, the diff counts, and Copy" />

  <AnatomyItem name="Lines" description="Numbered lines in a scrolling block" />

  <AnatomyItem name="Sign" description="A plus or minus before each line of a diff" />
</Anatomy>

Installing `code-view` brings [Button](https://www.honestui.com/docs/components/button) along as a registry dependency.

## Behavior [#behavior]

### Listing and diff [#listing-and-diff]

A listing numbers every line. In a diff, removed lines take no number, so the numbers match the new file.

### Copy [#copy]

Copy puts the code on the clipboard. For a diff, it copies the new file's lines and leaves removed lines out. The icon changes to a check for a moment. If the browser refuses clipboard access, the block says "Copy failed" instead of pretending it worked.

### Actions [#actions]

`actions` puts your own controls in the header, before Copy. Pass a button for each action your application handles, such as downloading the file. An embedded Code View has no header, so it shows none.

### Long lines [#long-lines]

Lines do not wrap. The block scrolls sideways.

### Embedded [#embedded]

`embedded` drops the frame and the header, leaving the numbered lines. Use it inside a surface that already names the file, such as an [Artifact Panel](/docs/components/artifact-panel). The header's Copy button and a diff's counts go with it, so supply Copy from the surrounding surface. The code region is still named by `filename`.

## Accessibility [#accessibility]

The block is a figure with the file name as its caption. The code region can take focus, so a keyboard user can scroll long lines, and it is named by the file.

Copy announces "Copied" or "Copy failed" through a polite status. Diff counts are read as "2 added" and "1 removed". A changed line is marked by its sign as well as its tint, and keeps the primary text color.

| Key                                  | Action                                         |
| ------------------------------------ | ---------------------------------------------- |
| <kbd>Tab</kbd>                       | Move to your actions, then Copy, then the code |
| Arrow keys                           | Scroll the focused code                        |
| <kbd>Enter</kbd> or <kbd>Space</kbd> | Copy                                           |

## Installation [#installation]


  

  
    <CliBlock commands="[&#x22;@honestai/code-view&#x22;]" />
  

  
    
      
        Add the components it is built from.
      

      <CliBlock commands="[&#x22;button&#x22;]" />

      
        Copy and paste this file into your project.
      

      ### components/ai/code-view.tsx

```tsx
"use client"

import * as React from "react"
import { Check as CheckIcon, Code as CodeIcon, Copy as CopyIcon } from "honestui/icons"

import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"

export type CodeDiffLine = {
  text: string
  /** Defaults to "context". */
  kind?: "context" | "added" | "removed"
}

export type CodeViewLabels = {
  copy: string
  copied: string
  copyFailed: string
  added: (count: number) => string
  removed: (count: number) => string
}

export const defaultCodeViewLabels: CodeViewLabels = {
  copy: "Copy",
  copied: "Copied",
  copyFailed: "Copy failed",
  added: (count) => `${count} added`,
  removed: (count) => `${count} removed`,
}

const COPIED_MILLISECONDS = 1500

// Changed lines keep the primary text color on a tint, which stays readable at this size.
const lineTone: Record<NonNullable<CodeDiffLine["kind"]>, string> = {
  context: "",
  added: "bg-[var(--hui-color-background-success-primary)]",
  removed: "bg-[var(--hui-color-background-danger-primary)]",
}

const lineSign: Record<NonNullable<CodeDiffLine["kind"]>, string> = {
  context: " ",
  added: "+",
  removed: "−",
}

export type CodeViewProps = Omit<React.ComponentProps<"figure">, "children" | "onCopy"> & {
  /** The file name or a short title, shown in the header. */
  filename: string
  /** The source to list, with line numbers. Ignored when `diff` is passed. */
  code?: string
  /** A unified diff. Each changed line is marked with a sign and a tint. */
  diff?: CodeDiffLine[]
  /** Colors one line. `index` counts the listed lines from zero. Without it, lines are plain text. */
  renderLine?: (text: string, index: number) => React.ReactNode
  /** Controls for the header, placed before Copy. */
  actions?: React.ReactNode
  /** Runs after the text reaches the clipboard. */
  onCopy?: (text: string) => void
  /** Drops the frame and the header, for a surface that supplies its own, such as an Artifact Panel. */
  embedded?: boolean
  labels?: Partial<CodeViewLabels>
}

/**
 * A file's code, or a diff of it, with a copy button. It does not highlight
 * syntax itself; pass `renderLine` to use the highlighter you already have.
 */
function CodeView({
  filename,
  code = "",
  diff,
  renderLine,
  actions,
  onCopy,
  embedded = false,
  labels: labelsProp,
  className,
  ...props
}: CodeViewProps) {
  const labels = React.useMemo(
    () => ({ ...defaultCodeViewLabels, ...labelsProp }),
    [labelsProp]
  )
  const [copyState, setCopyState] = React.useState<"idle" | "copied" | "failed">("idle")
  const resetTimer = React.useRef<number | undefined>(undefined)
  React.useEffect(() => () => window.clearTimeout(resetTimer.current), [])

  const lines: CodeDiffLine[] = diff ?? code.split("\n").map((text) => ({ text }))
  // Copy what the file would contain: a diff's removed lines are left out.
  const copyText = diff
    ? diff
        .filter((line) => line.kind !== "removed")
        .map((line) => line.text)
        .join("\n")
    : code
  const addedCount = diff?.filter((line) => line.kind === "added").length ?? 0
  const removedCount = diff?.filter((line) => line.kind === "removed").length ?? 0

  const copy = async () => {
    try {
      await navigator.clipboard.writeText(copyText)
      setCopyState("copied")
      onCopy?.(copyText)
    } catch {
      setCopyState("failed")
    }
    window.clearTimeout(resetTimer.current)
    resetTimer.current = window.setTimeout(() => setCopyState("idle"), COPIED_MILLISECONDS)
  }

  // Removed lines are not in the new file, so they take no number.
  const lineNumbers: (number | null)[] = []
  for (const line of lines) {
    const previous = lineNumbers.findLast((value) => value !== null) ?? 0
    lineNumbers.push(line.kind === "removed" ? null : previous + 1)
  }

  return (
    <figure
      data-slot="code-view"
      data-embedded={embedded ? "" : undefined}
      className={cn(
        // `not-prose` keeps a prose around this figure from restyling its code.
        "not-prose m-0 w-full min-w-0 text-[var(--hui-color-foreground-base-primary)]",
        !embedded &&
          "overflow-hidden rounded-[var(--hui-radius-4)] border-[0.5px] border-[var(--hui-color-border-base-primary)] bg-[var(--hui-color-background-base-primary)] shadow-[var(--hui-shadow-feather)]",
        className
      )}
      {...props}
    >
      {!embedded && (
        <figcaption className="flex min-w-0 items-center gap-[var(--hui-space-3)] border-b-[0.5px] border-[var(--hui-color-border-base-primary)] py-[var(--hui-space-2)] ps-[var(--hui-space-4)] pe-[var(--hui-space-2)] [font-size:var(--hui-font-size-small)]">
          <CodeIcon
            aria-hidden="true"
            className="size-3.5 shrink-0 text-[var(--hui-color-foreground-base-tertiary)]"
          />
          <span className="min-w-0 truncate font-mono [font-size:var(--hui-font-size-mono-small)]">
            {filename}
          </span>
          {diff && (
            <span className="shrink-0 font-mono tabular-nums [font-size:var(--hui-font-size-mono-mini)]">
              <span className="text-[var(--hui-color-foreground-success-primary-hover)]">
                <span aria-hidden="true">+{addedCount}</span>
                <span className="sr-only">{labels.added(addedCount)}</span>
              </span>{" "}
              <span className="text-[var(--hui-color-foreground-danger-primary-hover)]">
                <span aria-hidden="true">−{removedCount}</span>
                <span className="sr-only">{labels.removed(removedCount)}</span>
              </span>
            </span>
          )}
          <div className="ms-auto flex shrink-0 items-center gap-[var(--hui-space-2)]">
            {actions}
            <Button variant="link" size="sm" onClick={() => void copy()}>
              {copyState === "copied" ? <CheckIcon aria-hidden="true" /> : <CopyIcon aria-hidden="true" />}
              {labels.copy}
            </Button>
          </div>
          <span role="status" className="sr-only">
            {copyState === "copied" ? labels.copied : copyState === "failed" ? labels.copyFailed : ""}
          </span>
        </figcaption>
      )}
      {!embedded && copyState === "failed" && (
        <p className="m-0 border-b-[0.5px] border-[var(--hui-color-border-base-primary)] px-[var(--hui-space-4)] py-[var(--hui-space-2)] [font-size:var(--hui-font-size-small)]">
          {labels.copyFailed}
        </p>
      )}
      {/* Focusable so a keyboard user can scroll long lines. */}
      <pre
        tabIndex={0}
        aria-label={filename}
        className="m-0 overflow-x-auto py-[var(--hui-space-3)] font-mono outline-none [font-size:var(--hui-font-size-mono-small)] [line-height:1.65] focus-visible:[outline:var(--hui-focus-ring)] focus-visible:outline-offset-[var(--hui-focus-ring-offset-inset)]"
      >
        <code className="block w-max min-w-full">
          {lines.map((line, index) => {
            const kind = line.kind ?? "context"
            return (
              <span key={index} data-kind={kind} className={cn("flex", lineTone[kind])}>
                <span
                  aria-hidden="true"
                  className="w-10 shrink-0 select-none pe-[var(--hui-space-3)] text-end tabular-nums text-[var(--hui-color-foreground-base-secondary)]"
                >
                  {lineNumbers[index] ?? ""}
                </span>
                {diff && <span className="w-[2ch] shrink-0 select-none">{lineSign[kind]}</span>}
                <span className="pe-[var(--hui-space-4)] whitespace-pre">
                  {renderLine ? renderLine(line.text, index) : line.text}
                  {"\n"}
                </span>
              </span>
            )
          })}
        </code>
      </pre>
    </figure>
  )
}

export { CodeView }

```

      
        Update the import paths to match your project setup.
      
    
  


## Usage [#usage]

```tsx
import { CodeView } from "@/components/ai/code-view"

<CodeView filename={file.path} code={file.contents} />

<CodeView filename={edit.path} diff={edit.lines} renderLine={(text) => highlight(text, "ts")} />
```

## Examples [#examples]

### Diff [#diff]

Two lines added and one removed. The numbers follow the new file.

```tsx
import { CodeView } from "@/components/ai/code-view"

export function CodeViewDiffExample() {
  return (
    <div className="w-full max-w-md min-w-0">
      <CodeView
        filename="export.ts"
        diff={[
          { text: "function exportTotal(rows: Row[]) {" },
          { text: "  return rows.map(roundRow).reduce(sum, 0)", kind: "removed" },
          { text: "  const total = rows.reduce(sum, 0)", kind: "added" },
          { text: "  return roundTotal(total)", kind: "added" },
          { text: "}" },
        ]}
      />
    </div>
  )
}

```

## API reference [#api-reference]

### CodeView [#codeview]

Accepts native figure props beyond those listed.

| Prop         | Type                                         | Default  | Description                                                 |
| ------------ | -------------------------------------------- | -------- | ----------------------------------------------------------- |
| `filename`   | string                                       | Required | The file name or a short title.                             |
| `code`       | string                                       | `""`     | The source to list. Ignored when `diff` is passed.          |
| `diff`       | `CodeDiffLine[]`                             | None     | A unified diff.                                             |
| `renderLine` | `(text: string, index: number) => ReactNode` | None     | Colors one line. `index` counts the listed lines from zero. |
| `actions`    | ReactNode                                    | None     | Controls for the header, placed before Copy.                |
| `onCopy`     | `(text: string) => void`                     | None     | Runs after the text reaches the clipboard.                  |
| `embedded`   | boolean                                      | `false`  | Drops the frame and the header, including Copy.             |
| `labels`     | `Partial<CodeViewLabels>`                    | English  | Copy states and the names read for counts.                  |

### Types [#types]

| Export         | Shape                                                                 |
| -------------- | --------------------------------------------------------------------- |
| `CodeDiffLine` | `text`, and optional `kind` of `"context"`, `"added"`, or `"removed"` |
