# Session list

Threads, newest first, grouped by day, with pinned threads on top. A thread agents are working in shows a live dot; one that needs you pulses amber. Search filters by title. Double-click or the menu renames in place.

`groupBy` replaces the day groups with your own ("Needs you", "Projects", "Finished"), in `groupOrder`. A session's `detail` adds a second line, and `showStatus` prints each status in words beside it, so state is never colour alone. `virtualize` renders only the rows in or near view, for long lists. A session with `lastHeardAt` whose run has been silent past `staleAfter`, or whose host is offline, stops showing as running and says so instead. A session with an `href` is a link, so it opens in a new tab and its address can be copied; the open one is marked as the current page.

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

## Install

```bash
npx candor-ui add session-list
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/session-list`.
It brings in: dropdown-menu, locale, relative-time, run-heartbeat, status, status-dot, team, use-virtual-list, utils.
npm packages: lucide-react@^1.67.0.

## Example

```tsx
"use client"

import * as React from "react"
import { SessionList } from "@/components/session-list"
import type { Session } from "@/lib/team"

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

const initial: Session[] = [
  { id: "s1", title: "Add a region column to orders", updatedAt: minutesAgo(2), status: "running", detail: "Writing the migration" },
  { id: "s2", title: "Why is checkout slow on Staging?", updatedAt: minutesAgo(30), status: "waiting", detail: "Asked you about the cache" },
  { id: "s3", title: "Clean up stale feature flags", updatedAt: minutesAgo(60 * 5), unread: true },
  { id: "s4", title: "Release checklist", updatedAt: minutesAgo(60 * 24 * 3), pinned: true },
  { id: "s5", title: "Index orders.customer_id", updatedAt: minutesAgo(60 * 26) },
  { id: "s6", title: "Rename the billing repository", updatedAt: minutesAgo(60 * 24 * 4) },
]

// With an href each row is a real link: it opens in a new tab and its address can be copied. A plain click still goes
// through onOpen, for your router.
const linked = initial.map((s) => ({ ...s, href: `#thread-${s.id}` }))

export default function Example() {
  const [sessions, setSessions] = React.useState(linked)
  const [active, setActive] = React.useState("s1")
  const [byState, setByState] = React.useState(false)
  return (
    <div className="flex h-[26rem] w-72 flex-col overflow-hidden rounded-lg border bg-sidebar py-2">
      <label className="mx-3 mb-2 flex items-center gap-1.5 text-xs text-muted-foreground">
        <input type="checkbox" checked={byState} onChange={(e) => setByState(e.target.checked)} />
        Group by state
      </label>
      <SessionList
        className="min-h-0 flex-1"
        sessions={sessions}
        // Your own groups, in your order, with each status printed in words.
        groupBy={byState ? (s) => (s.status === "waiting" ? "Needs you" : s.status === "running" ? "Working" : "Everything else") : undefined}
        groupOrder={["Needs you", "Working"]}
        showStatus={byState}
        activeId={active}
        onOpen={setActive}
        onRename={(id, title) => setSessions((l) => l.map((s) => (s.id === id ? { ...s, title } : s)))}
        onPin={(id, pinned) => setSessions((l) => l.map((s) => (s.id === id ? { ...s, pinned } : s)))}
        onDelete={(id) => setSessions((l) => l.filter((s) => s.id !== id))}
      />
    </div>
  )
}
```

## <SessionList>

Props:

- `sessions: Session[]`: The threads; pinned ones first, the rest grouped by day, newest first.
- `activeId?: string | undefined`: The id of the open thread, marked as the current page.
- `groupBy?: ((session: Session) => string) | undefined`: Your own groups instead of pinned and days: returns a session's group heading. Pinned sessions aren't lifted out; return a "Pinned" group yourself if you want one. Newest first within each group.
- `groupOrder?: string[] | undefined`: The order of `groupBy`'s headings; headings not listed follow, in the order first seen.
- `showStatus?: boolean | undefined` (default `false`): Prints each session's status (or its `statusLabel`) in words on a second line, with the dot. Defaults to false.
- `staleAfter?: number | undefined` (default `STALE_AFTER`): How long a running session may be silent before it's no longer shown as running, in milliseconds. Defaults to two minutes.
- `virtualize?: boolean | undefined` (default `false`): Render only the rows in or near view, for lists of hundreds of sessions. Defaults to false.
- `onOpen?: ((id: string) => void) | undefined`: Called with a thread's id when it's chosen. For a session with an `href`, a plain click calls this instead of following the link (for an app's own router); other clicks (new tab, new window) open the link as usual.
- `onRename?: ((id: string, title: string) => void) | undefined`: Allows renaming in place; called with the id and new title when the edit is committed.
- `onPin?: ((id: string, pinned: boolean) => void) | undefined`: Adds Pin or Unpin to a thread's menu; called with the id and whether it should now be pinned.
- `onDelete?: ((id: string) => void) | undefined`: Adds Delete to a thread's menu; called with the id when it's chosen, without a confirmation.
- `className?: string | undefined`

## Strings (`sessionList`)

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

- `threads`: Threads
- `searchThreads`: Search threads
- `noMatch`: No threads match “{query}”.
- `noThreads`: No threads yet.
- `pinned`: Pinned
- `today`: Today
- `yesterday`: Yesterday
- `previous7Days`: Previous 7 days
- `older`: Older
- `threadName`: Thread name
- `unread`: Unread
- `actionsFor`: Actions for {title}
- `rename`: Rename
- `pin`: Pin
- `unpin`: Unpin
- `delete`: Delete

## 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` (fixed strings) and `TermsProvider` (nouns); entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
