# Run heartbeat

A run's state that never claims it's alive when nothing has been heard. Running and heard from recently, it says "Running · heard 1 minute ago". Running and silent past `staleAfter` (two minutes unless set), it says "No update since 10:42" instead, and that it may have stopped. A host the app can't reach says so, with when. A run that needs the person says what for, with a Respond the app handles. Before the page has a clock (server rendering) it gives the last update's time and claims nothing. Without `lastHeardAt` the app isn't tracking freshness, and the status is shown as given.

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

## Install

```bash
npx candor-ui add run-heartbeat
```

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

## Example

```tsx
"use client"

import { RunHeartbeat } from "@/components/run-heartbeat"

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

export default function Example() {
  return (
    <div className="flex max-w-md flex-col divide-y rounded-lg border bg-card">
      {/* Heard from a minute ago: running. */}
      <RunHeartbeat className="px-4 py-3" status="running" lastHeardAt={minutesAgo(1)} />
      {/* Silent for 14 minutes: it no longer says running. */}
      <RunHeartbeat className="px-4 py-3" status="running" lastHeardAt={minutesAgo(14)} />
      {/* Its machine can't be reached. */}
      <RunHeartbeat className="px-4 py-3" status="running" lastHeardAt={minutesAgo(7)} hostOffline={{ host: "Runner 3", since: minutesAgo(6) }} />
      {/* Waiting on a person, with what for. */}
      <RunHeartbeat className="px-4 py-3" status="waiting" needs="Approve dropping partner_tier_old" onRespond={() => {}} />
      <RunHeartbeat className="px-4 py-3" status="succeeded" />
    </div>
  )
}
```

## <RunHeartbeat>

Props:

- `status: RunStatus`: The run's status as the service last reported it.
- `lastHeardAt?: Date | string | undefined`: When the app last heard anything from the run: an event, a log line, a heartbeat.
- `hostOffline?: HostOffline | undefined`: Set when the machine or worker running it can't be reached. Shown ahead of everything but a finished run.
- `needs?: string | undefined`: What the run needs from the person, in plain words. Set, or with status "waiting", it shows as needing you.
- `label?: string | undefined`: Your word for the status ("Making"), used while the run is live or finished.
- `staleAfter?: number | undefined` (default `STALE_AFTER`): How long a running run may be silent before it's no longer called running, in milliseconds. Defaults to two minutes.
- `onRespond?: (() => void) | undefined`: Called when the person picks Respond on a run that needs them. Without it, there's no button.
- `compact?: boolean | undefined` (default `false`): One line, for lists: no hint and no button.
- `className?: string | undefined`

## Functions

### useLivenessWords

```ts
useLivenessWords(staleAfter = STALE_AFTER)
```

The words for a run that can't be called alive, for components that show many runs: `const words = useLivenessWords(); words(run)` gives its liveness and, when it's stale, offline or unchecked, the line to show instead of its status ("No update since 10:42"). For a live or finished run the text is null.

## Strings (`runHeartbeat`)

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

- `heard`: heard {time}
- `stale`: No update since {time}
- `noUpdate`: No update received
- `staleHint`: It may have stopped. Nothing has been heard from it since.
- `offline`: {host} is offline
- `offlineSince`: {host} offline since {time}
- `host`: The host
- `lastUpdate`: Last update {time}
- `needsYou`: Needs you
- `respond`: Respond

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