PixelDosa UI

Run Inbox

Every agent run in one place, with the ones that need a person at the top.

Runs

2 waiting on you · 6 in total

Needs you2

  • Per seat or flat pricing first?

    Research agent · 3 hours ago

  • Wants to email 3 people. Waiting for approval.

    Finance agent · 40 minutes ago

Working1

  • Reading 42 of 120 tickets

    Support agent · started 4 minutes ago

Stopped with a problem1

  • The CMS refused the update. Nothing was changed.

    Content agent · 1 hour ago

Finished1

  • 12 sources · 5 competitors

    Research agent · yesterday

Scheduled1

  • Inbox agent · tomorrow at 9:00

Installation

bash
npx shadcn@latest add @pixeldosa/run-inbox

Requires `cn` at `@/lib/utils`. Phrase `note` as the thing a person needs to decide or know, not as a status code.

Usage

"use client";

import { RunInbox } from "@/components/ui/run-inbox";

<RunInbox runs={runs} onOpen={open} onStop={stop} />

Why it exists

The way people work with background agents now is: give it a job, close the laptop, come back later. An inbox is the familiar shape for that — and its familiar failure is strict chronology, where the run that has been waiting three hours for an answer sits below the one that finished a minute ago.

Features

  • Grouped by what the run needs, not when it happened. Waiting on you comes first, whatever its age, then problems, then finished, then scheduled.
  • Counts per group, so the size of the pile is visible before you scroll.
  • Only running rows move. Rows waiting on you hold still, in the waiting colour.
  • Stop appears only on running rows, so it can't be aimed at a finished one.
  • Unread changes weight and is announced, rather than creating a separate "new" section.

Props

PropTypeNotes
runs{ id, title, state, agent?, note?, time?, unread? }[]state is needsYou, running, scheduled, done or failed. time is formatted by you.
onOpen(id: string) => voidRequired.
onStop(id: string) => voidShown on running rows.
emptyLabelstringShown when there are no runs.
titlestringDefaults to "Runs".
headingLevel2 | 3 | 4 | 5 | 6Defaults to 3.

Accessibility

  • A labelled <section>; each group is a heading with its count.
  • Each run's title is a button, so keyboard users move through runs, not decorations.
  • "Not opened yet" is announced for unread rows rather than shown only as weight.
  • The running dot's pulse stops under prefers-reduced-motion.

When not to use

  • The steps inside one run: use Agent Plan with statuses.
  • A notification feed: this is a work queue, not an activity stream.
  • Hundreds of runs: at that size you need search and filters around it.

why it's built this way

Engineering Notes

Grouping by state rather than sorting by time is the whole component, and it is why this is not a generic list: the research on agent management interfaces names the inbox's scaling problem exactly, and a pile ordered by recency buries the only rows that block progress. Counts sit on the group headers instead of a single unread badge, because 'four waiting on you' and 'four finished' are different feelings and one number cannot carry both. `unread` only changes weight and adds a screen-reader phrase rather than driving a separate 'new' section, since a run's state matters more than whether it has been opened. Stop appears only on running rows, so the control cannot be aimed at a run that has already ended.

how it moves, and when it doesn't

Motion Notes

Motion means the machine is busy: only a running row's dot pulses. Rows waiting on a person hold completely still in the waiting colour, which is the turn-taking grammar applied to a list — the eye is drawn by stillness and position rather than by something blinking for attention. Under `prefers-reduced-motion` the pulse stops and state is carried by colour, position and words.