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
npm install pixel-agent-officeRequires React 18 or newer.
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.
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.
| 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. |
| 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. |
| 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:{id, title, reason?, attention?}.SceneStage:{id, label, short?, color?, symbol?, activity?}. Colours default to a built-in palette.
- The map is only as true as your data. Keep
nodeId,verifiedandstalehonest; 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. PassonIssuesto see what was fixed, or callnormalizeSnapshot()in your own tests. - Versioning: additive optional fields stay in
version: 1. A breaking change becomesversion: 2. Given a newer version, the component renders the fields it knows and reports an issue.
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.
| 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).
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.
| 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. |
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.cssnpm 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.
MIT. The agent icons come from LobeHub Icons (MIT). Brand names and logos belong to their owners; see NOTICE.