PixelDosa UI

Live Status Line

One line saying what an agent is doing right now — replaced as work moves on, cheap enough to live in permanent product chrome.

Queued

Queued

0s

Installation

bash
npx shadcn@latest add @pixeldosa/live-status-line

Requires `cn` at `@/lib/utils`, the `pd-status-swap` keyframe from @pixeldosa/pixeldosa-theme, and @pixeldosa/agent-presence, which it composes for the indicator. Installing this pulls Agent Presence in automatically.

Usage

"use client";

import { LiveStatusLine } from "@/components/ui/live-status-line";

<LiveStatusLine
  state={run.state}
  status="Reading"
  detail="invoices/2026-q3.csv"
  startedAt={run.startedAt}
  onCancel={run.abort}
/>

Features

  • It replaces, it doesn't accumulate. That's the whole distinction from its neighbours — and it's why this is cheap enough to leave in a toolbar or sidebar footer permanently. History belongs to Reasoning Stream and Chain-of-Thought Timeline.
  • Composes Agent Presence for the indicator rather than drawing its own dots, so the turn-taking grammar holds here for free — a suggesting or awaitingApproval status stops moving without this component knowing anything about it.
  • Layout can't jitter. A minimum row height means changing text length never reflows the container, and the detail truncates before the verb does — so "Reading" stays legible when a long path can't.
  • The elapsed counter ticks itself, rather than making you re-render once a second for a display detail.
  • Swaps once per real change — keyed on the status text, so a re-render with unchanged text never re-animates.

Variants

Working, with an object

Reading invoices/2026-q3.csv

Readinginvoices/2026-q3.csv

Thinking

Planning the reconciliation

Planning the reconciliation

Your turn — it stops

Inherited from Agent Presence: the indicator holds still because nothing proceeds until you act.

Waiting for your approval

Waiting for your approval

Complete

Reconciled 5 invoices

Reconciled 5 invoices

Examples

In permanent chrome — a sidebar footer or toolbar, where it costs one row:

<footer className="border-t px-3">
  <LiveStatusLine state={run.state} status={run.status} startedAt={run.startedAt} onCancel={run.abort} />
</footer>

Per row in a table, where each row has its own agent job:

{rows.map((row) => (
  <td key={row.id}>
    <LiveStatusLine state={row.state} status={row.status} detail={row.target} />
  </td>
))}

Omit startedAt where elapsed time isn't meaningful — a sub-second action doesn't need a stopwatch:

<LiveStatusLine state="working" status="Saving" />

Props

PropTypeDefaultDescription
stateAgentState—Required. Agent Presence's vocabulary.
statusstring—Required. The current micro-action.
detailstring—What it's acting on. Truncates before the verb.
startedAtnumber | Date—Drives the live elapsed counter.
onCancel() => void—Shown while the agent is active.
indicator"mark" | "line""mark"The agent's drawn head, or three dots for the densest rows.

All other props forward to the wrapping <div>.

Accessibility

  • The status text is announced through Agent Presence's aria-live region — one announcement channel, not two competing ones.
  • The elapsed counter uses tabular-nums, so the line doesn't shift as seconds tick.
  • Stop is a real keyboard-reachable <button> with its hit area extended to ~44px while staying visually compact.
  • Under prefers-reduced-motion the swap is dropped; the text simply changes, which loses nothing since the information was always the words.

When not to use

The reasoning text itself is Reasoning Stream. A per-artifact generation surface is Generation Placeholder. The whole turn's history is Chain-of-Thought Timeline. And this isn't a toast — it describes ongoing work, it doesn't announce a completed event and disappear.

why it's built this way

Engineering Notes

The defining constraint is that this component replaces rather than accumulates. Accumulation is what makes agent activity surfaces expensive — a growing list needs scroll management, virtualisation at length, and a decision about when to collapse. Holding exactly one line keeps it cheap enough to live permanently in product chrome, and pushes the history question to Reasoning Stream and Chain-of-Thought Timeline where it belongs. It composes Agent Presence's `line` form rather than drawing its own dots, which is the system's compose-don't-reinvent rule applied where it genuinely fits: the turn-taking grammar then holds here for free, including the fact that a `suggesting` or `awaitingApproval` status stops moving. Two layout decisions prevent the jitter this pattern usually has: a minimum row height, so text of varying length cannot reflow the container around it, and truncation applied to the detail rather than the verb, so 'Reading' remains legible when a long filename cannot be. The elapsed counter is the one place this system deliberately breaks its own 'the caller owns async state' convention — every other component takes progress, busy and duration as props — because a live counter would otherwise force the consumer to re-render once a second purely for a display detail. The interval is only started while the agent is actually active, and never when no `startedAt` was supplied.

how it moves, and when it doesn't

Motion Notes

One animation, and it maps to one state change: when the status text is replaced, the new text fades up three pixels over `--pd-duration-fast` with `--pd-ease-decelerate`. Short deliberately — micro-actions can change every second or two, and anything longer would read as churn rather than as replacement. It is keyed on the status string so React's reconciliation fires it exactly once per genuine change; an unchanged status that re-renders does not re-animate, which is the same mechanism Progressive Reveal and Reasoning Stream use. The indicator's own motion is inherited from Agent Presence and is not duplicated here. Under `prefers-reduced-motion` the swap is dropped entirely: text simply changes, which loses nothing, because the information was always the words rather than the transition between them.