affectUI › Components › Agents

Agent Cursors

Agents working in a document like teammates in a multiplayer editor – live pointers with names, suggestions typed letter by letter, images they resize, code they fix and comments beside the page.

Part of affectUI Pro. Install: npx shadcn@latest add https://www.affectui.com/r/agent-cursors.json · Import from @/components/ui/agent-cursors

When to use

  • Agents that write or edit a document, a brief or a spec while you watch – text, images, code and comments – and you want to see who is where
  • Several agents on one text at once: each change stays a suggestion in its agent's color until you accept it
  • Making agent work feel like working with teammates – the same pointers people know from Figma or Google Docs

When not to use

  • One agent rewriting a reply – use Writing Assistant or Inline Diff
  • Changes to code or files – use Inline Diff or Change Summary
  • Who started which agent – use Agent Tree

Usage

import { AgentCursors, acceptSuggestion } from "@/components/ui/agent-cursors";

// One shared ink for all of them – their logos tell them apart (pass color to give one its own)
const agents = [
  { id: "research", name: "Researcher", avatar: <ClaudeLogo /> },
  { id: "writer", name: "Writer", avatar: <OpenAILogo /> },
];

export function Brief() {
  const [doc, setDoc] = useState({ blocks, suggestions: [] });
  // From your agents' stream: where each one is and what it does
  const cursors = {
    research: { block: "why", at: 24, select: 9, doing: "Checking the numbers" },
    writer: { block: "list", at: 31, doing: "Adding detail", typing: true },
  };

  return (
    <AgentCursors
      title="Launch brief"
      agents={agents}
      blocks={doc.blocks}
      suggestions={doc.suggestions}
      cursors={cursors}
      onAccept={(s) => setDoc((d) => acceptSuggestion(d.blocks, d.suggestions, s.id))}
      onReject={(s) => setDoc((d) => ({ ...d, suggestions: d.suggestions.filter((x) => x.id !== s.id) }))}
    />
  );
}

Props

NameTypeDefaultDescription
agentsCursorAgent[]{ id, name, color?, avatar?, tint?, cost? } – everyone who can be in the document; without a color every agent shares one ink (near-black, light in dark mode) and the logos tell them apart
blocksDocBlock[]{ id, kind?: "title" | "heading" | "paragraph" | "item" | "code" | "image", text?, … } – the document; consecutive items form one list. Images take width (0–1), ratio, float ("left" | "right" – the next text runs beside them), loading, src and alt; their text is the caption. Tables take rows of cell blocks (the first row is the header); a cell with a tone shows as a status pill
cursorsRecord<string, AgentCursor | null>Per agent: { block, at?, in?, select?, point?, doing?, typing? } – where it is (a block or a comment), a selected passage or a held image, the corner or middle of an image, what it's doing (on its flag) and whether it types; null or missing means it left
commentsDocComment[]{ id, agent, block, at?, x?, y?, text, open? } – comment pins on the page: on a character of the block (at), or at its top edge x along the page (0–1); y moves it down; open keeps the window open (e.g. while the agent writes), otherwise a dark round pin with the agent's mark that grows into the window on hover
onResolveComment(comment: DocComment) => voidThe black check in a comment's window – remove it from your list
onReply(comment: DocComment, text: string) => voidShows a reply field in the window; Enter sends
heightnumberA fixed page height in px – the rest of the document fades out towards the bottom
tabs / tab / onTabChange{ id, label }[] / string / (id) => voidTabs above the page – the open one white with a stroke; you switch the blocks and agents
agents[].costnumberPer agent: what it has cost so far – a small tag on its flag, the total in the header
agents[].tintstringPer agent: the fill of its face while you follow it – defaults to a light wash of its color
formatCost(amount: number) => stringdollars, e.g. "$0.42"Formats every cost
suggestionsDocSuggestion[][]{ id, agent, block, at, remove?, insert?, note? } – changes waiting for you: removed text is struck through, new text underlined, both in the agent's color
onAccept / onReject(suggestion: DocSuggestion) => voidFrom the window that opens on a suggestion – acceptSuggestion() writes one into your blocks
onAcceptAll() => voidShows "Accept all" in the header when there is more than one suggestion
following / onFollowingChangestring | nullThe agent you follow, controlled or uncontrolled – the other pointers and selections fade back
titlestring"Untitled"The document's name in the header
classNamestringExtra class on the root
acceptSuggestion(blocks, suggestions, id) => { blocks, suggestions }Helper: writes a suggestion into the text and moves the other suggestions in that block along

Keyboard

  • Tab: Moves to the faces in the header, then from suggestion to suggestion – a focused suggestion opens its window
  • Enter / Space: On a face: follow that agent, or stop following
  • Esc: Closes the window of a suggestion

Accessibility

  • The text is real text in headings, paragraphs and a list – pointers, carets and selections are decoration and hidden from screen readers
  • Every suggestion is focusable and labelled, e.g. "Researcher suggests adding: 38%"; its window is a dialog with Accept and Reject buttons
  • The faces are toggle buttons (aria-pressed) named after the agent and what it is doing; a followed face fills with its tint
  • New suggestions are announced politely, with the agent's note

Responsive

  • Every position is measured from the page itself, so any width, font size or zoom works – the pointers stay on their letters while the text wraps
  • Comment windows open towards the bottom right – towards the left near the right edge, upwards near the bottom
  • Flags always sit to the bottom right of their pointer; near the right edge they are cut off rather than moved
  • Below 520px the page gets tighter margins and the suggestion count in the header gives way to the faces
  • Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark"

Requirements

  • React 19

Related

  • Writing Assistant – AI chat card with a soft gradient glow, suggestions, streaming replies and live voice dictation
  • Inline Diff – AI edits to prose shown inline – struck and added words you accept or reject one by one, with keys, a popover and undo
  • Agent Tree – Who started whom – an orchestrator and its sub-agents as a live tree, with status, cost and time on every card, costs rolling up each branch, and branches that fold to one line
  • Comment Thread – One comment with replies – an assistant reports what it did, folds open details and answers @mentions