Skip to content
Tier

Presence

Pro

Who is looking at the same thing right now, people and agents, from the presence data the app passes: a stack of avatars that opens a list of each one and what they're doing (viewing, editing, idle since a time). A state the app doesn't report reads "State unknown", and null in place of the list says who's here isn't known, rather than showing nobody. With updatedAt, presence older than staleAfter (a minute unless set) is dimmed and stamped "As of 10:42", so an old list isn't read as now; before the page has a clock (server rendering) it claims neither.

It sits beside thread-header (in its actions) and in comment-thread's header (its presence). An agent is named by the app and marked as an agent. Agents are tiles and people are round, as in the thread header.

Markdown
Version 5.2.0· other versions

This page is for 5.2.0, the current release. An earlier release’s page is the Markdown that release published, so the docs match the version you have installed (candor-lock.json records it).

Not in the docs of 1.3.0, 1.4.0, 1.5.0, 1.5.1, 1.6.0, 1.7.0, 1.8.0, 1.8.1, 1.9.0, 1.9.1, 2.0.0, 2.1.0, 2.2.0, 2.3.0, 2.4.0, 2.5.0, 2.5.1, 2.6.0, 2.6.1, 2.6.2, 2.7.0 and 2.8.0: the page came later, or had another name then.

Docs pages weren’t kept for 1.0.0, 1.0.1, 1.1.0, 1.1.1, 1.2.0 and 1.2.1: they are kept from 1.3.0 on, and before that only llms.txt was published.

Preview

Loading the preview

Install

With the Candor CLI

First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/presence

Brings in: agent-avatar, avatar, bidi, chat, locale, popover, relative-time, tokens, utils · npm: lucide-react@^1.67.0

Usage

TSX · The example above
"use client"

import { ThreadHeader } from "@/components/candor/thread-header"
import { Presence, type PresenceEntry } from "@/components/candor/presence"

const ago = (m: number) => new Date(Date.now() - m * 60_000).toISOString()

const here: PresenceEntry[] = [
  { who: { person: { id: "u-ana", name: "Ana Ruiz" } }, state: "editing", editing: "step 3" },
  { who: { agent: { id: "a-review", name: "Checker", role: "reviewer" } }, state: "viewing" },
  { who: { person: { id: "u-jo", name: "Jo Okafor" } }, state: "idle", idleSince: ago(12) },
  { who: { person: { id: "u-lee", name: "Lee Park" } } },
]

export default function Example() {
  return (
    <div className="flex max-w-2xl flex-col gap-4">
      <div className="overflow-hidden rounded-lg border">
        <ThreadHeader title="Move invoices to the new ledger" actions={<Presence entries={here} updatedAt={ago(0)} />} />
      </div>
      <div className="flex items-center gap-3 rounded-lg border bg-card px-4 py-2">
        <p className="min-w-0 flex-1 truncate text-sm font-medium">Last night’s import</p>
        <Presence entries={here.slice(0, 2)} updatedAt={ago(6)} />
      </div>
    </div>
  )
}

<Presence>

PropTypeDefaultDescription
entriesrequiredPresenceEntry[] | null—Who is here, in the order to show them; leave out the viewer. `null` when the app doesn't know (its presence service is down).
updatedAtDate | string | undefined—When the app's presence data was last updated. Without it, no age is shown and none is judged.
staleAfternumber | undefined60_000How old the presence data may be before it's shown as stale, in milliseconds. Defaults to one minute.
maxnumber | undefined3How many avatars to show before "+2". Defaults to 3.
classNamestring | undefined—Extra classes for the outer element.

Strings

The fixed words, in English by default. Change any of them under presence in <LocaleProvider strings={{ … }}>; the table is exported as presenceStrings.

KeyEnglish
labelWho’s here
here{ one: "{count} here now: {who}", other: "{count} here now: {who}" }
whoState{name} ({state})
nobodyNo one else here
unknownWhoWho’s here isn’t known
viewingViewing
editingEditing
editingWhatEditing {what}
idleIdle
idleSinceIdle since {time}
unknownState unknown
agent{noun} agent
agentName{name}, {noun} agent
more+{count}
moreLabel{ one: "and {count} more", other: "and {count} more" }
staleAs of {time}
hereStale{here}. {stamp}
staleHintNot updated since {time}. Who’s here may have changed.

Motion

MomentFamilyWhat it does
tinttintThe button's background changes when the pointer is over it, and a ring fades in when it takes keyboard focus.

Set with --cd-motion-presence-<moment>-* in your brand file; the families are in the design reference.

Types

PresenceState
What someone is doing here, as the app's presence reports it.
"viewing" | "editing" | "idle"
PresencePerson
A person here: a teammate, a reviewer.
{ id: string; name: string; image?: string | undefined }
PresenceEntry
One person or agent looking at the same thing.
{ /** A person, or an agent named by the app (never by model output). */ who: { person: PresencePerson; } | { agent: AgentIdentity; }; /** What they're doing. Left out, it shows as unknown: nothing is guessed. */ state?: PresenceState | undefined; /** What they're editing, in the app's words, for "Editing step 3". */ editing?: string | undefined; /** When they went idle (ISO), for "Idle since 10:42". */ idleSince?: string | undefined; }

Types from other items

The props above take these. Each is defined by another item; import it from the path shown.

AgentIdentity from @/lib/candor/chat
{ id: string; /** The displayed name, e.g. "Architect agent". Never derived from model output. */ name: string; role: AgentRole; specialty?: string | undefined; }
AgentRole from @/lib/candor/chat
What an agent does: "assistant", "coder", "reviewer"… The app registers each role's glyph and hue (AgentRolesProvider).
string