# Agent Screen

> Show what an agent sees in a framed capture that opens in a larger viewer.

Source: https://ai.honestui.com/docs/components/agent-screen

```tsx
"use client"

import * as React from "react"

import { AgentScreen } from "@/components/ai/agent-screen"
import { Button } from "@/components/ui/button"

/** No agent is connected. The capture is a saved sample screenshot. */
export function AgentScreenDemo() {
  const [note, setNote] = React.useState("")

  return (
    <div className="flex w-full max-w-sm min-w-0 flex-col gap-[var(--hui-space-3)]">
      <AgentScreen
        title="Research agent's screen"
        src="/examples/crm-client-workspace.png"
        alt="A CRM workspace with a client list on the left and one client's details on the right."
        actions={
          <Button variant="secondary" size="sm" onClick={() => setNote("Take over was pressed.")}>
            Take over
          </Button>
        }
      />
      <p
        role="status"
        className="m-0 text-[var(--hui-color-foreground-base-secondary)] [font-size:var(--hui-font-size-small)]"
      >
        {note}
      </p>
    </div>
  )
}

```

## Overview [#overview]

Agent Screen frames a capture of an agent's screen, for agents that drive a browser or a desktop. The frame opens a larger viewer, where you can add your own controls, such as a button to take over.

It shows the image or video you pass. Connecting to the agent and keeping the capture current are your application's work, reported through `status`.

## Anatomy [#anatomy]

<Anatomy title="AgentScreen">
  <AnatomyItem name="Frame" description="The capture, as a button that opens the viewer" />

  <AnatomyItem name="Open" description="A label on the frame, always visible" />

  <AnatomyItem name="Title" description="Names the screen" />

  <AnatomyItem name="Viewer" description="A dialog with the capture, the title, and your actions" />
</Anatomy>

Installing `agent-screen` brings [Dialog](https://www.honestui.com/docs/components/dialog) along as a registry dependency.

## Behavior [#behavior]

### Status [#status]

`live` shows the capture. `connecting` shows a spinner and a message. `offline` shows a message and no capture. The frame can be opened only while live.

### Media [#media]

Pass an image by default, or set `mediaType` to `video`. A video does not start by itself: the viewer gives it controls, and the frame shows a still.

### Actions [#actions]

`actions` are placed in the viewer's header. What they do is yours.

## Accessibility [#accessibility]

The frame is a button named "Open: " followed by the title, and the Open label is always visible, so opening does not depend on hover. The viewer is the Honest UI [Dialog](https://www.honestui.com/docs/components/dialog): it traps focus, closes on <kbd>Escape</kbd>, and returns focus to the frame.

`alt` is required. Describe what is on the screen, and update it as the screen changes if people rely on it. The connecting and offline messages are statuses.

| Key                                  | Action           |
| ------------------------------------ | ---------------- |
| <kbd>Enter</kbd> or <kbd>Space</kbd> | Open the viewer  |
| <kbd>Escape</kbd>                    | Close the viewer |

## Installation [#installation]


  

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

  
    
      
        Add the components it is built from.
      

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

      
        Copy and paste this file into your project.
      

      ### components/ai/agent-screen.tsx

```tsx
"use client"

import * as React from "react"
import {
  CircleAlert as CircleAlertIcon,
  LoaderCircle as LoaderCircleIcon,
  Maximize2 as MaximizeIcon,
} from "honestui/icons"

import { cn } from "@/lib/utils"
import {
  Dialog,
  DialogBody,
  DialogHeader,
  DialogPopup,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog"

export type AgentScreenStatus = "connecting" | "live" | "offline"

export type AgentScreenLabels = {
  open: string
  connecting: string
  offline: string
}

export const defaultAgentScreenLabels: AgentScreenLabels = {
  open: "Open",
  connecting: "Connecting to the agent's screen…",
  offline: "The agent's screen is not available.",
}

export type AgentScreenProps = Omit<React.ComponentProps<"div">, "children" | "title"> & {
  /** Names the screen, such as "Research agent's screen". */
  title: string
  /** Defaults to "live". The media is shown only while live. */
  status?: AgentScreenStatus
  /** The capture or stream URL. */
  src?: string
  /** Set to "video" when `src` is a video. */
  mediaType?: "image" | "video"
  /** Describes what is on the screen, for people who cannot see it. */
  alt: string
  /** Controls shown in the viewer's header, such as a button to take over. */
  actions?: React.ReactNode
  labels?: Partial<AgentScreenLabels>
}

function ScreenMedia({
  src,
  mediaType,
  alt,
  isPreview = false,
  className,
}: Pick<AgentScreenProps, "src" | "mediaType" | "alt"> & {
  /** A still, unlabelled copy for the card. The viewer holds the real one. */
  isPreview?: boolean
  className?: string
}) {
  if (mediaType === "video") {
    // No autoplay: motion the person did not ask for should not start by itself.
    return isPreview ? (
      <video src={src} aria-hidden="true" muted playsInline preload="metadata" className={className} />
    ) : (
      <video src={src} aria-label={alt} controls muted playsInline className={className} />
    )
  }

  // A plain img keeps the copied component free of a framework image loader.
  // eslint-disable-next-line @next/next/no-img-element
  return <img src={src} alt={isPreview ? "" : alt} className={className} />
}

/**
 * A framed view of what an agent sees, which opens in a larger viewer. It
 * shows the image or video you pass; connecting to the agent is your
 * application's work, reported through `status`.
 */
function AgentScreen({
  title,
  status = "live",
  src,
  mediaType = "image",
  alt,
  actions,
  labels: labelsProp,
  className,
  ...props
}: AgentScreenProps) {
  const labels = { ...defaultAgentScreenLabels, ...labelsProp }
  const isLive = status === "live" && Boolean(src)

  const frame =
    "relative block aspect-video w-full overflow-hidden rounded-[var(--hui-radius-4)] border-[0.5px] border-[var(--hui-color-border-base-primary)] bg-[var(--hui-color-background-base-secondary)] p-0"

  return (
    <div
      data-slot="agent-screen"
      data-status={status}
      className={cn(
        "flex w-full min-w-0 flex-col gap-[var(--hui-space-3)] text-[var(--hui-color-foreground-base-primary)] [font-size:var(--hui-font-size-small)] [line-height:var(--hui-line-height-small)]",
        className
      )}
      {...props}
    >
      {isLive ? (
        <Dialog>
          <DialogTrigger
            aria-label={`${labels.open}: ${title}`}
            className={cn(
              frame,
              "group/screen cursor-pointer outline-none motion-safe:[transition:var(--hui-transition-interactive)] hover:shadow-[var(--hui-shadow-lifted)] focus-visible:[outline:var(--hui-focus-ring)] focus-visible:outline-offset-[var(--hui-focus-ring-offset-accent)]"
            )}
          >
            <ScreenMedia
              src={src}
              mediaType={mediaType}
              alt={alt}
              isPreview
              className="absolute inset-0 size-full object-cover"
            />
            {/* Always visible, so opening does not depend on hover. */}
            <span className="absolute end-[var(--hui-space-3)] bottom-[var(--hui-space-3)] inline-flex items-center gap-[var(--hui-space-2)] rounded-[var(--hui-radius-2)] bg-[var(--hui-color-background-base-primary)] px-[var(--hui-space-3)] py-[var(--hui-space-2)] shadow-[var(--hui-shadow-soft)] [font-weight:var(--hui-font-weight-medium)]">
              <MaximizeIcon aria-hidden="true" className="size-3.5" />
              {labels.open}
            </span>
          </DialogTrigger>
          <DialogPopup className="w-[min(960px,90vw)] max-w-[90vw]">
            <DialogHeader className="flex flex-row items-center justify-between gap-[var(--hui-space-4)] pe-[var(--hui-space-10)]">
              <DialogTitle>{title}</DialogTitle>
              {actions && <div className="flex shrink-0 items-center gap-[var(--hui-space-2)]">{actions}</div>}
            </DialogHeader>
            <DialogBody>
              <ScreenMedia
                src={src}
                mediaType={mediaType}
                alt={alt}
                className="block h-auto max-h-[70vh] w-full rounded-[var(--hui-radius-2)] object-contain"
              />
            </DialogBody>
          </DialogPopup>
        </Dialog>
      ) : (
        <div className={cn(frame, "flex items-center justify-center p-[var(--hui-space-5)]")}>
          <p
            role="status"
            className="m-0 flex max-w-full items-center gap-[var(--hui-space-3)] text-center text-[var(--hui-color-foreground-base-secondary)]"
          >
            {status === "connecting" ? (
              <LoaderCircleIcon aria-hidden="true" className="size-4 shrink-0 motion-safe:animate-spin" />
            ) : (
              <CircleAlertIcon strokeWidth={1.5} strokeLinecap="round" aria-hidden="true" className="size-4 shrink-0" />
            )}
            {status === "connecting" ? labels.connecting : labels.offline}
          </p>
        </div>
      )}
      <p className="m-0 truncate [font-weight:var(--hui-font-weight-medium)]">{title}</p>
    </div>
  )
}

export { AgentScreen }

```

      
        Update the import paths to match your project setup.
      
    
  


## Usage [#usage]

```tsx
import { AgentScreen } from "@/components/ai/agent-screen"

<AgentScreen
  title="Research agent's screen"
  status={session.status}
  src={session.captureUrl}
  alt={session.captureDescription}
  actions={<Button variant="secondary" size="sm" onClick={session.takeOver}>Take over</Button>}
/>
```

## Examples [#examples]

### Connecting and offline [#connecting-and-offline]

The two states with no capture.

```tsx
import { AgentScreen } from "@/components/ai/agent-screen"

export function AgentScreenStatusExample() {
  return (
    <div className="grid w-full max-w-xl min-w-0 gap-[var(--hui-space-5)] sm:grid-cols-2">
      <AgentScreen title="Billing agent's screen" status="connecting" alt="" />
      <AgentScreen title="Support agent's screen" status="offline" alt="" />
    </div>
  )
}

```

## API reference [#api-reference]

### AgentScreen [#agentscreen]

Accepts native div props beyond those listed.

| Prop        | Type                                  | Default   | Description                                |
| ----------- | ------------------------------------- | --------- | ------------------------------------------ |
| `title`     | string                                | Required  | Names the screen.                          |
| `alt`       | string                                | Required  | Describes what is on the screen.           |
| `status`    | `"connecting" \| "live" \| "offline"` | `"live"`  | Whether the capture can be shown.          |
| `src`       | string                                | None      | The capture or stream URL.                 |
| `mediaType` | `"image" \| "video"`                  | `"image"` | How to render `src`.                       |
| `actions`   | ReactNode                             | None      | Controls in the viewer's header.           |
| `labels`    | `Partial<AgentScreenLabels>`          | English   | The Open name and the two status messages. |
