Skip to main content

Feedback

AgentPresence

The person's always-visible sign of which of their agents are listening to something on their behalf, such as a meeting, with Stop one click away. A chip with the agent sparkle says "inbox agent listening", or "2 agents listening"; it opens a popover that lists each agent, what it listens for and a Stop, plus Stop all with two or more. It never hides itself while an agent listens (no fading, no collapsing), and renders nothing when none does. compact shows only the sparkle and a count, and unread a dot when an agent left something for the person.

Design review

Install

Once per app, with Tailwind CSS v4 and an @/* path alias (Ouro HX is built on Base UI, so --base base; skip -t in an existing app):

npx shadcn@latest init --base base --preset nova --yes --no-monorepo
npx shadcn@latest add ourostack/ouro-ui/ouro-base#v1.3.10

Then the component. From v0.8.0 it installs only the helpers it imports and never touches your stylesheet (Installing):

npx shadcn@latest add ourostack/ouro-ui/agent-presence#v1.3.10

Or, with the @ouro namespace set up:

npx shadcn@latest add @ouro/ouro-base @ouro/agent-presence

Its tests, stories and Playwright spec are optional. They need Vitest 4.1.11 or later, and from v0.8.0 never change a version your app has (Testing):

npx shadcn@latest add ourostack/ouro-ui/agent-presence#v1.3.10 ourostack/ouro-ui/agent-presence-tests#v1.3.10
import { AgentPresence } from "@/components/ouro/agent-presence/agent-presence";

Paths use shadcn's default @/ alias; the CLI rewrites them to yours.

Example

<AgentPresence
  label="Agents listening"
  agents={[{ id: "inbox", name: "inbox agent", doing: "listening for your name" }]}
  onStop={(id) => stopListening(id)}
/>
<AgentPresence label="Agents listening" agents={agents} onStop={stop} onStopAll={stopAll} compact unread />

Usage

The chip is a button named by the count and the names ("2 agents listening: inbox agent, research agent"). When an agent starts or stops listening, screen readers hear it once, politely ("inbox agent is now listening", "inbox agent stopped"). When a Stop goes with its agent, focus moves to the next Stop, and when the last agent goes, to the control after the chip.

Its agent side, agentPresenceAx in agent-presence-styles.ts, lets agents read who is listening and what for (consent none). An agent may stop only its own listening: stop is scope: "self", so Ouro AX refuses an agent's call on another agent's row. Stop on another agent's row and Stop all are the person's, because the person asked those agents to listen. It offers no way to start listening: that stays the person's.

Use instead when

  • Use Avatar or Badge instead to show who is present in a conversation, and toast() for a one-off notice.

Props

Type: AgentPresenceProps.

agents (required)
Type: readonly ListeningAgent[]
The agents listening now. With none, AgentPresence renders nothing; keep it mounted, so it can say when an agent stops or starts.
className
Type: string
Layout only: margins, placement (ms-auto). Appearance classes warn in development; use size.
compact
Type: boolean
Only the sparkle and a count, for a tight bar or a floating mini-window. Its name still says who is listening.
label (required)
Type: string
Names the list of agents: the popover's title, "Agents listening". It is always required.
labels
Type: AgentPresenceLabels
The words it says, for other languages.
onStop (required)
Type: (id: string) => void
Called with an agent's id when the person presses its Stop. Stop the agent and drop it from agents; AgentPresence says "inbox agent stopped" once it has gone. The person may stop any agent here.
onStopAll
Type: (() => void)
Called when the person presses Stop all, which shows with two agents or more. Without it, Stop all calls onStop for each agent.
ref
Type: Ref<HTMLSpanElement>
Reaches the rendered element.
size
Type: "sm" | "md" | "lg"
Default: "sm"
The chip's size, Button's sizes.
unread
Type: boolean
Shows a dot on the chip: an agent left something for the person. AgentPresence opens nothing for it; the app shows what was left where it belongs.

Don't use

These are not props. Passing one fails the type check with the fix in the error, and in development a console message names the fix.

  • autoHide: Not a prop. AgentPresence never hides while an agent listens; it renders nothing only when agents is empty.
  • listeners: Not a prop. The agents are agents: <AgentPresence agents={[{ id: "inbox", name: "inbox agent" }]}>.
  • onRemove: Not a prop. Ouro UI says onStop, called with the agent's id: <AgentPresence onStop={(id) => stop(id)}>.
  • variant: Not a prop. The small form is compact: <AgentPresence compact>.

className is for layout only

Pass margins, width and grid placement (mt-4, w-full, col-span-2).

Appearance comes from props, not classes. In development, a class that sets appearance logs one warning per component instance, naming the prop to use instead. A class sets appearance when, after its variants (hover:, sm:) and any ! are removed, it starts with bg-, text-, border-, rounded-, shadow-, font-, ring-, from-, via-, to-, fill- or stroke-, or is border, rounded, shadow or ring, or has a dark: variant. Text wrapping, overflow and table layout classes (text-wrap, text-ellipsis, border-collapse) are layout and do not warn.

Alignment classes (text-center) warn too: set alignment on a parent element.

Types

AgentPresenceLabels

The words AgentPresence says, for apps in other languages.

interface AgentPresenceLabels {
  /** The chip for one agent. @default (name) => `${name} listening` */
  one?: ((name: string) => string) | undefined;
  /** The chip for several. @default (count) => `${count} agents listening` */
  many?: ((count: number) => string) | undefined;
  /** Said once when an agent starts listening. @default (name) => `${name} is now listening` */
  started?: ((name: string) => string) | undefined;
  /** Said once when an agent stops, whoever stopped it. @default (name) => `${name} stopped` */
  stopped?: ((name: string) => string) | undefined;
  /**
   * Each agent's Stop button. Screen readers hear the agent's name after it. @default "Stop"
   */
  stop?: string | undefined;
  /**
   * The button that stops every agent. @default "Stop all"
   */
  stopAll?: string | undefined;
  /** What screen readers hear for the unread dot. @default "Something new from your agents" */
  unread?: string | undefined;
  /** The popover's close button, which shows when it has keyboard focus. @default "Close" */
  close?: string | undefined;
}

ListeningAgent

One agent listening for the person.

interface ListeningAgent {
  /** Stays the same while the agent listens; `onStop` gets it back. */
  id: string;
  /** What the person calls the agent, by its role: "inbox agent". */
  name: string;
  /** What it listens for, in a few words: "listening for your name, and anything about the empty state". */
  doing?: string | undefined;
}