# Assignee picker

Choose a person or an agent for a task. Each one shows their current load ("3 of 5 open tasks") and availability ("Away until Mon 16 Nov, 09:00") as the app passes them; what the app doesn't pass reads "Load not known" or "Availability not known", never zero or available. People and agents are listed apart, each in the app's order: the picker doesn't sort by load or suggest anyone, and nothing is chosen until the person chooses. An agent is named by the app and marked as an agent.

The list is searchable by name and by an agent's role. Someone who can't take this work stays in the list with the reason (`locked`). The button names who is assigned, and with `onUnassign` the list ends with Unassign. With `updatedAt`, the list says when the load and availability were measured.

- Registry name: `assignee-picker` (`@candor/assignee-picker`)
- 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/assignee-picker

## Install

```bash
npx candor-ui add assignee-picker
```

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

## Example

```tsx
"use client"

import * as React from "react"
import { AssigneePicker, type AssigneeCandidate, type AssigneeWho } from "@/components/candor/assignee-picker"

const inDays = (d: number) => new Date(Date.now() + d * 86_400_000).toISOString()
const measured = new Date().toISOString()

/* Load and availability come from the app: what it doesn't know (Lee's) reads as not known. */
const team: AssigneeCandidate[] = [
  { who: { person: { id: "u-ana", name: "Ana Ruiz" } }, availability: "available", load: { open: 3, limit: 6 } },
  { who: { person: { id: "u-jo", name: "Jo Okafor" } }, availability: "away", until: inDays(3), load: { open: 1 } },
  { who: { person: { id: "u-lee", name: "Lee Park" } } },
  { who: { person: { id: "u-kim", name: "Kim Sato" } }, availability: "available", load: { open: 0 }, locked: "Kim is a viewer here and can’t own tasks." },
  { who: { agent: { id: "a-recon", name: "Reconciler", role: "analyst" } }, availability: "busy", load: { open: 8, limit: 8 } },
  { who: { agent: { id: "a-check", name: "Checker", role: "reviewer" } }, availability: "available", load: { open: 2, limit: 10 } },
]

export default function Example() {
  const [assigned, setAssigned] = React.useState<AssigneeWho | null>(null)
  const [saving, setSaving] = React.useState(false)
  const save = (next: AssigneeWho | null) => {
    setSaving(true)
    // Your app saves the assignment, then passes the saved assignee back.
    window.setTimeout(() => {
      setAssigned(next)
      setSaving(false)
    }, 600)
  }
  return (
    <div className="flex max-w-md items-center justify-between gap-3 rounded-lg border bg-card px-4 py-3">
      <span className="min-w-0">
        <span className="block truncate text-sm font-medium">Match March payments to invoices</span>
        <span className="block text-xs text-muted-foreground">Due Friday</span>
      </span>
      <AssigneePicker
        candidates={team}
        assigned={assigned}
        busy={saving}
        updatedAt={measured}
        onAssign={(c) => save(c.who)}
        onUnassign={() => save(null)}
      />
    </div>
  )
}
```

## <AssigneePicker>

Props:

- `candidates: AssigneeCandidate[] | null`: Who the work can go to, people and agents, in the order to show them. `null` when the app doesn't know (its directory is down).
- `assigned?: AssigneeWho | null | undefined`: Who has the work now, from the app's record; `null` or left out when no one does. They needn't be among `candidates`.
- `onAssign: (candidate: AssigneeCandidate) => void`: Called with the candidate the person chose. Not called for the one already assigned, or one that's `locked`.
- `onUnassign?: (() => void) | undefined`: Called on Unassign. Without it there is no Unassign.
- `busy?: boolean | undefined` (default `false`): The assignment is being saved: the button is locked, with the reason.
- `locked?: string | null | undefined`: Why this person can't change the assignee, when they can't. Locks the button with it.
- `updatedAt?: Date | string | undefined`: When the app measured the load and availability shown. Without it, no time is shown.
- `label?: string | undefined`: What is being assigned, as the button's label: "Owner", "Reviewer". Defaults to "Assignee".
- `className?: string | undefined`: Classes for the button.

## Strings (`assigneePicker`)

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

- `label`: Assignee
- `trigger`: {label}: {name}
- `unassigned`: Unassigned
- `search`: Search people and agents…
- `people`: People
- `agents`: Agents
- `agent`: {noun} agent
- `assigned`: Assigned
- `unassign`: Unassign
- `noMatch`: No one matches.
- `nobody`: No one can be assigned.
- `unknownWho`: Who can be assigned isn’t known.
- `available`: Available
- `busy`: Busy
- `away`: Away
- `offline`: Offline
- `busyUntil`: Busy until {time}
- `awayUntil`: Away until {time}
- `offlineUntil`: Offline until {time}
- `availabilityUnknown`: Availability not known
- `load`: { one: "{count} open task", other: "{count} open tasks" }
- `loadLimit`: { one: "{count} of {limit} open task", other: "{count} of {limit} open tasks" }
- `loadUnknown`: Load not known
- `asOf`: Load and availability as of {time}
- `lockedAssigning`: The assignment is being saved.

## Types

### AssigneePerson

A person who can be assigned.

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

### AssigneeWho

Who is assigned, or can be: a person, or an agent named by the app (never by model output).

```ts
{ person: AssigneePerson } | { agent: AgentIdentity }
```

### AssigneeAvailability

Whether someone can take work now, as the app knows it.

```ts
"available" | "busy" | "away" | "offline"
```

### AssigneeLoad

How much is assigned to someone now, as the app counted it.

```ts
{
  /** Their open tasks (or whatever the app counts), measured. */
  open: number;
  /** The most the app lets them hold, when it sets one. */
  limit?: number | undefined;
}
```

### AssigneeCandidate

One person or agent the work can go to, with what the app knows of their load and availability.

```ts
{
  who: AssigneeWho;
  /** Whether they can take work now. Left out, it reads "Availability not known": nothing is guessed. */
  availability?: AssigneeAvailability | undefined;
  /** When that changes back, for "Away until Mon 16 Nov, 09:00". Ignored for `available`. */
  until?: Date | string | undefined;
  /** How much they hold now. Left out, it reads "Load not known", never zero. */
  load?: AssigneeLoad | undefined;
  /** Why they can't be assigned this work (not a member here, lacks the role). Printed on them; choosing them does nothing. */
  locked?: string | null | 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
```

## 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`.
