# Web UI

Roscoe ships with a React + Vite web app that covers the graph editor, live run
animation, run history, and settings. Reach for it when you want to draft a
workflow, eyeball a run as it executes, or skim recent history without dropping
into the terminal.

In dev mode (`bun run dev`) the UI runs on `http://localhost:3000` and talks to
the API on `:8080`. The compiled `roscoe serve` binary serves both on a single
port (default `http://localhost:7777`). See
[Environment variables](/docs/configuration/environment) for port overrides.

The sidebar switches projects. The active project decides which project store
the library reads, and run history filters to it by default, so switching
changes what those pages show. See
[Global vs Project](/docs/getting-started/global-vs-project).

## Workflows (`/workflows`)

The library. Lists every workflow visible to Roscoe, split into Project
(`<repo>/.roscoe/workflows/`) and Global (`~/.roscoe/workflows/`) sections.
Search by name or filter by source. The **New Workflow** action scaffolds a new
YAML file.

Roscoe dims and badges any global workflow row a same-id project copy shadows,
so you know which file actually runs. Delete lives in the workflow detail
page's kebab menu. There is no rename in the web UI.

If the install is empty the page shows an onboarding card with a **Create
starter workflows** button, which seeds the example workflows.

## Workflow editor (`/workflows/$id/edit`)

The authoring surface. The graph canvas is a React Flow view with one node per
state: click a node to open its config panel, and add nodes from the canvas add
menu. You author edges as transitions in that panel. The canvas auto-lays out
as you edit.

Edits validate against the workflow schema and surface errors inline. Save
writes the file on the server, and the watcher pushes the change to other open
tabs.

The editor enforces the same schema and integrity rules as `roscoe validate`,
so a workflow that saves cleanly is one that will start cleanly.

**Edit with a prompt** hands the workflow to Claude instead of the canvas.
Describe the change you want ("add a step", "handle a failure") and it edits
the file. Unsaved canvas edits are not included, and the editor says so before
it starts.

**Undo (⌘Z)** and **Redo (⌘⇧Z)** sit in the toolbar. If the file changes on
disk while you have unsaved edits, a notice offers you the new version rather
than overwriting either copy silently.

A [`subworkflow`](/docs/nodes/subworkflow) node opens its child read-only in
the same editor, tracked in the URL as `?drill=`. The `?from=` trail is how
the back link finds its way out again.

## Workflow detail (`/workflows/$id`)

A read-mostly preview. Renders the graph, surfaces validation errors when the
config fails to parse, and offers the primary **Run** action. A **History**
button jumps to the run history list pre-filtered to this workflow. The kebab
menu has **Move to Project** / **Move to Global** for promoting a workflow
between sources, plus **Delete workflow**; the moves round-trip through
`POST /api/workflows/:id/move`.

## Run viewer (`/runs/$id`)

The live run page. The header links its workflow title back to the workflow
detail page (dimmed and un-clickable only when the source workflow no longer
exists) and renders the run id and run metadata as click-to-copy chips. The
graph animates as nodes execute: running nodes pulse, completed nodes fill in,
and failed nodes go red. During a live run a **Follow active node** toggle
(top-right of the canvas) keeps the camera spotlighted on the currently
executing node, auto-panning as the run advances; click it to stop following
and pan freely.

The right rail shows a per-node trace with input, output, duration, error (if
any), and child sub-runs (e.g. one row per
[`consensus`](/docs/nodes/consensus) agent with its individual verdict and
reasoning). With no node selected, the rail follows the live front
of the run. It opens on the node currently executing, falling back to the
most-recently-finished node once nothing is active, so an in-progress run shows
what's running, not the last completed step. Click any node to pin the rail to
it.

Script ([`script`](/docs/nodes/script)) output streams live: a running node's
`stdout`/`stderr` appear in the rail as they are produced (coalesced
server-side so the poll stays cheap), rather than only landing once the node
finishes. Terminal streams
(`stdout`, `stderr`, `error`) render in a monospace block with ANSI colors
preserved: the SGR (Select Graphic Rendition) escapes that test runners and
build CLIs emit (vitest's green checks, red failures, dim notes) show as themed
color instead of dropping to plain text or leaking as literal `[0m` noise. Long
lines and box-drawing rules wrap rather than overflow.

When the run pauses at a [`human` node](/docs/getting-started/concepts), an
Awaiting input panel above the canvas surfaces transition buttons; click one to
call `POST /api/runs/:id/resume`. Consensus nodes get a dedicated panel showing
each agent's vote, the quorum threshold, and the final outcome.

While a run is **running** or **paused**, a **Cancel run** button (with a
confirmation modal) stops it. Cancelling marks the node the run was sitting on
as `cancelled`, and Roscoe kills the running script worker or `claude` CLI/SDK
call rather than leaving it to finish in the background. A cancelled node is
rendered neutral gray, distinct from the red of a failed node, since a
deliberate stop is not an error. You can cancel any run, including runs driven
over MCP (Model Context Protocol).

Runs started via MCP show an **MCP** badge; Claude Code controls those
transitions and no approval panel renders. When such a run is paused on an AI
handback node ([`ai_agent`](/docs/nodes/ai-agent) /
[`ai_judge`](/docs/nodes/ai-judge) / `consensus` /
[`round_robin`](/docs/nodes/round-robin), i.e. the
calling agent is actively generating a response), the status reads
**Processing** in blue with a pulsing dot, rather than the amber **Paused**
used for a human wait. A genuine `human` node still shows amber **Paused**,
because that one really is blocked on a person. The run page, the run history
list, and the Workflows "last run" column all read the same run field
(`pauseKind`: `'agent'` vs `'human'`), so the same run reads consistently
everywhere.

On a finished run the header also has a **Record** button (or **Edit
recording** once you have directed it). It opens the recording editor at
`/runs/$id/record`, where the run plays back fast on the real canvas and you
choose which steps the camera stops on before exporting a 1080p MP4. See
[Record a run](/docs/running/record-a-run).

A run carries its own cost and token totals, split between metered API spend
and the subscription-equivalent of work that ran on your Claude plan. A run
you capped that hit the cap stops as `cancelled` with a banner naming the cap
it hit. A run can carry both a dollar and a token cap and stops at whichever
comes first.

## Run history (`/runs`)

Every run, with branch and workflow filters and a **Clear all runs** action.
Use this to clear test debris or audit recent activity.

The page is **workspace-scoped**. A scope selector toggles between **this
workspace** (the default), which shows only runs for the workspace the nav
project switcher currently has active, and **all workspaces**, which shows
every run across Global and all projects with a per-row tag (a globe for
Global, a folder + project name otherwise) so mixed rows stay distinguishable.
The active workspace can be a project _or_ **Global**, where cwd-less runs
from the Claude desktop app land (see
[Global vs Project](/docs/getting-started/global-vs-project)).

When the active project has no runs, the empty state points you to where your
runs actually live: a **View Global runs** shortcut for desktop-app runs that
landed in Global rather than the project you're sitting in. Opening a run
(`/runs/$id`) carries the run's scope in the URL, and the `?project=…` links
the MCP returns auto-select the matching workspace.

Those MCP links point at `/open/runs/$id`, which hands the run to the desktop
app when it is installed and falls back to this browser UI when it is not —
see [Run links open in the app](/docs/getting-started/desktop).

## Settings (`/settings`)

Edit `roscoe.yaml` (the active merged config, global plus project overrides)
directly in the browser. Adjust the model list and default model, pick the AI
backend, set the CLI path, override ports, and tweak retry/timeout, analytics,
and preference defaults. Changes hit disk immediately and the file watcher
fires server-sent events (SSE) so other open tabs reload without a refresh. See
[roscoe.yaml](/docs/configuration/roscoe-yaml) for the schema and worked
example.

The **Updates** card at the bottom shows the running version next to the latest
release on the channel: a blue "Update available" badge appears
when a newer build exists, flipping to a filled "Update ready" once the
background pre-download finishes. Click **Download & install** to swap the
binary, then stop the server (Ctrl-C or `q`) and run `roscoe serve` again. The
sidebar chip mirrors this state machine in a single icon so you can tell at a
glance from any page.

## Sidebar version chip

The icon at the bottom of the left sidebar reflects the update state for the
running binary:

- Neutral check → the current version matches the latest manifest.
- Blue up-arrow with a static dot → a newer release exists and Roscoe is
  pre-downloading it in the background.
- Blue up-arrow with a pulsing dot → the new binary is on disk; one click
  takes you to Settings to apply.
- Teal reload icon with a teal dot → the new binary is installed; restart to
  run it.

## Docs (`/docs`)

These docs, served in-app. Useful when you're already in the UI and don't
want to context-switch to a browser tab.

## When the app cannot start normally

A fresh install opens an onboarding flow instead of the library: it checks the
prerequisites, offers to seed the example workflows, and hands you to the
Workflows page when it is done.

If the database cannot be opened, the app drops into a recovery screen rather
than failing page by page. The server reports the problem honestly instead of
serving errors from a server still claiming to be healthy. See
[Database recovery](/docs/troubleshooting/database-recovery).

## Default URLs

Dev (`bun run dev`) serves the UI at `http://localhost:3000`; `roscoe serve`
serves it at `http://localhost:7777`. Override either with `ROSCOE_WEB_PORT`
(dev) or `--port` / `ROSCOE_PORT` (`serve`).

## See also

- [Command-line interface](/docs/running/cli) — drive the same operations from the terminal.
- [Claude integration](/docs/running/mcp) — drive them from Claude Code.
- [roscoe.yaml](/docs/configuration/roscoe-yaml) — what the Settings page is editing.
- [REST API reference](/docs/reference/rest-api) — the HTTP API the web UI is built on.
