# Presence

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.

- Registry name: `presence` (`@candor/presence`)
- Tier: Pro. Installing it needs a Pro, Premium or Team licence key: the person runs `npx candor-ui key <key>` themselves; never ask for the key in chat or write it to a file.
- Docs: https://candor.themethodic.com/docs/presence

## Install

```bash
npx candor-ui add presence
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/presence`.
It brings in: agent-avatar, avatar, chat, locale, popover, relative-time, tokens, utils.
npm packages: lucide-react@^1.67.0.

## Example

```tsx
"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>

Props:

- `entries: 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` (default `60_000`): How old the presence data may be before it's shown as stale, in milliseconds. Defaults to one minute.
- `max?: number | undefined` (default `3`): How many avatars to show before "+2". Defaults to 3.
- `className?: string | undefined`

## Strings (`presence`)

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

- `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}
- `staleHint`: Not updated since {time}. Who’s here may have changed.

## Types

### PresenceState

What someone is doing here, as the app's presence reports it.

```ts
"viewing" | "editing" | "idle"
```

### PresencePerson

A person here: a teammate, a reviewer.

```ts
{ id: string; name: string; image?: string | undefined }
```

### PresenceEntry

One person or agent looking at the same thing.

```ts
{
  /** 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

Import: `import type { AgentIdentity } from "@/lib/candor/chat"` (item `chat`, https://candor.themethodic.com/docs/chat.md)

```ts
{
  id: string;
  /** The displayed name, e.g. "Architect agent". Never derived from model output. */
  name: string;
  role: AgentRole;
  specialty?: string | undefined;
}
```

### AgentRole

Import: `import type { AgentRole } from "@/lib/candor/chat"` (item `chat`, https://candor.themethodic.com/docs/chat.md)

What an agent does: "assistant", "coder", "reviewer"… The app registers each role's glyph and hue (AgentRolesProvider).

```ts
string
```

## Motion

- **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.

## Rules for using Candor UI

- Components take plain typed data. Never pass model output as HTML or render it as a link; messages are typed blocks.
- A control that can't be used says why: wrap it in `<Locked reason="…">`, don't just disable it.
- Don't show a value you don't have: leave it out (it renders as —) rather than guess. An estimate is labelled as one.
- Nothing is pre-selected in a decision a person makes, and the agent is named from your app's data, never from model output.
- Your app's words go in `LocaleProvider`: its fixed strings in `strings`, its nouns in `nouns`; entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
