Presence
ProWho 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.
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
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
"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>
| Prop | Type | Default | Description |
|---|---|---|---|
| entriesrequired | PresenceEntry[] | 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). |
| updatedAt | Date | string | undefined | — | When the app's presence data was last updated. Without it, no age is shown and none is judged. |
| staleAfter | number | undefined | 60_000 | How old the presence data may be before it's shown as stale, in milliseconds. Defaults to one minute. |
| max | number | undefined | 3 | How many avatars to show before "+2". Defaults to 3. |
| className | string | 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.
| Key | English |
|---|---|
| label | Who’s here |
| here | { one: "{count} here now: {who}", other: "{count} here now: {who}" } |
| whoState | {name} ({state}) |
| nobody | No one else here |
| unknownWho | Who’s here isn’t known |
| viewing | Viewing |
| editing | Editing |
| editingWhat | Editing {what} |
| idle | Idle |
| idleSince | Idle since {time} |
| unknown | State unknown |
| agent | {noun} agent |
| agentName | {name}, {noun} agent |
| more | +{count} |
| moreLabel | { one: "and {count} more", other: "and {count} more" } |
| stale | As of {time} |
| hereStale | {here}. {stamp} |
| staleHint | Not updated since {time}. Who’s here may have changed. |
Motion
| Moment | Family | What it does |
|---|---|---|
| tint | tint | The 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