Elements

Elements · Tool use

Computer use

The screen the agent is driving, with a cursor trail and what it is doing right now.

github.com/assistant-ui/assistant-ui/issues
clickIssues tab1/4
fig. 01 · plays once, replay from the corner

Installation

npx shadcn@latest add "@assistant-ui/elements-computer-use"
First time? Set up a runtime

Runtime components read their state from an assistant-ui runtime. Add one to an existing project:

npx assistant-ui@latest init

Then wrap your app in a runtime provider:

import { AssistantRuntimeProvider } from "@assistant-ui/react";
import { useChatRuntime, AssistantChatTransport } from "@assistant-ui/ai-sdk";

export default function App() {
  const runtime = useChatRuntime({
    transport: new AssistantChatTransport({ api: "/api/chat" }),
  });

  return (
    <AssistantRuntimeProvider runtime={runtime}>
      {/* your components */}
    </AssistantRuntimeProvider>
  );
}

The installation guide covers new projects, templates, and API routes.

A browser chrome frame around whatever the agent is looking at, with a cursor that moves to each action and a fading trail behind it. With a runtime this is a composition of grouped tool calls, not a shipped primitive; standalone you pass the steps and the active index in.

Getting started

There's no built-in "computer use" tool. This is a computer tool you define yourself, one call per action, rendered together by grouping consecutive calls to it.

Define the action as a tool call

app/computer-toolkit.tsx
"use client";

import type { ToolCallMessagePartComponent } from "@assistant-ui/react";
import { defineToolkit } from "@assistant-ui/react";
import { MousePointer2Icon } from "lucide-react";
import { z } from "zod";

type ComputerArgs = { action: string; target: string; x: number; y: number };

const ComputerStepMarker: ToolCallMessagePartComponent<ComputerArgs> = ({ args, status }) => (
  <MousePointer2Icon
    className="absolute size-4 fill-blue-500 text-blue-500 transition-[left,top] duration-500"
    style={{ left: `${args.x}%`, top: `${args.y}%`, opacity: status?.type === "running" ? 1 : 0.4 }}
  />
);

const toolkit = defineToolkit({
  computer: {
    type: "backend",
    description: "Click, type, or scroll at a point on the screen.",
    parameters: z.object({ action: z.string(), target: z.string(), x: z.number(), y: z.number() }),
    render: ComputerStepMarker,
  },
});

action, target, x, and y are this example's own parameters schema, not a built-in shape; a computer-use provider's real tool defines whatever fields it needs.

Group consecutive actions into one screen

components/assistant-ui/elements/thread.aui.tsx
import { MessagePrimitive } from "@assistant-ui/react";

function AssistantMessage() {
  return (
    <MessagePrimitive.GroupedParts
      groupBy={(part) =>
        part.type === "tool-call" && part.toolName === "computer" ? ["group-computer"] : []
      }
    >
      {({ part, children }) => {
        switch (part.type) {
          case "group-computer":
            return <div className="relative overflow-hidden rounded-2xl border">{children}</div>;
          case "tool-call":
            return part.toolUI ?? null;
          default:
            return null;
        }
      }}
    </MessagePrimitive.GroupedParts>
  );
}

groupBy branches on part.toolName here instead of groupPartByType's part-type map, because the grouping key isn't the part type, it's which tool was called. The group wrapper is position: relative; each ComputerStepMarker positions itself absolute inside it, so consecutive calls lay out as a trail with no extra plumbing.

Anatomy

<div data-slot="computer-use">
  <div>{/* traffic-light dots, url */}</div>
  <div>{/* children (the screen), trail dots, cursor */}</div>
  {/* footer: action, target, "n/total"; only when steps is non-empty */}
</div>

activeIndex clamps into 0…steps.length - 1, so a negative or out-of-range index still resolves to a real step rather than nothing. The trail is the two steps before the active one plus the active one itself, at opacity 0.18 * (i + 1), fading in toward the cursor's full-opacity mark. With zero steps there's no cursor and no footer, only the header and children.

Examples

Provide the screen content

The group wrapper from step two is where a live screen goes, alongside the markers children supplies through MessagePrimitive.GroupedParts:

case "group-computer":
  return (
    <div className="relative overflow-hidden rounded-2xl border">
      <img src={screenshotUrl} alt="" className="absolute inset-0 size-full object-cover" />
      {children}
    </div>
  );

Fade the marker as the call settles

ComputerStepMarker above already reads status?.type; a call still "running" gets the full-opacity cursor treatment, and one that finished settles into a dimmer trail dot the next call's marker renders past.

Restyle the frame

Both lanes take className on the root. The chrome uses the shared field, mono, and paper tokens; the traffic-light dots and cursor color are literal Tailwind classes on the element itself, so restyling those means editing the component's className strings directly rather than a shared token.

API reference

There's no dedicated computer-use primitive: this is MessagePrimitive.GroupedParts plus an ordinary tool-call renderer, so the table below names the render props that pattern reads, not a shipped API surface.

Render props used

FieldTypeDescription
argsyour tool's TArgsWhatever parameters schema you declare; action/target/x/y above is one convention, not a built-in shape.
statusToolCallMessagePartStatusRead status?.type === "running" to distinguish the in-progress call from settled ones.