Skip to content

Repository files navigation

pixel-agent-office

A pixel-art office map for AI agent task boards, as a React component. Your scheduler already knows which agent runs which task on which machine. Turn that into one JSON snapshot and the office draws it:

  • One desk per parallel slot. Each server gets an area with a rack and as many desks as it can run jobs in parallel.
  • Agents walk to work. A new job walks from the lounge to dispatch, picks up its card and sits at a free desk on its server. When it finishes, it walks the card back.
  • Stage-specific animations. Screens and poses change with the activity: planning, coding, testing, building, reviewing or delivering. A failed job holds its head.
  • Nothing is invented. Unverified jobs stand in the aisle and do not work. Stale data stops all work animation. Screen loops are decoration and never show a percentage.
  • Stage pipeline, roster and paging. Columns follow your stages. Every agent can be located from the roster. Up to six servers fit in one office; more servers go on further pages.
  • English and Simplified Chinese, keyboard and pointer navigation, and support for prefers-reduced-motion.

Live demo: https://zhx8702.github.io/pixel-agent-office/ · 中文界面 · JSON playground

简体中文

Install

npm install pixel-agent-office

Requires React 18 or newer.

Quick start

import {PixelOffice, type SceneSnapshot} from 'pixel-agent-office';
import 'pixel-agent-office/style.css';

const snapshot: SceneSnapshot = {
  version: 1,
  nodes: [{id: 'mac', name: 'Mac Studio', status: 'online', parallel: 3}],
  jobs: [{
    id: 'run-41', taskId: 'task-41', title: 'Add dark mode',
    stage: 'implement', status: 'running', nodeId: 'mac',
    agent: {name: 'Coder', model: 'claude-opus-4.6'},
    startedAt: Date.now() - 5 * 60_000, message: 'Editing theme tokens',
  }],
  queue: [{id: 'task-44', title: 'Write changelog'}],
};

export function Office() {
  return <PixelOffice snapshot={snapshot} locale="en" onSelect={selection => console.log(selection)} />;
}

The live demo has a simulated scheduler and a JSON playground that validates pasted snapshots. Add ?lang=zh-CN or ?mode=paste to the URL to open it in Chinese or straight in the playground. Run npm run dev for the same page locally.

Protocol: SceneSnapshot v1

The map renders a full snapshot, not a stream of events. Send a new snapshot whenever something changes, by polling, SSE or WebSocket. The component compares it with the previous one and animates the difference. The formal definition is schema/scene-snapshot.v1.schema.json, also exported as pixel-agent-office/schema.json. A complete example is in examples/snapshot.json.

Snapshot

Field Type Required Meaning
version 1 yes Protocol version.
nodes SceneNode[] yes Servers that can run jobs.
jobs SceneJob[] yes Jobs that currently hold a desk, plus jobs that just finished.
queue SceneQueueItem[] no Waiting tasks, shown at dispatch.
stages SceneStage[] no Pipeline columns, up to 8. Omit to use plan, implement, review, verify and delivery.
stale boolean no true when this is the last known state. Stops all work animation and labels servers "Last known".
generatedAt number no Epoch milliseconds when the snapshot was produced.

SceneNode

Field Type Required Meaning
id string yes Stable id, 1–128 characters.
name string yes Display name.
status online | offline | draining | recovering | revoked yes Only online nodes light up and animate.
parallel integer ≥ 1 | null no Parallel slots, which is the number of desks drawn. Use null when unknown; desks then follow the current load and the header says so.
platform string no Free text.

SceneJob

Field Type Required Meaning
id string yes Stable across snapshots. It keeps the agent on the same desk and drives the walk.
title string yes Task title.
stage string yes Id of the column the job appears in.
status running | cancelling | done | failed yes Anything but running makes the agent return its card and leave; cancelling and failed are shown as failures.
taskId string no Owning task. Selecting a task highlights all of its jobs.
nodeId string | null no The node actually executing the job. Leave it out when unknown; the agent then waits at dispatch. Do not fill it from a planned or pinned route.
verified boolean no, default true Set to false when you hold a resource but cannot prove work is happening. The agent stands instead of working.
activity plan | code | test | build | review | deliver | wait no Desk animation. Defaults to the stage's activity.
agent {brand?, name?, model?, tool?} no Who is working. brand (openai, grok, claude, gemini, deepseek, cursor, generic or unknown) is inferred from model/tool when omitted. Without an agent, the worker shows as an unknown agent.
startedAt number no Epoch milliseconds; values that look like epoch seconds are converted. Shows elapsed time. When the office first opens, jobs that started in the last 25 s walk in and older ones are already seated; jobs that appear later always walk in.
message string no Latest progress line, at most 280 characters. Shown on the lane card and on the desk screen when zoomed in. Redact secrets before sending.
note string no Replaces the default explanation in the hover card.
label string no Short tag shown instead of the stage name, for example "Health check".
location string no Replaces the node name in location text.
background boolean no Holds a desk but belongs to another view (for example another project). Drawn as an occupied desk without an agent.

SceneQueueItem and SceneStage

  • SceneQueueItem: {id, title, reason?, attention?}.
  • SceneStage: {id, label, short?, color?, symbol?, activity?}. Colours default to a built-in palette.

Rules and degradation

  • The map is only as true as your data. Keep nodeId, verified and stale honest; they are what stops the office from showing work that is not happening.
  • Input is validated on every render. Invalid entries are dropped or downgraded: an unknown node status becomes offline, and an unknown job status makes the job unverified. Text is trimmed and clamped, duplicate ids are ignored and collections are capped. Nothing throws. Pass onIssues to see what was fixed, or call normalizeSnapshot() in your own tests.
  • Versioning: additive optional fields stay in version: 1. A breaking change becomes version: 2. Given a newer version, the component renders the fields it knows and reports an issue.

Connecting a data source

Write one adapter that maps your scheduler's model to SceneSnapshot, then deliver snapshots however you like:

function toSnapshot(state: MySchedulerState): SceneSnapshot {
  return {
    version: 1,
    nodes: state.workers.map(w => ({id: w.id, name: w.hostname, status: w.alive ? 'online' : 'offline', parallel: w.slots ?? null})),
    jobs: state.runs.map(r => ({
      id: r.id, taskId: r.ticket, title: r.ticketTitle, stage: r.phase,
      status: r.state, nodeId: r.workerId ?? null, verified: r.heartbeatOk,
      agent: {name: r.agentName, model: r.model}, startedAt: r.startedAt, message: r.lastLog,
    })),
    queue: state.pending.map(p => ({id: p.ticket, title: p.title})),
  };
}

// Polling
const [snapshot, setSnapshot] = useState<SceneSnapshot>();
useEffect(() => {
  const load = () => fetch('/api/state').then(r => r.json()).then(s => setSnapshot(toSnapshot(s)));
  load(); const id = setInterval(load, 3000); return () => clearInterval(id);
}, []);

// Server-sent events
useEffect(() => {
  const events = new EventSource('/api/office');
  events.onmessage = e => setSnapshot(JSON.parse(e.data));
  events.onerror = () => setSnapshot(s => s && {...s, stale: true});
  return () => events.close();
}, []);

Mark the snapshot stale when the connection drops or the data is too old. The office then keeps the last picture and stops pretending things are moving.

Props

Prop Type Default Meaning
snapshot SceneSnapshot (any value is accepted and validated) — What to draw.
locale 'en' | 'zh-CN' | any BCP 47 tag 'en' Any zh* tag selects Simplified Chinese.
messages MessageOverrides — Override any UI string, for example {dispatch: 'Front desk'}.
selection OfficeSelection | null uncontrolled Controlled selection.
onSelect (selection) => void — Called with {kind: 'job', id, taskId}, {kind: 'task', id} (a queue chip), {kind: 'node', id} or {kind: 'stage', id}.
reducedMotion boolean false Stops walking and screen loops. System prefers-reduced-motion and hidden tabs pause animation anyway.
iconFor (icon) => string | undefined bundled SVGs URL for an agent icon key (openai, claude, …, generic).
caption string 'AGENT OFFICE' Text painted on the floor.
lanes 'compact' | 'full' | 'none' 'compact' Stage strip above the map: one row of stage chips, full cards per stage, or hidden.
onIssues (issues: string[]) => void — Validation report, called when it changes.
className string — Extra class on the root .pixel-office.

Other exports: normalizeSnapshot, resolveMessages, locales, pickLocale, defaultStages, brandOf, resolveBrand, agentBadge, AgentIcon and the protocol constants (PROTOCOL_VERSION, LIMITS, NODE_STATUSES, JOB_STATUSES, ACTIVITIES).

Styling

Import pixel-agent-office/style.css once. Every class starts with office- under a root .pixel-office, so styles do not leak into the host page. Override any rule from your own stylesheet. The map uses its own palette; the host page only needs to provide a font.

At 100% zoom the whole room fits the map without scrollbars. With no height from the host, the map picks one from the window size. To fill a full-screen layout, give .pixel-office a height (for example flex: 1 in a column that is 100dvh tall) and the map takes whatever the strips above and below leave.

Drawing limits

Limit Value What happens past it
Servers per office 6 Further offices are on pages, and a server bar lists every server.
Desks drawn per server 18 The extra desks are counted in the area header. Their jobs still count as working.
Pipeline cards per column 1 compact, 2 with lanes="full" "+N" / "N more" selects the stage.
Roster entries per page 12 The roster is paged.
Stages 8 Extra stages are ignored with an issue.

Development

npm install
npm run dev        # demo with simulation and JSON playground
npm test -- --run
npm run typecheck
npm run build      # dist/index.js, type declarations and style.css

npm run build:demo builds the demo into dist-demo/ with relative paths, so it works on any static host. On every push to main, .github/workflows/pages.yml runs the tests and publishes that build to GitHub Pages. Enable this once under Settings → Pages → Source: GitHub Actions.

npm run icons regenerates src/brandIcons.ts from assets/brands/*.svg.

License

MIT. The agent icons come from LobeHub Icons (MIT). Brand names and logos belong to their owners; see NOTICE.

Releases

Packages

Contributors

Languages