---
title: "The stream deck (Ulanzi D200) — Robion user manual"
description: "The stream deck: keys, pages, permission answers, the status window, running the daemon."
url: "https://robion.app/docs/deck/"
lang: en
---

# The stream deck (Ulanzi D200)

An Ulanzi Stream Controller D200 on the desk mirrors the run buttons of the shown project and
takes permission answers. This file covers the daemon, the keys, the pages and the status window.

## What it is

- A standalone daemon, `robion-deck` (`packages/deck` in the Robion repository), that drives a
  locally attached Ulanzi D200 as a client of the Robion server, next to the desktop and the
  phone. It is not part of the Robion app: it runs on the system Node and is built from the
  repository. Available since 0.11; permission keys, the limits plate and the task overview
  page since 0.12.
- The deck's world is the **project control panels** of the shown project (the **CONTROLS**
  panel the agent builds with `set_controls`); the deck follows the project the desktop shows.

## Running it

- Build and start (from the repository checkout):
  `pnpm --filter @robion/shared build && pnpm --filter @robion/deck build`, then
  `node packages/deck/dist/index.js` for the installed app (port 7666). Against a dev server
  add `--port 7667 --app-support <the "Robion Dev" Application Support folder>`.
- Options: `--project <id-or-label>` (the initial project), `--brightness <0-100>`,
  `--app-support <dir>` (or `ROBION_APP_SUPPORT`), `--port <n>` (or `ROBION_PORT`).
- The token comes from `<app-support>/token`, so the server must have started at least once.
  The daemon reconnects with backoff and re-discovers the device every 3 s — start order does
  not matter. The first log line says which server and app-support folder it drives.
- One daemon per machine: macOS opens the device shared, and a second instance (say, against
  the dev server) would paint into the same D200 — flicker and misplaced icons. A lock file
  (`robion-deck.lock` in the temp folder) makes the second instance exit with the running
  pid; stop the production daemon before starting a dev one.

## The 13 keys

- The keys show the **run buttons** of the shown project's control panels — project scope
  only, panels in the order the panel lists them. A button with a **hotkey** digit is pinned
  to that key (1 = top-left … 9 = key 9), the same digits the desktop panel binds; the rest
  fill the free keys in order. Buttons beyond the free keys are dropped.
- Faces use the same icon set as the panels (the button's `icon`); the label is drawn by the
  device. Colours follow the live run state: blue = running, green = ok, red = error,
  grey = stopped. A disabled button is dimmed.
- A button with **confirm** asks for a second press within 3 s (the face turns amber)
  instead of a dialog.
- Keep deck buttons few and their labels short: the key face is small, only run buttons
  reach it, and task-scope panels never do (only project panels are mirrored).

## Permission asks on keys

- An agent asking for a permission takes a free key: an amber check face with the task's
  title, the oldest ask first. A short press allows; holding the key ≥ 0.6 s denies. The key
  disappears by itself once the state moves on.

## The mode key, the tasks page and the overview page

- The last key is the mode key. Short press = next project. Long press (≥ 0.6 s) toggles the
  **tasks page**: every key is one open task of the project (in creation order); the colour
  is the most urgent agent state — amber = waits for you, red = error, blue = running,
  green = a finished unseen turn, like the desktop dots; a plain shell is dark with a terminal
  glyph. Pressing a task key opens its terminal in the desktop. A blue frame and a list glyph
  on the mode key mark the tasks page.
- Holding the big touch screen ≥ 0.6 s toggles the **overview page** (violet frame and grid
  glyph on the mode key): open tasks of every project, the same colours, with the project's
  name as a small line above the task title. Tasks that need attention get a key first, then
  the quiet ones newest first; a task keeps the key it sits on and a state change recolours
  it in place. Pressing a task asks the desktop to open its terminal while the deck stays on
  the overview — it is a monitoring surface.

## The status window

- The small screen shows the connection dot, the project label and the `‹ position/count ›`
  of project paging. Tapping it cycles: project name → clock (date and weekday) → host CPU and
  RAM bars → Claude rate limits (the same windows as the rail's usage widget, as bars).
- At ≥ 90 % of a limit the plate turns red and the small window switches to it by itself.
- Holding it ≥ 0.6 s toggles the overview page (see above).

## Following the desktop, the D200X encoder

- The desktop reports the project it shows and the deck switches with it; a freshly started
  daemon starts on the desktop's current project. Switching on the deck itself still works.
- On a D200X the encoder pages through projects (press = focus). The D200 has none.

## Firmware and device notes

- Update the device firmware with Ulanzi Studio: old firmware flashes its own clock on every
  key press and sends two presses per tap and no release, so a tap on the status window may
  cycle but the page toggles do not work there. Then quit Ulanzi Studio — while it runs, key
  presses fire twice.
- Not detected at all (no Ulanzi device in `ioreg -p IOUSB`)? Almost always a charge-only
  USB-C cable or port — use a data cable; a USB 2.0 hub between the D200 and the host is a
  known workaround for enumeration failures.
- The firmware has a ~5 s watchdog: without traffic it brings its own clock screen over the
  keys. The daemon feeds it every 2 s and repaints the keys once a minute, so a brief flash of
  the native clock after a press is expected on un-updated devices.

## Limits

- Only run buttons of project-scope panels: no toggles, selects, sliders or other widgets, no
  task-scope panels, no target task for a `send` button. One server per daemon — projects of
  a remote Linux server are not on the deck.
